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 nodotransform/joinsenzadeadLetterpropria ma nello scope di un guardper-recordviene eseguito isolato per-record (riuso diisolatedTransformOutput/isolatedJoinOutput) con le reiezioni instradate sulla coda del ramocatch(recordCaught), invece che confastTransformOutput. rawinoltrato: ilDeadLettersulcatchporta il record fallito (fix delraw: undefineddel 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
deadLetterdel nodo: un nodo condeadLetterpropria non entra nello scope del guard (già così incomputeTryScopes) — 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-bydurante l'implementazione).
Conseguenze
- Le demo
orders-enriched-dailyecrm-leads-syncmostrano ora N record nelcatch(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 usamode: "circuit-breaker". - Nuovo marcatore
Transform.stateful(defaultfalse), valorizzato dagroupBy.

