Ruby
リトライと冪等性
何がリトライされ、何をあえてリトライしないのか、そしてリトライされた送信が重複しない理由。
送信
クライアントはすべての送信(emails.send、emails.send_batch、templates.send、broadcasts.send)に Idempotency-Key を付与します。キーは**呼び出し**ごとに oe- とランダムな UUID で 1 回生成され、その呼び出しのリトライで再利用されます。API は何かを配送する前にそのキーを確保するため、リトライは 2 通目を送るのではなく元のメッセージを再生します。一方、意図的に send を 2 回呼べば 2 通送られます。この 2 つは異なる意図であり、区別されたままです。
自分の idempotency_key: を渡すと、この保証をプロセスをまたいで広げられます。クラッシュして再実行されたジョブは、送信を繰り返すのではなく再生します。再生の応答では replayed が true になり、保存されているメッセージが現在の状態で返ります。
invoice_id = "inv_2026_09_4192" sent = client.emails.send( from: "[email protected]", to: "[email protected]", subject: "Your September invoice", text: "Invoice attached.", idempotency_key: "invoice:#{invoice_id}") puts sent[:id], sent[:replayed]キーは、その送信が必要になった理由から導き出してください。時刻から作ってはいけません。異なるボディでキーを再利用すると、黙って再生されるのではなく 422 idempotency_key_reuse で拒否されます。キーは英字、数字、_、.、:、- からなる 1〜255 文字で、それ以外は 400 invalid_idempotency_key になります。
その他すべて
GET はすべてリトライされます。書き込みは、同一のリクエストを 2 回送っても 1 回目と異なる意味になり得ない場合に限ってリトライされます。送信がこれに当てはまるのは、冪等性キーによって繰り返しが再生に変わるからです。
| 呼び出し | リトライ | 理由 |
|---|---|---|
| すべての GET | する | 何も変化しないため。 |
| emails.send, emails.send_batch, templates.send, broadcasts.send | する | 冪等性キーが繰り返しを再生に変えるため。 |
| emails.cancel, emails.reschedule, broadcasts.cancel, forms.pause, forms.resume, threads.restore | する | 名前の付いた状態を設定するだけの操作であるため。 |
| threads.update, threads.trash | する | ラベルの設定であり、2 回適用することは 1 回適用することに等しいため。 |
| threads.snooze, threads.unsnooze | する | 復帰時刻はボディに含まれており、到着時刻から導出されないため。 |
| emails.update, labels.update, webhooks.update, settings.update, roles.update, members.update, domains.update, domains.update_address, domains.update_address_forward, contacts.update, audiences.update, keys.update, forms.update, branding.update, threads.update_note, chats.rename, account.set_email_notification, account.set_push_muted, app_host.set, workspaces.set_active | する | 名前の付いたフィールドを設定するだけの操作であるため。 |
| members.grant_address, members.grant_domain, rules.reorder, threads.reorder_notes | する | 付与は upsert であり、順序は完全な形で指定されます。 |
| templates.publish, forms.publish, imports.start | する | 公開済みのものを公開しても、開始済みのインポートを開始しても、変更されずにそのまま返ります。 |
| templates.preview, templates.render, broadcasts.preview, rules.test | する | 描画、集計または評価を行うだけで、何も書き込まないため。 |
| domains.verify, app_host.verify, senders.research | する | チェックを繰り返しても、変わるのは実行時刻だけです。 |
| contacts.save, contacts.set_audiences, contacts.remove_photo, contacts.block, contacts.unblock, contacts.delete_many, keys.revoke, files.revoke_link, files.revoke_all_links, app_host.delete, account.remove_photo, branding.remove_image, domains.remove_logo, domains.remove_logo_certificate, domains.remove_address_photo, account.accept_invitation, account.decline_invitation, forms.approve_submission, subscriptions.move | する | いずれも最終的な結果を指定する操作であり、2 回目の呼び出しは 1 回目が残した状態をそのまま残すため。 |
| audiences.add_contact, audiences.add_contacts, audiences.remove_contacts, audiences.import_contacts, suppressions.add, domains.create_address | する | 繰り返しの呼び出しは 1 回目の処理がすでに済んでいることを検出し、2 回行うのではなくそれを報告するため。 |
| contacts.set_photo, account.set_photo, branding.upload_image, domains.set_logo, domains.set_logo_certificate, domains.set_address_photo, imports.upload_chunk | する | 再送されたバイト列が、1 回目の試行で保存されたものを置き換えるため。 |
| drafts.create, labels.create, webhooks.create, templates.create, rules.create, roles.create, temp_mail.create, files.upload | しない | リトライするとオブジェクトが 2 つ残るため。 |
| drafts.update | しない | 送った id を再利用するのではなく、各書き込みの結果から id を読み取ること。 |
| drafts.delete, labels.delete, webhooks.delete, templates.delete, rules.delete, roles.delete, members.remove, members.revoke_address, temp_mail.delete, temp_mail.delete_message | しない | 応答が失われた後にリトライすると、成功した処理について失敗が報告されるため。 |
| webhooks.rotate_secret | しない | 2 回目のローテーションが、1 回目の試行で返されたシークレットを無効にするため。 |
| webhooks.test | しない | 2 つ目のテスト配信を送ってしまうため。 |
| webhooks.replay_delivery | しない | イベントが受信側にもう一度送られてしまうため。 |
| emails.translate, emails.compose, emails.rewrite, emails.suggest_subject | しない | どれもモデル呼び出しを消費するため、応答のなかったリクエストのあとにリトライすると、同じ答えに 2 回支払うことになります。 |
| security.begin_step_up, security.verify_step_up | しない | リトライによって 2 通目のメールが送られたり、コードの試行を 2 回分消費したりするおそれがあります。 |
| GET 以外のその他すべての呼び出し | しない | 1 回だけ送信され、失敗は繰り返されるのではなく報告されるため。 |
バックオフ
- クライアントの
max_retries:で上限が決まり、既定では追加で 2 回試行します。max_retries: 0でリトライを無効にします。 - ネットワーク障害か、
408、500、502、503、504の後だけです。429はRetry-Afterが付いている場合にだけリトライされますが、この API はそれを送らないため、レート制限はすぐに例外を送出します。それ以外のステータスは即座に例外を送出します。 - 0.5 秒から 8 秒までの指数バックオフで、ジッターを加えます。各待機はその上限の半分から上限までの間のランダムな値なので、多数のクライアントが復旧時に再び同期することはありません。
Retry-Afterがあればその指示に従う。delay-seconds 形式と HTTP-date 形式のどちらにも対応する。サーバーが待機時間を指定した場合、クライアントはバックオフではなくその時間だけ正確に待つ。- サーバーが 1 分を超える待機を求めた場合は、待機ではなく中止の指示として扱い、
retry_after_secondsを付けたエラーを送出します。求められたより早く戻ることは、その指示に従ったことにはなりません。 - タイムアウトも他と同じネットワーク障害なので、繰り返しても安全な呼び出しはタイムアウト後に再試行され、
timeout:は試行ごとに新たに適用されます。 - 待機は呼び出し内部の
sleepなので、呼び出し元のスレッドも待ちます。呼び出しが戻るか例外を送出するのは、最後の試行が終わってからです。