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.

55 min Debugging

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:

  1. Legge Idempotency-Key.
  2. Cerca se ha già processato quella chiave.
  3. Se sì: ritorna lo stesso risultato precedente. Non riprocessa.
  4. 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:

  1. Allarme: “DLQ ha 1234 messaggi”.
  2. Apri primo messaggio. Cerca il pattern.
  3. Cause comuni: schema cambiato, dipendenza giù, dato corrotto, bug recente.
  4. 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:

  1. Chiama un endpoint fittizio che fallisce casualmente al 30%.
  2. Aggiunge retry con backoff esponenziale + jitter.
  3. Aggiungi idempotency key.
  4. Simula lato server: il “server” memorizza chiavi processate e ritorna lo stesso risultato per chiavi ripetute.
  5. 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.