コンテンツにスキップ

ライフスパンイベント

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

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

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

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

ユースケース

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

リクエストを処理するために使用したい機械学習モデルがあるとしましょう。🤖

同じモデルがリクエスト間で共有されるため、リクエストごとに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などのリソースを解放する可能性があります。

ヒント

シャットダウンは、アプリケーションを停止するときに発生します。

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

ライフスパン関数

まず注目すべきは、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におけるコンテキストマネージャーは、例えば open() がコンテキストマネージャーとして使用できるなど、with ステートメントで使用できるものです。

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 パラメータを指定した場合、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の技術仕様において、これはライフスパンプロトコルの一部であり、startupshutdown というイベントを定義しています。

情報

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

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

サブアプリケーション

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