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'і вважайте це в бек-офф.
При високих «5хх »/таймаутах - знижуйте частоту ретраїв і загальний паралелізм.

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 = не повторювати, 5хх/таймаут = повторити.

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).

Натискаючи кнопку, ви погоджуєтесь на обробку даних.