Nordlet

← Dokumentacija / Rječnik

Što je idempotentnost? Zašto je računovodstvenim i platnim API-jima potrebna

Kako ponovljeni zahtjev stvara dvostruki račun, što jamči ključ idempotentnosti, s kojim se načinima kvarova mora nositi i kako ga ispravno implementirati.

Operacija je idempotentna ako njezino dvokratno izvršavanje ima isti učinak kao i jednokratno. Čitanje zapisa prirodno je idempotentno. Stvaranje računa nije - pozovite ga dvaput i imat ćete dva računa, dva potraživanja i kupca koji pita zašto mu je naplaćeno dvaput.

Taj je jaz važan jer mreže otkazuju na specifičan, neugodan način: zahtjev uspije, a odgovor se izgubi. Klijent vidi istekom zadanog vremena (timeout) i ne može razlikovati situaciju u kojoj zahtjev "nikad nije stigao" od one u kojoj je "stigao, obradio se, a odgovor se izgubio". Jedina mu se sigurna opcija čini ponovni pokušaj, a upravo ponovni pokušaj stvara dvostruki unos.

Zašto računovodstvo stvar čini gorom

U većini sustava dvostruki unos je gnjavaža. U glavnoj knjizi (ledger) to je složeni problem:

  • Dvostruki račun troši broj iz neprekidnog niza (gapless sequence), stoga brisanje nije opcija - nepromjenjivost znači da ispravak zahtijeva odobrenje (credit note), što je dokument koji vaš kupac vidi.
  • Duplikat knjiži stavke dnevnika, pa su prihod, PDV i potraživanja prenapuhani dok netko to ne primijeti.
  • Ulazi u obrazac PDV-a i u i.SAF registre u Litvi.
  • Ako je pokrenuo plaćanje, novac je doista premješten.

Ni "samo provjeri duplikate" ne funkcionira, jer su dva istinski odvojena računa istom kupcu za isti iznos na isti dan sasvim normalna stvar. Sustav ne može zaključiti namjeru iz sadržaja - klijent je mora navesti.

Kako funkcioniraju ključevi idempotentnosti

Klijent generira jedinstveni ključ po logičkoj operaciji i šalje ga uz zahtjev:

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

Poslužitelj tada jamči:

  1. Prvi zahtjev s ovim ključem - obradi normalno, pohrani ishod uz taj ključ.
  2. Ponovni pokušaj s istim ključem i istim podatkovnim sadržajem (payload) - nemoj ponovno izvršavati, vrati pohranjeni odgovor.
  3. Isti ključ, drugačiji podatkovni sadržaj - odbij. Ključ je tvrdnja da je ovo ista operacija; drugačije tijelo zahtjeva znači da nije, a tiho vraćanje starog rezultata bilo bi gore od pogreške.
  4. Ponovni pokušaj dok je izvorni zahtjev još u tijeku - odbij s pogreškom koja dopušta ponovni pokušaj umjesto istovremenog izvršavanja.

Četvrta točka je ona koju naivne implementacije propuštaju. Klijent kojem istekne vrijeme nakon 5 sekundi i ponovi pokušaj nakon 6 sudarit će se sa svojim vlastitim zahtjevom koji je već u obradi; bez zaštite od istovremenosti (concurrency guard), oba će se nastaviti i dobit ćete duplikat koji je mehanizam trebao spriječiti.

Ispravno generiranje ključeva

Ključ mora identificirati operaciju, a ne pokušaj:

  • Ispravno: UUID generiran jednom kada korisnik klikne "izdaj račun", koji se ponovno koristi za svaki ponovni pokušaj tog klika.
  • Ispravno: deterministički ključ izveden iz vaše vlastite domene - order-1041-invoice.
  • Pogrešno: svježi UUID po HTTP pokušaju. Svaki ponovni pokušaj dobiva novi ključ, pa je svaki ponovni pokušaj nova operacija i niste implementirali ništa.
  • Pogrešno: hash podatkovnog sadržaja. Dve legitimno identične operacije tada će se sudariti, a druga će biti tiho progutana.

Ključevi trebaju razdoblje zadržavanja - dovoljno dugo da pokriju realne prozore za ponovne pokušaje i prekide rada klijenta, ali ne zauvijek.

Što idempotentnost ne rješava

Idempotentnost štiti od dvostrukog izvršavanja istog zahtjeva. Ne štiti od:

  • Dva različita korisnika koja stvaraju isti račun iz različitih sesija - to je problem poslovnih pravila, riješen ograničenjima jedinstvenosti (uniqueness constraints) kao što je jedan račun po narudžbi.
  • Logičkih duplikata s različitim ključevima.
  • Nedeterminizma unutar operacije - ako upravljački program (handler) generira vremensku oznaku ili broj, pohranjeni odgovor bilježi prvi, što je željeno ponašanje, ali ga vrijedi razumjeti.

Ona nadopunjuje, umjesto da zamjenjuje, ograničenja baze podataka. Nordletova glavna knjiga koristi oboje: ključevi idempotentnosti zaustavljaju ponovljene zahtjeve, dok jedinstveni indeksi zaustavljaju dvostruke serije namirenja po ID-ju isplate, dvostruke ulazne račune po dobavljaču i broju dokumenta te dvostruke obračune amortizacije po sredstvu i razdoblju.

Kako Nordlet to implementira

Pošaljite zaglavlje Idempotency-Key uz svaki zahtjev koji mijenja stanje - neprazan niz od najviše 255 znakova.

Prvi zahtjev s datim ključem se izvršava i njegov se ishod pohranjuje. Ponovni pokušaj s istim ključem i istim podatkovnim sadržajem vraća pohranjeni odgovor bez ponovnog izvršavanja, tako da se račun stvara točno jednom bez obzira na to koliko se puta poziv ponovi.

Dva načina kvara su izričita umjesto tiha:

  • Isti ključ, drugačiji podatkovni sadržaj → odbijeno: "Idempotency-Key was already used with a different request payload." Ovo hvata doista opasan slučaj u kojem klijent ponovno koristi ključ dok mijenja iznos.
  • Isti ključ, izvorni zahtjev još u tijeku → odbijeno uz mogućnost ponovnog pokušaja: "A request with this Idempotency-Key is still being processed, retry shortly." Nema istovremenog dvostrukog izvršavanja.

Ključevi su ograničeni po tvrtki, tako da se dva klijenta (tenants) koja generiraju isti UUID nikada ne sudaraju.

Komplementarno jamstvo teče u suprotnom smjeru. Isporuka webhooka je po načelu barem jednom (at-least-once), što znači da će vaše krajnje točke povremeno primiti isti događaj dvaput - nakon isteka vremena u kojem je isporuka zapravo uspjela ili pri ponovnoj isporuci. Svaki događaj nosi ID, a vaš upravljački program trebao bi bilježiti obrađene ID-jeve i ignorirati ponavljanja. Idempotentnost je svojstvo koje je potrebno objema stranama integracije; vodič kroz API konvencije detaljno obrađuje stranu zahtjeva.

FAQ

Što je ključ idempotentnosti?

Identifikator koji generira klijent, a šalje se uz zahtjev te omogućuje poslužitelju da prepozna ponovni pokušaj iste logičke operacije. Prvi zahtjev se izvršava; ponovni pokušaji s istim ključem vraćaju pohranjeni rezultat umjesto ponovnog izvršavanja.

Zašto računovodstvenim API-jima treba idempotentnost?

Zato što dvostruko upisivanje stvara stvarni dokument sa stvarnim posljedicama - drugi račun koji troši serijski broj, knjiži se u glavnu knjigu i ulazi u obrazac PDV-a. Za razliku od dvostrukog čitanja, on se ne može jednostavno izbrisati; mora se ispraviti odobrenjem.

Trebam li generirati novi ključ za svaki ponovni pokušaj?

Ne - to u potpunosti uništava mehanizam. Generirajte jedan ključ po logičkoj operaciji i ponovno ga koristite za svaki ponovni pokušaj te operacije. Novi ključ po pokušaju znači da se svaki pokušaj tretira kao nova operacija.

Što se događa ako ponovno upotrijebim ključ s drugačijim podatkovnim sadržajem?

Zahtjev se odbija. Ključ tvrdi da je ovo ista operacija, pa promijenjeno tijelo zahtjeva znači da nešto nije u redu - najvjerojatnije pogreška ponovne upotrebe ključa na strani klijenta. Tiho vraćanje ranijeg rezultata bilo bi gore.

Sprečava li idempotentnost sve duplikate?

Ne. Ona sprječava dvostruko izvršavanje istog zahtjeva. Dva različita korisnika koja neovisno stvaraju isti račun napravila su dva različita zahtjeva, što je problem poslovnih pravila za ograničenja jedinstvenosti, a ne nešto što ključ idempotentnosti može vidjeti.