リクエストが失敗してもリソースを閉じる
目標
起動・終了・例外の経路を分離して、FastAPIのlifespanを実際に実行します。
なぜ重要なのか
テストは通ったのに、本番の再起動のときに接続が残りました。TestClientをcontext managerなしで使ったため、起動と終了のコードが実行されていなかったのです。正常なレスポンスを1回見るだけのテストでは、アプリがリソースをいつ開いて、いつ閉じるのかわかりません。ここでは、外部の接続の代わりに、イベントを記録する小さなリソースで、ライフサイクルを観察します。
ステップ
/root/work/fa-resource-lifecycle-lab/service.pyで、new_resource()は、{open:False, events:[]}の新しい辞書で、呼び出し同士でeventsを共有しません。
最初に一度だけ準備してください。既存のファイルは上書きしません。
mkdir -p /root/work/fa-resource-lifecycle-lab
test -e /root/work/fa-resource-lifecycle-lab/service.py || cp /opt/fixtures/ten_labs/fa-resource-lifecycle-lab/service.py /root/work/fa-resource-lifecycle-lab/service.py
cd /root/work/fa-resource-lifecycle-lab
-
/root/work/fa-resource-lifecycle-lab/service.pyで、start(resource)は、すでに開いていればValueError、そうでなければopen=Trueに変えて、eventsに'open'を追加します。 -
/root/work/fa-resource-lifecycle-lab/service.pyで、stop(resource)は、開いているときだけopen=Falseに変えて、'close'をeventsに追加します。すでに閉じていれば、そのままにします。 -
/root/work/fa-resource-lifecycle-lab/service.pyで、read(resource)は、閉じていればRuntimeError、開いていれば{ready:True}を返します。 -
/root/work/fa-resource-lifecycle-lab/service.pyで、scope(resource)は、contextmanagerです。入るときにstart、ブロックの中にはresourceをyieldし、ブロックの成功でも失敗でも、stopで閉じます。ブロックの例外は伝播します。 -
/root/work/fa-resource-lifecycle-lab/service.pyで、lifespan_for(resource)は、asynccontextmanagerの関数lifespan(app)を返します。scope(resource)の中でapp.state.resourceを設定して、yieldします。 -
/root/work/fa-resource-lifecycle-lab/service.pyで、create_app(resource)は、lifespan_forを使います。GET /readyは、app.state.resourceをreadした結果を返します。contextの終了時に、リソースを閉じなければなりません。 -
/root/work/fa-resource-lifecycle-lab/service.pyで、exercise(resource, fail=False)は、with TestClient(create_app(resource))の中で、GET /readyを呼び出します。fail=Trueなら、その中でRuntimeErrorを出し、そうでなければレスポンスのJSONを返します。どちらの場合も、リソースが閉じられていなければなりません。
参考
- インターネットやパッケージのインストールなしで、既存のlab-dev環境で行います。
- 各ステップは、45秒の採点バジェットの中で実行されます。実際のsleepやネットワーク呼び出しを追加しないでください。
- 採点は、提出されたモジュールを新しく読み込んで、独立した入力と一時的なDBで検査します。期待値を定数で返す代わりに、契約を実装してください。
- FastAPI公式ドキュメント・pytest公式ドキュメント・Python sqlite3
- 限界: 学習用のリソースの辞書は、実際のDB接続プールの代わりになる、観測のための仕組みです。本番では、部分的な初期化の失敗、接続プールの並行性、キャンセルの処理と終了のタイムアウトも、設計する必要があります。イベントの文字列をレポートに書くのではなく、学習者のコードが実行して変えたオブジェクトの状態を検査します。
リソースの状態を独立させる
/root/work/fa-resource-lifecycle-lab/service.pyで、new_resource()は、{open:False, events:[]}の新しい辞書で、呼び出し同士でeventsを共有しません。
最初に一度だけ準備してください。既存のファイルは上書きしません。
mkdir -p /root/work/fa-resource-lifecycle-lab
test -e /root/work/fa-resource-lifecycle-lab/service.py || cp /opt/fixtures/ten_labs/fa-resource-lifecycle-lab/service.py /root/work/fa-resource-lifecycle-lab/service.py
cd /root/work/fa-resource-lifecycle-lab
変更可能なリストを、グローバルやデフォルト引数で共有しません。
保存したら、bash /opt/lab/checks/fa-resource-lifecycle-lab/01-contract.shで確認してください。
重複した開始を拒否する
/root/work/fa-resource-lifecycle-lab/service.pyで、start(resource)は、すでに開いていればValueError、そうでなければopen=Trueに変えて、eventsに'open'を追加します。
2回開始して、リソースを1つ失ってしまう動作を拒否します。
保存したら、bash /opt/lab/checks/fa-resource-lifecycle-lab/02-contract.shで確認してください。
終了を冪等にする
/root/work/fa-resource-lifecycle-lab/service.pyで、stop(resource)は、開いているときだけopen=Falseに変えて、'close'をeventsに追加します。すでに閉じていれば、そのままにします。
複数の後始末の経路が重なっても、重複したcloseのイベントが出てはいけません。
保存したら、bash /opt/lab/checks/fa-resource-lifecycle-lab/03-contract.shで確認してください。
閉じたリソースの使用を防ぐ
/root/work/fa-resource-lifecycle-lab/service.pyで、read(resource)は、閉じていればRuntimeError、開いていれば{ready:True}を返します。
準備状態と、オブジェクトの存在は、別です。オブジェクトがあっても、閉じていることがあります。
保存したら、bash /opt/lab/checks/fa-resource-lifecycle-lab/04-contract.shで確認してください。
例外の経路にfinallyを置く
/root/work/fa-resource-lifecycle-lab/service.pyで、scope(resource)は、contextmanagerです。入るときにstart、ブロックの中にはresourceをyieldし、ブロックの成功でも失敗でも、stopで閉じます。ブロックの例外は伝播します。
yieldのあとだけにcloseを書くと、例外が出た場合、その行に到達できません。
保存したら、bash /opt/lab/checks/fa-resource-lifecycle-lab/05-contract.shで確認してください。
アプリのライフサイクルとリソースをつなぐ
/root/work/fa-resource-lifecycle-lab/service.pyで、lifespan_for(resource)は、asynccontextmanagerの関数lifespan(app)を返します。scope(resource)の中でapp.state.resourceを設定して、yieldします。
lifespanの関数自体を呼び出すのではなく、FastAPIのコンストラクターに渡します。
保存したら、bash /opt/lab/checks/fa-resource-lifecycle-lab/06-contract.shで確認してください。
準備状態を実際のリクエストで読む
/root/work/fa-resource-lifecycle-lab/service.pyで、create_app(resource)は、lifespan_forを使います。GET /readyは、app.state.resourceをreadした結果を返します。contextの終了時に、リソースを閉じなければなりません。
with TestClientを使ってはじめて、lifespanの起動と終了を、どちらも実行します。
保存したら、bash /opt/lab/checks/fa-resource-lifecycle-lab/07-contract.shで確認してください。
リクエストのあとの失敗も後始末する
/root/work/fa-resource-lifecycle-lab/service.pyで、exercise(resource, fail=False)は、with TestClient(create_app(resource))の中で、GET /readyを呼び出します。fail=Trueなら、その中でRuntimeErrorを出し、そうでなければレスポンスのJSONを返します。どちらの場合も、リソースが閉じられていなければなりません。
正常な経路と例外の経路を、同じ後始末の構造にまとめれば、抜けている終了の経路を減らせます。
保存したら、bash /opt/lab/checks/fa-resource-lifecycle-lab/08-contract.shで確認してください。