Ce este idempotența? De ce au nevoie API-urile de contabilitate și plăți de aceasta
Cum o cerere reîncercată creează o factură duplicat, ce garantează o cheie de idempotență, modurile de eșec pe care trebuie să le gestioneze și cum să o implementezi corect.
O operațiune este idempotentă dacă executarea ei de două ori produce același efect ca executarea ei o singură dată. Citirea unei înregistrări este în mod natural idempotentă. Crearea unei facturi nu este - o apelezi de două ori și ai două facturi, două creanțe și un client care întreabă de ce a fost facturat de două ori.
Această discrepanță contează deoarece rețelele eșuează într-un mod specific și neplăcut: cererea reușește, iar răspunsul se pierde. Clientul vede o expirare a timpului de așteptare (timeout) și nu poate distinge între „nu a ajuns niciodată” și „a ajuns, a funcționat, răspunsul s-a pierdut”. Singura opțiune care pare sigură este reîncercarea, iar reîncercarea este exact ceea ce creează duplicatul.
De ce contabilitatea înrăutățește lucrurile
În cele mai multe sisteme, un duplicat este un deranj. Într-un registru contabil (ledger), acesta devine o problemă amplificată:
- O factură duplicat consumă un număr dintr-o secvență continuă (gapless sequence), așa că ștergerea ei nu este o opțiune - imuabilitatea înseamnă că corectarea ei necesită o notă de credit, care este un document pe care clientul tău îl vede.
- Duplicatul generează înregistrări contabile, astfel încât veniturile, TVA-ul și creanțele sunt toate supraevaluate până când cineva observă.
- Intră în decontul de TVA și, în Lituania, în registrele i.SAF.
- Dacă a declanșat o plată, banii s-au mișcat efectiv.
Nici „verificarea simplă a duplicatelor” nu funcționează, deoarece două facturi complet separate emise aceluiași client pentru aceeași sumă în aceeași zi sunt perfect normale. Sistemul nu poate deduce intenția din conținut - clientul trebuie să o declare.
Cum funcționează cheile de idempotență
Clientul generează o cheie unică pentru fiecare operațiune logică și o trimite împreună cu cererea:
POST /v1/sales/invoices/create
Idempotency-Key: 8f14e45f-ea1b-4c2b-9f3a-2d7c1e0b5a91
Serverul garantează apoi următoarele:
- Prima cerere cu această cheie - se procesează în mod normal, iar rezultatul este stocat pe baza cheii.
- Reîncercare cu aceeași cheie și același payload - nu se reexecută; se returnează răspunsul stocat.
- Aceeași cheie, payload diferit - se respinge. Cheia este o afirmație că aceasta este aceeași operațiune; un corp diferit înseamnă că nu este, iar returnarea silențioasă a vechiului rezultat ar fi mai rea decât o eroare.
- Reîncercare în timp ce cererea originală este încă în curs de execuție - se respinge cu o eroare care poate fi reîncercată, în loc să se execute concurent.
Punctul 4 este cel pe care implementările naive îl ratează. Un client care expiră după 5 secunde și reîncearcă la 6 se va ciocni cu propria cerere aflată în desfășurare; fără o protecție împotriva concurenței (concurrency guard), ambele continuă și obții duplicatul pe care mecanismul exista tocmai pentru a-l preveni.
Generarea corectă a cheilor
Cheia trebuie să identifice operațiunea, nu încercarea:
- Corect: un UUID generat o singură dată când utilizatorul face clic pe „emitere factură”, reutilizat pentru fiecare reîncercare a acelui clic.
- Corect: o cheie deterministă derivată din propriul tău domeniu -
order-1041-invoice. - Greșit: un UUID nou pentru fiecare încercare HTTP. Fiecare reîncercare primește o cheie nouă, deci fiecare reîncercare este o operațiune nouă și nu ai implementat nimic.
- Greșit: un hash al payloadului. Două operațiuni perfect identice se vor ciocni atunci, iar a doua va fi ignorată în tăcere.
Cheile au nevoie de o perioadă de retenție - suficient de lungă pentru a acoperi ferestre realiste de reîncercare și întreruperi ale clientului, dar nu pentru totdeauna.
Ce nu rezolvă idempotența
Idempotența protejează împotriva execuției duplicate a aceleiași cereri. Ea nu protejează împotriva:
- Doi utilizatori diferiți care creează aceeași factură din sesiuni diferite - aceasta este o problemă de regulă de business, rezolvată de constrângeri de unicitate, cum ar fi o singură factură per comandă.
- Duplicatelor logice cu chei diferite.
- Non-determinismului din interiorul operațiunii - dacă handlerul generează un marcaj temporal (timestamp) sau un număr, răspunsul stocat îl va captura pe primul, ceea ce reprezintă comportamentul dorit, dar merită înțeles.
Ea completează constrângerile bazei de date, în loc să le înlocuiască. Registrul contabil al Nordlet le folosește pe amândouă: cheile de idempotență opresc cererile reîncercate, în timp ce indexurile unice opresc loturile de decontare duplicate per ID de plată, facturile de achiziție duplicate per furnizor și număr de document, și rulajele duplicate de amortizare per activ și perioadă.
Cum implementează Nordlet acest lucru
Trimite un antet Idempotency-Key pentru orice cerere care schimbă starea - un șir nevid de cel mult 255 de caractere.
Prima cerere cu o anumită cheie se execută, iar rezultatul său este stocat. O reîncercare cu aceeași cheie și același payload returnează răspunsul stocat fără a se reexecuta, astfel încât factura este creată exact o dată, indiferent de câte repetări are apelul.
Două moduri de eșec sunt explicite, în loc să fie tăcute:
- Aceeași cheie, payload diferit → respins: "Idempotency-Key was already used with a different request payload." Aceasta detectează cazul cu adevărat periculos în care un client reutilizează o cheie în timp ce schimbă suma.
- Aceeași cheie, originalul încă în curs de execuție → respins ca reîncercabil: "A request with this Idempotency-Key is still being processed, retry shortly." Fără nicio dublă execuție concurentă.
Cheile sunt delimitate per companie (scoped), astfel încât doi chiriași (tenants) care generează același UUID să nu se ciocnească niciodată.
Garanția complementară funcționează în celălalt sens. Livrarea webhook-urilor se face în regim de cel puțin o dată (at-least-once), ceea ce înseamnă că punctului tău final (endpoint) i se va întâmpla ocazional să primească același eveniment de două ori - după un timeout în care livrarea a reușit efectiv, sau la o relansare a livrării. Fiecare eveniment poartă un ID, iar handlerul tău ar trebui să înregistreze ID-urile procesate și să ignore repetările. Idempotența este o proprietate de care au nevoie ambele părți ale unei integrări; ghidul privind convențiile API acoperă partea de cereri în detaliu.
FAQ
Ce este o cheie de idempotență?
Un identificator generat de client și trimis împreună cu o cerere, care permite serverului să recunoască o reîncercare a aceleiași operațiuni logice. Prima cerere se execută; reîncercările cu aceeași cheie returnează rezultatul stocat în loc să se execute din nou.
De ce au nevoie API-urile de contabilitate de idempotență?
Deoarece o scriere duplicată creează un document real cu consecințe reale - a doua factură consumă un număr secvențial, se înregistrează în registrul contabil și intră într-un decont de TVA. Spre deosebire de o citire duplicată, ea nu poate fi pur și simplu ștearsă; trebuie corectată cu o notă de credit.
Ar trebui să generez o cheie nouă pentru fiecare reîncercare?
Nu - asta anulează complet mecanismul. Generează o singură cheie per operațiune logică și reutilizază-o pentru fiecare reîncercare a acelei operațiuni. O cheie nouă per încercare înseamnă că fiecare încercare este tratată ca o operațiune nouă.
Ce se întâmplă dacă reutilizează o cheie cu un payload diferit?
Cererea este respinsă. Cheia afirmă că aceasta este aceeași operațiune, așa că un corp modificat înseamnă că ceva este în regulă - cel mai probabil o eroare de reutilizare a cheii de către client. Returnarea silențioasă a rezultatului anterior ar fi mai rea.
Previne idempotența toate duplicatele?
Nu. Ea previne execuția duplicată a aceleiași cereri. Doi utilizatori diferiți care creează independent aceeași factură au făcut două cereri diferite, ceea ce reprezintă o problemă de regulă de business pentru constrângerile de unicitate, nu ceva ce o cheie de idempotență poate detecta.