Гарантії доставки вебхуків
Вебхукі - асинхронні повідомлення «з системи до передплатника» по HTTP (S). Мережа ненадійна: відповіді втрачаються, пакети приходять дублікатами або поза порядком. Тому гарантії доставки будуються не «по TCP», а на рівні протоколу вебхуків і доменної ідемпотентності.
Ключова мета: забезпечити at-least-once доставку з порядком по ключу (там, де необхідно), дати передплатнику матеріали для ідемпотентної обробки та інструмент reconcile для відновлень.
1) Рівні гарантій
Best-effort - одноразова спроба, без ретраїв. Прийнятно тільки для «неважливих» подій.
At-least-once (рекомендується) - можливі дублікати і out-of-order, але подія буде доставлена за умови доступності передплатника в розумний термін.
Effectively-exactly-once (на рівні ефекту) - досягається комбінацією ідемпотентності і dedup-сховища на стороні передплатника/відправника. На транспорті HTTP «exactly-once» неможливо.
2) Контракт вебхука: мінімально необхідне
Заголовки (приклад):
X-Webhook-Id: 5d1e6a1b-4f7d-4a3d-8b3a-6c2b2f0f3f21 # глобальный ID события
X-Delivery-Attempt: 3 # номер попытки
X-Event-Type: payment.authorized.v1 # тип/версия
X-Event-Time: 2025-10-31T12:34:56Z # ISO8601
X-Partition-Key: psp_tx_987654 # ключ порядка
X-Seq: 418 # монотонный номер по ключу
X-Signature-Alg: HMAC-SHA256
X-Signature: t=1730378096,v1=hex(hmac(secret, t body))
Content-Type: application/json
Тіло (приклад):
json
{
"id": "5d1e6a1b-4f7d-4a3d-8b3a-6c2b2f0f3f21",
"type": "payment.authorized.v1",
"occurred_at": "2025-10-31T12:34:56Z",
"partition_key": "psp_tx_987654",
"sequence": 418,
"data": {
"payment_id": "psp_tx_987654",
"amount": "10.00",
"currency": "EUR",
"status": "AUTHORIZED"
},
"schema_version": 1
}
Вимога до одержувача: відповідати швидко «2xx» після буферизації і валідації підпису, а бізнес-обробку робити асинхронно.
3) Порядок і причинність
Порядок за ключем: гарантія «не поїде» тільки всередині одного'partition _ key'( напр.,'player _ id','wallet _ id','psp _ tx _ id').
Глобальний порядок не гарантується.
На стороні відправника - черга з серіалізацією по ключу (один споживач/шардинг), на стороні одержувача - inbox з'( source, event_id)'і опціонально очікування пропущених'seq'.
Якщо пропуски критичні - надайте pull-API'GET/events? after = checkpoint'для статусу «наздогнати і звіритися».
4) Ідемпотентність і дедуплікація
Кожен вебхук несе стабільний'X-Webhook-Id'.
Одержувач зберігає'inbox (event_id)': PK — `source + event_id`; повтори → no-op.
Побічні ефекти (запис в БД/гаманець) виконуються тільки один раз при першому «баченні» події.
Для команд «з ефектом» використовуйте Idempotency-Key і кеш результатів на час вікна ретраїв.
5) Ретраї, backoff і вікна
Політика ретраїв (референс):- Ретраїти на'5xx/timeout/connection error/409-Conflict (retryable )/429'.
- Не ретраїти на «4xx» крім «409/423/429» (і тільки при узгодженій семантиці).
- Експоненціальний backoff + full jitter: 0. 5s, 1s, 2s, 4s, 8s, … до'max = 10-15 хв'; TTL вікна ретраїв: наприклад, 72 години.
- Поважати'Retry-After'у одержувача.
- Мати загальний дедлайн: «визнати подію не доставленою» і перевести її в DLQ.
yaml retry:
initial_ms: 500 multiplier: 2.0 jitter: full max_delay_ms: 900000 ttl: 72h retry_on: [TIMEOUT, 5xx, 429]
6) DLQ и redrive
DLQ - «кладовище» отруйних або закінчених по TTL подій з повною метаінформацією (пейлоад, заголовки, помилки, спроби, хеші).
Веб-консоль/API для redrive (точкова повторна доставка) з опціональною правкою endpoint/секрету.
Rate-limited redrive і batch-redrive з пріоритезацією.
7) Безпека
mTLS (по можливості) або TLS 1. 2+.
Підпис тіла (HMAC з секретом per tenant/endpoint). Верифікація:1. Витягти't'( timestamp) з заголовка, перевірити ковзне вікно (наприклад, ± 5 хв).
8) Квоти, rate limits і справедливість
Fair-Queue per tenant/subscriber: щоб один підписник/тенант не забив загальний пул.
Квоти і burst-ліміти на вихідний трафік і per-endpoint.
Реакція на «429»: шанувати'Retry-After', тротлити потік; при тривалому лімітуванні - degrade (відправка тільки критичних типів подій).
9) Життєвий цикл підписки
Register/Verify: POST endpoint → challenge/response або out-of-band підтвердження.
Lease (за бажанням): підпис діє до «valid _ to»; пролонгація - явна.
Secret rotation: `current_secret`, `next_secret` с `switch_at`.
Test ping: штучна подія для перевірки маршруту перед включенням основних топіків.
Health-проби: періодичні HEAD/GET з перевіркою latency і TLS профілю.
10) Еволюція схем (версії подій)
Версіонування типу події: `payment. authorized. v1` → `…v2`.
Еволюція - additive (нові поля → MINOR версії API), breaking → новий тип.
Регістр схем (JSON-Schema/Avro/Protobuf) + автоматична валідація перед відправкою.
Заголовок'X-Event-Type'і поле'schema _ version'в тілі - обидва обов'язкові.
11) Спостережуваність і SLO
Метрики (за типом/тенанту/передплатнику):- `deliveries_total`, `2xx/4xx/5xx_rate`, `timeout_rate`, `signature_fail_rate`.
- 'attempts _ avg','p50/p95/p99 _ delivery _ latency _ ms'( від публікації до 2xx).
- `dedup_rate`, `out_of_order_rate`, `dlq_rate`, `redrive_success_rate`.
- `queue_depth`, `oldest_in_queue_ms`, `throttle_events`.
- Частка доставок ≤ 60 c (p95) - 99. 5% для критичних подій.
- DLQ ≤ 0. 1% за 24 год.
- Signature failures ≤ 0. 05%.
Логи/трейсинг: `event_id`, `partition_key`, `seq`, `attempt`, `endpoint`, `tenant_id`, `schema_version`, `trace_id`.
12) Референсний алгоритм відправника
1. Записати подію в транзакційний outbox.
2. Визначити partition_key і seq; помістити в чергу.
3. Воркер бере по ключу, формує запит, підписує, відправляє з таймаутами (connect/read).
4. При «2xx» - визнати доставленим, зафіксувати латентність і seq-чекпоінт.
5. При'429/5xx/timeout'- ретрай згідно з політикою.
6. По TTL → DLQ і алерт.
13) Референсний обробник (одержувач)
1. Прийняти запит, перевірити TLS/proto.
2. Валідація підпису і вікна часу.
3. Швидкий ACK 2xx (після синхронного запису в локальний inbox/чергу).
4. Асинхронний воркер читає «inbox», перевіряє «event _ id» (дедуп), при необхідності - впорядковує по «seq» всередині «partition _ key».
5. Виконує ефекти, пише «offset/seq чекпоінт» для reconcile.
6. У разі помилки - локальні ретраї; «отруйні» завдання → локальна DLQ з алертами.
14) Reconcile (пулл-контур)
Для «непробивних» інцидентів:- `GET /events? partition_key=...&after_seq=...&limit=...' - віддати всі пропущені.
- Токен-чекпоінт: 'after = opaque _ token'замість seq.
- Ідемпотентний redelivery: ті ж'event _ id', той же підпис по новому't'.
15) Корисні заголовки та коди
2xx - прийняв (навіть якщо бізнес-обробка пізніше).
410 Gone - endpoint закритий (відправник припиняє доставку і позначає підписку як «в архів»).
409/423 - тимчасове блокування ресурсу → ретрай розумний.
429 - занадто часто → троттл і backoff.
400/401/403/404 - конфігураційна помилка; зупинити ретраї, відкрити тікет.
16) Мульти-тенант і регіони
Окремі черги та ліміти per tenant/endpoint.
Data residency: відправка з регіону даних; наскрізні заголовки'X-Tenant','X-Region'.
Ізоляція збоїв: падіння одного підписувача не впливає на інших (separate pools).
17) Тестування
Contract tests: фіксовані приклади тіл/підписів, перевірка валідації.
Chaos: дроп/дублікати, shuffle порядку, затримки мережі,'RST','TLS'-помилки.
Load: burst-шторму, завмер p95/p99.
Security: анти-реплей, застарілі timestamp, неправильні секрети, ротація.
DR/Replay: масовий redrive з DLQ в ізольованому стенді.
18) Плейбуки (runbooks)
1. Зростання'signature _ fail _ rate '
Перевірити дрейф годинника, що минув'tolerance', ротацію секретів; тимчасово увімкнути «dual secret».
2. Черга старіє ('oldest _ in _ queue _ ms'↑)
Збільшити воркери, включити пріоритизацію критичних топіків, тимчасово знизити частоту «галасливих» типів.
3. Шторм «429» у передплатника
Увімкнути тротлінг і паузи між спробами; зрушити менш критичні типи подій.
4. Масові'5xx '
Відкрити circuit-breaker для конкретного endpoint, перевести в режим defer & batch; сигнал передплатнику.
5. Заповнення DLQ
Зупинити не-критичну публікацію, включити batch-redrive з низьким RPS, підняти алерти власникам підписок.
19) Типові помилки
Синхронна важка обробка до відповіді 2xx → ретраї і дублікати.
Немає підпису тіла/вікна часу → вразливість до підміни/реплею.
Відсутність'event _ id'і'inbox'→ неможливо зробити ідемпотентність.
Спроба «глобального порядку» → вічні блокування черг.
Ретраї без jitter/лімітів → посилення інциденту (thundering herd).
Єдиний загальний пул на всіх передплатників → «галасливий» кладе всіх.
20) Чек-лист перед продом
- Контракт: `event_id`, `partition_key`, `seq`, `event_type. vN', підпис HMAC і timestamp.
- Відправник: outbox, серіалізація за ключем, ретраї з backoff + jitter, TTL, DLQ і redrive.
- Одержувач: швидкий запис в inbox + 2xx; ідемпотентна обробка; локальна DLQ.
- Безпека: TLS, підписи, анти-реплей, dual-secret, ротація.
- Квоти/ліміти: fair-queue per tenant/endpoint, повага'Retry-After'.
- Reconcile API і чекпоінти; документація для передплатників.
- Спостережуваність: p95/потоки/помилки/DLQ, трасування по'event _ id'.
- Версіонування подій і політика еволюції схем.
- Плейбуки інцидентів і «кнопка» глобального пауза/розморожування.
Висновок
Надійні вебхуки - це протокол поверх HTTP, а не просто «POST з JSON». Чіткий контракт (ID, ключ порядку, підпис), ідемпотентність, ретраї з jitter, справедлива черга і добре налагоджені плейбуки перетворюють «кращий-випадок» в передбачуваний і вимірюваний механізм доставки. Будуйте at-least-once + порядок за ключем + reconcile, і система спокійно переживе мережу, піки навантаження і людські помилки.