TT Lab
はじめる
学ぶ 学習パス コース

FastAPI — 型がそのまま契約だ

リクエストが失敗してもリソースを閉じる

TT Labで続きを見る

目標

起動・終了・例外の経路を分離して、FastAPIのlifespanを実際に実行します。

なぜ重要なのか

テストは通ったのに、本番の再起動のときに接続が残りました。TestClientをcontext managerなしで使ったため、起動と終了のコードが実行されていなかったのです。正常なレスポンスを1回見るだけのテストでは、アプリがリソースをいつ開いて、いつ閉じるのかわかりません。ここでは、外部の接続の代わりに、イベントを記録する小さなリソースで、ライフサイクルを観察します。

ステップ

  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
  1. /root/work/fa-resource-lifecycle-lab/service.pyで、start(resource)は、すでに開いていればValueError、そうでなければopen=Trueに変えて、eventsに'open'を追加します。

  2. /root/work/fa-resource-lifecycle-lab/service.pyで、stop(resource)は、開いているときだけopen=Falseに変えて、'close'をeventsに追加します。すでに閉じていれば、そのままにします。

  3. /root/work/fa-resource-lifecycle-lab/service.pyで、read(resource)は、閉じていればRuntimeError、開いていれば{ready:True}を返します。

  4. /root/work/fa-resource-lifecycle-lab/service.pyで、scope(resource)は、contextmanagerです。入るときにstart、ブロックの中にはresourceをyieldし、ブロックの成功でも失敗でも、stopで閉じます。ブロックの例外は伝播します。

  5. /root/work/fa-resource-lifecycle-lab/service.pyで、lifespan_for(resource)は、asynccontextmanagerの関数lifespan(app)を返します。scope(resource)の中でapp.state.resourceを設定して、yieldします。

  6. /root/work/fa-resource-lifecycle-lab/service.pyで、create_app(resource)は、lifespan_forを使います。GET /readyは、app.state.resourceをreadした結果を返します。contextの終了時に、リソースを閉じなければなりません。

  7. /root/work/fa-resource-lifecycle-lab/service.pyで、exercise(resource, fail=False)は、with TestClient(create_app(resource))の中で、GET /readyを呼び出します。fail=Trueなら、その中でRuntimeErrorを出し、そうでなければレスポンスのJSONを返します。どちらの場合も、リソースが閉じられていなければなりません。

参考

リソースの状態を独立させる

/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で確認してください。