Kas yra idempotentiškumas? Kodėl jo reikia apskaitos ir mokėjimų API
Kaip pakartota užklausa sukuria dublikatinę sąskaitą faktūrą, ką garantuoja idempotentiškumo raktas, kokias klaidų situacijas jis turi apdoroti ir kaip jį teisingai realizuoti.
Operacija yra idempotentinė (idempotent), jei ją atlikus du kartus gaunamas toks pat rezultatas, kaip ir atlikus vieną kartą. Įrašo skaitymas iš prigimties yra idempotentinis. Sąskaitos faktūros sukūrimas toks nėra – iškvieskite operaciją du kartus ir turėsite dvi sąskaitas faktūras, dvi gautinas sumas bei klientą, klausiantį, kodėl jam buvo pateikta dviguba sąskaita.
Šis skirtumas yra svarbus, nes tinklo ryšys trūkinėja specifiniu, nemaloniu būdu: užklausa įvykdoma sėkmingai, bet atsakymas pasimeta. Klientinė programa (client) užfiksuoja laiko limito pabaigą (timeout) ir negali atskirti „niekada nepasiekė“ nuo „pasiekė, suveikė, bet atsakymas pasimetė“. Vienintelė saugi išeitis atrodo užklausos pakartojimas (retry), tačiau būtent šis pakartojimas ir sukuria dublikatą.
Kodėl apskaitoje tai sukelia dar daugiau problemų
Daugumoje sistemų dublikatas tėra nedidelis nepatogumas. Didžiojoje knygoje tai yra sudėtinė problema:
- Dubliuota sąskaita faktūra sunaudoja numerį iš nepertraukiamos sekos, todėl jos ištrinti tiesiog negalima – nekintamumas reiškia, kad norint ją ištaisyti, būtina išrašyti kreditinę sąskaitą faktūrą, o tai yra dokumentas, kurį matys jūsų klientas.
- Dublikatas sugeneruoja žurnalo įrašus, todėl pajamos, PVM ir gautinos sumos yra nepagrįstai padidintos, kol kas nors tai pastebi.
- Jis patenka į PVM deklaraciją ir, Lietuvoje, į i.SAF registrus.
- Jei tai inicijavo mokėjimą, realiai pervedami pinigai.
Sprendimas „tiesiog tikrinti dublikatus“ irgi neveikia, nes dvi atskiros, bet visiškai vienodos sąskaitos faktūros, išrašytos tam pačiam klientui, tai pačiai sumai ir tą pačią dieną, yra visiškai normali praktika. Sistema negali nuspėti ketinimo iš turinio – klientinė programa pati turi jį nurodyti.
Kaip veikia idempotentiškumo raktai
Klientinė programa sugeneruoja unikalų raktą kiekvienai loginei operacijai ir siunčia jį kartu su užklausa:
POST /v1/sales/invoices/create
Idempotency-Key: 8f14e45f-ea1b-4c2b-9f3a-2d7c1e0b5a91
Tuomet serveris garantuoja:
- Pirmoji užklausa su šiuo raktu – apdorojama įprastai, rezultatas išsaugomas susiejant su raktu.
- Pakartojimas su tuo pačiu raktu ir tuo pačiu pranešimo kūnu (payload) – operacija nevykdoma iš naujo; grąžinamas išsaugotas atsakymas.
- Tas pats raktas, bet kitas turinys – atmetama. Raktas yra teiginys, kad tai ta pati operacija; kitoks pranešimo kūnas reiškia, kad taip nėra, o tylus senojo rezultato grąžinimas būtų blogiau nei klaida.
- Pakartojimas, kol tebevykdoma pirminė užklausa – atmetama grąžinant klaidą, po kurios užklausą galima kartoti (retryable error), užuot vykdžius operacijas lygiagrečiai.
Ketvirtasis punktas yra tas, kurio pasigendama naiviose realizacijose. Klientinė programa, kurios laukimo laikas baigiasi po 5 sekundžių ir kuri pakartoja užklausą 6-ąją sekundę, susidurs su savo pačios vis dar vykdoma užklausa; be lygiagretumo apsaugos abi užklausos bus įvykdytos, ir jūs gausite dublikatą, kurio šis mechanizmas turėjo išvengti.
Kaip teisingai sugeneruoti raktus
Raktas turi identifikuoti operaciją, o ne bandymą:
- Teisinga: UUID, sugeneruotas vieną kartą, kai vartotojas paspaudžia „išrašyti sąskaitą faktūrą“, pakartotinai naudojamas kiekvienam šio paspaudimo pakartojimui.
- Teisinga: deterministinis raktas, suformuotas iš jūsų pačių domeno –
order-1041-invoice. - Neteisinga: naujas UUID kiekvienam HTTP bandymui. Kiekvienas pakartojimas gauna naują raktą, todėl kiekvienas pakartojimas traktuojamas kaip nauja operacija, ir jūs taip nieko neišsprendėte.
- Neteisinga: pranešimo kūno maiša (hash). Dvi teisėtai identiškos operacijos tada susiduria viena su kita, ir antroji nepastebimai atmetama.
Raktams reikalingas saugojimo laikotarpis – pakankamai ilgas, kad apimtų realius pakartojimų „langus“ ir klientinės programos sutrikimus, bet ne amžinas.
Ko šis mechanizmas neišsprendžia
Idempotentiškumas apsaugo nuo pakartotinio tos pačios užklausos vykdymo. Jis neapsaugo nuo:
- Dviejų skirtingų vartotojų, kuriančių tą pačią sąskaitą faktūrą iš skirtingų sesijų – tai verslo logikos problema, kurią sprendžia unikalumo ribojimai (pavyzdžiui, viena sąskaita faktūra vienam užsakymui).
- Loginių dublikatų su skirtingais raktais.
- Nedeterminuotumo pačioje operacijoje – jeigu apdorojimo funkcija (handler) sugeneruoja laiko žymą (timestamp) arba numerį, išsaugotas atsakymas užfiksuoja pirmąjį variantą. Nors tai ir yra pageidaujamas elgesys, jį verta suprasti.
Jis papildo, bet nepakeičia duomenų bazės apribojimų. Nordlet didžiojoje knygoje naudojami abu būdai: idempotentiškumo raktai sustabdo pakartotas užklausas, o unikalūs indeksai neleidžia sukurti besidubliuojančių atsiskaitymo paketų (settlement batches) tam pačiam išmokos ID, besidubliuojančių pirkimo sąskaitų faktūrų tam pačiam tiekėjui ir dokumento numeriui bei besidubliuojančių nusidėvėjimo skaičiavimų tam pačiam turtui ir laikotarpiui.
Kaip tai realizuoja Nordlet
Kartu su bet kokia būseną keičiančia užklausa siųskite Idempotency-Key antraštę – netuščią teksto eilutę, sudarytą iš ne daugiau kaip 255 simbolių.
Pirmoji užklausa su nurodytu raktu yra įvykdoma, o jos rezultatas išsaugomas. Pakartojus užklausą su tuo pačiu raktu ir tuo pačiu pranešimo kūnu, grąžinamas išsaugotas atsakymas nebevykdant operacijos iš naujo, todėl sąskaita faktūra sukuriama lygiai vieną kartą, nepriklausomai nuo to, kiek kartų užklausa bus kartojama.
Dvi klaidų situacijos grąžina aiškius pranešimus užuot tyliai atmetusios:
- Tas pats raktas, kitas turinys → atmetama: "Idempotency-Key was already used with a different request payload." Tai padeda išvengti tikrai pavojingos situacijos, kai klientinė programa pakartotinai panaudoja raktą, kartu pakeisdama sumą.
- Tas pats raktas, pirminė operacija dar vykdoma → atmetama kaip pakartojama: "A request with this Idempotency-Key is still being processed, retry shortly." Taip išvengiama lygiagretaus dvigubo vykdymo.
Raktai galioja įmonės lygmeniu (scoped per company), todėl dvi skirtingos aplinkos (tenants), sugeneravusios tą patį UUID, niekada nesusidurs.
Papildoma garantija veikia ir priešinga kryptimi. Tinklų gaudyklių (webhook) pranešimai pristatomi bent vieną kartą (at-least-once), o tai reiškia, kad jūsų galinis taškas (endpoint) kartais gaus tą patį įvykį du kartus – po laiko limito pabaigos, nors pristatymas iš tikrųjų pavyko, arba pakartotinio pristatymo atveju. Kiekvienas įvykis turi ID, todėl jūsų apdorojimo programa turėtų registruoti jau apdorotus ID ir ignoruoti pasikartojimus. Idempotentiškumas yra savybė, kurios reikia abiem integracijos pusėms; API konvencijų vadove išsamiai aprašoma užklausų pusė.
D.U.K.
Kas yra idempotentiškumo raktas?
Tai kliento sugeneruotas identifikatorius, siunčiamas kartu su užklausa, leidžiantis serveriui atpažinti tos pačios loginės operacijos pakartojimą. Pirmoji užklausa yra įvykdoma; pakartojimai su tuo pačiu raktu grąžina išsaugotą rezultatą, o ne vykdo operaciją iš naujo.
Kodėl apskaitos API reikia idempotentiškumo?
Todėl, kad dubliuotas įrašymas sukuria realų dokumentą su realiomis pasekmėmis – antroji sąskaita faktūra sunaudoja nuoseklų numerį, yra įrašoma į didžiąją knygą ir patenka į PVM deklaraciją. Skirtingai nei dubliuotas skaitymas, ji negali būti paprasčiausiai ištrinta; ji turi būti taisoma išrašant kreditinę sąskaitą faktūrą.
Ar turėčiau sugeneruoti naują raktą kiekvienam pakartojimui?
Ne – tai visiškai paneigia šio mechanizmo prasmę. Sugeneruokite vieną raktą loginei operacijai ir naudokite jį pakartotinai kiekvienam tos operacijos pakartojimui. Naujas raktas kiekvienam bandymui reiškia, kad kiekvienas bandymas traktuojamas kaip visiškai nauja operacija.
Kas nutiks, jei tą patį raktą panaudosiu su kitu pranešimo kūnu?
Užklausa atmetama. Raktas patvirtina, kad tai ta pati operacija, todėl pakeistas turinys reiškia, kad kažkas negerai – greičiausiai kliento programoje yra rakto pakartotinio naudojimo klaida. Tyliai grąžinti ankstesnį rezultatą būtų blogiau.
Ar idempotentiškumas užkerta kelią visiems dublikatams?
Ne. Jis užkerta kelią tos pačios užklausos dvigubam įvykdymui. Du skirtingi vartotojai, nepriklausomai vienas nuo kito kuriantys tą pačią sąskaitą faktūrą, sukuria dvi skirtingas užklausas. Tai yra unikalumo ribojimų problema (verslo logika), kurios idempotentiškumo raktas atpažinti negali.