Vai al contenuto
Operatività

Operatività EDA: delivery semantics, idempotenza e DLQ 🚨

Se l’EDA è la parte “cool”, questa è la parte “ti salva in produzione”. Spoiler: è più importante.

Delivery semantics: cosa stai promettendo davvero 📦

Le tre etichette classiche:

  • At-most-once: potrebbe perdersi qualche evento, ma niente duplicati.
  • At-least-once: non perdi eventi (idealmente), ma puoi avere duplicati.
  • Exactly-once: l’obiettivo più ambizioso. Kafka supporta EOS (Exactly-Once Semantics) tramite producer idempotente e transazioni, ma questa garanzia vale solo tra topic Kafka (operazioni Kafka-to-Kafka). Non appena esci dal cluster — scrivi su un DB, chiami un’API, aggiorni un’altra store — la garanzia EOS non si estende. In scenari cross-system, quello che ottieni davvero è effectively-once: idempotenza applicativa + deduplicazione.

Regola pratica: in architetture distribuite, assumi duplicati e progetta di conseguenza. L’EOS del broker non ti solleva dall’obbligo di idempotenza sul consumer.

Idempotenza: rendere i consumer “a prova di replay” 🛡️

Un consumer è idempotente se processare lo stesso evento più volte porta allo stesso risultato.

Strategie comuni:

  • dedup store: salva event_id processati e scarta duplicati;
  • upsert su chiave business (es. order_id) invece di insert puro;
  • side effects controllati: invio email con “exactly-once” applicativo via lock/dedup.

Ordering e partitioning: l’ordine non è gratis 🧵

Molte piattaforme garantiscono l’ordine solo:

  • dentro una partizione;
  • per eventi con la stessa key.

Quindi la domanda diventa: qual è la key corretta?

  • per ordini: spesso order_id;
  • per utenti: spesso user_id.

Se scegli una key “casuale”, l’ordine diventa un concetto poetico.

Error handling: retry, backoff, DLQ 🧯

    flowchart LR
  B[(Broker)] --> C[Consumer]
  C -->|OK| ACK[Ack / Commit]
  C -->|Fail| R{Retry\n< max?}
  R -->|Sì| B
  R -->|No| DLQ[(DLQ)]
  DLQ --> OPS[Runbook\ntriage + reprocess]
  

Un modello robusto include:

  • retry con backoff (evita storm);
  • limite tentativi;
  • DLQ (Dead Letter Queue) per messaggi “poison”;
  • alerting su DLQ e su consumer lag.

Classificazione degli errori e DLQ: non un cestino, un processo 🧠

Non tutti gli errori meritano lo stesso trattamento. Gli errori transitori (rete, timeout, dipendenze lente) richiedono retry con backoff; gli errori permanenti (schema invalido, dato corrotto) richiedono stop, invio in DLQ e triage. Deciderlo “durante l’incidente” è il modo migliore per peggiorare l’incidente.

Esempio pratico: se un consumer fallisce per timeout verso un servizio esterno, prova retry con backoff e circuit breaker; se fallisce per schema unrecognized, invia su DLQ e segnala per triage manuale.

La DLQ non è un limbo silenzioso: è utile solo se c’è un processo attivo che la presidia. Condizioni minime:

  • sai distinguere errori transitori vs permanenti (e lo decidi a priori, non durante l’incidente);
  • hai un runbook: chi guarda, con quale priorità, come si reprocessa;
  • puoi correggere e riprocessare in modo sicuro (idempotenza — sempre, non «probabilmente»).

Runbook minimale per DLQ:

  • alert immediato quando un messaggio finisce in DLQ;
  • classificazione automatica (schema error, transient, business error) dove possibile;
  • assegnazione a un owner e procedura di reprocess con idempotenza verificata;
  • metriche DLQ nel dashboard principale, non in una tab nascosta.

Consumer lag e backpressure 📈

In EDA, spesso “non hai errori”… hai ritardi.

Il consumer lag misura quanti eventi il consumer non ha ancora processato rispetto all’ultimo offset pubblicato. Un lag crescente è il primo segnale che il consumer non riesce a stare al passo: dipendenza lenta, saturazione CPU, bug di performance, o semplicemente traffico aumentato. In Kafka il lag si misura per partizione e per consumer group; in altri broker il concetto è analogo.

La backpressure è la pressione che si accumula a monte quando un consumer rallenta. Se non gestita, si propaga verso il producer (che continua a scrivere) e può degradare l’intero sistema a cascata. Alcune piattaforme offrono meccanismi nativi di backpressure; altrove tocca gestirla esplicitamente.

Strategie pratiche per gestire picchi di lag:

  • scalatura orizzontale del consumer group (più istanze = più parallelismo, nel limite del numero di partizioni);
  • batch processing per ammortizzare il costo per-evento su operazioni costose;
  • circuit breaker verso dipendenze lente, per evitare che un servizio esterno draini silenziosamente tutta la capacità;
  • alerting su soglie di lag (es. “lag > X per più di Y minuti”) con escalation graduata.

Metriche tipiche da monitorare:

  • consumer lag (per topic/partizione e per consumer group);
  • throughput (input vs output, per evidenziare il delta);
  • error rate per tipo (transient vs permanent);
  • tempo medio e percentile p99 di processamento per evento.

Osservabilità end-to-end: correlation ID o caos 🧬

Senza correlazione, debuggare un flusso asincrono è come cercare un ago in un pagliaio… al buio… mentre il pagliaio prende fuoco.

Pratiche minime:

  • correlation_id propagato su eventi e log;
  • tracing distribuito quando possibile;
  • dashboard per flussi di business (non solo metriche tecniche).

Se vuoi una base “standard”, OpenTelemetry ti aiuta a correlare segnali, ma serve comunque disciplina applicativa.

Responsabilità di producer e consumer 🤝

Un producer serio pubblica eventi coerenti, con metadata obbligatori e semantica stabile. Un consumer serio assume duplicati, gestisce out-of-order quando necessario e fallisce in modo osservabile. Quando questa responsabilità non è esplicita, il sistema “funziona” finché non scala.

Cosa ci si aspetta da un producer responsabile:

  • pubblica eventi con schema stabile e versioning esplicito;
  • include sempre event_id, timestamp e correlation_id nei metadata;
  • non rompe la compatibilità dello schema senza preavviso e senza versione;
  • documenta la semantica dell’evento (cosa significa, quando viene emesso, cosa non significa).

Cosa ci si aspetta da un consumer responsabile:

  • non assume unicità dei messaggi: implementa deduplicazione o idempotenza;
  • non assume ordine globale: gestisce out-of-order almeno per gli scenari noti;
  • logga ogni errore in modo strutturato e correlato;
  • espone metriche di salute (lag, error rate, throughput) senza che qualcuno le debba cercare.

Quando questi contratti non sono scritti da nessuna parte — non nell’ADR, non nel runbook, non nella documentazione del topic — vengono «negoziati» durante il primo incidente in produzione. Un timing subottimale.

Checklist operativa (minima) ✅

Qui non c’è “best practice da slide”: ci sono i mattoni minimi per non trasformare i retry in una stampante di incidenti. Se devi scegliere cosa fare prima, fai queste cose prima.

  • Retry policy documentata per ogni consumer.
  • DLQ con ownership e runbook.
  • Idempotenza verificata (test e revisione design).
  • Dashboard: lag, errori, DLQ, throughput.
  • Logging strutturato + correlation ID.

Prossimi passi 🚀

Quando l’operatività è a posto, puoi iniziare a progettare flussi multi-step e migrazioni senza vivere nel terrore del dual-write.

Ultimo aggiornamento il