TT Lab
はじめる
学ぶ 学習パス コース

キューと非同期API

202 Acceptedが始める契約

TT Labで続きを見る

一言でいうと

202は「やった」ではなく「受け取った」です。したがって、202を返すAPIは、必ず「どこで結果を確認するのか」を一緒に知らせる必要があります。

なぜ必要なのか

30秒かかる作業を同期APIで作ると、3つが同時に壊れます。ロードバランサーのアイドルタイムアウト(たいてい60秒)にかかり、クライアントがリトライすると同じ作業が2回動き、サーバーのワーカーが30秒ずつ塞がります。

非同期APIは、これを2段階に分けます。受付と確認です。受付はすぐに終わって識別子を返します。確認は、その識別子で状態を問い合わせます。

どう動くのか

HTTPには、このパターンのための語彙がすでにあります。

受付の応答は202 Acceptedで、Locationヘッダーに状態取得のURLを入れます。本文には、ジョブの識別子と現在の状態を入れます。この3つがないと、クライアントは何をすべきかわかりません。

状態の取得は、200で現在の状態を返します。状態の語彙は、単純なほどよいです。queued、running、succeeded、failedくらいです。まだ進行中なら、Retry-Afterで次のポーリングの時点を提案します。これがないと、クライアントがそれぞれ1秒ごとにポーリングして、状態APIが新しいボトルネックになります。

完了したら、結果をどう渡すかは2つの流れがあります。状態応答の本文に結果も一緒に入れるか、303 See Otherで結果リソースのURLを指すかです。後者がRESTらしいですが、クライアントの実装が増えます。結果が小さければ、前者で十分です。

提出も冪等である必要があります。クライアントが202を受け取る前に接続が切れてリトライすると、同じジョブが2つ作られます。提出に冪等キーを要求し、同じキーには同じjob idを返せばよいのです。

Webhookは、ポーリングの代替です。完了時にサーバーがクライアントのURLにPOSTします。遅延がなく、ポーリングの負荷もありませんが、代償があります。クライアントが公開エンドポイントを持つ必要があり、そのエンドポイントが死んでいることもありうるので、サーバーがリトライとDLQを備える必要があり、なりすましを防ぐために署名が必要です。実務では、Webhookとポーリングを一緒に提供する場合が多いです。

現場での姿

最もよくある設計のミスは、job idとして、本当の結果リソースのIDをそのまま使うことです。まだ作られていないリソースのIDを先に渡すということなので、失敗したとき、そのIDは永遠に幽霊になります。job idとresult idは、分けるほうが安全です。

2つ目。ジョブの状態を永遠に保管しないでください。完了したジョブの状態にTTLを置き、期限切れになった取得には404ではなく410 Goneを返せば、クライアントは「なかったもの」と「過ぎたもの」を区別できます。

202を返したあとの契約

202 Acceptedは「受け取った」にすぎず、「実行される」ではありません。そのため、3つを一緒に渡して初めて、クライアントがコードを書けます。

HTTP/1.1 202 Accepted
Location: /api/jobs/7f3a-91cd
Retry-After: 3
Content-Type: application/json

{"job_id": "7f3a-91cd", "status": "queued",
 "poll_url": "/api/jobs/7f3a-91cd", "estimated_sec": 30}

状態の取得では、最低4つの状態を区別します。

{"status": "queued"}                                  → 아직 시작 안 함
{"status": "running", "progress": 0.4}                → 진행 중
{"status": "succeeded", "result_url": "/files/…"}     → 결과가 있다
{"status": "failed", "error": {"code": "…","message": "…"}, "retriable": false}

retriableが重要です。クライアントが、もう一度送るのか、人に尋ねるのかを、 この値で決めます。

ポーリングの代わりに通知

ポーリングは単純ですが、無駄です。3つの代替があり、それぞれ価値が違います。

方式 サーバーの負担 クライアントの複雑さ ファイアウォール
ポーリング リクエスト数に比例 最も単純 問題なし
ロングポーリング 接続を維持 単純 プロキシのタイムアウトに注意
SSE 接続を維持 普通 たいてい通過する
Webhook 最も少ない 受信エンドポイントが必要 相手が公開アドレスである必要がある

Webhookを使うなら、リトライと署名を一緒に設計する必要があります。受け取る側が一時的に死んだら、 再送する必要があり、なりすましを防ぐには、本文にHMAC署名を付ける必要があります。

冪等キーで二重提出を防ぐ

クライアントが202を受け取る前にタイムアウトになると、同じリクエストをまた送ります。すると、 ジョブが2つできます。

POST /api/jobs
Idempotency-Key: 8f14e45f-ea3b-4d29-9a1c-2b3c4d5e6f70

サーバーはこのキーを保存し、同じキーがまた来たら、新しく作らずに、最初に作ったジョブの 202をそのまま返します。 キーはクライアントが作りますが、リトライの間で変わってはいけないので、 リクエストの内容から決定的に導くか、リクエストの開始時点で一度だけ生成して保管します。

キーの保持期間は、24時間程度が一般的です。それより長く置くとストレージが増え、短いと 遅いリトライを捕まえられません。

次のラボですること

ジョブ提出APIを作って、202とLocationを正確に返し、ワーカーが状態を遷移させ、Retry-Afterでポーリングを調整し、提出を冪等にして、最後に完了Webhookまで付けます。