コンテンツへスキップ

ライフスパンイベント

アプリケーションが**起動する前**に実行されるべきロジック(コード)を定義できます。これは、このコードがアプリケーションが**リクエストの受信を開始する前**に**一度だけ**実行されることを意味します。

同様に、アプリケーションが**シャットダウンする際**に実行されるべきロジック(コード)を定義できます。この場合、このコードは、おそらく**多数のリクエスト**を処理した後、**一度だけ**実行されます。

このコードは、アプリケーションがリクエストの**受け付けを開始する前**に、そしてリクエストの処理を**終了した直後**に実行されるため、アプリケーションの**ライフスパン全体**をカバーします(「ライフスパン」という言葉はすぐに重要になります 😉)。

これは、アプリ全体で使用する必要があり、リクエスト間で**共有**され、後で**クリーンアップ**する必要がある**リソース**を設定するのに非常に役立ちます。例えば、データベース接続プールや、共有の機械学習モデルのロードなどです。

ユースケース

**ユースケース**の例から始め、次にこれでそれを解決する方法を見ていきましょう。

リクエストを処理するために使用したい**機械学習モデル**があると想像してください。🤖

同じモデルがリクエスト間で共有されるため、リクエストごとに1つのモデル、ユーザーごとに1つのモデルなどではありません。

モデルのロードは、ディスクから多くの**データを読み込む**必要があるため、**かなりの時間がかかる**と想像しましょう。そのため、すべてのリクエストに対してそれを行いたくありません。

モジュール/ファイルのトップレベルでロードすることもできますが、それは簡単な自動テストを実行しているだけでも**モデルをロードする**ことを意味し、そのテストはコードの独立した部分を実行する前にモデルがロードされるのを待つ必要があるため、**遅く**なります。

それが私たちが解決することです。リクエストが処理される前にモデルをロードしますが、コードがロードされている間ではなく、アプリケーションがリクエストの受信を開始する直前のみロードします。

ライフスパン

この*起動*および*シャットダウン*ロジックは、FastAPIアプリのlifespanパラメータと「コンテキストマネージャ」(後ほど説明します)を使用して定義できます。

例から始め、次に詳細を見ていきましょう。

次のようにyieldを持つ非同期関数lifespan()を作成します。

from contextlib import asynccontextmanager

from fastapi import FastAPI


def fake_answer_to_everything_ml_model(x: float):
    return x * 42


ml_models = {}


@asynccontextmanager
async def lifespan(app: FastAPI):
    # Load the ML model
    ml_models["answer_to_everything"] = fake_answer_to_everything_ml_model
    yield
    # Clean up the ML models and release the resources
    ml_models.clear()


app = FastAPI(lifespan=lifespan)


@app.get("/predict")
async def predict(x: float):
    result = ml_models["answer_to_everything"](x)
    return {"result": result}

ここでは、yieldの前に機械学習モデルを持つ辞書に(偽の)モデル関数を配置することで、モデルのロードという高価な*起動*操作をシミュレートしています。このコードは、*起動*中に、アプリケーションが**リクエストの受け付けを開始する前**に実行されます。

そして、yieldの直後でモデルをアンロードします。このコードは、アプリケーションが**リクエストの処理を終了した後**、*シャットダウン*の直前に実行されます。これにより、例えばメモリやGPUのようなリソースが解放される可能性があります。

ヒント

アプリケーションを**停止する**ときにshutdownが発生します。

新しいバージョンを開始する必要があるかもしれませんし、実行に疲れただけかもしれません。🤷

ライフスパン関数

まず注目すべきは、yieldを持つ非同期関数を定義していることです。これは、yieldを持つ依存関係と非常によく似ています。

from contextlib import asynccontextmanager

from fastapi import FastAPI


def fake_answer_to_everything_ml_model(x: float):
    return x * 42


ml_models = {}


@asynccontextmanager
async def lifespan(app: FastAPI):
    # Load the ML model
    ml_models["answer_to_everything"] = fake_answer_to_everything_ml_model
    yield
    # Clean up the ML models and release the resources
    ml_models.clear()


app = FastAPI(lifespan=lifespan)


@app.get("/predict")
async def predict(x: float):
    result = ml_models["answer_to_everything"](x)
    return {"result": result}

関数の最初の部分(yieldの前)は、アプリケーションが**起動する前**に実行されます。

そして、yieldの後の部分は、アプリケーションが**終了した後**に実行されます。

非同期コンテキストマネージャ

確認すると、関数には@asynccontextmanagerがデコレートされています。

これは、関数を「**非同期コンテキストマネージャ**」と呼ばれるものに変換します。

from contextlib import asynccontextmanager

from fastapi import FastAPI


def fake_answer_to_everything_ml_model(x: float):
    return x * 42


ml_models = {}


@asynccontextmanager
async def lifespan(app: FastAPI):
    # Load the ML model
    ml_models["answer_to_everything"] = fake_answer_to_everything_ml_model
    yield
    # Clean up the ML models and release the resources
    ml_models.clear()


app = FastAPI(lifespan=lifespan)


@app.get("/predict")
async def predict(x: float):
    result = ml_models["answer_to_everything"](x)
    return {"result": result}

Pythonの**コンテキストマネージャ**は、withステートメントで使用できるものです。例えば、open()はコンテキストマネージャとして使用できます。

with open("file.txt") as file:
    file.read()

最近のバージョンのPythonには、**非同期コンテキストマネージャ**もあります。これはasync withと一緒に使用します。

async with lifespan(app):
    await do_stuff()

上記のようにコンテキストマネージャまたは非同期コンテキストマネージャを作成すると、withブロックに入る前にyieldの前のコードが実行され、withブロックを終了した後でyieldの後のコードが実行されます。

上記のコード例では、直接使用するのではなく、FastAPIに渡して使用してもらいます。

FastAPIアプリのlifespanパラメータは**非同期コンテキストマネージャ**を受け取るため、新しいlifespan非同期コンテキストマネージャをそれに渡すことができます。

from contextlib import asynccontextmanager

from fastapi import FastAPI


def fake_answer_to_everything_ml_model(x: float):
    return x * 42


ml_models = {}


@asynccontextmanager
async def lifespan(app: FastAPI):
    # Load the ML model
    ml_models["answer_to_everything"] = fake_answer_to_everything_ml_model
    yield
    # Clean up the ML models and release the resources
    ml_models.clear()


app = FastAPI(lifespan=lifespan)


@app.get("/predict")
async def predict(x: float):
    result = ml_models["answer_to_everything"](x)
    return {"result": result}

代替イベント(非推奨)

Warning

*起動*と*シャットダウン*を処理する推奨される方法は、上記で説明したようにFastAPIアプリのlifespanパラメータを使用することです。lifespanパラメータを提供すると、startupおよびshutdownイベントハンドラは呼び出されなくなります。すべてlifespanか、すべてイベントか、両方ではありません。

この部分はスキップしても構いません。

*起動時*と*シャットダウン時*に実行されるべきこのロジックを定義する別の方法があります。

アプリケーションが起動する前、またはアプリケーションがシャットダウンするときに実行する必要があるイベントハンドラ(関数)を定義できます。

これらの関数は、async defまたは通常のdefで宣言できます。

startup イベント

アプリケーションが起動する前に実行されるべき関数を追加するには、イベント"startup"でそれを宣言します。

from fastapi import FastAPI

app = FastAPI()

items = {}


@app.on_event("startup")
async def startup_event():
    items["foo"] = {"name": "Fighters"}
    items["bar"] = {"name": "Tenders"}


@app.get("/items/{item_id}")
async def read_items(item_id: str):
    return items[item_id]

この場合、startupイベントハンドラ関数は、アイテムの「データベース」(単なるdict)をいくつかの値で初期化します。

複数のイベントハンドラ関数を追加できます。

そして、すべてのstartupイベントハンドラが完了するまで、アプリケーションはリクエストの受信を開始しません。

shutdown イベント

アプリケーションがシャットダウンするときに実行されるべき関数を追加するには、イベント"shutdown"でそれを宣言します。

from fastapi import FastAPI

app = FastAPI()


@app.on_event("shutdown")
def shutdown_event():
    with open("log.txt", mode="a") as log:
        log.write("Application shutdown")


@app.get("/items/")
async def read_items():
    return [{"name": "Foo"}]

ここでは、shutdownイベントハンドラ関数がテキスト行"Application shutdown"をファイルlog.txtに書き込みます。

情報

open()関数では、mode="a"は「追記」を意味するため、そのファイルにあるものの後にその行が追加され、以前の内容を上書きすることはありません。

ヒント

この場合、ファイルとやり取りする標準のPython open()関数を使用していることに注意してください。

したがって、I/O(入出力)が含まれ、ディスクへの書き込みを「待つ」必要があります。

しかし、open()asyncawaitを使用しません。

したがって、イベントハンドラ関数はasync defではなく標準のdefで宣言します。

startupshutdown をまとめて

*起動*と*シャットダウン*のロジックは関連している可能性が高く、何かを開始してから終了したり、リソースを取得してから解放したりする場合があります。

ロジックや変数を共有しない個別の関数でそれを行うことは、グローバル変数や同様のトリックに値を格納する必要があるため、より困難です。

そのため、上記で説明したように、代わりにlifespanを使用することが推奨されるようになりました。

技術的な詳細

好奇心旺盛なオタクのための技術的な詳細です。🤓

内部的には、ASGI技術仕様において、これはLifespanプロトコルの一部であり、startupshutdownと呼ばれるイベントを定義しています。

情報

Starletteのlifespanハンドラについては、StarletteのLifespanドキュメントで詳しく読むことができます。

コードの他の領域で使用できるライフスパンの状態を処理する方法も含まれています。

サブアプリケーション

🚨 これらのライフスパンイベント(起動とシャットダウン)は、メインアプリケーションに対してのみ実行され、サブアプリケーション - マウントに対しては実行されないことに注意してください。