止まらないキューと、糸くずを並べる男
雑居ビルの3階に着いたのは、朝の10時過ぎだった。
エレベーターの扉が開く前から、どことなく来てよかったのかどうか半信半疑だった。先輩から聞いた話では「怪しいが腕は確か」だという。今日の段階では「腕が確か」の方に全力で賭けるしかない状況だった。
扉には「レガシー・コード・インベスティゲーション」と書かれたプレートがかかっていた。先輩は嘘をついていなかった。本当に怪しかった。
ノックして、返事を待たずにドアを開けた。
室内はデスクトップPCの排熱でじんわりと暑く、飲みかけのエナジードリンクの缶が三本並んでいた。そのうち一本はキャップが半開きのままだった。机の向こうに座っていた男は、缶にも僕にも目を向けず、机の上に糸くずを一本ずつ、等間隔で並べていた。
「あの——」
「今日は出社前に直行したのかね、ワトソン君」
顔も上げずに言った。
「……えっ?」
「問い合わせは増え続けているが、ログを見る人間が誰もいないから飛び込んだ。違うかね」
まだ何も話していない。なのに「問い合わせ」という言葉が出た。
「どこで——」と言いかけたとき、男はようやくこちらを見た。
「ロックだ。座りたまえ、ワトソン君。君のキューが止まっているのは7時間と14分前からだ」
糸くずはまだ並べ続けていた。
「……ロックさん。名前、今教えてもらいました。それと、ワトソンは僕の名前じゃないんですが」
「そうかね。では用件を聞こう」
返答がなかった。それより、「7時間14分前」の方が気になった。まだ何も話していないのに、どうしてそれが分かるのか。
まあ、今はそれどころじゃない。
「注文確認メールの送信キューが詰まってます。826件、止まってます。問い合わせが100件を超えてきていて……先週のデータ移行で何か壊れたと思うんですが、どこが悪いのかは——」
「止まれ」
「は?」
「一度に全部話そうとするな。まず確認する。コンシューマは正常に起動しているか」
「はい、動いてます」
「最後に成功した処理はいつだ」
「ログを——」スマートフォンで確認した。「7時間前です」
「その7時間で、コンシューマは何回処理を試みた」
「……4800回以上です」
ロックさんは机の上の糸くずを一本だけ、指先でつまんだ。
「4800回試みて、一件も成功していない。——それがどういう意味か、分かるかね」
「失敗し続けた、ということです」
「もう少し正確に言いたまえ」
正確に。分かっているようで、分かっていなかった。
826通の足止め——1通の決定論
「コードを見せたまえ」
ラップトップを取り出して、テーブルに置いた。キューのコンシューマのコードを開く。
| |
ロックさんは画面を一度だけ見て、すぐに視線を上げた。
「先週の移行で、SKUコードに特殊文字が混ざったのだろう。パーセントエンコードが展開された状態のまま入った」
「そうです。%2F が含まれたSKUの注文データが何件か——」
「そのメッセージはJSONシリアライズで毎回例外を投げる。ここを見たまえ」
指先が、push @$queue, $item の行を指した。
「……はい」
「失敗したメッセージを、キューの末尾に戻している。次の処理で再び取り出され、再び失敗し、再び末尾に戻る。——SKUコードの特殊文字が消えることはあるかね」
「ありません。コードを直さない限り」
「つまり、このメッセージは何回処理しても、同じ例外を投げ続ける。決定論的に失敗する。そういうエラーを非一時的エラーと呼ぶ」
非一時的エラー。
「リトライは、治る見込みのある問題にしか効かないんですか」
「ネットワークの瞬断、DBの一時的な過負荷——それは数回の猶予を与えれば回復する可能性がある。しかし特殊文字が混ざったペイロードは、君のコードが直らない限り何回試みても同じ結果を返す。リトライは一時的な傷に貼る絆創膏だ。骨折には効かない」
「じゃあ、このメッセージを……どうすればいいんですか。捨てますか」
ロックさんは、机の上に置いていた小さな紙箱を指先でこちらへ滑らせた。
「捨てるのか。証拠を壊す探偵がいるかね、ワトソン君」
「……比喩ですか、それ」
「隔離だ。ゴミ箱ではない。処理できなかったメッセージを、調査できる場所へ移す。失敗の理由とともに。後で読めるように。そして、問題を修正した後に、もう一度処理できるように」
隔離、調査、再処理。
僕はメモに三つだけ書いた。
隔離室の設計——差し戻しではなく、保全する
「まず、メッセージ自身に何回試みたかを持たせる」
| |
「attempt_count をメッセージ側に持たせるのは——今まで何回失敗したかを、処理系ではなくメッセージ自身が知っているべきだからです」
「その通りだ」
次に隔離先を作る。
| |
「on_dead_letter は何ですか」
ロックさんは紙箱の蓋を指でたたいた。
「通報口だ。メッセージが隔離室に入った瞬間に呼ばれる。通知先は君が決める。Slackでも、監視基盤でも」
「——隔離しても、誰かが気づかなければ意味がない」
「ようやく先が見えてきたな、ワトソン君」
先が見えてきた、というより、怖くなってきた。もしこのDLQを設定せずに運用を続けていたら、826件だけでは済まなかった。隔離もなく、通知もなく、メッセージは音もなく消えていったはずだ。
最後に処理系を作る。
| |
「max_attempts を3にしたのは、根拠があるんですか」
「3は出発点だ。一時的なエラーが回復するのに十分な猶予が必要で、かつ非一時的なエラーが永遠に残らないために上限が必要だ。0は論外——それは隔離ではなく即廃棄になる」
「0に設定したら、一回でも失敗したら全部DLQに……」
「そういうことだ。一時的なネットワークエラーまで隔離することになる。要件によって2でも5でも変える。しかし、決して無制限にはしない」
「なぜDLQにメッセージが積まれたとき、自動で知らせる仕組みが必要なんですか。積まれたときにログを見ればいいんじゃないですか」
「今日の826件は、ログを見ていたから発覚したのか」
違う。問い合わせが来たから気づいた。
「……先に通知があれば、7時間前に対処できていた」
「だからon_dead_letterがある。DLQは隔離するだけでは不完全だ。隔離した事実を知らせる仕組みが要る。調べずに捨てれば、次の826件を防ぐ手がかりも一緒に捨てる」
826通が動き出す
スマートフォンが鳴った。開発チームから「SKUコードの処理修正が入りました」という連絡だった。
「——修正が入りました。DLQの826件、どうやって元に戻しますか」
ロックさんはredriveというメソッドのコードを示した。
| |
「attempt_countをゼロにリセットして返す。修復された証拠を、再審理のために送り返す——ということですね」
「そうだ。DLQは一方通行の廃棄場ではない。問題が修正された後に戻ってくる経路がある」
redrive を呼ぶと、826件のメッセージが attempt_count = 0 の状態で戻ってきた。それを一件ずつ enqueue に流した。
| |
ラップトップの監視画面を開いた。キューのメッセージ件数が、少しずつ減り始めた。
826 → 801 → 774 → ……
「動いてます」
ロックさんは返事をしなかった。また糸くずを並べていた。
「……さっきの糸くず、何のためだったんですか」
「さあ」
「DLQの比喩、でしたか」
「こちらへ来たとき、君は扉のドアノブを握っては離すを3回繰り返した。私はその動作を3回見た。4回目は開けた」
「……それ、max_attemptsの実演ですか」
「どうだろうな」
数字が減り続けていた。399 → 352 → ……
「ありがとうございました」
ロックさんは、糸くずを一本だけ小さな紙箱に入れた。
「ワトソン君。最初のメッセージが隔離室に入ったのは、何時間前だ」
ログを確認した。「7時間14分前です」
「では君のボーナス申請は7時間14分後に受理されるとしよう。それまでにon_dead_letterのアラート先を監視基盤に繋いでおきたまえ。次の事件を7時間14分以内に検知できるように」
「……報酬の概念が違います」
返事はなかった。手はすでに設定ファイルを開いていた。
探偵の調査報告書
| 容疑(アンチパターン) | 真実(パターン) | 証拠(効果) |
|---|---|---|
| 失敗時にメッセージをキューへ戻し続ける無限リトライ | Dead Letter Queue による隔離 | 非一時的エラーのメッセージがキューを占拠しなくなる |
attempt_count を持たないメッセージ | QueueMessage に attempt_count を持たせる | どのメッセージが何回試みられたかを追跡できる |
| 隔離後のメッセージを誰も知らない設計 | on_dead_letter アラートフック | DLQ にメッセージが積まれた瞬間に通知が届く |
| DLQ からの回復経路がない(廃棄のみ) | redrive で attempt_count リセット後に再投入 | 問題修正後、隔離メッセージを一括で再処理できる |
推理のステップ
- キューの失敗が「一時的エラー」か「非一時的エラー」かを見極める——後者は何回リトライしても同じ結果になる
QueueMessageにmessage_id・attempt_count・enqueued_atを持たせるDeadLetterQueueに隔離ストアとon_dead_letterアラートフックを用意するMessageProcessorのprocess_nextでmax_attempts超過時に DLQ へルーティングするon_dead_letterに通知処理を渡し、DLQ に積まれた瞬間にアラートを出す- 問題修正後は
redriveを呼び、attempt_countをリセットしたメッセージを元キューに返す
ロックより
リトライは一時的な傷に貼る絆創膏だ。骨折に絆創膏を貼り続けた結果が、今日の826件だった。
隔離とは見捨てることではない。後で読める形で保存することだ。失敗の理由とともに、調べられる場所へ移す。調べずに捨てれば、次の826件を防ぐ手がかりも一緒に捨てる。
DLQ を設定した今日から、君のキューは止まらない。止まるとしたら、それは次の事件の始まりだ。
