Що таке ідемпотентність? Чому вона необхідна бухгалтерським та платіжним API
Як повторений запит створює дублікат інвойсу, що гарантує ключ ідемпотентності, які типи збоїв він має обробляти та як реалізувати його правильно.
Операція є ідемпотентною, якщо її виконання двічі дає такий самий результат, як і виконання один раз. Читання запису є ідемпотентним за своєю суттю. Створення інвойсу – ні: викличте його двічі, і ви отримаєте два інвойси, дві дебіторські заборгованості та клієнта, який запитує, чому йому виставили рахунок двічі.
Ця прогалина має значення через те, що мережі дають збої у специфічний, неприємний спосіб: запит успішно виконано, а відповідь втрачено. Клієнт бачить таймаут і не може відрізнити ситуацію "ніколи не доходило" від "дійшло, спрацювало, відповідь загубилася". Його єдиний виглядний як безпечний варіант – повторити спробу, і саме цей повтор створює дублікат.
Чому в бухгалтерії все ще гірше
У більшості систем дублікат – це дрібна неприємність. У головній книзі (ledger) це проблема, що наростає як снігова куля:
- Дублікат інвойсу споживає номер із нерозривної послідовності (gapless sequence), тому видалити його не можна – незмінність (immutability) означає, що для його виправлення потрібна кредитова нота, а це документ, який бачить ваш клієнт.
- Дублікат створює проводки в журналі (journal entries), тому дохід, ПДВ та дебіторська заборгованість завищені, доки хтось це не помітить.
- Він потрапляє до декларації з ПДВ та, у Литві, до реєстрів i.SAF.
- Якщо він ініціював платіж, кошти фактично переказалися.
Варіант „просто перевірити на наявність дублікатів“ також не працює, оскільки два абсолютно окремі інвойси одному й тому ж клієнту на ту саму суму в той самий день є абсолютно нормальним явищем. Система не може вивести намір із вмісту – клієнт має його задекларувати.
Як працюють ключі ідемпотентності
Клієнт генерує унікальний ключ для кожної логічної операції та надсилає його із запитом:
POST /v1/sales/invoices/create
Idempotency-Key: 8f14e45f-ea1b-4c2b-9f3a-2d7c1e0b5a91
Після цього сервер гарантує:
- Перший запит із цим ключем – обробляється у звичайному режимі, результат зберігається разом із ключем.
- Повторна спроба з тим самим ключем і тим самим пейлоадом (payload) – повторно не виконується; повертається збережена відповідь.
- Той самий ключ, інший пейлоад – відхилення. Ключ є твердженням, що це та сама операція; інше тіло означає, що це не так, і мовчки повертати старий результат було б гірше, ніж помилка.
- Повторна спроба, поки оригінальний запит усе ще виконується – відхилення з помилкою, яку можна повторити, замість паралельного виконання.
Четвертий пункт – це те, що пропускають примітивні реалізації. Клієнт, у якого спрацьовує таймаут через 5 секунд і який робить повторну спробу через 6, зіткнеться зі своїм власним запитом, що перебуває в процесі виконання; без захисту від паралельності обидва запити виконаються, і ви отримаєте дублікат, для запобігання якому й існував цей механізм.
Правильна генерація ключів
Ключ має ідентифікувати операцію, а не спробу:
- Правильно: UUID, згенерований один раз, коли користувач натискає „виставити інвойс“, який використовується повторно для кожного нового кліку на цей же виставлений інвойс.
- Правильно: детермінований ключ, похідний від вашого власного домену –
order-1041-invoice. - Неправильно: новий UUID для кожної HTTP-спроби. Кожна повторна спроба отримує новий ключ, тому кожна спроба є новою операцією, і ви нічого не реалізували.
- Неправильно: хеш пейлоаду. У такому разі дві легітимно ідентичні операції зіштовхнуться, і другу буде мовчки проігноровано.
Ключі потребують періоду зберігання – достатньо довгого, щоб охопити реалістичні вікна повторних спроб і збоїв на стороні клієнта, але не назавжди.
Чого він не вирішує
Ідемпотентність захищає від повторного виконання того самого запиту. Вона не захищає від:
- Двох різних користувачів, які створюють однаковий інвойс із різних сесій – це проблема бізнес-логіки, що вирішується обмеженнями унікальності, такими як один інвойс на замовлення.
- Логічних дублікатів із різними ключами.
- Недетермінізму всередині операції – якщо обробник генерує мітку часу (timestamp) або номер, збережена відповідь фіксує першу з них, що є бажаною поведінкою, але це варто розуміти.
Вона доповнює обмеження бази даних, а не замінює їх. Головна книга Nordlet використовує обидва підходи: ключі ідемпотентності зупиняють повторні запити, тоді як унікальні індекси зупиняють дублювання пакетів розрахунків за ідентифікатором виплати, дублювання вхідних інвойсів за постачальником і номером документа, а також дублювання нарахування амортизації за активтом і періодом.
Як це реалізовано в Nordlet
Надсилайте заголовковий рядок Idempotency-Key у будь-якому запиті, що змінює стан, – це непорожній рядок довжиною не більше 255 символів.
Перший запит із певним ключем виконується, і його результат зберігається. Повторна спроба з тим самим ключем і тим самим пейлоадом повертає збережену відповідь без повторного виконання, тому інвойс створюється рівно один раз, незалежно від того, скільки разів повторюється виклик.
Два типи збоїв явно відхиляються, а не ігноруються мовчки:
- Той самий ключ, інший пейлоад → відхилено: "Idempotency-Key was already used with a different request payload." Це виловлює справді небезпечний випадок, коли клієнт використовує ключ повторно, змінюючи суму.
- Той самий ключ, оригінальний запит усе ще в процесі виконання → відхилено як запит, що підлягає повтору: "A request with this Idempotency-Key is still being processed, retry shortly." Жодного паралельного подвійного виконання.
Ключі охоплюють компанії ізольовано (scoped per company), тому два орендарі (tenants), які генерують однаковий UUID, ніколи не зіштовхнуться.
Додаткова гарантія працює у протилежному напрявку. Доставка вебхуків працює за принципом "принаймні один раз" (at-least-once), що означає, що ваш кінцевий пункт призначення (endpoint) час від часу отримуватиме ту саму подію двічі – після таймауту, коли доставка насправді була успішною, або під час повторної доставки. Кожна подія має ідентифікатор, і ваш обробник повинен записувати оброблені ідентифікатори та ігнорувати повтори. Ідемпотентність – це властивість, необхідна обом сторонам інтеграції; у посібнику з конвенцій API детально розглядається сторона запитів.
FAQ
Що таке ключ ідемпотентності?
Це згенерований клієнтом ідентифікатор, який надсилається із запитом і дозволяє серверу розпізнати повторну спробу тієї самої логічної операції. Перший запит виконується; повторні спроби з тим самим ключем повертають збережений результат замість повторного виконання.
Чому бухгалтерським API потрібна ідемпотентність?
Оскільки дубльований запис створює реальний документ із реальними наслідками – другий інвойс споживає порядковий номер, потрапляє в головну книгу та в декларацію з ПДВ. На відміну від дубльованого читання, його не можна просто видалити; його потрібно виправити за допомогою кредитової ноти.
Чи варто генерувати новий ключ для кожної повторної спроби?
Ні, це повністю нівелює сенс механізму. Генеруйте один ключ на одну логічну операцію та використовуйте його повторно для кожної спроби виконання цієї операції. Новий ключ для кожної спроби означає, що кожна спроба розглядається як нова операція.
Що станеться, якщо я використаю ключ повторно з іншим пейлоадом?
Запит буде відхилено. Ключ підтверджує, що це та сама операція, тому змінене тіло означає наявність якоїсь проблеми, найімовірніше – помилки повторного використання ключа на стороні клієнта. Мовчазне повернення попереднього результату було б гіршим варіантом.
Чи запобігає ідемпотентність появі всіх дублікатів?
Ні. Вона запобігає повторному виконанню того самого запиту. Двоє різних користувачів, які незалежно один від одного створюють однаковий інвойс, зробили два різні запити, що є проблемою бізнес-логіки для обмежень унікальності, а не тим, що може розпізнати ключ ідемпотентності.