'O Ragù — Saga Orchestration
“‘O ragù nun perdona chi ha fretta.” — Proverbio della cucina partenopea
Esistono problemi che non si risolvono in un attimo. Transazioni distribuite che coinvolgono più servizi, più nodi, più risorse — ognuna con il proprio ritmo, i propri fallimenti, le proprie compensazioni. Sono i problemi che richiedono il trattamento del ragù: fuoco basso, pazienza, e la certezza che alla fine il risultato ripaga ogni attesa.
‘O Ragù è il modulo di saga orchestration del Pasta Protocol. Implementa il pattern Saga con compensazioni automatiche, checkpointing distribuito e resilienza integrata. Ogni saga è una ricetta a lunga cottura — e come il ragù della nonna, non si improvvisa.
Cos’è una Saga
Una saga è una sequenza di transazioni locali, ciascuna eseguita su un singolo servizio o nodo. Se una transazione fallisce, la saga non tenta un rollback globale — esegue invece le compensazioni: operazioni inverse progettate per annullare semanticamente il lavoro già fatto.
interface Saga<TInput, TOutput> { readonly id: string; readonly name: string; readonly steps: ReadonlyArray<SagaStep>; readonly compensations: ReadonlyArray<SagaCompensation>; readonly timeoutCotture: number; execute(input: TInput): Promise<SagaResult<TOutput>>; resume(sagaId: string): Promise<SagaResult<TOutput>>; compensate(sagaId: string): Promise<CompensationResult>;}
interface SagaStep { readonly name: string; readonly service: string; readonly operation: string; readonly retryPolicy: RetryPolicy; readonly timeoutCotture: number;}
interface SagaCompensation { readonly forStep: string; readonly service: string; readonly operation: string; readonly idempotencyKey: (sagaId: string) => string;}Il Ciclo di Vita di una Saga
Ogni saga attraversa un ciclo di vita rigoroso, scandito come i passi di una ricetta collaudata.
-
Prepara gli ingredienti
Prima di avviare la saga, il sistema valida l’input, verifica la disponibilità dei servizi coinvolti e crea il record di saga nella Dispensa con stato
PREPARAZIONE. Ogni step riceve un ID idempotente che garantisce che — in caso di retry — la stessa operazione non venga eseguita due volte.const saga = await ragu.prepare({name: 'ordine-completo',input: { clienteId: '42', piatti: ['margherita', 'calzone'], tavolo: 7 },timeoutCotture: 24, // 12 ore al massimo});// saga.id = 'saga_ord_20240315_007'// saga.status = 'PREPARAZIONE' -
Inizia la cottura
La saga entra in stato
IN_COTTURAe il Capocuoco assegna l’esecuzione a un nodo Pizzaiolo dedicato. Gli step vengono eseguiti in sequenza, con retry automatico su errori transienti. Ogni step completato viene marcato nella Dispensa come checkpoint — se il nodo cade, la saga può riprendere dall’ultimo checkpoint.const esecuzione = await saga.start();// Esegue in sequenza:// Step 1: verifica-disponibilita-ingredienti → OK// Step 2: blocca-tavolo → OK// Step 3: invia-ordine-a-cucina → OK (checkpoint) -
Monitora la temperatura
Durante l’esecuzione, la saga emette eventi di monitoraggio a ogni step. Puoi sottoscrivere questi eventi tramite GarlicBreadcast per tenere traccia del progresso, inviare notifiche o aggiornare l’UI in tempo reale.
ragu.on('step:completed', (event: SagaStepEvent) => {logger.voce(`Saga ${event.sagaId}: step "${event.stepName}" completato`, {duration: event.durationMs,checkpoint: event.checkpointId,});});ragu.on('step:failed', (event: SagaStepEvent) => {logger.grido(`Saga ${event.sagaId}: step "${event.stepName}" fallito`, {error: event.error,attemptNumber: event.attempt,willRetry: event.willRetry,});}); -
Servi quando è pronto
Una saga completa con successo entra in stato
SERVITA. Se un qualsiasi step fallisce definitivamente (esauriti i retry), la saga entra in statoCOMPENSAZIONEe avvia l’esecuzione inversa degli step completati. Una saga completamente compensata entra in statoRIMANDATO_IN_CUCINA.const risultato = await saga.awaitCompletion();if (risultato.status === 'SERVITA') {const ordineId = risultato.output.ordineId;// Tutto perfetto, il piatto è pronto.} else if (risultato.status === 'RIMANDATO_IN_CUCINA') {// La saga è stata compensata. Tutti i servizi sono tornati allo stato iniziale.const motivo = risultato.compensationReason;throw new SagaError('VESUVIO', `Saga fallita e compensata: ${motivo}`);}
Definire una Saga
Il Pasta Protocol offre un builder fluente per definire le saghe:
import { SagaBuilder } from '@pasta-protocol/ragu';
const ordineSaga = new SagaBuilder('ordine-completo') .timeout(24) // cotture massime .step('verifica-magazzino') .calls('magazzino-service', 'verificaDisponibilita') .compensateWith('magazzino-service', 'rilasciaRiserva') .retries(3) .timeoutCotture(2) .step('crea-ordine') .calls('ordini-service', 'creaOrdine') .compensateWith('ordini-service', 'annullaOrdine') .retries(1) .timeoutCotture(1) .step('addebita-pagamento') .calls('pagamenti-service', 'addebita') .compensateWith('pagamenti-service', 'rimborsa') .retries(2) .timeoutCotture(3) .step('notifica-cliente') .calls('notifiche-service', 'invia') .noCompensation() // le notifiche non si annullano .retries(5) .timeoutCotture(1) .build();Stati della Saga
| Stato | Descrizione |
|---|---|
PREPARAZIONE | Saga creata, input validato, non ancora avviata |
IN_COTTURA | Esecuzione degli step in corso |
IN_PAUSA | Saga in attesa di un evento esterno o approvazione umana |
COMPENSAZIONE | Uno step ha fallito, compensazioni in esecuzione |
SERVITA | Tutti gli step completati con successo |
RIMANDATO_IN_CUCINA | Saga compensata con successo dopo un fallimento |
BRUCIATA | Compensazione fallita — intervento manuale necessario |
Il Dead Letter Fornello
Quando una saga entra in stato BRUCIATA — fallimento sia dell’esecuzione che della compensazione — il Pasta Protocol la invia al Dead Letter Fornello, una coda speciale per intervento manuale:
ragu.onBurned(async (sagaBruciata: BurnedSaga) => { await alertingService.urgente({ livello: 'TERREMOTO', messaggio: `Saga ${sagaBruciata.id} bruciata — intervento manuale`, stepFallito: sagaBruciata.failedStep, compensazioniFallite: sagaBruciata.failedCompensations, });});Dal CLI:
pasta> saga list --status BRUCIATA saga_ord_20240315_003 ordine-completo BRUCIATA (step: addebita-pagamento) saga_ord_20240315_009 ordine-completo BRUCIATA (comp: rimborsa)
pasta> saga inspect saga_ord_20240315_003 [...]
pasta> saga compensate-manual saga_ord_20240315_003 Avvio compensazione manuale...Come il ragù, una saga ben scritta richiede tempo, attenzione e rispetto dei passaggi. Ma una volta pronta, nutre il sistema per ore — robusta, affidabile, e impossibile da affrettare.