PHP
リトライと冪等性
何がリトライされ、何をあえてリトライしないのか、そしてリトライされた送信が重複しない理由。
送信
クライアントはすべての送信(emails->send、emails->sendBatch、templates->send、broadcasts->send)に Idempotency-Key を付与します。キーは呼び出しごとに oe- とランダムな UUID で 1 回生成され、その呼び出しのリトライで再利用されます。API は何かを配送する前にそのキーを確保するため、リトライは 2 通目を送るのではなく元のメッセージを再生します。一方、意図的に send を 2 回呼べば 2 通送られます。この 2 つは異なる意図であり、区別されたままです。
自分の idempotencyKey: を渡すと、この保証をプロセスをまたいで広げられます。クラッシュして再実行されたジョブは、送信を繰り返すのではなく再生します。再生の応答では replayed が true になり、保存されているメッセージが現在の状態で返ります。
$invoiceId = 'inv_2026_09_4192'; $sent = $client->emails->send([ 'from' => '[email protected]', 'to' => '[email protected]', 'subject' => 'Your September invoice', 'text' => 'Invoice attached.',], idempotencyKey: 'invoice:' . $invoiceId); echo $sent['id'], ' ', ($sent['replayed'] ?? false) ? 'replayed' : 'sent', PHP_EOL;キーは、その送信が必要になった理由から導き出してください。時刻から作ってはいけません。異なるボディでキーを再利用すると、黙って再生されるのではなく 422 idempotency_key_reuse で拒否されます。キーは英字、数字、_、.、:、- からなる 1〜255 文字で、それ以外は 400 invalid_idempotency_key になります。
その他すべて
GET はすべてリトライされます。書き込みは、同一のリクエストを 2 回送っても 1 回目と異なる意味になり得ない場合に限ってリトライされます。送信がこれに当てはまるのは、冪等性キーによって繰り返しが再生に変わるからです。
| 呼び出し | リトライ | 理由 |
|---|---|---|
| すべての GET | する | 何も変化しないため。 |
| emails->send, emails->sendBatch, 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->updateAddress, domains->updateAddressForward, contacts->update, audiences->update, keys->update, forms->update, branding->update, threads->updateNote, chats->rename, account->setEmailNotification, account->setPushMuted, appHost->set, workspaces->setActive | する | 名前の付いたフィールドを設定するだけの操作であるため。 |
| members->grantAddress, members->grantDomain, rules->reorder, threads->reorderNotes | する | 付与は upsert であり、順序は完全な形で指定されます。 |
| templates->publish, forms->publish, imports->start | する | 公開済みのものを公開しても、開始済みのインポートを開始しても、変更されずにそのまま返ります。 |
| templates->preview, templates->render, broadcasts->preview, rules->test | する | 描画、集計または評価を行うだけで、何も書き込まないため。 |
| domains->verify, appHost->verify, senders->research | する | チェックを繰り返しても、変わるのは実行時刻だけです。 |
| contacts->save, contacts->setAudiences, contacts->removePhoto, contacts->block, contacts->unblock, contacts->deleteMany, keys->revoke, files->revokeLink, files->revokeAllLinks, appHost->delete, account->removePhoto, branding->removeImage, domains->removeLogo, domains->removeLogoCertificate, domains->removeAddressPhoto, account->acceptInvitation, account->declineInvitation, forms->approveSubmission, subscriptions->move | する | いずれも最終的な結果を指定する操作であり、2 回目の呼び出しは 1 回目が残した状態をそのまま残すため。 |
| audiences->addContact, audiences->addContacts, audiences->removeContacts, audiences->importContacts, suppressions->add, domains->createAddress | する | 繰り返しの呼び出しは 1 回目の処理がすでに済んでいることを検出し、2 回行うのではなくそれを報告するため。 |
| contacts->setPhoto, account->setPhoto, branding->uploadImage, domains->setLogo, domains->setLogoCertificate, domains->setAddressPhoto, imports->uploadChunk | する | 再送されたバイト列が、1 回目の試行で保存されたものを置き換えるため。 |
| drafts->create, labels->create, webhooks->create, templates->create, rules->create, roles->create, tempMail->create, files->upload | しない | リトライするとオブジェクトが 2 つ残るため。 |
| drafts->update | しない | 送った id を再利用するのではなく、各書き込みの結果から id を読み取ること。 |
| drafts->delete, labels->delete, webhooks->delete, templates->delete, rules->delete, roles->delete, members->remove, members->revokeAddress, tempMail->delete, tempMail->deleteMessage | しない | 応答が失われた後にリトライすると、成功した処理について失敗が報告されるため。 |
| webhooks->rotateSecret | しない | 2 回目のローテーションが、1 回目の試行で返されたシークレットを無効にするため。 |
| webhooks->test | しない | 2 つ目のテスト配信を送ってしまうため。 |
| webhooks->replayDelivery | しない | イベントが受信側にもう一度送られてしまうため。 |
| emails->translate, emails->compose, emails->rewrite, emails->suggestSubject | しない | どれもモデル呼び出しを消費するため、応答のなかったリクエストのあとにリトライすると、同じ答えに 2 回支払うことになります。 |
| security->beginStepUp, security->verifyStepUp | しない | リトライによって 2 通目のメールが送られたり、コードの試行を 2 回分消費したりするおそれがあります。 |
| GET 以外のその他すべての呼び出し | しない | 1 回だけ送信され、失敗は繰り返されるのではなく報告されるため。 |
バックオフ
- クライアントの
maxRetries:で上限が決まり、既定では追加で 2 回試行します。maxRetries: 0でリトライを無効にします。 - リトライするのは、ネットワーク障害の後、または
408、500、502、503、504が返った場合だけである。429はRetry-Afterが付いている場合にのみリトライされるが、この API はそれを送らないため、レート制限は即座に例外となる。その他のステータスも直ちに例外となる。 - 0.5 秒から 8 秒までの指数バックオフで、ジッターを加えます。各待機はその上限の半分から上限までの間のランダムな値なので、多数のクライアントが復旧時に再び同期することはありません。
Retry-Afterがあればその指示に従う。delay-seconds 形式と HTTP-date 形式のどちらにも対応する。サーバーが待機時間を指定した場合、クライアントはバックオフではなくその時間だけ正確に待つ。- サーバーが 1 分を超える待機を求めた場合は、待機ではなく中止の指示として扱い、
retryAfterSecondsを付けた例外をスローします。求められたより早く戻ることは、その指示に従ったことにはなりません。 - タイムアウトも他と同じネットワーク障害なので、繰り返しても安全な呼び出しはタイムアウト後に再試行され、
timeout:は試行ごとに新たに適用されます。 - 待機は呼び出し内部の
sleepとusleepなので、スクリプトも待ちます。呼び出しが戻るか例外をスローするのは、最後の試行が終わってからです。人が待っている Web リクエストの中では、timeout:とmaxRetries:を小さく保ってください。