GH GambleHub

Стратегии повторов и идемпотентность

1) Зачем это нужно

В сетях сбои — норма: таймауты, transient-ошибки, сетевые флаппинги, перегрузка. Ретраи повышают надежность только если:

1. повтор безопасен (идемпотентен),

2. соблюдаются выдержки между повторами,

3. уважаются лимиты/квоты и «здоровье» зависимостей.

Цель — effectively-once поведение на уровне бизнес-операций без ложных дублей и гонок.

2) Таксономия семантики доставки

At-most-once: без повторов, риск потери (логирование, fire-and-forget).
At-least-once: возможны дубликаты → нужна идемпотентность потребителя (большинство очередей, вебхуки).
Effectively-once: дубликаты возможны, но корректно дедуплицируются (ключи, транзакции, outbox).

3) Когда ретраить, а когда нет

Ретраить имеет смысл: `408`, `429` (соблюдая `Retry-After`), `425` (Too Early), `499` (client closed на периметре), `5xx`, `504`, сетевые таймауты/разрывы, `502` у шлюза, «connection reset».
Не ретраим без изменения запроса: `400/401/403/404/422`.
Спорные кейсы: `409 Conflict` (обычно не ретраим; сначала читаем статус операции/переподтверждаем намерение).

4) Таймауты, backoff и джиттер

4.1 Правила

Сначала таймаут, потом ретраии: у каждого запроса должен быть «deadline».
Exponential backoff: `delay_n = base 2^n`, ограничиваем `max_delay`.
Jitter обязателен: добавляйте случайность для развязки «тупых синхронных волн».

4.2 Шаблоны джиттера

Full jitter: `sleep = rand(0, base2^n)` — лучший общий выбор.
Decorrelated jitter: `sleep = min(max_delay, rand(base, sleep_prev3))` — для долгих диалогов.
Equal jitter: `sleep = base2^n/2 + rand(0, base2^n/2)` — мягкая вариация.

4.3 Retry-budget

Ограничьте долю ретраев:
  • `retry_budget_per_min = max(α success_rps, floor β)`; обычно `α = 0.1–0.2`.
  • При исчерпании бюджета — переключаемся на fail-fast/circuit breaker «open».

5) Взаимодействие с rate limiting и Circuit Breaker

Уважайте `Retry-After`, `RateLimit-Reset` и считайте это в бэк-офф.
При высоких `5xx`/таймаутах — понижайте частоту ретраев и общий параллелизм.

Circuit breaker:
  • Half-open: допускает ограниченную пробу.
  • Open: мгновенно отклоняет (экономит ресурс).
  • Closed: обычная работа.
  • На write-операциях предпочтительнее вернуть 409/503 с четкой подсказкой, чем крутить агрессивные ретраи.

6) Идемпотентность write-операций

6.1 Общая идея

Одинаковые намерения → один результат. Основа — ключ идемпотентности и хранилище записей исполнения.

6.2 HTTP-контракт

Клиент посылает заголовок:

Idempotency-Key: 7a6b7f9e-2a46-4d0b-9c3a-2b30e1c3c9e3
Idempotency-Key-Expiry: 24h # optional
Сервер:
  • при первом успешном выполнении сохраняет (ключ → результат, статус, хэш тела);
  • при повторе возвращает прежний ответ и заголовок `Idempotency-Replay: true`;
  • при конфликте тела (тот же ключ, но другой payload) — `409 Conflict`.

6.3 Хранилище и TTL

Таблица/ключ-значение: `idempotency_key`, `request_hash`, `result`, `status`, `expiry_at`.
TTL = окно возможных повторов и поздних доставок (обычно 24–72 ч для платежей).
Индексы по `idempotency_key`; для высокой нагрузки — шардирование по хешу.

6.4 Пример схемы (SQL)

sql
CREATE TABLE idempo_store (
key UUID PRIMARY KEY,
req_hash BYTEA NOT NULL,
status INT NOT NULL,
response JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expiry_at TIMESTAMPTZ NOT NULL
);

6.5 Псевдокод обработчика

pseudo handle_write(req):
k = req. headers["Idempotency-Key"]
h = hash(req. body)
rec = idempo_store. get(k)

if rec and rec. req_hash == h:
return rec. status, rec. response, {"Idempotency-Replay": "true"}

if rec and rec. req_hash!= h:
return 409, problem("IDEMPOTENT_CONFLICT")

begin tx result = apply_business_mutation (req) # change status upsert once (idempo_store, key = k, req_hash=h, status = 201, response = result, expiry = now () + 2d)
commit

return 201, result

7) Паттерны «effectively-once»

Transactional Outbox: запись бизнес-события и отправка сообщения из той же БД-транзакции через фонового релэйера; потребитель идемпотентен.
Inbox/Processed-table у потребителя: сохраняем `event_id`, чтобы игнорировать дубли.
Exactly-once на Kafka ≠ exactly-once в бизнесе: даже при EOS продюсера/консьюмера прикладная логика все равно должна быть идемпотентной.
Компенсирующие транзакции (Saga): если шаги ретраятся и вызывают побочные эффекты, возвращаем систему к инварианту.

8) Частные случаи: платежи и финансовые операции

Strong idempotency: ключ привязан к логике операции (например, `external_payment_id`).
Дедупликация на PSP: храните `merchant_reference` → при повторе PSP вернет прежний результат.
Ретраи «от клиента»: разрешать только при `Idempotency-Key`, иначе риск двойного списания.
Конкурентность: блокировки «на аккаунт/инструмент/договор» на время исполнения; при повторе возвращайте 409/423.
Наблюдаемость: метрики `idempo_replay_total`, `idempo_conflict_total`.

9) Вебхуки и внешние вызовы

Подписи HMAC и окно времени; сначала проверка, потом обработка.
Ретраи отправителя: экспоненциальный backoff + джиттер, `max_attempts` и DLQ.
Потребитель — идемпотентный: `event_id` → таблица/in-memory cache; «аккуратный» порядок не гарантирован.
Коды: 2xx = успешно, 4xx = не повторять, 5xx/таймаут = повторить.

10) Очереди и фоновые задачи

At-least-once по умолчанию → дубликаты неизбежны.
Храните `task_id`/`event_id` и статус выполнения; при дубли — короткий путь «replay».
DLQ и poison-messages: счетчик попыток, карантин, ручной разбор.
Конкурентные лимиты (семафоры) и идемпотентные воркеры.

11) Версионирование и «натуральные» ключи

Натуральные ключи (номер счета + дата + номер документа) повышают устойчивость к повторам.
При смене схемы/версии включайте ключ версии в `Idempotency-Key` или в хэш запроса.

12) HTTP-заголовки и подсказки клиенту

`Idempotency-Key`, `Idempotency-Replay`, `Retry-After`, `Prefer: wait=<sec>` (на долгих операциях), `If-Match`/`ETag` (оптимистические блокировки).
409 при конфликте ключа, 425/429/503 с валидным `Retry-After`.
Для «длинных» операций — прием асинхронного статуса (`202 Accepted` + `Location` на ресурс статуса).

13) Тестирование и хаос-сценарии

Negative-тесты: двойная отправка, повтор с другим телом, рассинхрон часов.
Нарушение порядка: `t2` приходит раньше `t1`.
Инъекция таймаутов/`RST`/`EOF`, половинчатые запросы (slow-POST).
Упавшее хранилище idempotency → поведение fail-closed (лучше отказ, чем двойное списание).

14) Метрики и алерты

`retries_total{reason}`, `retry_budget_used{route}`, `backoff_seconds_bucket`.
`idempo_replay_total`, `idempo_conflict_total`, `duplicate_detected_total`.
Доля 409/425/429/5xx по маршрутам; p95/p99 «времени до успеха» с ретраями.
Алерты: burn-rate бюджета ретраев, всплеск конфликтов идемпотентности, рост DLQ.

15) Антипаттерны

Ретраить все ошибки подряд.
Отсутствие джиттера → синхронные волны ретраев.
Долгоживущие ключи без TTL и очистки.
Сохранение результата после коммита побочного эффекта (нарушение outbox).
Логи без `trace_id`/`idempotency_key` → невозможна форензика.
Агрессивные параллельные ретраи на write-операциях.

16) Чек-лист prod-готовности

  • Единая политика: что ретраим, что нет; коды и подсказки клиенту.
  • Экспоненциальный backoff + full jitter; задан `retry_budget`.
  • Контракт `Idempotency-Key` + хранение результатов с TTL.
  • Outbox/Inbox для событий; DLQ; лимиты конкурентности.
  • Интеграция с circuit breaker, respect `Retry-After`.
  • Метрики/алерты по ретраям/дубликатам/конфликтам.
  • Набор хаос-тестов и эмуляция сетевых сбоев.
  • Документация для клиентов: примеры бэк-оффов и статусы.

17) TL;DR

Ретраи полезны только вместе с идемпотентностью. Вводите `Idempotency-Key` и хранилище результатов, применяйте экспоненциальный backoff с джиттером и retry-budget, уважайте `Retry-After`, интегрируйтесь с circuit breaker. Для событий — outbox/inbox; для платежей — строгая дедупликация и блокировки. Меряйте ретраи и конфликты, тестируйте дубликаты и таймауты.

Contact

Свяжитесь с нами

Обращайтесь по любым вопросам или за поддержкой.Мы всегда готовы помочь!

Telegram
@Gamble_GC
Начать интеграцию

Email — обязателен. Telegram или WhatsApp — по желанию.

Ваше имя необязательно
Email необязательно
Тема необязательно
Сообщение необязательно
Telegram необязательно
@
Если укажете Telegram — мы ответим и там, в дополнение к Email.
WhatsApp необязательно
Формат: +код страны и номер (например, +380XXXXXXXXX).

Нажимая кнопку, вы соглашаетесь на обработку данных.