コンテンツにスキップ

HTTP APIの振り返り・解説

出題の入口から先に回答し、必要な節だけ参照する。以下はAIによる教材用の設計例であり、本人の実務・実測ではない。対象はPostgreSQL 18(検証環境18.6)、HTTP RFC 9110。外部決済の例はStripe公開ドキュメント(2026-09-08確認、APIバージョン未固定)を参照し、導入時は利用APIバージョンの契約を再確認する。

解説図:削除でずれる位置と、保存する境界値

このページの解説を理解し、疑問を深掘りしたいときは、次のプロンプトをコピーしてChatGPTに貼ってください。気になる見出しや図名があれば、末尾に追記できます。

ChatGPT開始文
https://github.com/MFQWKMR4/tech2026 の次のファイルを読み、
architecture-api-idempotency の振り返り・解説を一緒に読む復習セッション(learn)を始めてください。
共通ルール:
- CHATGPT.md
- AGENTS.md
- src/content/docs/career/index.md
このページと回答記録:
- src/content/docs/architecture/api-idempotency.mdx
- public/diagrams/architecture-api-idempotency/cursor-deletion.svg
- reviews/architecture-api-idempotency.yaml
- sessions/2026-09-08-architecture-api-idempotency-01.yaml
関連する図・実験資料:
- diagrams/architecture-api-idempotency/retry.json
- diagrams/architecture-api-idempotency/payment.json
- diagrams/architecture-api-idempotency/pagination.json
reviewにこれより新しいsessionがあれば、そちらも確認してください。
最初に実際に参照できたファイルとcommitを短く示してください。
読めないファイルは読めないと伝え、内容を推測しないでください。
図のJSONは要素・矢印・処理順の根拠として読み、描画を見たとは扱わないでください。
まず、私が気になっている節や図があるか一つだけ聞いて、回答を待ってください。
指定がなければneeds_revisitに関係する解説から始めてください。
一節ずつ、説明 → 私の疑問 → 具体例や図による深掘り、の順で進めてください。
追加疑問が出たらそちらを優先し、一度に説明を進めすぎないでください。
区切りで自分の言葉で説明できるか一問ずつ確かめ、ヒント後の理解と自発的な説明を区別してください。
教材の例・Codexの検証結果を、私が実施した実績として扱わないでください。
終了時は .github/ISSUE_TEMPLATE/session-sync.md に従い、session_type: learn で
終了時は実際の回答を根拠に、CHATGPT.mdと既存テンプレートに従ってYAML+Session narrativeを含む [Session Sync] GitHub IssueをMFQWKMR4/tech2026に作成し、URLを返してください。作成できない場合は未作成と明示し、コピー可能なタイトルと本文を返してください。
参照した見出し・図のpathを残し、教材に戻したい説明をrepository_requestsに含めてください。

2026-09-10の横断面接:回答・支援・不足別の再学習

最終レビュー:2026-09-10 · 説明した(Explained)。ヒント後の理解と自力の説明を区別して記録しています。

次回、自力で確かめたいこと

  • 2026-09-08 / 2026-09-08-architecture-api-idempotency-01: 次回はヒントなしで、注文作成APIの契約としてidempotency keyの生成主体・scope・payload mismatch時の扱い・DB一意制約・再送時のresponse再現まで一連で説明する。
  • 2026-09-08 / 2026-09-08-architecture-api-idempotency-01: 外部決済を含む場合、order_idとpayment_attempt_idを分け、外部決済サービス側のidempotency key、状態照会/reconciliation、ローカルstate machineをどう組み合わせるかを再出題する。 2026-09-10 [2026-09-10-int-burst-01-01] provider keyの提案は条件提示後・解法ヒントなしで再確認。key/照会のあり・なし別にretry、照合、手動復旧、at-most-onceを説明し、ローカルclaimとの保証境界を再確認する。外部key非対応の限界は今回独力未解決。
  • 2026-09-08 / 2026-09-08-architecture-api-idempotency-01: paginationは `ORDER BY created_at DESC, order_id DESC` と複合cursorを前提に、次ページWHERE条件をヒントなしで説明する。offsetとの差をinsert/delete両方で説明する。

2026-09-10のセッション

INT-BURST-01 v1(2026-09-09)の口頭interview。開始問題「イベント開始時に受付が集中し、受付後に外部サービスを使って処理するシステム」で、初回独力回答ではAPI serverのhorizontal scaling、queueによるproducer/consumer分離、外部APIの処理能力に合わせたconsumer側rate調整、queue容量設計、受付完了と処理中/完了の状態表示を提案した。進捗表示では当初「受付APIではDB書込みを発生させたくない」とし、queue投入時に受付成功を返し、consumer開始後にDBへprocessing、完了後にcompletedを記録して状態取得APIで見せる案を出した。

並行consumer条件後にattempt ID/UNIQUEを提案(安定キー詳細unknown)。外部成功/DB前停止にprovider keyを条件付きで自発提案し過去missingを限定更新。key非対応では独力未解決、at-most-onceは解法説明後。

問い・回答の流れをIssueで読む

2026-09-08のセッション

注文作成APIのタイムアウト再送から始め、idempotency key、DB一意制約、並行再送、外部決済との境界、キー保持期間、cursor-based paginationまで条件を一つずつ変えて検討した。開始時は再送前に最新状態をfetchする案で、同一操作を識別する仕組みは自発的には出なかった。ヒント後は、クライアント生成の一意IDをDBへ保存し注文作成と同一transactionで確定する、一意制約で競合を防ぐ、競合時に既存結果を再読込する、と段階的に設計できた。外部決済ではDBと外部APIの境界に迷い、決済成功後・DB書き戻し前の障害に対して外部サービスの状態照会を正本として再確認する案へ到達した。外部サービス自身のidempotency keyを使う設計はChatGPTの説明後に理解した。paginationではoffsetの位置ずれを自発的に指摘し、どこまで見たかをcursorとして渡す発想を出した。created_at同値時のtie-breakerと複合cursorのSQL条件はヒント・説明後に理解した。

Issue #7の回答要約: タイムアウト後の成功可能性、UNIQUE、競合後の既存結果返却、決済状態照会、offset位置ずれは自発的。操作ID・外部決済キー・複合cursor SQLはヒントまたは説明後。payload hash案は自己修正。本人による実装・実測なし。詳細とヒント区分はsessionおよびIssueのSession narrativeを参照。

問い・回答の流れをIssueで読む

回答で示せたこと(ヒントの有無を含む)
  • 2026-09-08 / 2026-09-08-architecture-api-idempotency-01: タイムアウトは処理失敗とは限らず、DB処理が完了してレスポンスだけ失われた可能性を考慮した。回答要約:『DB側の処理まで終わってて、クライアントに帰るまでの間にタイムアウトした可能性』を挙げ、再送前に状態確認を考えた。ヒント: なし。
  • 2026-09-08 / 2026-09-08-architecture-api-idempotency-01: idempotency keyの説明後、サーバー側でキーをDBに記録し、注文作成とキー記録をtransaction完了とともに確定し、再送時はキーがあれば新規作成しない設計を説明した。回答要約:『トランザクションの完了とともに書き込む』『再送時にはそれで検索して、もしあったら何もしない、なければ新しい注文』。ヒント: あり(クライアントが操作専用の一意IDを生成し再送でも同じIDを使う説明)。
  • 2026-09-08 / 2026-09-08-architecture-api-idempotency-01: 同じidempotency keyで異なるpayloadが来た場合、先に記録された内容を正とし後発を弾くべきと判断した。回答要約:『同じIDでも既に記録されてるんだったら、そっちが正しい』『後半に来た内容の違うリクエストに関しては弾かれるべき』。ヒント: なし。
  • 2026-09-08 / 2026-09-08-architecture-api-idempotency-01: 並行再送で双方が未登録と判断するraceに対し、DBの一意制約で最終的に片方だけcommitさせる案を自発的に出した。回答要約:『一意の制約を持っていれば、書き込もうとしたときにDB制約で弾ける』。ヒント: なし。
  • 2026-09-08 / 2026-09-08-architecture-api-idempotency-01: 一意制約違反後はそのまま重複エラーを返すのではなく、DBを再読込し既存注文が確定していればその内容を返すと判断した。回答要約:『データベースを確認』『本当にその注文が記録されているのかを確認し、その内容をクライアントに返す』『エラーにはせずに既に記録されていますみたいな形』。ヒント: なし。
  • 2026-09-08 / 2026-09-08-architecture-api-idempotency-01: 外部決済をDB transaction内に直接含めるのは筋が悪いと認識し、決済結果を後からDBへ反映する境界問題を指摘した。回答要約:『データベーストランザクションの中に外部決済API呼ぶやつって入れるんだっけ?それは筋が悪いか』『成功したらデータベースにもう一回書き込み』。ヒント: なし。
  • 2026-09-08 / 2026-09-08-architecture-api-idempotency-01: 決済成功後・ローカルDB更新前に障害が起きた場合、外部決済サービスへ状態を問い合わせ、その結果を基にローカル状態を復旧する案を出した。回答要約:『外部APIもそれを決済情報がどうなってるかを返すAPIが別にあるべき』『外部サービスでどうなってるかを聞いてGETした結果で支払えてるのかどうかを判定』。ヒント: なし。
  • 2026-09-08 / 2026-09-08-architecture-api-idempotency-01: 注文と決済試行の単位を分離し、注文金額が変わる場合は決済側に別IDが必要と判断した。回答要約:『注文と決済は別IDで持つべき』『金額が増えた場合は、またその差分を新しい決済が必要になるから別で持つべき』。ヒント: あり(外部決済APIのidempotency keyとpayment_attempt_idの説明後)。
  • 2026-09-08 / 2026-09-08-architecture-api-idempotency-01: クライアントが誤って新しい注文idempotency keyを送った場合、payload一致だけで同一操作を推測せず別注文として扱うべきと判断した。回答要約:『それはさすがに別注文として受け入れるしかない』『そこまで考慮し出すとややこしい』。ヒント: なし。
  • 2026-09-08 / 2026-09-08-architecture-api-idempotency-01: idempotency recordの保持期間について、短すぎると期限後再送が新規注文扱いになり、長すぎると保存件数・コストが増えるtrade-offを説明した。回答要約:『短くした場合、そのキーが重複するリスク』『再送する間隔がめっちゃ空いた場合に新しい注文として計上』『30日保持なら30億件保存』。ヒント: あり(注文レコードとidempotency用制御レコードを分ける例、1日1億件の追加条件)。
  • 2026-09-08 / 2026-09-08-architecture-api-idempotency-01: offset paginationでは、1ページ目取得後に新規注文が挿入されると2ページ目に既に見たデータが再登場する位置ずれを自発的に指摘した。回答要約:『2ページ目にさっき見ていた1ページ目のデータが登場する可能性』。ヒント: なし。
  • 2026-09-08 / 2026-09-08-architecture-api-idempotency-01: offsetの代案として、どこまで見たかを次リクエストへ渡す発想を自発的に出した。回答要約:『どこまで見たかみたいなのがわかるようなクエリパラメーターをつける』。ヒント: なし。その後ChatGPTがcursor-based paginationと説明。
  • 2026-09-08 / 2026-09-08-architecture-api-idempotency-01: 前方の注文削除に対して、cursor方式なら位置ではなく境界値を基準にするため次ページ開始点が維持されると説明した。回答要約:『カーソルベースである場合はどこまで見ていったかが厳密にわかる』『どこが削除されようと次の見るべき始まりのポイントはわかる』。ヒント: あり(複合cursorとWHERE条件の説明後)。
  • 2026-09-10 [2026-09-10-int-burst-01-01] 同一IDのmessageを複数consumerが並行処理する条件で、attempt IDを一意キーとしてDB制約を置き、transaction内のatomicな書込みで一つだけ処理開始権を取る案を提示した。 支援・留保: 同じIDの並行consumerが未処理と読むraceという条件提示後。解法提示は記録なし。共有キーの安定性・生成主体・lease世代はunknown。
  • 2026-09-10 [2026-09-10-int-burst-01-01] 外部serviceがidempotency keyを提供するなら同じkeyをrequestに含めて二重副作用を防ぐ方針を自発的に提示し、『この前提が許される場合』と保証条件を明示した。 支援・留保: 外部成功/DB更新前停止という条件提示後。provider keyの解法ヒントなし。scope/保持期限の確認は未実施。
  • 2026-09-10 [2026-09-10-int-burst-01-01] 解法説明後の確認では、二重課金がcriticalな条件ならat-most-onceを選ぶと回答した。これは解法説明後の理解であり初回独力とは区別する。 支援・留保: 外部key非対応・二重課金criticalの条件と解法説明後。独力とは分離。
自力では説明しきれなかったこと
  • 2026-09-08 / 2026-09-08-architecture-api-idempotency-01: 最初の問いでは、同一操作を識別するidempotency keyという契約を自発的に提示できなかった。回答:『あんまり考えたことがなかった』。ヒント: あり(クライアントが操作専用の一意IDを生成し、再送でも同じIDを使う説明)。
  • 2026-09-08 / 2026-09-08-architecture-api-idempotency-01: created_atが同値になるpaginationで、(created_at, unique tie-breaker)の複合順序と複合cursorを自発的には提示できなかった。本人は『ミリ秒単位で取るとかはダメですかね』と回答し、その後ChatGPTがorder_idをtie-breakerにする設計を説明した。
  • 2026-09-08 / 2026-09-08-architecture-api-idempotency-01: 複合cursorの次ページWHERE条件は自発的に完全な条件へ落とし込めなかった。本人は境界created_atを含め、order_idで除外する方向を説明し、その後ChatGPTが `created_at < cursor_created_at OR (created_at = cursor_created_at AND order_id < cursor_order_id)` を提示した。
  • 2026-09-10 [2026-09-10-int-burst-01-01] 外部serviceがidempotency key非対応で、外部成功直後に自DB成功記録前で停止した結果不明ケースについて、自システムだけでは成功/失敗を判定できず完全なexactly-once side effectを保証できない境界を自力では説明できなかった。回答:『難しいな。出なかった。出せませんでした、ソリューションが』。その後ChatGPTが結果不明、照会/reconciliation、照会不能時のat-most-once判断を説明。
理由・設計を詰めたいこと
  • 2026-09-08 / 2026-09-08-architecture-api-idempotency-01: 開始時は『最新の情報をfetchする』ことで再送問題を解こうとしたが、複数注文があり得る場合にどの注文が元リクエストか識別できない点まで初案では詰められていなかった。回答要約:『一旦クライアント側で最新の情報をfetchするべき』。ヒント: なし。
  • 2026-09-08 / 2026-09-08-architecture-api-idempotency-01: 同じidempotency keyで異なるpayloadを弾く判断自体は明確だったが、比較対象としてrequest hashや保存したcanonical requestを持つこと、どのHTTPエラー契約にするかまでは説明しなかった。回答:『前提が壊れるから…サーバーとしてはもう1択』。ヒント: なし。
  • 2026-09-08 / 2026-09-08-architecture-api-idempotency-01: 外部決済では、DB更新と外部副作用のatomicityがないことには気づいたが、payment attemptの状態遷移、retry/reconciliation/compensationをどう分担するかは未整理だった。回答要約:『この辺の境界難しいよね』。ヒント: なし。
  • 2026-09-08 / 2026-09-08-architecture-api-idempotency-01: idempotency keyをpayload hashで作る案を終盤に一度考えたが、自分で『同じ内容で複数別のパターンがあるから…それじゃだめか』と修正した。内容一致と同一操作は別である点には到達したが、クライアント/サーバーそれぞれのキー生成責任を最終的に定式化するところは次回再確認したい。ヒント: あり(セッション全体でkey生成主体とpayment_attempt_idを説明済み)。
  • 2026-09-10 [2026-09-10-int-burst-01-01] idempotency非対応の外部serviceに対して当初『自前で冪等性のある呼び出し』『二回実行されても外部呼び出しはexactly-onceになる仕組み』としたが、外部成功と自DB記録のatomicityを自システムだけで作れない点までは具体化できなかった。
修正が必要な説明

記録された項目はありません。

以下の解説は、セッションで残った疑問を補う教材です。読んだことだけで学習記録の状態は変わりません。

Default:注文操作に一つのキーを割り当てる

Section titled “Default:注文操作に一つのキーを割り当てる”

タイムアウトは結果不明を意味する。最新注文をGETしても、同じ商品を短時間に二度買えるなら元の操作を識別できない。クライアントは購入を確定する操作ごとにランダムな一意IDを生成し、送信前にpending操作とともに保持する。同じ操作の再送では同じキーと内容を使い、意図した別注文には新しいキーを使う。HTTPの冪等性は同じ要求の意図した効果についての性質であり、POSTの安全な再送には業務契約が必要になる。RFC 9110 §9.2.2

教材では次の契約を採用する。ステータスの選択はこのAPIの設計例で、全サービス共通の規則ではない。

条件 契約例
認証・scope 認証済みaccountと操作名をキーに結合。再送でも認可を行う
初回成功 201、注文IDを含むbodyとLocationを保存してcommit後に返す
同じキー・同じ内容 保存したstatus/body/Locationを再現。最新状態は別のGETで取得
同じキー・異なる内容 409idempotency_payload_mismatch。既存内容を変更しない
キー欠落・不正 このAPIでは400。実行前の認証・入力検証失敗は成功記録を作らない
並行処理の待機期限超過 rollbackし503、再試行間隔を案内。同じキーで再送する
処理途中の結果不明 キーを変えず照会・再送。新しい注文を推測で作らない

注文の現在値から再構築すると、発送後の再送で初回と異なるbodyになる。保存responseは必要な業務フィールドに限定し、cookieや認証情報を保存・再送しない。業務上の拒否結果を保存するか、障害の500を保存するかも契約に含める。本例はDB成功結果を保存する。Stripeは実行開始後の結果について500も再現し、実行前のvalidation・並行競合は保存しないという別の契約を採る。Stripe: Idempotent requests

Why:キーと内容の指紋は役割が違う

Section titled “Why:キーと内容の指紋は役割が違う”
操作 キー 内容fingerprint 扱い
本を1冊購入 K1 H1 新しい注文
通信断で同じ購入を再送 K1 H1 元の結果
もう1冊、意図して購入 K2 H1 別注文
K1のまま数量を2冊へ変更 K1 H2 mismatch

payload hashそのものをキーにすると、3行目の正当な購入を消してしまう。サーバーは入力を正規化したcanonical request(通貨、商品、数量、配送指定など効果を左右する値)を保存するか、その暗号学的hashを保存して同じキーの内容を検証する。JSONのプロパティ順、既定値、数値表現、API版を揃える規則が必要。hashは入力の同一性検証であり、操作の意図や認可の証明にはならない。

並行retry:UNIQUEを獲得したtransactionだけが作成する

Section titled “並行retry:UNIQUEを獲得したtransactionだけが作成する”

最小schema例。注文の業務データと、期限付きretry制御を分ける。ここではcanonical requestをJSONBで保存し、正規化後の比較を行う。

CREATE TABLE orders (
order_id uuid PRIMARY KEY,
account_id uuid NOT NULL,
created_at timestamptz NOT NULL,
amount_minor bigint NOT NULL CHECK (amount_minor >= 0),
currency text NOT NULL
);
CREATE TABLE idempotency_records (
account_id uuid NOT NULL,
operation text NOT NULL,
key text NOT NULL,
canonical_request jsonb NOT NULL,
order_id uuid REFERENCES orders(order_id),
response_status integer,
response_body jsonb,
response_location text,
expires_at timestamptz NOT NULL,
PRIMARY KEY (account_id, operation, key)
);

PRIMARY KEYがscope内の一意性を守る。以下の手順はRead Committedを前提にする。

  1. 認証・認可と正規化後、transactionを開始する。
  2. キー行をINSERT ... ON CONFLICT (account_id, operation, key) DO NOTHING RETURNING keyで予約する。競合側は先行transactionの終了を待つ。
  3. 1行返れば所有者。注文をINSERTし、同じtransactionでキー行に注文ID・responseを保存してcommitする。途中失敗なら全部rollbackする。未完成行をcommitしないことがこの手順の不変条件。
  4. 0行なら次のSQL文でキー行をSELECTし、canonical requestを比較して元の結果またはmismatchを返す。同じINSERT文内のsnapshotで必ず既存行が読めるとは限らない。
  5. 期限削除との競合で行がなければtransactionを終えて限定回数だけ再試行する。期限後は新規扱いとなる契約を適用する。削除処理は進行中の操作と調停する。

並行再送のsequence図ではAがcommitする場合を示す。Aがrollbackした場合はBが予約を獲得して作成できる。単なるSELECT → なければINSERTだけでは排他にならない。通常のINSERTで一意制約違反を捕捉する実装なら、失敗transactionをrollback(または適切なsavepointへrollback)してから再読込する。別の制約違反まで重複再送と誤認しない。PostgreSQL 18: isolationROLLBACK

Trade-offと例外:保持期間は保証の境界

Section titled “Trade-offと例外:保持期間は保証の境界”

キー保存には容量と個人情報管理のコストがある。演習上の追加条件「1日1億操作、30日保持」なら約30億行であり、index・response・replicaの容量は別途必要。注文が残っていても制御行を削除すれば、古いキーとの対応を失い再送が新規注文になり得る。保持期間は最大retry期間、offline復帰、外部決済側の有効期間を合わせて決め、APIに明記する。期限を越えた注文の重複が許されないなら、永続的な業務操作IDや小さなtombstoneの保持を検討する。Stripeの少なくとも24時間という例を自サービスへそのまま転用しない。Stripeの保持契約

外部決済:orderとpayment_attemptを分ける

Section titled “外部決済:orderとpayment_attemptを分ける”

DB transactionを開いたままHTTPを呼んでも、外部決済をDB rollbackで取り消せない。長いlock保持も増える。バックエンドは外部呼出しpayment_attempt_id、order_id、確定金額・通貨、provider識別子、状態をDBへ保存する。同じ試行の通信retryではattempt IDも金額も固定し、別の意図した追加支払いや確定失敗後の再試行は業務判断を経て新attemptにする。結果不明のまま新attemptを作らない。

注文と決済試行の状態遷移図は教材用の簡略状態機械。Stripe固有の状態名とは異なる。

対象 遷移・意味
order awaiting_payment → paidは必要額の支払確認後。同一DB transactionでattempt結果と更新
attempt pending → unknownは送信を開始する前に記録。クラッシュ後に未送信か成功済みか断定しない
attempt unknown → succeeded / failedはproviderの確定結果を確認して遷移
unknownの継続 状態照会、webhook、同じキーでの安全な再試行。処理中・通信失敗は確定失敗ではない
取消・返金 成功を過去から消さず、別の補償操作を作り、その結果も照合

workerがattemptを処理し、結果とprovider payment IDを保存する。複数workerのclaim/leaseを調停し、期限切れworkerの遅い応答で成功状態を上書きしない条件付き更新を行う。通知の重複はevent IDでdeduplicateし、順序逆転時はproviderの最新状態を確認する。照会できるようprovider IDまたは検索可能なmerchant referenceを確保する。作成応答とともにIDを失う場合は、同じキーでの再送からIDを回収できるかも確認する。Stripe: Payment Intents

外部APIの契約 結果不明時の扱い 限界
安定キーで重複排除あり 有効期間内は同attempt・同内容でretryし照会と照合 scope・保存期間・対象endpointの制約を確認
重複排除なし、状態照会あり referenceで照会し、確定しない間は保留・運用確認 「見つからない」が処理未完了なら再課金の安全性を証明できない
両方なし 自動再課金を止め、provider側の調査・手動照合へ 自サービスのUNIQUEだけでは二重課金を防げない

outboxは注文・attemptと「処理すべき仕事」を同じDB transactionで保存し、commit後・queue送信前のクラッシュによる仕事の消失を防ぐ。配送は重複し得るので、外部APIの重複排除や照合を代替しない。単純な構成ならpending attemptをDBから再取得するworkerでもよい。retryは同じ仕事の再実行、reconciliationは実際の状態との照合、compensationは返金などの新たな業務操作であり、DB rollbackではない。AWS: Transactional outbox

ネットワーク越しに全工程のexactly-onceを主張せず、at-least-onceの再実行と、契約の範囲内の重複排除・照合で業務効果を制御する。安全な再実行手段がなければ、自動retryより結果不明として保留することを選ぶ。

業務ID、処理開始権、外部の一回性は別

Section titled “業務ID、処理開始権、外部の一回性は別”

問い:UNIQUEを取ったworkerだけが開始するなら、外部課金も一度だけになるか。 同じ意図した操作には同じ安定したIDを使い、同じscope/keyに対する開始権をDBで競合させる。consumer A/Bが別々の新規attempt IDを生成してそれぞれINSERTすれば、どちらも一意なので競合しない。IDを一意に発行することと、重複した仕事を同じIDで識別することは違う。PostgreSQL 18 Unique Constraints(2026-09-10確認)。

安定した業務操作IDとは別に、worker所有者やlease世代を持つ設計は可能。旧世代からのDB更新を条件付きで拒めても、外部が世代を検証しなければ古いworkerの外部呼出しまで止められない。processingが長いことは停止の疑いであり、外部未実行の証拠ではない。再開は外部契約と結果確認に基づける。

結果不明からat-most-onceを選ぶ意味

Section titled “結果不明からat-most-onceを選ぶ意味”

次は演習上の追加条件:外部は変更不能、keyによる重複排除も状態照会もない。同じ操作に対する複数呼出しは二重効果になり得る。自DBに呼出し許可の消費をcommitしてから外部へ送る設計例を考える。実測ではない。

停止の実際 再起動後の自DB 外部の実際
呼出し許可の消費commit後、送信前に停止 呼出し許可は消費済み・結果不明 未実行
外部成功後、成功記録前に停止 呼出し許可は消費済み・結果不明 実行済み

同じDB状態から二つを識別できない。前者の漏れを消そうと無条件に再送すると、後者で重複する。外部成功後だけDBへ記録する順序にしても、成功と記録の間の隙間は残る。既存の外部副作用図の停止位置と合わせて読む。

at-most-onceを選ぶとは、結果不明をfailedに確定することではない。自システムが管理する全worker・再配送・client再送を同じ操作IDへ束ね、durableに消費した呼出し許可をtimeoutやlease期限だけで再発行しない。HTTP client/SDK等の自動retryも同じ方針に合わせる。この設計は自システムからの再呼出しを抑えるもので、外部の内部動作を保証する魔法ではない。未実行のまま残る可能性と、運用確認が終わるまで確定結果を返せない代償を受け入れる。

非冪等なrequestは、実際には冪等だと分かるか、元の要求が適用されていないと分かる場合以外に自動retryしない、というHTTPの原則とも対応する。RFC 9110 §9.2.2(2022、2026-09-10確認)。

外部契約を変えた別条件 次の判断
keyで重複排除できる 同じ操作・同じ内容・有効なscope/期間でretry。保存期限が過ぎていないか確認
keyなし、信頼できる照会あり 相関IDで照合。not foundが可視化遅延や処理中を含むなら再実行せず保留
keyも照会もなし 二重効果を避ける要件なら自動再送を止め、調査。人でも証拠なしに結果は確定できない

provider契約の具体例としてStripeはkeyに対応する結果を保存して再送に返すが、保持や実行開始前のエラー等の条件がある。他providerへ一般化しない。Stripe Idempotent requests(更新型API資料、2026-09-10確認、特定API versionを固定した実験ではない)。

利用者には「確認中」など確定情報だけを示し、新しいkeyでの再課金を促さない。運用ではunknownの滞留・照合失敗・問い合わせ先を管理する。manual recoveryは新しい成功証拠を得て判断する手順であり、再送の安全性を自動で作るものではない。compensationも外部で成立した効果に対する新しい業務操作で、元の処理とのatomic rollbackではない。

Pagination:位置ではなく最後に返した値を境界にする

Section titled “Pagination:位置ではなく最後に返した値を境界にする”

演習上の追加条件:順序はcreated_at DESC, order_id DESC、両列はNOT NULLかつ変更しない。1ページ2件、初期順はA,B,C,D。

ページ1のA,B取得後 OFFSET 2のページ2 Bを境界にしたkeyset
先頭へXをINSERT B,C(Bが重複) C,D
AをDELETE(別の実験) D(Cを取りこぼす) C,D
境界BをDELETE(別の実験) D 保存済みBの値でC,D

境界と複合indexの図。cursorに行IDだけでなく境界値を保存すれば、境界行の削除後も再取得が不要。返すはずだったC自体が削除された場合は、当然Cは返せない。keysetはsnapshotを固定する仕組みではない。PostgreSQL 18: LIMIT/OFFSET

境界の行が消えたら、何を持ち越せばよいか

Section titled “境界の行が消えたら、何を持ち越せばよいか”

既存図でcursorとindexの対応を確認したら、ここでは表の「BをDELETE」を具体的な行で追う。各列は左から新しい順。②と③は①からBを削除した同じ状態への、別々のページ2取得である。

ページ1でA B取得後Bを削除。OFFSETはA Cを飛ばしDだけを返す。Bの時刻とIDを保存したkeysetはC Dを返す。

図を拡大する

OFFSETが持ち越すのは「2行を飛ばす」という数で、飛ばす対象の行ではない。削除後の2行目はCへ変わる。一方、keysetが持ち越すのはBの並び順を決める値なので、Bの実体を読み直さず、その値より後のCから続けられる。

前提はこの節のNOT NULL・変更しない順序キー・一意なtie-breaker。図のA〜Dは行の呼び名であり、文字の大小をSQLの順序に使うものではない。順序キーの更新やsnapshot固定は後述の別問題として残る。PostgreSQL 18 LIMIT/OFFSET(2026-09-09再確認)。

同時刻の注文はミリ秒にしても同値になり得る。uniqueなorder_idを第二キーにし全順序を定義する。UUIDは時間順でなくても、DB内で一貫して比較でき一意ならtie-breakerになる。UUIDから作成順を推測しない。

CREATE INDEX orders_account_cursor_idx
ON orders (account_id, created_at DESC, order_id DESC);
-- $1: 認証済みaccount、$2/$3: 検証済みcursor、$4: 上限内のpage size
SELECT order_id, created_at, amount_minor, currency
FROM orders
WHERE account_id = $1
AND (created_at < $2::timestamptz
OR (created_at = $2::timestamptz AND order_id < $3::uuid))
ORDER BY created_at DESC, order_id DESC
LIMIT $4;

両列DESC・NOT NULLなので境界条件を(created_at, order_id) < ($2::timestamptz, $3::uuid)とも書ける。先頭ページには境界条件を付けない。次cursorは最後に返した行の値から作る。accountの等価条件を先頭、順序列を後ろに置く複合B-treeが候補。OR形とrow比較形のplanは同一とは限らず、実データ量と分布でEXPLAIN (ANALYZE, BUFFERS)のIndex Cond、Filter、Sort、走査件数を確認する。row comparisonmulticolumn indexes

cursorは例えば{v, created_at, order_id, filter_digest}をbase64urlで包み、署名を検証する。base64は暗号化ではない。認証accountはサーバーで決定し、cursorから認可しない。時刻精度を落とさず、filter・順序・版の変更を検知して不整合cursorを拒否する。

更新可能なsort keyが境界をまたぐとkeysetでも重複・欠落が起こる。新規行が境界より前に入れば今の走査には現れず、過去時刻で後ろに入れば途中から現れる。厳密な一時点の全件出力は別途snapshot/exportの契約が必要で、初回request timeだけでは後日commitされた過去時刻の行を排除できない。OFFSETは任意ページへの移動や小さな固定一覧には簡単だが、深いoffsetの読み捨てと更新中の位置ずれを受け入れる必要がある。

症状 最初に辿る根拠
重複注文 account/operation/key、期限切れ、新キー生成、transaction境界を照合
retryが遅い UNIQUE待機、transaction時間、rollback、connection枯渇を確認
支払いが結果不明 attempt IDとprovider reference、unknown滞留時間、照会・webhook失敗を追う
仕事が進まない pending/outbox滞留、lease、retry回数とbackoff、運用介入対象を確認
一覧の重複・欠落 cursorの時刻精度・第二キー・filter一致・sort key更新履歴を確認

成功率だけでなく再送率、mismatch率、結果不明の件数と最古経過時間、二重課金の照合差分、深いページの走査件数を見る。キーやpayloadの生データを無制限にログへ残さず、調査用IDと保持範囲を決める。

補強元:Session Sync #72026-09-08-architecture-api-idempotency-01(2026-09-08)。一般化した質問は「タイムアウト再送」「外部成功後のDB更新失敗」「hashと操作識別」「挿入・削除中のcursor」。各節の一次資料を同日確認。説明・図・SQLの教材検証は本人の習得実績に含めない。

図表改善(2026-09-09):説明用SVGはSVG自体が正本。前提と判断は本文、状態・比較は図、通信順序・依存関係は既存図、実測は元の記録を参照する。教材編集を新しい学習実績にはしない。

2026-09-10補強:2026-09-10-int-burst-01-01Issue #13)の「開始権で外部も一回にできるか」を一般化し、IDの安定性と結果不明時の再呼出し抑止・実行漏れを追記。既存図を再利用し、新規SQL/API実験・本人の実績は追加していない。