失敗したお菓子の注文を消さない
一言でいうと
隔離は、失敗を成功に変えることではありません。失敗の根拠を残して自動の繰り返しを止めたあと、人が許可した再投入だけを、新しいバジェットで始めます。
なぜ必要なのか
お菓子の注文1件に、ずっと誤った内容が入っています。同じ仕事をすぐに繰り返すと、あとの正常な注文を処理する席まで奪います。かといって、行を削除してしまうと、注文者が受け付けた仕事がどこへ行ったのか、説明できません。今回は、業務をdoneとdeadに分け、なぜ止まったのかというreasonを保存します。deadが増えるのは、正常な完了率が上がったのではなく、調査が必要な仕事が増えたということです。
前のモジュールのoutboxの送信ループは、最初の失敗で止まりました。今回のキューは、互いに独立したお菓子の注文を扱うので、予約時刻が過ぎたほかの仕事を処理できます。この選択が、すべてのイベントストリームに合うわけではありません。銀行の残高計算や文書の編集のように、順序を変えると意味が変わる作業に、同じように適用してはいけません。失敗した項目を別に隔離することが、順序の保証にどう影響するかを、先に決める必要があります。
どう動くのか
finishは、送信アダプターの結果を、ok、retry、permanentで受け取ります。okはdone、permanentはdeadです。retryでも、最大試行に達していたらexhausted、予約しようとする時刻が元の締め切りに達していたらdeadlineを理由にdeadになります。まだバジェットが残っていれば、pendingと次のavailable_atを保存します。どの経路でも、業務IDや数量は消さず、ownerとlease_untilだけを解除します。
run_onceは、ネットワークアダプターが正確なTrueを返したときにだけ、okに分類します。1や文字列のokは、成功とはみなしません。一時的な失敗の例外はRetryable、永続的な失敗はPermanentと明示します。予想外の例外やBadAckは、呼び出し側に伝え、まだ確定していないleased状態を残します。次のclaimでリースの期限切れを確認して回収するので、失敗を隠す代わりに、復旧の根拠が維持されます。
隔離された業務をもう一度実行するには、redriveを明示的に呼び出します。pendingやleasedやdoneには許可しません。運用者が残すnoteは、診断チケットや修正作業を指す識別子です。ラボでは、ASCIIの識別子だけを受け付けて、元のエラーメッセージや個人情報が、うっかり監査記録に入らないようにします。実際のサービスなら、誰が承認したかとアクセス権限も必要ですが、このローカル関数は、その認証の仕組みを実装しません。
再投入は、監査の行の追加と業務状態の更新を、1つのトランザクションで行います。redrivesには、id、以前のtoken、at_ms、noteを記録します。jobsのattemptsは0に戻して新しいdeadlineを与えますが、tokenは戻しません。前のAのtoken=1が生きているのに、再投入のあとでまたtoken=1を使うと、古い完了メッセージが新しい作業に通用してしまうおそれがあるからです。回数バジェットの新しい開始と、作業の所有の世代の再利用は、別の問題です。
現場での姿
DLQという名前が付いていても、運用者が実際に見る人がいなければ、失敗を別の場所へ移しただけです。隔離の数、理由別の比率、最も古い隔離業務、最後の調査時刻と再投入の結果を、観測する必要があります。アラートには、業務の本文をそのまま入れるのではなく、必要最小限の識別子を使います。一時的な障害で多くの業務が隔離されたなら、一度にすべてを戻して再び負荷を作る代わりに、復旧の速度も制限する必要があります。
AWSのDLQの案内は、処理できなかったメッセージを分離して、原因を分析し、再投入する流れを説明しています。また、正確な順序が必要な作業では、隔離によって順序が崩れる影響を警告しています。今回のラボのdeadの行は、別のAWSのキューではなく、ローカルのjobsの状態で、保管の期限切れやサービスの権限ポリシーも違います。言葉が同じだからといって、その製品の機能や保証まで備えているとは言いません。
総合テストでは、配達員Aが実際のHTTPリクエストを送り、受信サーバーがIDと在庫の効果をコミットしたあとで、Aを終了コード73で終わらせます。finallyの後始末やfinishは実行されません。別のプロセスBが、期限切れの境界でtoken=2として仕事を回収し、同じIDをもう一度送ります。HTTPリクエストは実際に2回来ますが、受信側の在庫は7しか増えず、キューはdoneになります。遅れて来たtoken=1の完了要求は、Falseでなければなりません。
この結果を、外部決済のちょうど1回の処理へ拡大解釈しません。受信サーバーが同一IDをアトミックに記録する実装を提供しているので、そのサーバーの効果が1回なのです。外部のプロバイダーが同じ契約をサポートしていないなら、照会・調整・手動の確認が必要になることがあります。キューをうまく作ったという事実が、外部システムの状態までアトミックに束ねてくれるわけではありません。
再起動も、範囲をはっきりさせます。今回の検証は、同じローカルディスクでのプロセスの終了と再開です。ディスクが消えたりDBが壊れたりしたときに復旧するバックアップ、複数のホストの間の合意、リースの更新、送信のキャンセル、グローバルなリクエスト速度の制限は、実装しません。前のスナップショットのラボのWAL設定は持ち込まず、短いキューの書き込みに合ったrollback journalで、別のDBを作ります。読み取りの要求と書き込みの要求に応じて、保存方式を選ぶ練習です。
次のラボですること
ステップ8の総合ラボで、リトライ関数・永続的な受付・先取り・完了・再投入・1回の実行を完成させます。実際の2人の配達員の先取りの競合と、HTTP受信後の終了を検査し、正解だけでなく、期限切れの境界・トークンの初期化・緩いACKのような誤答も拒否されるかを見ます。セッションは110分の予定なので、途中で時間を延長し、必要なコードを別に保管します。
参考: SQSのDLQの設計、SQLiteのエラーとロールバック。このラボの再投入は、人がすでに承認したローカルの動作だという前提です。