Nordlet

← Alapok / Magyarázatok

Mi az az idempotencia? Miért van rá szükségük a könyvelési és fizetési API-knak

Hogyan hoz létre egy megismételt kérés duplikált számlát, mit garantál az idempotencia-kulcs, milyen hibamódokat kell kezelnie, és hogyan kell helyesen implementálni.

Egy művelet akkor idempotens, ha kétszer elvégezve ugyanazt az eredményt adja, mint egyszer. Egy rekord olvasása természeténél fogva idempotens. Egy számla létrehozása viszont nem az – hívja meg kétszer, és kap két számlát, két követelést, valamint egy ügyfelet, aki megkérdezi, miért számlázták ki neki kétszer ugyanazt.

Ez a rés azért lényeges, mert a hálózatok egy specifikus, kellemetlen módon hibásodnak meg: a kérés sikeres, de a válasz elveszik. A kliens időtúllépést tapasztal, és nem tudja megkülönböztetni a „soha nem érkezett meg” állapotot a „megérkezett, lefutott, a válasz elveszett” állapottól. Az egyetlen biztonságosnak tűnő opciója az újrapróbálkozás, és pont ez az újrapróbálkozás hozza létre a duplikációt.

Miért ront a helyzeten a könyvelés?

A legtöbb rendszerben a duplikáció csak egy kis kellemetlenség. Egy főkönyvben (ledger) azonban halmozódó problémát jelent:

  • A duplikált számla felemészt egy sorszámot a számlaszámlánc (gapless sequence) hézagmentes sorrendjéből, így a törlése nem opció – az immutabilitás miatt a helyesbítéshez helyesbítő számlára van szükség, amelyet az ügyfél is láthat.
  • A duplikátum könyvelési tételeket generál, így az árbevétel, az áfa és a vevőkövetelések egyaránt túlértékeltek maradnak mindaddig, amíg valaki észre nem veszi.
  • Bekerül a bevallásba, valamint Magyarországon az e-Áfa rendszerbe vagy más joghatóságok megfelelőibe (például Litvániában az i.SAF nyilvántartásokba).
  • Ha fizetést is elindított, akkor tényleges pénzmozgás történt.

Az „ellenőrizzük a duplikációkat” megközelítés sem működik, mivel teljesen normális, ha ugyanannak a vevőnek két különálló számlát állítanak ki ugyanabban az összegben, ugyanazon a napon. A rendszer nem tudja a tartalomból kitalálni a szándékot – a kliensnek kell azt egyértelműen jeleznie.

Hogyan működnek az idempotencia-kulcsok?

A kliens logikai műveletenként egyedi kulcsot generál, és elküldi azt a kéréssel együtt:

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

A szerver ezután az alábbiakat garantálja:

  1. Az első kérés ezzel a kulccsal – a szokásos módon feldolgozza, az eredményt pedig eltárolja a kulcshoz rendelve.
  2. Újrapróbálkozás ugyanezzel a kulccsal és tartalommal (payload) – nem futtatja újra, hanem visszaadja eltárolt választ.
  3. Ugyanaz a kulcs, eltérő tartalom – elutasítja. A kulcs egy olyan állítás, hogy ez ugyanaz a művelet; egy eltérő törzs (body) azt jelenti, hogy nem az, és a régi eredmény csendes visszaadása rosszabb lenne, mint egy hiba.
  4. Újrapróbálkozás, amíg az eredeti még fut – egy újrapróbálható hibával elutasítja ahelyett, hogy párhuzamosan végrehajtaná.

A 4. pont az, amit a naiv implementációk figyelmen kívül hagynak. Az a kliens, amelyik 5 másodbornál időtúllépést tapasztal, majd 6 másodbornál újrapróbálkozik, összeütközik a saját folyamatban lévő kérésével; egyidejűségi védelem nélkül mindkettő lefut, és létrejön a duplikáció, aminek a megakadályozására a mechanizmust létrehozták.

A kulcsok helyes generálása

A kulcsnak a műveletet kell azonosítania, nem pedig a kísérletet:

  • Helyes: egy UUID, amelyet egyszer generál a rendszer, amikor a felhasználó a „számla kiállítása” gombra kattint, és a kattintás minden egyes újrapróbálkozásához ugyanezt használja.
  • Helyes: a saját tartományból származtatott determinisztikus kulcs – order-1041-invoice.
  • Helytelen: egy új UUID minden egyes HTTP-kísérlethez. Minden újrapróbálkozás új kulcsot kap, így minden újrapróbálkozás új műveletnek számít, és semmit sem valósított meg.
  • Helytelen: a tartalom hash értéke. Két jogszerűen azonos művelet ilyenkor ütközik, és a másodikat a rendszer csendesen elnyeli.

A kulcsoknak megőrzési idejük (retention period) van – elég hosszúnak kell lennie ahhoz, hogy lefedje a reális újrapróbálkozási ablakokat és a kliens leállásait, de nem tart örökké.

Amit nem old meg

Az idempotencia védi a rendszert ugyanazon kérés duplikált végrehajtásától. Nem nyújt védelmet a következők ellen:

  • Két különböző felhasználó ugyanazt a számlát hozza létre különböző munkamenetekből – ez egy üzletszabály-probléma, amelyet egyediségi megszorításokkal (unique constraints), például rendelésenkénti egy számlával lehet megoldani.
  • Logikai duplikációk különböző kulcsokkal.
  • Nem-determinizmus a műveleten belül – ha a kezelő (handler) időbélyeget vagy sorszámot generál, a tárolt válasz az elsőt rögzíti, ami a kívánt viselkedés, de érdemes megérteni.

Az adatbázis-megszorításokat nem helyettesíti, hanem kiegészíti azokat. A Nordlet főkönyve mindkettőt használja: az idempotencia-kulcsok megállítják a megismételt kéréseket, míg az egyedi indexek (unique indexes) megakadályozzák a kifizetésenkénti duplikált kifizetési tételeket, a szállító és bizonylatszám szerinti duplikált bejövő számlákat, valamint az eszköz és időszak szerinti duplikált értékcsökkenési futásokat.

Hogyan implementálja ezt a Nordlet?

Küldjön egy Idempotency-Key fejlécet (header) bármilyen állapotmódosító kérésben – ez egy legfeljebb 255 karakter hosszúságú, nem üres karakterlánc.

Egy adott kulccsal érkező első kérés végrehajtásra kerül, és az eredménye tárolódik. Az ugyanezzel a kulccsal és tartalommal történő újrapróbálkozás a tárolt választ adja vissza újrafuttatás nélkül, így a számla pontosan egyszer jön létre, függetlenül attól, hogy a hívást hányszor ismétlik meg.

Két hibamód explicit módon, csendes elnyelés helyett jelenik meg:

  • Ugyanaz a kulcs, eltérő tartalom → elutasítva: "Idempotency-Key was already used with a different request payload." (Az idempotencia-kulcsot már felhasználták egy eltérő kéréstartalommal.) Ez szűri ki azt a kifejezetten veszélyes esetet, amikor a kliens úgy ismétli fel a kulcsot, hogy közben megváltoztatja az összeget.
  • Ugyanaz a kulcs, az eredeti még folyamatban van → újrapróbálható hibaként elutasítva: "A request with this Idempotency-Key is still being processed, retry shortly." (Az ezzel az idempotencia-kulccsal ellátott kérés feldolgozása még folyamatban van, kérjük, próbálja újra később.) Nincs egyidejű duplikált végrehajtás.

A kulcsok hatóköre cégenként (tenant) külön van elválasztva, így két különböző ügyfél, ha történetesen ugyanazt az UUID-t generálja, soha nem ütközik össze.

A kiegészítő garancia ellenkező irányban is működik. A webhookok kézbesítése legalább egyszeri (at-least-once) elven történik, ami azt jelenti, hogy a végpontja időnként kétszer is megkapja ugyanazt az eseményt – egy olyan időtúllépés után, ahol a kézbesítés valójában sikeres volt, vagy újrakézbesítés alkalmával. Minden esemény rendelkezik egy azonosítóval, és a kezelőjének rögzítenie kell a feldolgozott azonosítókat, a ismétléseket pedig figyelmen kívül kell hagynia. Az idempotencia egy olyan tulajdonság, amelyre az integráció mindkét oldalának szüksége van; az API-konvenciók útmutatója részletesen tárgyalja a kérések oldalát.

FAQ

Mi az az idempotencia-kulcs?

Egy kliens által generált azonosító, amelyet a kéréssel együtt küldenek, és amely lehetővé ugyanazon logikai művelet ismétlésének felismerését a szerver számára. Az első kérés lefut; az ugyanezzel a kulccsal végzett újrapróbálkozások a tárolt eredményt adják vissza az újrafuttatás helyett.

Miért van szükségük a könyvelési API-knak idempotenciára?

Mivel a duplikált írás egy valós bizonylatot hoz létre valós következményekkel – egy második számlát, amely felemészt egy sorszámot, bekerül a főkönyvbe és a bevallásba. Ellentétben a duplikált olvasással, ezt nem lehet egyszerűen törölni; helyesbítő számlával kell javítani.

Kell-e új kulcsot generálnom minden egyes újrapróbálkozáshoz?

Nem – az teljesen érvénytelenítené a mechanizmust. Logikai műveletenként egy kulcsot kell generálni, és azt kell újra felhasználni a művelet minden egyes újrapróbálkozásához. A kísérletenkénti új kulcs azt jelenti, hogy minden kísérletet új műveletként kezel a rendszer.

Mi történik, ha újra felhasználok egy kulcsot egy eltérő tartalommal?

A kérés elutasításra kerül. A kulcs azt állítja, hogy ez ugyanaz a művelet, így a megváltozott törzs azt jelenti, hogy valami nincs rendben – legvalószínűbben egy kulcsújrafelhasználási hiba a kliens oldalán. A korábbi eredmény csendes visszaadása ennél rosszabb lenne.

Megakadályozza az idempotencia az összes duplikációt?

Nem. Megakadályozza ugyanazon kérés duplikált végrehajtását. Két különböző felhasználó, aki egymástól függetlenül hozza létre ugyanazt a számlát, két különböző kérést adott le, ami az egyedi megszorítások üzletszabály-problémája, nem pedig olyasmi, amit egy idempotencia-kulcs láthatna.