Nordlet

← Документация / Речник

Какво е идемпотентност? Защо счетоводните и платежните API се нуждаят от нея

Как повтарянето на заявка създава дублирана фактура, какво гарантира един идемпотентен ключ, с какви режими на неизправност трябва да се справя системата и как да я реализирате правилно.

една операция е идемпотентна, ако изпълнението ѝ два пъти има същия ефект като изпълнението ѝ веднъж. Четенето на запис е естествено идемпотентно. Създаването на фактура не е – извикайте я два пъти и ще имате две фактури, две вземания и клиент, който пита защо му е било таксувано два пъти.

Този проблем е важен, защото мрежите се провалят по специфичен и неприятен начин: заявката е успешна, а отговорът е загубен. Клиентът вижда времезакъснение (timeout) и не може да разграничи „никога не е пристигнало“ от „пристигнало е, изпълнено е, а отговорът е загубен“. Единственият изглеждащ безопасно изход е повторен опит (retry), а именно повторният опит създава дублирането.

Защо счетоводството влошава нещата

В повечето системи дублирането е просто неудобство. В главната книга (ledger) то е трупащ се проблем:

  • Една дублирана фактура консумира номер от непрекъсната поредица (gapless sequence), така че изтриването ѝ не е опция – непроменяемостта (immutability) означава, че коригирането изисква кредитно известие, което е документ, видим за вашия клиент.
  • Дубликатът генерира счетоводни статии, така че приходите, ДДС и вземанията са преувеличени, докато някой не забележи.
  • Той влиза в справката-декларация по ДДС и в регистрите i.SAF в Литва.
  • Ако е задействал плащане, реално са преместени пари.

Подходът „просто провери за дубликати“ също не работи, защото две напълно отделни фактури към един и същ клиент за същата сума на същата дата са напълно нормални. Системата не може да изведе намерението от съдържанието – клиентът трябва да го заяви изрично.

Как работят идемпотентните ключове

Клиентът генерира уникален ключ за всяка логическа операция и го изпраща със заявката:

POST /v1/sales/invoices/create
Idempotency-Key: 8f14e45f-ea1b-4c2b-9f3a-2d7c1e0b5a91

След това сървърът гарантира:

  1. Първа заявка с този ключ – обработва се нормално, а резултатът се запазва срещу ключа.
  2. Повторен опит със същия ключ и същия полезен товар (payload) – операцията не се изпълнява отново, а се връща запазеният отговор.
  3. Същият ключ, различен полезен товар – заявката се отхвърля. Ключът е твърдение, че това е същата операция; различно тяло означава, че не е така, а тихото връщане на стария резултат би било по-лошо от грешка.
  4. Повторен опит, докато оригиналната заявка все още се изпълнява – отхвърля се с грешка, подлежаща на нов опит (retryable), вместо да се изпълнява едновременно.

Точка 4 е тази, която наивните реализации пропускат. Клиент, който получи времезакъснение (timeout) на 5-ата секунда и направи повторен опит на 6-ата, ще се сблъска със собствената си текуща заявка; без защита срещу едновременности и двете ще продължат и ще получите дублирането, което механизмът е създаден да предотврати.

Правилно генериране на ключове

Ключът трябва да идентифицира операцията, а не опита:

  • Правилно: UUID, генериран веднъж, когато потребителят щракне върху „издаване на фактура“, използван повторно за всяко кликване при нов опит.
  • Правилно: детерминистичен ключ, изведен от вашия собствен домейн – order-1041-invoice.
  • Грешно: нов UUID за всеки HTTP опит. Всеки повторен опит получава нов ключ, така че всеки опит е нова операция и нищо не сте реализирали.
  • Грешно: хеш на полезния товар. Тогава две напълно идентични операции се сблъскват и втората се поглъща мълчаливо.

Ключовете се нуждаят от период на съхранение – достатъчно дълъг, за да покрие реалистични прозорци за нови опити и прекъсвания при клиента, но не и завинаги.

Какво не решава идемпотентността

Идемпотентността предпазва от дублирано изпълнение на една и съща заявка. Тя не предпазва от:

  • Двама различни потребители, които създават една и съща фактура от различни сесии – това е проблем на бизнес логиката, разрешен от ограничения за уникалност, като например една фактура за поръчка.
  • Логически дубликати с различни ключове.
  • Недетерминизъм вътре в самите операции – ако манипулаторът генерира времеви печат (timestamp) или номер, запазеният отговор улавя първия, което е желаното поведение, но си струва да се разбере.

Тя се съчетава с бази данни, вместо да ги заменя. Главната книга на Nordlet използва и двете: идемпотентните ключове спират повторните заявки, докато уникалните индекси спират дублираните партиди за сетълмент по ID на изплащане, дублираните фактури за покупка по доставчик и номер на документ, както и дублираните процедури по амортизация по актив и период.

Как 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." Без едновременно двойно изпълнение.

Ключовете са обвързани с обхват за всяка компания, така че двама наематели (tenants), генериращи един и същ UUID, никога не се сблъскват.

Допълващата гаранция работи в обратна посока. Доставката на уеб куки (webhook delivery) е поне веднъж (at-least-once), което означава, че вашият краен пункт (endpoint) периодично ще получава едно и също съобщение два пъти – след времезакъснение, при което доставката всъщност е била успешна, или при повторна доставка. Всяко съобщение носи ID, а вашият манипулатор трябва да записва обработените ID-та и да игнорира повтарящите се. Идемпотентността е свойство, от което се нуждаят и двете страни на интеграцията; ръководството за API конвенции разглежда страната на заявките в детайли.

FAQ

Какво е идемпотентен ключ?

Идентификатор, генериран от клиента и изпращан със заявката, който позволява на сървъра да разпознае повторен опит на същата логическа операция. Първата заявка се изпълнява; повторните опити със същия ключ връщат запазения резултат, вместо да се изпълняват отново.

Защо счетоводните API се нуждаят от идемпотентност?

Тъй като дублираното записване създава реален документ с реални последици – втора фактура, която консумира пореден номер, отразява се в главната книга и влиза в справката-декларация по ДДС. За разлика от дублираното четене, тя не може просто да бъде изтрита; трябва да бъде коригирана с кредитно известие.

Трябва ли да генерирам нов ключ за всеки повторен опит?

Не – това напълно проваля механизма. Генерирайте един ключ за логическа операция и го използвайте повторно за всеки нов опит на тази операция. Нов ключ за всеки опит означава, че всеки опит се третира като нова операция.

Какво се случва, ако използвам повторно ключ с различен полезен товар?

Заявката се отхвърля. Ключът твърди, че това е същата операция, така че промененото тяло означава, че нещо не е наред – най-вероятно грешка при повторното използване на ключ от страна на клиента. Тихото връщане на по-ранния резултат би било по-лошо.

Предотвратява ли идемпотентността всички дубликати?

Не. Тя предотвратява дублираното изпълнение на една и съща заявка. Двама различни потребители, които независимо един от друг създават една и съща фактура, са направили две различни заявки, което е проблем на бизнес логиката за ограниченията за уникалност, а не нещо, което един идемпотентен ключ може да види.