二重処理は、こうして防ぐ — キュー可視性タイムアウトとリトライの整合、そして冪等キー
「同じ注文が2件できる」はなぜ起きるのか。可視性タイムアウトとリトライ時間の関係から、防ぎ方までを設計として整理する。
STRUCTURE構成:キューの先で「重い同期処理」を呼ぶ
非同期処理の定番構成は、メッセージキュー → コンシューマ → その中で実処理、という並びだ。Vegas でも、注文確定のような重要な副作用を伴う処理を、この形で流している。問題はコンシューマの中身にある。ここで呼ぶのが負荷によって時間の伸びる「重い同期処理」で、しかも一時的な失敗に備えてリトライまで行う場合、処理全体の所要時間は状況次第で大きく揺れる。
- キューはメッセージを取り出したワーカーに、一定時間だけ「他から見えない」状態を与える。これが可視性タイムアウトだ。
- ワーカーは処理を終えてからメッセージを明示的に削除する。削除前にタイムアウトが切れると、メッセージは再び可視化される。
FAILURE失敗機序:可視性 < 処理時間 で再配信が起きる
事故は「時間の逆転」で起きる。可視性タイムアウトが、リトライを含む実処理の最大所要時間より短いとどうなるか。ワーカーがまだ処理を続けている最中にタイムアウトが切れ、メッセージが再可視化され、別のワーカーへ再配信される。二台目は、一台目がまだ完了していないことを知らないまま、同じ入力で同じ処理を始める。結果として同じ副作用が二度実行され、注文が2件生成される、といった重複が生まれる。
FIX対策:時間の整合、冪等キー、部分失敗の報告
防ぎ方は三段構えで考えると漏れがない。
- 時間の整合:可視性タイムアウト ≥ 最大処理時間(リトライ上限を含む)に設定する。リトライ回数と各試行の上限から最悪値を見積もり、その値以上を可視性に割り当てる。
- 冪等キー:リクエスト固有のIDを処理側で受け取り、「同じキーなら作らない」重複排除を入れる。再配信が起きても副作用は一度きりになる。
- 部分失敗の報告:バッチ処理では成功と失敗を切り分けて報告し、成功済みのメッセージまで巻き添えで再処理させない。
CODE冪等キーによる重複排除
時間の整合は前提だが、それだけに頼るのは危うい。処理時間は見積もりを超えうるし、外部要因で伸びることもある。最後の砦として、処理側で冪等キーによる重複排除を必ず入れておく。
def handle(message):
key = message.idempotency_key # リクエスト固有のID
if already_processed(key): # 既に処理済みなら
return # 何もせず終了(副作用ゼロ)
result = process(message) # 重い同期処理を実行
mark_processed(key, result) # キーを記録し二度目を弾く
return resultLESSON教訓:整合で見る、冪等性は前提
リトライ設計とキュー設定は、別々の設定値としてではなく「時間の整合」として一体で見るべきだ。片方を変えたらもう片方の最悪値を必ず問い直す。そして冪等性は「あれば安心」の保険ではなく、自動処理やAI連携が同じ入力を何度でも投げうる時代の前提条件だ。副作用を持つ処理には、最初から冪等キーを設計に織り込んでおきたい。
可視性タイムアウトは「リトライ込みの最大処理時間以上」に。その上で冪等キーによる重複排除を処理側に必ず置く。時間の整合と冪等性、この二つが揃って初めて二重処理は防げる。