Повторы и идемпотентность
Что повторяется, что намеренно не повторяется и почему повторённая отправка не может продублировать письмо.
Отправки
Клиент добавляет Idempotency-Key к каждой отправке (emails.send, emails.sendBatch и templates.send); ключ генерируется один раз на **вызов** и переиспользуется повторами этого вызова. API занимает этот ключ до того, как что-либо отправит, поэтому повтор воспроизводит исходное письмо, а не отправляет второе, тогда как два намеренных вызова send() по-прежнему отправляют дважды. Это разные намерения, и они остаются разными.
Передайте собственный idempotencyKey, чтобы растянуть эту гарантию на несколько процессов: задача, которая упала и запустилась снова, воспроизведёт свои отправки, а не повторит их.
await openemail.emails.send(message, { idempotencyKey: `invoice:${invoice.id}` })Выводите его из того, что сделало отправку необходимой. Никогда — из часов. Переиспользование ключа с другим телом отклоняется с idempotency_key_reuse, а не воспроизводится молча.
Всё остальное
Любое чтение повторяется. Запись повторяется только там, где второй идентичный запрос не может означать ничего иного, чем первый, а отправка подходит под это, потому что её ключ идемпотентности превращает повтор в воспроизведение.
| Вызов | Повторяется | Почему |
|---|---|---|
| Любое чтение | Да | Ничего не меняется. |
| `emails.send`, `emails.sendBatch`, `templates.send` | Да | Ключ идемпотентности превращает повтор в воспроизведение. |
| `emails.cancel`, `emails.reschedule` | Да | Чистая установка названного состояния. |
| `threads.update`, `threads.trash` | Да | Установка набора ярлыков. Применить её дважды — то же самое, что применить один раз. |
| `threads.snooze`, `threads.unsnooze` | Да | Момент пробуждения задан в теле, а не выводится из времени поступления запроса. |
| `labels.update`, `webhooks.update`, `settings.update`, `roles.update`, `members.update` | Да | Чистая установка названных полей. |
| `members.grantAddress`, `rules.reorder` | Да | Выдача выполняется как upsert, а порядок задан полностью. |
| `templates.publish` | Да | Публикация вершины, которая уже опубликована, возвращает её без изменений. |
| `templates.preview`, `rules.test` | Да | Они выполняют отрисовку или вычисление и ничего не записывают. |
| `drafts.create`, `labels.create`, `webhooks.create`, `templates.create`, `rules.create`, `roles.create`, `tempMail.create` | Нет | Повтор оставит два объекта. |
| `drafts.update` | Нет | Читайте идентификатор из результата каждой записи, а не переиспользуйте тот, что отправили. |
| `drafts.delete`, `labels.delete`, `webhooks.delete`, `templates.delete`, `rules.delete`, `roles.delete`, `members.remove`, `members.revokeAddress`, `tempMail.delete`, `tempMail.deleteMessage` | Нет | Повтор после потерянного ответа сообщит об ошибке для работы, которая удалась. |
| `webhooks.rotateSecret` | Нет | Вторая ротация обесценивает секрет, который вернула первая попытка. |
| `webhooks.test` | Нет | Он отправил бы вторую синтетическую доставку. |
| `emails.translate` | Нет | Он тратит вызовы модели, поэтому повтор после оставшегося без ответа запроса оплачивает один и тот же ответ дважды. |
| Любая другая запись | Нет | Отправляется один раз, и о сбое сообщается, а не повторяется запрос. |
Схема отката
- Ограничено параметром
maxRetriesна клиенте; по умолчанию — две дополнительные попытки. - Только после сетевого сбоя или
408,500,502,503либо504.429повторяется, только если он несётRetry-After, а этот API его не присылает, поэтому превышение лимита сразу выбрасывает исключение. Любой другой статус выбрасывает исключение немедленно. - Экспоненциально от половины секунды до восьми, с разбросом, чтобы парк машин не синхронизировался заново на восстановлении.
- Темп задаётся заголовком
Retry-Afterв любой из его форм — delay-seconds и HTTP-date. Когда сервер называет время ожидания, клиент ждёт ровно столько, а не отступает по своей схеме. - Сервер, просящий подождать дольше минуты, трактуется как велящий клиенту остановиться, а не поспать, поэтому ошибка выбрасывается с полем
retryAfterSeconds. Вернуться раньше, чем он просил, — значит не выполнить его просьбу. AbortSignalвызывающего никогда не приводит к повтору. Прерывание немедленно выбрасываетOpenEmailNetworkError— из запроса или из ожидания перед следующей попыткой.