コンテンツへスキップ

ライフスパンイベント

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

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

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

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

ユースケース

まずは例となるユースケースから始め、その後、これをどう解決するかを見ていきましょう。

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

同じモデルがリクエスト間で共有されるため、リクエストごと、ユーザーごとなどに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}

代替イベント (非推奨)

警告

起動シャットダウンを処理する推奨される方法は、上記の通りFastAPIアプリのlifespanパラメータを使用することです。lifespanパラメータを提供した場合、startupshutdownイベントハンドラは呼び出されなくなります。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の技術仕様の下では、これはライフスパンプロトコルの一部であり、startupshutdownというイベントが定義されています。

情報

Starletteのlifespanハンドラーの詳細については、Starletteのライフスパンに関するドキュメントをご覧ください。

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

サブアプリケーション

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