ライフスパンイベント¶
アプリケーションが起動する前に実行されるべきロジック (コード) を定義できます。これは、アプリケーションがリクエストの受信を開始する前に、このコードが一度だけ実行されることを意味します。
同様に、アプリケーションがシャットダウンしている時に実行されるべきロジック (コード) を定義できます。この場合、このコードは、おそらく多くのリクエストを処理した後、一度だけ実行されます。
このコードは、アプリケーションがリクエストの受け入れを開始する前に実行され、リクエストの処理を終了した直後に実行されるため、アプリケーションのライフスパン全体をカバーします (「ライフスパン」という言葉は後で重要になります 😉)。
これは、アプリケーション全体で使用する必要があり、リクエスト間で共有され、かつ/または後でクリーンアップする必要があるリソースを設定するのに非常に役立ちます。例えば、データベース接続プールや、共有の機械学習モデルのロードなどです。
ユースケース¶
まずは例となるユースケースから始め、その後、これをどう解決するかを見ていきましょう。
リクエストを処理するために使用したい機械学習モデルがいくつかあると想像してください。🤖
同じモデルがリクエスト間で共有されるため、リクエストごと、ユーザーごとなどに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
パラメータを提供した場合、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()
はasync
とawait
を使用しません。
そのため、イベントハンドラー関数はasync def
ではなく、標準のdef
で宣言します。
startup
と shutdown
をまとめて¶
起動とシャットダウンのロジックが関連している可能性が非常に高く、何かを開始して終了したり、リソースを取得して解放したりしたいと思うかもしれません。
ロジックや変数を共有しない個別の関数でそれを行うことは、グローバル変数や同様のトリックに値を保存する必要があるため、より困難です。
そのため、代わりに上記のlifespan
を使用することが現在推奨されています。
技術的な詳細¶
好奇心旺盛なオタクのための技術的な詳細です。🤓
ASGIの技術仕様の下では、これはライフスパンプロトコルの一部であり、startup
とshutdown
というイベントが定義されています。
情報
Starletteのlifespan
ハンドラーの詳細については、Starletteのライフスパンに関するドキュメントをご覧ください。
これには、コードの他の領域で使用できるライフスパンの状態を処理する方法も含まれます。
サブアプリケーション¶
🚨 これらのライフスパンイベント (起動とシャットダウン) は、メインアプリケーションのみで実行され、サブアプリケーション - マウントでは実行されないことに注意してください。