遅れて戻った配達員の完了印を拒否する
一言でいうと
リースは、しばらく預かる権利です。先取りトークン(claim token)は、その権利が何回目のものかを区別します。古いワーカーは、生きて戻ってきても、新しいワーカーの記録を書き換えられてはいけません。
なぜ必要なのか
配達員Aがお菓子の注文を持っていったあと、止まってしまいました。キューから注文を完全に削除して、Aのメモリにだけ置いていたなら、注文も一緒に消えてしまいます。逆に、キューにそのまま見える状態にしておくと、Bも同時に持っていけます。そこで、leased状態を保存し、一定時間Aに預けたという印を置きます。時間が過ぎても完了できなければ、別のワーカーがもう一度持っていけます。
問題は、期限切れがAを終了させてくれるわけではないことです。Aがネットワークの応答を長く待っていただけなら、Bが新しく先取りしたあとで、Aも成功の応答を受け取れます。そのときAが、idだけを条件に完了印を付けると、Bの現在の作業を横取りしてしまいます。実行している人の名前だけを検査しても、十分ではありません。同じ名前のワーカーが再起動したときに、以前の実行と新しい実行を区別できないからです。
どう動くのか
jobsの行には、owner、lease_until、token、attemptsがあります。claimが成功したときに、attemptsとtokenをそれぞれ1ずつ上げます。ownerは説明できるワーカーの名前で、tokenは、その業務の先取りの世代を区別する、単調増加の番号です。1つのclaimの結果には、id・qty・owner・token・attempt・lease_untilが入ります。finishは、受け取った印と現在の行がすべて一致し、まだリースと業務の締め切りが残っているときにだけ、結果を反映します。
0ms A가 선점: token=1, lease_until=1000
1000ms B가 재선점: token=2
1001ms A가 token=1로 완료 요청 → False, 현재 상태 유지
境界は、now < lease_untilのときにだけ有効です。nowが期限切れの時刻と同じなら、すでに権利は終わっています。新しい先取りは、逆にlease_until <= nowの仕事を持っていけます。2つの条件を違うように解釈すると、同じ瞬間に2人が有効だと考えたり、誰も預かれないすきまができたりします。テストは、期限切れの前・ちょうど期限切れの時刻・期限切れのあとを、それぞれ入れます。
先取りは、照会と更新を1つの書き込みトランザクションで束ねます。先に可能な仕事を読んで、トランザクションをあとから始めると、AとBが同じpendingの行を読むことがあります。今回のキューは、SQLiteの短いBEGIN IMMEDIATEトランザクションで、選択とトークンの更新を直列化します。読み取り時点のスナップショットを長く維持する理由がない作業キューなので、rollback journalを使います。前のラボのWALスナップショットのエクスポートとは、求められる並行性の形が違います。
先取りをコミットしたあとは、ロックを解いてから送信します。DBトランザクションの中でネットワークの応答まで待つと、この仕事と関係のない新しい受付や、別のワーカーの先取りも塞いでしまいます。テストのsendコールバックの中で、別の接続に別の業務を実際に受け付けさせて、ロックが残っていないかを確認します。外部の作業をしない短いトランザクションという意図が、実際の実行で守られているかが、採点の対象です。
現場での姿
SQSのvisibility timeoutも、受け取ったメッセージを一定期間、ほかのコンシューマーから見えなくする、関連する概念です。ただし、その製品の配信保証と、今回のローカルSQLiteキューの契約は同じではありません。このラボは、SQSを実装も呼び出しもしません。公式の説明でも、visibility timeoutが重複配信の可能性を完全になくしてくれる保証ではないので、受信する業務の冪等性は、別に考える必要があります。
ここでの先取りトークンは、キューの中の古い完了印を拒否します。受信サーバーがトークンを検査する、外部リソースのfencingまで実装したものではありません。Aのリクエストがすでに受信サーバーに届いていたなら、キューがAのfinishを拒否しても、その外部の効果が元に戻るわけではありません。そのため、総合テストの受信サーバーは、業務IDと数量を一緒に記録し、同じIDの再送では効果を再び加算しません。キューの所有権と、受信の効果の境界が、それぞれ必要な理由です。
リースをとても長くすると、死んだワーカーの仕事を再び預けるまでの時間が長くなり、とても短くすると、まだ正常なワーカーの仕事を不必要に送り直します。今回の実装は、リースを更新するheartbeatを作りません。長い作業を運用するときは、処理時間、更新の周期、安全なキャンセル、受信の冪等性を、あわせて設計する必要があります。単にlease_msを大きく変えるだけでは、完結した解決策ではありません。
元の業務の締め切りより、リースが長くならないようにもします。例えば、deadline=100なのにnow=0、lease_ms=1000なら、実際のlease_untilは100です。キューに保存されたリースが、業務のバジェットと矛盾してはいけません。試行を使い切った期限切れの業務は、新しいトークンを与える前に隔離し、すでに有効なリースを持つ仕事を、繰り返しの照会だけで別のワーカーに渡すことはしません。
次の確認ですること
続くクイズでは、名前・業務ID・先取りの番号が、それぞれ何を区別するのかを判断します。あとの総合ラボで、実際の子プロセス2つを同時に出発させ、先取りの結果が1つだけかを見ます。完了の境界の失敗メッセージには、現在の結果と期待する結果が一緒に出てきて、同じワーカーの名前で再先取りしても、以前のトークンが拒否されるかを確認します。
参考: SQLiteのトランザクション、SQS visibility timeout。数値のトークンは、アクセス権限や認証の手段ではありません。