処理を後回しにしても、仕事を失わず二重反映を防ぐには?
まず図全体の流れを見て、細部は「原寸で読む」を開いて確認してください。原寸の図は横にスクロールできます。正常な流れの次に、失敗時の分岐を追いましょう。
非同期にする理由と守る条件
非同期処理は、仕事を受け付ける時間と実行する時間を切り離す設計です。長い文書の取り込み、会話の要約、音声変換などで、利用者を一つの接続に待たせずに済みます。ただしキューを挟むだけでは、受理した仕事が消える問題も、同じ仕事が二度実行される問題も解決しません。この章は独自の参照設計であり、Kumyuの運用実態ではありません。
先に不変条件を書きます。「受理を返したjobには追跡可能な記録がある」「同じ業務結果を重複反映しない」「終わらないjobを発見できる」の三つです。キューは作業予定の配送手段で、jobの現在状態の唯一の正本にはしません。
全体像:二重書き込みを一つの記録へ変える
原寸で読む(横にスクロール)
図1は独自の概念図です。 API からDBへの矢印は一つのコミット、relayからキューへの矢印は後続配送を示します。DBへのjob登録後に送信すると、その間のクラッシュで取りこぼします。先に送信すると、DBのロールバック後に実体のないjobが届きます。この二つの書き込みのずれを扱うのがAWSのoutboxパターンです。
この設計ではjobとoutboxを同じDBトランザクションで保存します。relayは未配送のoutboxを読み、キューへ送ります。送信に成功してから送信済みにするため、送信後の障害では再送があり得ます。取りこぼしの問題を、検出可能な重複へ置き換えたのです。
Amazon SQS標準キューの説明も重複配送に備えるよう求めます。ただし、すべてのキューが同じ保証を持つわけではありません。採用する製品について順序、再配送、保持期限を別々に確認します。
状態機械:リースは所有権の期限
原寸で読む(横にスクロール)
図2は、結果と重複排除IDの確定後にackする順序を示します。これに加え、jobの状態をqueued → leased → succeededとし、回復可能な失敗にはretry_waitを設けます。リースとは一定時間だけ処理を担当できる権利です。ワーカーを信じて永久に所有させず、heartbeatと期限で障害を回収します。期限が切れても古いプロセスが動いている可能性があるので、所有世代generationも必要になります。
{
"job_id": "job-example",
"state": "leased",
"attempt": 2,
"generation": 7,
"lease_until": "2026-10-23T03:05:00Z",
"input_version": 4
}
これは未実行例です。結果保存時は現在のgenerationが7かを確認し、job結果とsucceededを同時確定してからackします。ackとは配送完了の通知です。この順序がhappens-before、つまり後段が前段の完了を前提にできる関係を作ります。ackを先にすると、保存前のクラッシュで仕事を失います。
同じjobを再配送された場合は、job_idに対応する確定結果を確認します。すでに完了なら再計算せずackします。処理中なら所有権を奪わず、現在のリースを尊重します。単なる「処理済みID追加」と結果保存を別々に行うと、その間にクラッシュする穴が残ります。
障害:再試行で障害を増幅させない
原寸で読む(横にスクロール)
図3は結果コミット後でack前の障害を示します。これ以外の失敗地点も含め、回復先を分けます。結果コミット前なら再処理、コミット後でack前なら結果を再利用します。外部への送金や通知はDBトランザクションに入らないため、ツール実行側の冪等キーや照会による照合が必要です。
一時的な接続失敗には上限付き指数バックオフとjitterを使い、入力不正には再試行しません。AWSの再試行解説は、タイムアウトでも副作用が起きている可能性と、再試行が負荷を増やす問題を説明しています。本設計では再試行を管理する層を一つに決め、総試行回数と経過時間に予算を置きます。
上限到達後は隔離先へ移すだけで終わりません。失敗理由、入力バージョン、最終試行時刻、再投入の担当を記録します。原因を修正した後、同じ業務キーで再投入するか、新しい入力バージョンとして扱うかを決めます。黙って捨てればキューが空でも仕事は未完了のままです。
選択肢を比較する
| 選択 | 得られること | 引き受ける課題 |
|---|---|---|
| 同期処理 | 成否を同じ応答で返せる | 接続時間と処理時間が結びつく |
| キューとoutbox | 受理後の配送を回復できる | relayと重複排除が必要 |
| 全jobを直列化 | 順序を説明しやすい | 一つの遅いjobが全体を止める |
| 会話・文書ごとに直列化 | 関連する順序だけ守る | キー分割と偏りへの対処が必要 |
設計例:文書のインデックス作成
仮に毎分60件の文書を受理し、平均処理時間が10秒とします。安定時の平均処理中件数の目安は到着率1件/秒×10秒で10件になります。これは仮定からの計算で、必要ワーカー数の実測ではありません。実際には処理時間のばらつき、再試行、外部 API 上限、遅延の目標を加えて検証します。
jobにはdocument_idとinput_versionを固定します。古いバージョンの完成後に新しい文書へ上書きしないよう、公開中の索引バージョンを条件付きで切り替えます。取り込み速度だけでなく、最古の待機jobの年齢、到着数と完了数の差、期限切れリース、隔離件数を見ます。
練習と解答
結果保存の直後、ack送信の前にワーカーが落ちました。キューが同じjobを再配送したとき、もう一度モデルを呼ぶべきでしょうか。
確定結果とjobの状態を読み、一致する入力バージョンですでにsucceededなら、その結果を採用してackします。まだ未確定なら有効な所有世代を取得して再実行します。「重複して届く」と「二重に業務へ反映する」を別の問題として解けることが重要です。
押さえたいポイント
キューの受信回数と業務の反映回数を分け、再実行できる境界を作る。
出典・さらに読む
- AWS: Transactional outbox pattern内容の確認日: 2026-10-23
- Amazon SQS: At-least-once delivery内容の確認日: 2026-10-23
- AWS: Timeouts, retries, and backoff with jitter内容の確認日: 2026-10-23
学習と設計検討のための概念的な構成例です。稼働中のKumyuの実装を表す図ではありません。