Da qualche parte in ogni contratto di integrazione c'è una frase del tipo "ogni ordine sarà trasmesso esattamente una volta". Non è mai stata vera, nemmeno una volta. La rete si è partizionata a metà richiesta e il mittente ha riprovato; la coda ha riconsegnato dopo il crash di un consumer; un operatore di magazzino ha premuto due volte il pulsante di esportazione. I duplicati non sono una modalità di guasto - sono meteorologia.
Chiavi di idempotenza, scelte con cura
La soluzione è vecchia e poco affascinante: ogni messaggio porta una chiave, e l'elaborazione è un no-op quando la chiave è già stata vista. Le decisioni interessanti stanno tutte nella scelta della chiave.
- Le chiavi naturali battono quelle generate. Una sincronizzazione ordini con chiave
order_id + statussopravvive alla rigenerazione dell'outbox da parte del mittente; un UUID casuale coniato al momento dell'invio no. - Delimitate la chiave all'effetto, non al messaggio. Se un messaggio crea una fattura e prenota le scorte, questi sono due effetti con due chiavi - altrimenti un fallimento parziale vi lascia incapaci di riprovare in sicurezza entrambi.
- Fate scadere le chiavi deliberatamente. Un archivio di deduplicazione che cresce all'infinito è una bomba a orologeria; uno che scade troppo presto reintroduce i duplicati. Di default usiamo 3 volte l'orizzonte massimo di retry del mittente.
Il test che conta
Ogni integrazione che consegniamo supera quello che internamente chiamiamo il test del doppio tocco: riproducete l'intero log dei messaggi di ieri contro il sistema di oggi, due volte, e confrontate lo stato risultante. Se qualcosa è cambiato al secondo passaggio, la pipeline non è finita.
È un test brutale e fallisce costantemente durante lo sviluppo - sui timestamp, sui campi di audit "creato da", sui contatori di sequenza. Ogni fallimento è un punto in cui una riconsegna alle 3 del mattino avrebbe silenziosamente corrotto i dati. Molto meglio scoprirlo in fase di sviluppo che in produzione.