Повторы и идемпотентность
Что повторяется, что намеренно не повторяется и почему повторённая отправка не может продублировать письмо.
Отправки
Клиент добавляет Idempotency-Key к каждой отправке (emails.send, emails.send_batch, templates.send и broadcasts.send); ключ генерируется один раз на **вызов** и переиспользуется повторами этого вызова. API занимает этот ключ до того, как что-либо отправит, поэтому повтор воспроизводит исходное письмо, а не отправляет второе, тогда как два намеренных вызова send() по-прежнему отправляют дважды. Это разные намерения, и они остаются разными.
Передайте собственный idempotency_key, чтобы растянуть эту гарантию на несколько процессов: задача, которая упала и запустилась снова, воспроизведёт свои отправки, а не повторит их.
invoice_id = 'inv_4192' client.emails.send( {'from': sender, 'to': recipient, 'subject': subject, 'text': text}, idempotency_key=f'invoice:{invoice_id}',)Выводите его из того, что сделало отправку необходимой. Никогда не выводите его из часов. Переиспользование ключа с другим телом отклоняется с idempotency_key_reuse, а не воспроизводится молча.
Всё остальное
Любое чтение повторяется. Запись повторяется только там, где второй идентичный запрос не может означать ничего иного, чем первый, а отправка подходит под это, потому что её ключ идемпотентности превращает повтор в воспроизведение.
| Вызов | Повторяется | Почему |
|---|---|---|
| Любое чтение | Да | Ничего не меняется. |
| emails.send, emails.send_batch, templates.send и broadcasts.send | Да | Ключ идемпотентности превращает повтор в воспроизведение. |
| emails.cancel, emails.reschedule, broadcasts.cancel, forms.publish, forms.pause, forms.resume, forms.approve_submission, account.accept_invitation и account.decline_invitation | Да | Чистая установка названного состояния. |
| threads.update, threads.trash и threads.restore | Да | Установка набора ярлыков. Применить её дважды равносильно тому, чтобы применить её один раз. |
| threads.snooze, threads.unsnooze | Да | Момент пробуждения задан в теле, а не выводится из времени поступления запроса. |
| labels.update, webhooks.update, settings.update, roles.update, members.update, domains.update, contacts.update, audiences.update, keys.update, emails.update, forms.update, branding.update, chats.rename, threads.update_note, domains.update_address, domains.update_address_forward, 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, imports.start | Да | Публикация уже опубликованной вершины или запуск уже запущенного импорта возвращает объект без изменений. |
| templates.preview, templates.render, broadcasts.preview и rules.test | Да | Они выполняют отрисовку, подсчёт или вычисление и ничего не записывают. |
| domains.verify, app_host.verify | Да | Повторная проверка меняет только время проверки. |
| contacts.save, contacts.set_audiences, contacts.remove_photo, contacts.block, contacts.unblock, contacts.delete_many, keys.revoke, account.remove_photo, branding.remove_image, domains.remove_logo, domains.remove_logo_certificate, domains.remove_address_photo, app_host.delete, files.revoke_link, files.revoke_all_links и subscriptions.move | Да | Каждый задаёт конечный результат, поэтому второй вызов оставляет то же, что оставил первый. |
| audiences.add_contact, audiences.add_contacts, audiences.remove_contacts, audiences.import_contacts, suppressions.add, domains.create_address и senders.research | Да | Повтор обнаруживает, что работа первого вызова уже сделана, и сообщает об этом, а не выполняет её дважды. |
| contacts.set_photo, imports.upload_chunk, account.set_photo, branding.upload_image, domains.set_logo, domains.set_logo_certificate и domains.set_address_photo | Да | Повторно отправленные байты заменяют то, что сохранила первая попытка. |
| drafts.create, labels.create, webhooks.create, templates.create, rules.create, roles.create, temp_mail.create, files.upload, templates.design и forms.design | Нет | Повтор оставит два объекта. |
| drafts.update | Нет | Читайте идентификатор из результата каждой записи, а не переиспользуйте тот, что отправили. |
| 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 | Нет | Вторая ротация обесценивает секрет, который вернула первая попытка. |
| webhooks.test | Нет | Он отправил бы вторую синтетическую доставку. |
| webhooks.replay_delivery | Нет | Он отправил бы событие вашему получателю второй раз. |
| emails.translate | Нет | Он тратит вызовы модели, поэтому повтор после оставшегося без ответа запроса оплачивает один и тот же ответ дважды. |
| emails.compose, emails.rewrite и emails.suggest_subject | Нет | Каждая попытка тратит ещё одно действие ИИ и возвращает другой ответ. |
| Любой другой вызов | Нет | Отправляется один раз, и о сбое сообщается, а не повторяется запрос. |
forms.update повторяется даже с expectedUpdatedAt, так что повтор после потерянного ответа может вернуть 409 version_conflict: первая попытка уже прошла. Перечитайте форму, прежде чем пробовать снова.
client.raw.request повторяет GET, а всё остальное отправляет один раз, если вы не передали repeatable=True.
Схема отката
- Ограничено параметром
max_retriesна клиенте, по умолчанию допускаются две дополнительные попытки. - Только после сетевого сбоя или
408,500,502,503либо504.429повторяется, только если он несётRetry-After, а этот API его не присылает, поэтому превышение лимита сразу выбрасывает исключение. Любой другой статус выбрасывает исключение немедленно. - Экспоненциально от половины секунды до восьми, с разбросом, чтобы парк машин не синхронизировался заново на восстановлении.
- Темп задаётся заголовком
Retry-Afterв любой из его форм: delay-seconds и HTTP-date. Когда сервер называет время ожидания, клиент ждёт ровно столько, а не отступает по своей схеме. - Сервер, просящий подождать дольше минуты, трактуется как велящий клиенту остановиться, а не поспать, поэтому ошибка выбрасывается с полем
retry_after_seconds. Вернуться раньше, чем он просил, означает не выполнить его просьбу. timeoutограничивает каждую попытку, поэтому вызов, который использует оба своих повтора, может занять три таймаута плюс паузы между ними.- Отменённый вызов
AsyncOpenEmailникогда не повторяется. Отмена распространяется сразу, из запроса или из ожидания перед следующей попыткой.