Skip to content

0031. Guard try/catch: cattura per-record di default (N record per branch)

  • Stato: Accettata
  • Data: 2026-07-24
  • Decisori: Team BI/ETL

Contesto

Con ADR-0029 il nodo guard (try-catch) instradava gli errori dei nodi protetti sul ramo catch in modalità circuit-breaker: un nodo protetto senza deadLetter propria girava in modalità "fast" (streaming) e, al primo EtlError, il generator terminava — deviando un solo record sul catch (per giunta con raw: undefined) e fermando la sub-stream. Nella pratica finiva sempre al massimo 1 record per nodo nel catch, e i record validi successivi non venivano più processati.

È controintuitivo: un guard dovrebbe raccogliere tutti i record problematici di un branch continuando a caricare i validi (vedi #75). Il pattern per-record esisteva già per la deadLetter per-nodo (ADR-0027, isolatedTransformOutput), ma instradava verso la dead-letter del nodo, non verso il ramo catch del guard.

Decisione

Il guard cattura per-record di default: ogni record che fallisce in un nodo protetto è deviato sul ramo catch come DeadLetter completo (con il suo raw) e lo stream continua. N record invalidi nello stesso branch ⇒ N record nel catch.

  • Runner (src/core/graph-runner.ts): un nodo transform/join senza deadLetter propria ma nello scope di un guard per-record viene eseguito isolato per-record (riuso di isolatedTransformOutput/isolatedJoinOutput) con le reiezioni instradate sulla coda del ramo catch (recordCaught), invece che con fastTransformOutput.
  • raw inoltrato: il DeadLetter sul catch porta il record fallito (fix del raw: undefined del circuit-breaker), così il nodo di gestione errori ha il payload.
  • Opt-in circuit-breaker: .tryCatch(from, id, { mode: "circuit-breaker" }) conserva il comportamento "fermati al primo errore del branch" (mode: "per-record" è il default; TryCatchGraphNode.mode).
  • Precedenza alla deadLetter del nodo: un nodo con deadLetter propria non entra nello scope del guard (già così in computeTryScopes) — gestisce da sé i suoi scarti.
  • Transform stateful: una trasformazione stateful (es. groupBy, Transform.stateful) non viene mai isolata per-record — spezzarla record-per-record ne falserebbe l'aggregazione. Resta streaming (fail-fast) e, in scope guard, è catturata in stile circuit-breaker.
  • Non-EtlError (bug di programmazione) continua ad abortire la run.

Alternative considerate

  • Mantenere il circuit-breaker come default — scartata: è la fonte del problema (#75), non-dimostrativo e sorprendente per chi si aspetta di raccogliere gli scarti.
  • Rimuovere del tutto il circuit-breaker — scartata: il "fermati al primo errore del branch" è un pattern legittimo; conservato come opt-in.
  • Isolare per-record anche le transform stateful — impossibile: falserebbe le aggregazioni (dimostrato dalla regressione su group-by durante l'implementazione).

Conseguenze

  • Le demo orders-enriched-daily e crm-leads-sync mostrano ora N record nel catch (con dati demo che contengono più record invalidi nello stesso branch).
  • Cambio di semantica: i test che asserivano il circuit-breaker (caught: 1, sub-stream ferma) sono aggiornati al per-record. Chi vuole il vecchio comportamento usa mode: "circuit-breaker".
  • Nuovo marcatore Transform.stateful (default false), valorizzato da groupBy.

Noeva è un marchio registrato di 4D S.R.L.