一覧SE-09-3 · 保守性と性能の改善

タイムアウトとキャンセル

待ち時間の予算がどの範囲に適用され、期限切れや取消しがどこへ伝わるかを確認します。

説明

処理が複数の待機を順に行う場合、各呼出しへ同じ待ち時間を与えても、要求全体の期限を守れるとは限りません。また、期限切れを成功扱いに変えたり、取消しから保護した処理が後で完了したりする場合があります。

Pythonのasyncioで、DB相当の待機0.02秒と後続の待機0.5秒を実行します。要求全体の予算は0.25秒です。実際の待機と取消しを使い、個別のtimeout、全体のtimeout、例外を握り潰す応答、shieldで続く処理を順に確認します。

待ち時間の予算がどの範囲に適用され、期限切れや取消しがどこへ伝わるかを確認します。

1期限と失敗時の契約

待ち時間と応答の契約を読みます。asyncioのawaitは、その処理の完了を待ちつつ他の非同期処理へ実行を渡せる箇所です。

cat contract.txt全体予算と期限切れ時の応答を確認する

各待機の上限2秒は、要求全体の予算0.25秒より大きい値です。どこに期限を設けるかによって、同じ待機列でも結果が変わります。

2個別の上限だけでは全体を超える

各処理へ2秒のtimeoutを与えて、順に待ちます。

python3 deadline.py per-call個別のtimeoutだけで連続処理する

両方とも完了しますが、budget_exceededはTrueです。単調時計で測った全体時間が0.25秒を超えています。個々の処理が自分の上限内で終わっても、全体の期限は別に守る必要があります。

3全体の期限を共有する

今度は二つの待機を、同じasyncio.timeout(0.25)の内側で実行します。

python3 deadline.py shared一つの期限を二つの待機で共有する

DBは完了し、後続処理はremote.cancelledとなり、全体はtimeoutです。後続へ新たな0.25秒を与えるのではなく、先行処理で消費した時間を含む同じ期限で扱っています。

4期限内の処理は完了する

後続の待機を0.01秒にした同じ構造で実行します。

python3 deadline.py fast短い後続処理を同じ期限内で実行する

DBと後続の両方がdoneとなり、statusはokです。期限を設けることは常に失敗させることではなく、許容時間内の完了を成功として扱うことです。

5期限切れを成功応答に変える問題

request.pyはhandlerを通して要求を処理します。現在のhandlerはTimeoutErrorを捕まえ、status=200でfallbackを返します。

python3 request.py期限切れを受ける応答処理まで実行する

内部ではremote.cancelledですが、呼出し側には成功扱いの200が返っています。この教材の契約は期限切れ時に504なので、例外の扱いが契約を変えてしまっています。

6応答契約を試験で確認

期限切れ時のstatusと、後続が取消された記録を検査します。

python3 run_tests.py test_handler.Contract期限切れ時の応答を検査する

200と504の不一致でFAILになります。処理内部でtimeoutが起きたことだけでなく、呼出し側へどう通知されるかも確認する必要があります。

7期限切れを失敗として返す

handlerの修正案を反映し、同じ試験と要求を実行します。

cp handler-fixed.py handler.pyTimeoutErrorを504へ対応させる python3 run_tests.py test_handler.Contract同じ契約で再試験する python3 request.py呼出し側が受け取る応答を読む

試験が成功し、status=504とdeadline exceededが返ります。処理の取消しと、呼出し側への失敗通知の両方が契約に一致しました。

8呼出し側の期限後も続く処理

shieldを使った例では、呼出し側の取消しから後続タスクを保護します。呼出し側の期限後、残ったタスクの終了まで追います。

python3 deadline.py shield取消しから保護されたタスクの完了を追う

caller.timeoutの後にremote.doneがあり、worker_finished_after_timeoutはTrueです。呼出し側が待つのをやめても、保護した処理は完了します。期限切れを、処理の中断や副作用の取消しと同一視できません。

まとめ

個々のtimeoutだけでは、複数処理を合わせた期限を守れるとは限りません。

全体の期限と、期限切れを呼出し側へ伝える契約を両方確認します。

取消しから保護されたタスクは期限後も完了できます。待機の終了と処理・副作用の停止は別です。

操作履歴と解説を振り返れます。

シェル / Python 3

1
cat contract.txt
要求全体の予算は0.25秒。DBは0.02秒、後続処理は0.5秒待つ例を使う。
個々の呼出しに2秒を与えても、要求全体の0.25秒は守れない。
全体の期限を超えたら呼出し側へstatus=504を返す契約。
正常に取れた値のようにstatus=200で代替値を返さない。
2
python3 deadline.py per-call
{"budget_exceeded": true, "events": ["db.start", "db.done", "remote.start", "remote.done"], "status": "ok"}
3
python3 deadline.py shared
{"events": ["db.start", "db.done", "remote.start", "remote.cancelled"], "status": "timeout"}
4
python3 deadline.py fast
{"events": ["db.start", "db.done", "remote.start", "remote.done"], "status": "ok"}
5
python3 request.py
{"status": 200, "value": "fallback"}
['db.start', 'db.done', 'remote.start', 'remote.cancelled']
6
python3 run_tests.py test_handler.Contract
FAIL test_handler.Contract.test_timeout_status: 200 != 504
tests=1 failures=1 errors=0
7
cp handler-fixed.py handler.py python3 run_tests.py test_handler.Contract python3 request.py
PASS test_handler.Contract.test_timeout_status
tests=1 failures=0 errors=0
{"error": "deadline exceeded", "status": 504}
['db.start', 'db.done', 'remote.start', 'remote.cancelled']
8
python3 deadline.py shield
{"events": ["remote.start", "caller.timeout", "remote.done"], "worker_finished_after_timeout": true}

演習完了