Перейти к документации
Python

Повторы и идемпотентность

Что повторяется, что намеренно не повторяется и почему повторённая отправка не может продублировать письмо.

Отправки

Клиент добавляет Idempotency-Key к каждой отправке (emails.send, emails.send_batch, templates.send и broadcasts.send); ключ генерируется один раз на **вызов** и переиспользуется повторами этого вызова. API занимает этот ключ до того, как что-либо отправит, поэтому повтор воспроизводит исходное письмо, а не отправляет второе, тогда как два намеренных вызова send() по-прежнему отправляют дважды. Это разные намерения, и они остаются разными.

Передайте собственный idempotency_key, чтобы растянуть эту гарантию на несколько процессов: задача, которая упала и запустилась снова, воспроизведёт свои отправки, а не повторит их.

idempotency.py
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 никогда не повторяется. Отмена распространяется сразу, из запроса или из ожидания перед следующей попыткой.