net·devs
← Tutti gli articoli
File insight
3 min di letturaDi Kyrylo Osadchukfintechengineering

L'idempotenza è l'unica cosa che ti separa da un rimborso duplicato

I sistemi di pagamento vengono ritentati. Le reti cadono a metà chiamata. I telefoni ruotano. La differenza tra un libro contabile pulito e un incubo per il supporto clienti è se i tuoi endpoint sono onestamente idempotenti.

La prima volta che abbiamo visto un rimborso duplicato emesso in produzione era quasi dieci anni fa. Un client mobile inviò una richiesta di rimborso. Il server la elaborò. La risposta 200 non tornò mai al telefono — l'utente era entrato in un tunnel.

Il telefono ritentò, come fanno i telefoni. Il server la elaborò di nuovo. Il cliente ricevette due rimborsi. Il commerciante se ne accorse tre mesi dopo. La conversazione fu spiacevole.

La correzione richiese dieci minuti. La prevenzione richiede riflessione ogni singola volta che si progetta un endpoint di scrittura. La maggior parte dei team costruisce un vero livello di idempotenza la prima volta che viene scottata. Alcuni vengono scottati due volte.

Cosa significa davvero idempotente

Un endpoint è idempotente quando chiamarlo due volte con lo stesso input produce lo stesso effetto che chiamarlo una volta. Non la stessa risposta — lo stesso effetto sul mondo. Questa distinzione conta.

Un endpoint GET /balance è idempotente per caso, perché la lettura non cambia nulla. Un endpoint POST /refund è idempotente solo se lo si rende tale. Non esiste nessuna funzionalità del linguaggio che lo faccia per voi.

Il pattern standard è la chiave di idempotenza: il client genera un identificatore univoco per operazione logica e lo invia come header. Il server registra la chiave la prima volta che la vede, memorizza il risultato, e ad ogni retry con la stessa chiave restituisce il risultato memorizzato invece di rieseguire.

Dove l'implementazione diventa sottile

Il pattern sembra semplice. Tre punti in cui di solito sbaglia.

Memorizzare la chiave dopo il lavoro, non prima. L'implementazione ingenua esegue l'operazione, poi memorizza la chiave di idempotenza. Se il server va in crash tra le due scritture, il retry successivo rieseguirà. Memorizzare la chiave prima, nella stessa transazione dell'operazione, oppure usare un database che garantisce che le due scritture siano atomiche.

Memorizzare la risposta ma non lo stato dell'operazione. Un retry deve restituire la stessa risposta, ma deve anche sapere se l'operazione originale è effettivamente completata o è ancora in corso. Memorizzare solo la risposta perde lo stato in-flight, e un originale lento più un retry veloce possono andare in race condition. Il record di idempotenza ha bisogno di tre stati: in-progress, completato-con-risposta, fallito.

Non limitare la durata della chiave. Le chiavi di idempotenza vivono per sempre nel design ingenuo, e quella tabella cresce senza limiti. Limitarle — una finestra di 24 ore è generosa per la maggior parte dei casi d'uso di pagamento — e documentare il limite in modo che i client sappiano di dover usare chiavi nuove per i retry oltre quella finestra.

Di cosa sono responsabili i client

Il server può fare tutto correttamente e ottenere comunque scritture duplicate se il client riprova con una nuova chiave ad ogni tentativo. Il contratto deve estendersi al lato client.

Il client genera un UUID per operazione logica, non per tentativo HTTP. Lo stesso rimborso logico = stessa chiave, indipendentemente da quanti retry di rete occorrono per consegnarlo. Persistere quella chiave in locale fino a quando l'operazione non è completata o non ha superato il limite.

Questa è la parte che viene mancata negli SDK mobile sorprendentemente spesso, perché il livello di rete viene trattato come qualcosa da ritentare in modo trasparente. Le chiavi di idempotenza escono dal livello di rete e entrano nel livello applicativo. Non sono una preoccupazione di trasporto; sono una preoccupazione di dominio.

Quando iniziare

Adesso. Prima del primo rimborso duplicato. Abbiamo visto team dire "lo aggiungeremo prima del lancio" e poi rimandare il lancio per ragioni di lancio; il lavoro sull'idempotenza slippa, il primo incidente insegna loro perché era importante, e la pulizia è disordinata perché ci sono ora record in produzione creati senza chiavi.

È una delle abitudini più economiche con cui iniziare, e una delle più costose da aggiungere a posteriori.

Vogliamo sentire le tue opinioni.

il nostro CTO Kyrylo Osadchuk risponderà entro 24 ore. Niente funnel di vendita.