modulo 08 / lezione 44
Debugging di dati, backend e integrazioni
Idempotenza, retry, DLQ e integrazioni
Capire come retry mal progettati creano duplicati, perché serve idempotency-key, quando un messaggio finisce in dead letter queue.
In parole semplici
Le integrazioni con sistemi esterni (pagamenti, email, webhook, code di messaggi) hanno una caratteristica scomoda: a volte le richieste non arrivano, o arrivano due volte, o arrivano fuori ordine. La risposta non è “sperare”. È progettare per idempotenza, retry, dead letter queue.
Parole nuove
- idempotenza: ripetere l’operazione produce lo stesso risultato del farla una volta.
- idempotency-key: chiave che identifica una richiesta univoca tra retry.
- retry: ripetizione di una richiesta fallita.
- backoff: pausa crescente tra retry consecutivi.
- dead letter queue (DLQ): coda dove finiscono messaggi non processabili dopo tutti i retry.
Cosa impari
- Distinguere operazioni naturalmente idempotenti da non.
- Progettare retry con backoff esponenziale.
- Usare idempotency key per evitare duplicati.
- Riconoscere quando un messaggio va in DLQ.
Idempotenza: la prima salvaguardia
Operazione idempotente: ripeterla ha lo stesso effetto della prima.
Esempi:
PUT /utenti/42 { nome: "Carlo" }→ ripetuto = stesso stato.DELETE /ordini/99→ ripetuto = stato “cancellato”.
Operazione non idempotente:
POST /ordini { totale: 50 }→ ripetuto = due ordini da 50.POST /pagamenti { importo: 100 }→ due addebiti.
Per le non idempotenti, servono protezioni.
Idempotency key
Il client genera una chiave univoca per richiesta:
const idempotencyKey = crypto.randomUUID();
fetch("/api/pagamenti", {
method: "POST",
headers: {
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json",
},
body: JSON.stringify({ importo: 100 }),
});
Il server:
- Legge
Idempotency-Key. - Cerca se ha già processato quella chiave.
- Se sì: ritorna lo stesso risultato precedente. Non riprocessa.
- Se no: processa e salva risultato associato alla chiave.
Lato server (pseudocodice):
async function gestisciPagamento(req) {
const key = req.headers["idempotency-key"];
const cached = await idempotencyStore.get(key);
if (cached) return cached;
const result = await processaPagamento(req.body);
await idempotencyStore.set(key, result, { ttl: 24 * 3600 });
return result;
}
Stripe, Square, AWS API e tanti altri implementano questo schema.
Retry: come, quando, quanto
Un retry “ingenuo”:
async function fetchConRetry(url, max = 3) {
for (let i = 0; i < max; i++) {
try { return await fetch(url); }
catch (e) { if (i === max - 1) throw e; }
}
}
Problemi:
- Nessuna pausa: se il server è giù, peggiori il problema (thundering herd).
- Tutti i client retryano insieme.
- Non distingue errori “ritentabili” (5xx, network) da “non ritentabili” (400, 404).
Retry corretto: backoff esponenziale + jitter
async function fetchConRetry(url, opts = {}) {
const max = opts.max || 5;
for (let i = 0; i < max; i++) {
try {
const r = await fetch(url, opts);
if (r.ok) return r;
if (r.status >= 400 && r.status < 500 && r.status !== 408 && r.status !== 429) {
// errori client non ritentabili
return r;
}
} catch (_) { /* network error: ritentabile */ }
if (i === max - 1) throw new Error("Max retry exceeded");
const base = 300 * Math.pow(2, i); // 300, 600, 1200, 2400, 4800 ms
const jitter = Math.random() * 300;
await new Promise(r => setTimeout(r, base + jitter));
}
}
Componenti:
- Backoff esponenziale: 2x ad ogni retry.
- Jitter: rumore casuale per non sincronizzare retry tra client.
- Whitelist di codici ritentabili: 408, 429, 500, 502, 503, 504.
Retry-After
Se il server risponde 429 o 503 con header Retry-After: 30:
const retryAfter = r.headers.get("Retry-After");
if (retryAfter) {
await new Promise(r => setTimeout(r, parseInt(retryAfter) * 1000));
}
Rispetta sempre Retry-After. Ignorarlo aggrava il problema.
Dead letter queue (DLQ)
In code di messaggi (Kafka, RabbitMQ, SQS), dopo N retry il messaggio finisce in una coda separata: la DLQ.
Vantaggi:
- Non blocca la coda principale.
- Permette di indagare con calma.
- Si può reprocessare dopo fix.
Diagnosi tipica:
- Allarme: “DLQ ha 1234 messaggi”.
- Apri primo messaggio. Cerca il pattern.
- Cause comuni: schema cambiato, dipendenza giù, dato corrotto, bug recente.
- Fix → reprocessa DLQ.
Webhook
Anti-pattern: confidare che un webhook arrivi una sola volta. Provider seri ritentano webhook fino a successo (200 OK). Implicazioni:
- Endpoint webhook deve essere idempotente.
- Verifica firma (HMAC) per autenticità.
- Rispondi 2xx anche se la business logic fallisce (poi gestisci in modo asincrono).
Esempio realistico
E-commerce: utente paga, sistema deve creare ordine e mandare email.
Anti-pattern:
async function checkout(carrello) {
await paga(carrello.totale);
await creaOrdine(carrello);
await mandaEmail();
}
Se paga ha successo ma creaOrdine fallisce: pagamento senza ordine.
Pattern corretto:
async function checkout(carrello, idempotencyKey) {
const pagamento = await paga(carrello.totale, idempotencyKey);
await coda.push({ type: "crea-ordine", carrello, pagamentoId: pagamento.id });
// crea-ordine viene processato in modo idempotente, con retry, DLQ
return pagamento;
}
Ogni step idempotente, ognuno retryabile, fallimenti in DLQ.
Esercizio guidato
Crea un piccolo client che:
- Chiama un endpoint fittizio che fallisce casualmente al 30%.
- Aggiunge retry con backoff esponenziale + jitter.
- Aggiungi idempotency key.
- Simula lato server: il “server” memorizza chiavi processate e ritorna lo stesso risultato per chiavi ripetute.
- Verifica che dopo 5 retry totali con stessa chiave, l’operazione è eseguita una sola volta.
Errori frequenti del principiante
- Retry senza backoff: thundering herd.
- Retry su 4xx: peggiori il problema, sovraccarichi.
- Webhook senza idempotenza: duplicati.
- DLQ ignorata: si accumulano messaggi finché esplode.