GH GambleHub

Exactly-once семантика

Що таке exactly-once насправді

Під «exactly-once» часто розуміють дві різні речі:
  • Доставка: повідомлення буде доставлено споживачеві рівно один раз.
  • Обробка: кінцевий побічний ефект (запис в БД, зміна балансу, емісія іншої події) відбудеться рівно один раз, навіть якщо доставок або спроб було більше.

У розподілених системах надійніше говорити про семантику обробки. Доставку рівно один раз забезпечити важко (дублікати і повтори неминучі), але можна зробити так, щоб підсумковий стан було еквівалентно єдиній обробці.


Коли EOS потрібна, а коли ні

Потрібно EOS, якщо:
  • Грошові транзакції та баланси: подвійне списання неприпустимо.
  • Облік ліцензій/квот, білінгові лічильники.
  • Незворотні зовнішні виклики (наприклад, одноразова активація ключа).
Можна обійтися at-least-once + ідемпотентність, якщо:
  • Ефекти оборотні або компенсовані (саги, повернення).
  • Допускаються тимчасові дублікати у вітринах/логах.
  • Дешевше забезпечити ідемпотентний sink, ніж тягнути транзакції через весь тракт.

Модель: end-to-end vs. hop-by-hop

Hop-by-hop EOS: кожна ділянка (джерело → процесор → приймач) гарантує, що свою дію він застосує рівно один раз.
End-to-end EOS: весь ланцюжок гарантує, що від «факту» до «сайда-ефекту» результат еквівалентний єдиній обробці.

На практиці end-to-end досягається комбінацією транзакцій і/або ідемпотентності на кожному хопі.


Базові будівельні блоки

1. Ідемпотентні операції

Повтор одного і того ж запиту по ключу операції дає той же результат.

Ключі: `idempotency_key`/`event_id`/`operation_id`.
Реалізація: таблиця «побачених» операцій з TTL ≥ ретенції вхідного логу.

2. Транзакції «читаю-обробляю-пишу» (read-process-write)

В одній атомарній одиниці роботи фіксуються і побічний ефект, і прогрес читання (офсети/позиція). Це усуває «привиди» при падінні між кроками.

3. Версіонування/SEQUENCE

Для агрегату зберігається версія/лічильник; зміни застосовуються тільки якщо'expected _ version'збігається. Повтори однакової події не підвищують версію → ефект один раз.

4. Дедуплікація

Індекс по'( consumer_id, event_id)'або по природному'business _ id'операції.


Патерни реалізації

1) Транзакційний лог + транзакційний sink з фіксацією офсету

Ідеально підходить для стрім-процесингу.

Читаємо з лога (тільки підтверджені записи).
Виконуємо обробку.

В одній транзакції:
  • a) записуємо ефект в sink (БД/таблицю),
  • b) фіксуємо «прочитаний до офсету N» (у цій же БД).
  • Коміт. При рестарті або все закоммічено (і офсет зрушений), або нічого.

Властивості: повторне виконання не шкодить; «рівно один раз» за ефектом, навіть якщо повідомлення читалося двічі.

2) Outbox + ідемпотентний консьюмер

Для транзакційних сервісів-продюсерів.

В одній БД-транзакції: змінюємо доменний запис і пишемо подію в outbox.
Републікатор доставляє подію в шину з тим же'event _ id'.
Консьюмери застосовують події ідемпотентно (дедуп по'event _ id').

Властивості: продюсер гарантує, що факт не загубиться; консьюмери гарантують рівно один ефект.

3) EOS в Kafka/Flink-подібних системах (концептуально)

Ідемпотентний продюсер: захищає від дублів при ретраях відправки.
Транзакції продюсера: група записів в топіки + зрушення консюмера коммітяться атомарно; читачі використовують ізоляцію'read _ committed'.
Сторона процесингу зберігає стан (state store) і комітить його разом з транзакцією.

Властивості: повторний запуск стори/таска не призводить до подвійного ефекту; дублікати «не видно» downstream.

4) Ідемпотентні «сікі» (sinks) через upsert/merge

Sink приймає'operation _ id '/' event _ id'і виконує'UPSERT... WHERE NOT EXISTS`.
Побічний ефект (наприклад, нарахування) виконується атомарно з перевіркою «чи не застосовувалося вже».

Властивості: дешевий спосіб EOS на кордоні зі сховищем, без розподілених транзакцій.


Ключові деталі реалізації

Ідентифікатори операції

Повинні бути детерміновані для повторів (не генеруйте новий UUID при ретраї).
Мати стійку область видимості (на консьюмера/на агрегат/на систему).

Таблиця дедуплікації

Колонки: `consumer_id`, `operation_id`, `applied_at`, `ttl_expires_at`.
Індекси по'( consumer_id, operation_id)'.
TTL ≥ максимальному вікну повторів (ретенція лога + потенційні затримки).

Оптимістична конкуренція

У write-моделі зберігайте версію агрегату.
При застосуванні події/команди використовуйте'WHERE version =:expected`; дублікат не збільшить версію.

Замовлення/порядок

EOS не дорівнює «точно той же порядок». Забезпечте консистентність через ключ партії (всі події агрегату → одна партія) і/або порівняння «sequence».

Ідемпотентні зовнішні виклики

Для небезпечних методів (наприклад, HTTP-вебхуки в сторонній сервіс) додавайте'Idempotency-Key'і вимагайте, щоб партнер підтримував його.


Часті пастки

EOS тільки в одному місці: якщо sink ідемпотентний, але ви емітите вторинні події без ідемпотентності, отримаєте «рівно багато разів» downstream.
Два коміти: спочатку в БД, потім коміт офсету в брокері - падіння між ними створює дублікати ефектів.
Сирі CDC назовні: зміна схеми БД ламає ідемпотентність споживачів.
Нестійкі ключі: 'operation _ id'залежить від часу/рандому і змінюється при ретраї.


Вартість і компроміси

Латентність: транзакції/ізольовані читання → зростання p95/p99.
Оверхед сховищ: таблиці дедупа, state stores, логи транзакцій.
Складність експлуатації: таймаути транзакцій, ребаланс потоків, «залиплі» сесії.
Діагностика: більше станів ("в каміті", "видно як read_committed", "відкотилося").

Вибирайте EOS точково: для критичних агрегатів і ефектів; інше покривайте ідемпотентністю і компенсаціями.


Тестування exactly-once

1. Fault-injection: падіння процесу між кроками «записав ефект» і «зафіксував офсет».
2. Дублікати: прокачайте те ж повідомлення 2-5 разів, переконайтеся в одному ефекті.
3. Рестарти і ребаланс: зупинка/перезапуск воркерів, перевірка відсутності подвійної обробки.
4. Мережеві флаппі: таймаути в середині транзакції, повтор коміту.
5. Навантажувальні тести: зростання черг → чи немає деградації до «назавжди в транзакції».


Міні-шаблони (псевдо)

Ідемпотентний sink з фіксуванням офсету

pseudo begin tx if not exists(select 1 from dedup where consumer_id=:c and op_id=:id)
then apply_effect(...)    -- upsert / merge / add_one_time_action insert into dedup(c, id, applied_at) values(:c,:id, now)
end if update offsets set pos=:pos where consumer_id=:c commit

Команда з версією агрегату

pseudo begin tx update account set balance = balance +:delta,
version = version + 1 where id=:account_id and version=:expected_version;
if row_count=0 then error CONCURRENT_MODIFICATION commit

Безпека та комплаєнс

PII/PCI в таблицях дедупа: зберігайте мінімум, використовуйте токени замість «сирих» даних.
Аудит: логуйте'operation _ id','trace _ id', результат (APPLIED/ALREADY_APPLIED).
Політика зберігання: TTL на дедуп-таблицях, архівування офсетів/логів.


Анти-патерни

«Справжня exactly-once доставка»: спроба виключити дублі на рівні транспортного протоколу без ідемпотентності ефекту.
Глобальні розподілені транзакції на все: XA/2PC через всі сервіси - крихко і повільно.
Змішування неідемпотентних побочок (наприклад, e-mail відправлений до коміту офсету).
Відсутність ключів операції: сподівання на «унікальність» корисного навантаження.


Чек-лист продакшену

  • На кожному критичному ефекті є ідемпотентний ключ.
  • Офсет/позиція читання фіксується в одній транзакції з ефектом.
  • Таблиці дедупа проіндексовані; TTL ≥ ретенції лога.
  • Для агрегатів включена оптимістична конкуренція (версія/sequence).
  • Потоки/топіки читаються в режимі «тільки закоммічені» (якщо доступно).
  • Тести дублікатів і падінь присутні в CI/CD.
  • Дашборди: частка повторів, невдалих транзакцій, час блокувань, лаги.
  • Документація для інтеграторів по'Idempotency-Кеу '/повторам/таймаутам.

FAQ

Чи можна забезпечити EOS без транзакцій?
Часто так - через ідемпотентність sink'ів (upsert/merge) і версіонування агрегатів. Транзакції спрощують гарантії, але підвищують вартість.

Чи потрібен «exactly-once» всім?
Ні, ні. Він дорогий. Застосовуйте точково там, де компенсація неможлива/дорога.

Як зв'язати листи/вебхуки з EOS?
Буферизуйте повідомлення до коміту, відправляйте після фіксації ефекту; зберігайте'notification _ id'і робіть відправку ідемпотентною.

Що важливіше - доставка або обробка?
Обробка. Доставки можуть повторюватися; підсумковий стан має бути коректним і єдиним.


Підсумок

Exactly-once - це про коректність ефекту, а не про відсутність дублів в проводці. Її досягають поєднанням ідемпотентності, атомарної фіксації ефекту і прогресу читання, розумного партіонування і дисципліни версіонування. Застосовуйте EOS там, де вартість помилки неприйнятна, і перевіряйте її реальність тестами падінь і дублів - не вірою в транспорт.

Contact

Зв’яжіться з нами

Звертайтеся з будь-яких питань або за підтримкою.Ми завжди готові допомогти!

Розпочати інтеграцію

Email — обов’язковий. Telegram або WhatsApp — за бажанням.

Ваше ім’я необов’язково
Email необов’язково
Тема необов’язково
Повідомлення необов’язково
Telegram необов’язково
@
Якщо ви вкажете Telegram — ми відповімо й там, додатково до Email.
WhatsApp необов’язково
Формат: +код країни та номер (наприклад, +380XXXXXXXXX).

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