ジョブ投入と状態ポーリングのAPIを作る
目標
202 Acceptedで始まる非同期APIの全体の契約(受付、状態取得、ポーリングの調整、冪等な提出、完了通知)を、自分で実装します。
なぜ重要なのか
30秒かかる作業を同期APIで作ると、3つが同時に壊れます。ロードバランサーのアイドルタイムアウトにかかり、クライアントのリトライが同じ作業を2回動かし、ワーカーが30秒ずつ塞がります。非同期APIは、これを受付と確認に分けます。ところが、202を返すだけで終わるAPIが、意外に多くあります。クライアントは、結果をどこで見ればよいかを知らず、どれだけ待てばよいかを知らず、リトライしてよいかもわかりません。このラボは、その契約の欠けた部品を1つずつ埋めます。Locationヘッダー、状態の語彙、Retry-After、冪等キー、そしてWebhookです。これらをすべて備えれば、その時点からクライアントは、安心してリトライできます。
ステップ
/root/aj/api.pyを127.0.0.1:8140で起動します。POST /jobsは{"n":5}を受け取って、キューq:jobs2に入れます。POST /jobsの応答は202で、Location: /jobs/<job_id>ヘッダーと、本文{"job_id":"...","status":"queued"}を返します。GET /jobs/<job_id>は、200とstatusを返します。値は、queued、running、succeeded、failedのいずれかです。存在しないidは404です。/root/aj/worker.pyは、キューから取り出して状態をrunningに変え、処理後にsucceededに変えます。- 完了したジョブの取得の応答に、
resultキーがある必要があります。queuedの状態では、resultキーがあってはいけません。 queuedまたはrunningの状態の応答には、Retry-Afterヘッダーが整数の秒数で付き、succeededの応答には付きません。- 同じ
Idempotency-Keyで2回提出すると、同じjob_idを返し、キューには1件だけ入ります。 /opt/app/hooksink.pyを127.0.0.1:8141で起動します。提出時にcallback_urlも一緒に受け取り、完了時にそのアドレスにPOSTします。/root/aj/hook.logに、job_idとstatus=succeededが記録されます。
参考
- 202は「やった」ではなく「受け取った」です。Locationがなければ、契約は未完成です。
- 完了したジョブの状態にはTTLを置き、期限切れの取得には404ではなく410 Goneを返せば、クライアントが区別できます。
- よくある間違い1は、job idとして、まだ作られていない結果リソースのIDを使うことです。失敗すると、幽霊IDが残ります。
- よくある間違い2は、完了の応答にも
Retry-Afterを付けて、クライアントが永遠にポーリングするようにしてしまうことです。
ジョブ提出APIを起動する
/root/aj/api.pyを127.0.0.1:8140で起動してください。POST /jobsは{"n":5}を受け取って、キューq:jobs2に入れます。
提出は、キューに入れてすぐ答えます。実際の処理はしないでください。それが非同期の核心です。
202とLocationヘッダーを返す
POST /jobsの応答は202で、Location: /jobs/<job_id>ヘッダーと、本文{"job_id":"...","status":"queued"}を返してください。
「受け取った」と「どこで確認するのか」が一緒にあって初めて、契約が成り立ちます。ヘッダーと本文の両方が必要です。
状態取得のエンドポイントを作る
GET /jobs/<job_id>は、200とstatusを返してください。値は、queued、running、succeeded、failedのいずれかです。存在しないidは404です。
状態の語彙は、4つで十分です。存在しないjob idには404を返してください。
ワーカーが状態を遷移させる
/root/aj/worker.pyは、キューから取り出して状態をrunningに変え、処理後にsucceededに変えてください。
キューから取り出したらrunning、終わったらsucceededです。状態はRedisのハッシュに置くと便利です。
完了後に結果を入れる
完了したジョブの取得の応答に、resultキーがあるようにしてください。queuedの状態では、resultキーがあってはいけません。
結果が小さければ、状態の応答に一緒に入れてもかまいません。完了前には、resultキーがあってはいけません。
ポーリング間隔を提案する
queuedまたはrunningの状態の応答には、Retry-Afterヘッダーが整数の秒数で付き、succeededの応答には付かないようにしてください。
進行中のときだけ、ヘッダーを付けます。完了した応答にこのヘッダーがあると、クライアントがポーリングし続けます。
提出を冪等にする
同じIdempotency-Keyで2回提出すると、同じjob_idを返し、キューには1件だけ入るようにしてください。
202を受け取る前に接続が切れると、クライアントはリトライします。同じキーには、同じ識別子を返してください。
完了のWebhookを送る
/opt/app/hooksink.pyを127.0.0.1:8141で起動してください。提出時にcallback_urlも一緒に受け取り、完了時にそのアドレスにPOSTします。/root/aj/hook.logに、job_idとstatus=succeededが記録されます。
受信側を先に起動し、提出するときにコールバックのアドレスも一緒に受け取ります。受信の記録が残って初めて採点されます。