Immaginiamo un processo composto da cinque passaggi. I primi tre terminano correttamente, il quarto chiama un servizio esterno che non risponde e l’esecuzione si interrompe. Al tentativo successivo il sistema deve ripartire dall’inizio, riprendere dal quarto passaggio oppure annullare ciò che è già stato eseguito?
La risposta non può essere lasciata al caso. Quando un processo aggiorna dati, chiama sistemi esterni o produce effetti che non possono essere annullati con una semplice transazione SQL, la gestione dell’errore diventa parte dell’architettura.
Un workflow affidabile non è soltanto una sequenza di metodi. È un processo osservabile, persistente e riprendibile, capace di distinguere un errore temporaneo da un errore definitivo e di evitare che un retry generi duplicati.
Quando una sequenza di metodi non è più sufficiente
Per molte operazioni applicative è sufficiente ricevere una richiesta, eseguire alcune istruzioni e restituire una risposta. Finché tutto avviene nello stesso processo, in pochi secondi e con una sola risorsa transazionale, il modello è semplice da comprendere.
La situazione cambia quando il flusso comprende operazioni con caratteristiche differenti, per esempio:
- validazione della richiesta;
- lettura di informazioni da un sistema esterno;
- aggiornamento del database locale;
- invio di un comando a un altro servizio;
- generazione di un documento o di una notifica;
- registrazione dell’esito finale.
Ognuno di questi passaggi può avere tempi, dipendenze e modalità di errore diverse. Un servizio remoto può essere temporaneamente indisponibile, un dato può non superare una regola di validazione oppure l’applicazione può essere riavviata mentre il processo è ancora in corso.
Il punto chiave
Un metodo applicativo vive normalmente per la durata della richiesta che lo ha avviato. Un processo aziendale può invece dover sopravvivere a timeout, riavvii, attese, indisponibilità temporanee e interventi manuali.
Il problema degli errori parziali
Quando alcuni passaggi sono già stati completati
Il caso più delicato non è il fallimento immediato. È il fallimento che si verifica dopo che il processo ha già prodotto effetti osservabili.
Se i primi passaggi hanno aggiornato il database o inviato una richiesta a un sistema esterno, ricominciare dall’inizio può ripetere operazioni già concluse. Il risultato può essere una registrazione duplicata, una seconda notifica, un nuovo comando remoto oppure uno stato locale non più coerente con quello esterno.
Perché il rollback non è sempre possibile
Una transazione SQL è efficace quando tutte le modifiche avvengono nello stesso database. Non può però annullare automaticamente un messaggio già pubblicato, un’email già inviata o un’operazione completata da un servizio remoto.
Nei processi distribuiti non esiste sempre un rollback globale. È quindi necessario progettare in anticipo come riconoscere ciò che è già stato completato e come riportare il processo in uno stato coerente quando qualcosa fallisce.
Errore tecnico ed errore funzionale
Non tutti gli errori devono essere trattati allo stesso modo. Un timeout o un servizio temporaneamente indisponibile possono giustificare un nuovo tentativo. Una richiesta non valida o un’operazione non autorizzata, invece, non cambieranno risultato soltanto perché vengono ripetute.
| Tipo di errore | Esempio | Comportamento consigliato |
|---|---|---|
| Temporaneo | Timeout, HTTP 429, HTTP 503 | Retry controllato con attesa progressiva |
| Funzionale | Dati incompleti o regola non soddisfatta | Interruzione senza retry automatico |
| Autorizzativo | Operazione non consentita | Blocco e segnalazione |
| Stato incerto | Timeout dopo l’invio di un comando | Verifica dell’esito prima di ripetere |
Persistenza dello stato del workflow
Un workflow diventa realmente riprendibile quando il suo stato non esiste soltanto in memoria. Se l’applicazione viene riavviata, il sistema deve poter ricostruire che cosa stava elaborando, quali passaggi erano già terminati e quale attività debba essere eseguita successivamente.
Stato generale e stato dei singoli passaggi
È utile distinguere lo stato complessivo del processo dallo stato delle singole attività. Il workflow può essere, per esempio, in esecuzione o in attesa di un retry, mentre alcuni passaggi risultano completati e altri non ancora avviati.
Workflow:
Pending
Running
Completed
Failed
WaitingForRetry
Cancelled
Singolo passaggio:
NotStarted
Running
Completed
Failed
Skipped
Che cosa dovrebbe essere registrato
- identificativo univoco del workflow;
- chiave dell’elemento o della richiesta elaborata;
- stato complessivo;
- passaggio corrente;
- numero del tentativo;
- data di avvio e di completamento;
- ultimo errore rilevato;
- stato e risultato dei singoli passaggi;
- eventuale identificativo di correlazione;
- riferimenti restituiti dai sistemi esterni.
Richiesta | v Creazione istanza workflow | v Passaggio A -------- completato | v Passaggio B -------- completato | v Passaggio C -------- errore temporaneo | v Attesa / Retry | v Ripresa dal Passaggio C
In questo modello il retry non ricomincia necessariamente dall’inizio. Il sistema legge lo stato persistito, riconosce i passaggi conclusi e riprende dal primo punto ancora incompleto.
Retry: quando riprovare e quando fermarsi
Un retry non è una soluzione universale. Ripetere automaticamente qualsiasi operazione può aumentare il carico, nascondere errori funzionali e moltiplicare gli effetti di un comando non idempotente.
Errori temporanei
Sono tipicamente ritentabili gli errori che possono risolversi senza modificare la richiesta:
- timeout di rete;
- servizio temporaneamente non disponibile;
- limite di richieste raggiunto;
- database momentaneamente non raggiungibile;
- conflitto o lock transitorio;
- errore infrastrutturale non permanente.
Errori non ritentabili
Un retry automatico non è generalmente appropriato quando il problema dipende dal contenuto o dai permessi della richiesta:
- campo obbligatorio mancante;
- risorsa inesistente;
- operazione non autorizzata;
- regola funzionale non rispettata;
- formato non valido;
- rifiuto esplicito da parte del sistema destinatario.
Numero massimo di tentativi e backoff
I retry devono avere un limite. Senza un numero massimo di tentativi, un processo difettoso può continuare a consumare risorse e produrre log per ore senza possibilità di completarsi.
È inoltre preferibile aumentare progressivamente l’intervallo tra i tentativi. Un backoff esponenziale evita di richiamare continuamente un servizio già sotto pressione. L’aggiunta di una piccola variazione casuale, spesso chiamata jitter, riduce il rischio che molte elaborazioni ripartano tutte nello stesso istante.
Una policy di retry dovrebbe definire
- quali errori sono ritentabili;
- quanti tentativi sono consentiti;
- quanto attendere tra un tentativo e il successivo;
- quali informazioni registrare;
- che cosa accade dopo l’ultimo fallimento.
Idempotenza: evitare operazioni duplicate
Un’operazione è idempotente quando può essere richiesta più volte senza produrre nuovi effetti dopo che la prima esecuzione è stata completata.
Questo principio è fondamentale perché un timeout non dimostra che l’operazione sia fallita. La richiesta potrebbe essere arrivata al sistema remoto, essere stata completata correttamente e avere perso soltanto la risposta.
Se il chiamante ripete il comando senza alcuna protezione, il sistema può creare un duplicato. In un processo aziendale questo può significare registrazioni ripetute, comunicazioni duplicate o comandi eseguiti più volte.
Strategie pratiche
- assegnare una chiave di idempotenza a ogni operazione;
- salvare la chiave insieme allo stato e al risultato;
- applicare un vincolo univoco nel database;
- restituire il risultato già prodotto quando la stessa chiave viene ricevuta di nuovo;
- verificare lo stato esterno prima di ripetere un comando con esito incerto;
- rendere idempotenti anche i singoli passaggi del workflow.
public async Task<OperationResult> ExecuteAsync(
string idempotencyKey,
CancellationToken cancellationToken)
{
var existingOperation = await _repository
.GetByIdempotencyKeyAsync(
idempotencyKey,
cancellationToken);
if (existingOperation?.IsCompleted == true)
{
return existingOperation.Result;
}
await _repository.CreatePendingAsync(
idempotencyKey,
cancellationToken);
var result = await ExecuteOperationAsync(cancellationToken);
await _repository.MarkCompletedAsync(
idempotencyKey,
result,
cancellationToken);
return result;
}
L’esempio è volutamente semplificato. In un sistema reale è necessario considerare concorrenza, transazioni, fallimenti tra l’esecuzione e il salvataggio dell’esito e gestione di record rimasti nello stato intermedio.
Riprendere il workflow senza ricominciare da zero
Prima di eseguire un passaggio, il sistema dovrebbe determinare se l’attività non sia mai stata avviata, sia fallita, sia già terminata oppure debba essere ignorata perché il suo risultato è già disponibile.
Retry del singolo passaggio o dell’intero processo
Riavviare tutto il workflow è semplice, ma spesso inefficiente o pericoloso. La ripresa dal punto di errore richiede più stato e più controllo, ma evita di ripetere operazioni già concluse.
- retry automatico del solo passaggio fallito;
- retry schedulato dopo un intervallo;
- retry manuale da parte di un operatore;
- creazione di una nuova esecuzione collegata alla precedente;
- annullamento del processo quando il recupero non è possibile.
Processi concorrenti
Un altro problema frequente è l’avvio simultaneo dello stesso processo. Può accadere per una doppia richiesta dell’utente, per un retry del client o per due worker che leggono lo stesso elemento dalla coda.
Per evitarlo si possono combinare identificativi univoci, vincoli nel database, lock logici, controllo dello stato e concorrenza ottimistica. La sola verifica applicativa non è sempre sufficiente: due esecuzioni possono leggere entrambe lo stato iniziale prima che una delle due riesca ad aggiornarlo.
Compensazione: quando non è possibile fare rollback
Quando un passaggio ha prodotto un effetto che non può essere annullato con una transazione, può essere necessaria un’azione di compensazione.
La compensazione è un’operazione esplicita che riduce o neutralizza gli effetti di un passaggio precedente. Non è necessariamente l’inverso perfetto dell’operazione originale: il suo scopo è riportare il processo in uno stato aziendalmente coerente e tracciabile.
Per esempio, se una risorsa è stata creata correttamente ma un passaggio successivo rende impossibile completare il processo, il sistema può marcarla come annullata, inviare un comando di revoca oppure aprire un’attività di verifica manuale.
La compensazione va progettata
Non tutte le operazioni sono reversibili e alcune compensazioni possono fallire a loro volta. È quindi necessario registrare anche il loro stato, i tentativi eseguiti e l’eventuale necessità di intervento manuale.
Logging tecnico e tracciamento funzionale non sono la stessa cosa
I log applicativi sono indispensabili, ma non sostituiscono il modello di stato del workflow. Uno stack trace può spiegare perché una chiamata sia fallita; non sempre permette però di capire quali passaggi del processo siano stati completati e che cosa debba accadere successivamente.
Logging tecnico
- eccezioni e stack trace;
- tempi di esecuzione;
- endpoint o dipendenze chiamate;
- codici di risposta;
- metriche e informazioni infrastrutturali.
Tracciamento del processo
- stato complessivo del workflow;
- ultimo passaggio completato;
- numero dei tentativi;
- motivo del blocco;
- data del prossimo retry;
- azioni manuali consentite;
- storico delle transizioni di stato.
Correlation ID
Un identificativo di correlazione permette di collegare la richiesta HTTP iniziale, l’istanza del workflow, i singoli passaggi, le chiamate ai sistemi esterni e i log prodotti durante l’intera elaborazione.
Serve sempre un workflow engine?
No. Introdurre un motore di workflow in un processo breve e completamente sincrono può aggiungere complessità senza produrre un vantaggio reale.
Quando può bastare una soluzione applicativa
- pochi passaggi semplici;
- durata limitata alla singola richiesta;
- assenza di attese o callback;
- operazioni concentrate nello stesso database;
- rischio contenuto di duplicazione;
- gestione dell’errore facilmente comprensibile.
Quando valutare un workflow engine
- processi che durano minuti, ore o giorni;
- molte integrazioni esterne;
- necessità di persistenza e ripresa automatica;
- attese, callback o interventi umani;
- retry differenziati per passaggio;
- necessità di audit e supervisione operativa;
- processi soggetti a evoluzione e versionamento.
La scelta non dovrebbe partire dal prodotto o dalla libreria. Dovrebbe partire dalle caratteristiche del processo, dal rischio operativo e dalla necessità di comprendere e governare ciò che accade quando l’esecuzione non segue il percorso ideale.
Come collegare workflow e API ASP.NET Core
Un endpoint può essere il punto di ingresso del processo, ma non deve necessariamente eseguire in modo sincrono ogni attività che lo compone. Nei flussi lunghi è spesso preferibile validare la richiesta, creare l’istanza del workflow e restituire un identificativo con cui seguirne lo stato.
Questo principio completa quanto descritto nella guida sulla progettazione di API Enterprise con ASP.NET Core: l’API definisce il contratto e il confine di comunicazione, mentre il workflow governa l’esecuzione affidabile del processo.
L’introduzione di un livello di orchestrazione può inoltre far parte di una modernizzazione progressiva delle applicazioni .NET Framework, isolando processi critici senza richiedere la riscrittura immediata dell’intero sistema.
Checklist per progettare un workflow affidabile
Stato e persistenza
- Lo stato sopravvive al riavvio dell’applicazione?
- È possibile sapere quale passaggio sia stato completato?
- Ogni tentativo viene registrato?
- Il risultato dei passaggi importanti viene conservato?
Errori e retry
- Gli errori temporanei sono distinti da quelli definitivi?
- Esiste un numero massimo di tentativi?
- Il retry utilizza un intervallo progressivo?
- Dopo l’ultimo tentativo il processo entra in uno stato chiaro?
Idempotenza
- Una chiamata ripetuta può produrre duplicati?
- Esiste una chiave univoca dell’operazione?
- Il sistema può recuperare un risultato precedente?
- I singoli passaggi sono sicuri in caso di ripetizione?
Monitoraggio
- È possibile individuare i workflow bloccati?
- Esiste un correlation ID?
- Un operatore può riprendere o annullare il processo?
- Lo storico delle transizioni è consultabile?
Errori comuni nella progettazione dei workflow
1. Ritentare qualsiasi eccezione
Un retry indiscriminato non corregge dati errati o autorizzazioni mancanti. Può invece aumentare il carico e ritardare l’individuazione del vero problema.
2. Salvare soltanto lo stato finale
Sapere che il workflow è fallito non basta. È necessario conoscere il passaggio interessato, i tentativi eseguiti e gli effetti già prodotti.
3. Affidarsi soltanto ai log
I log aiutano la diagnosi tecnica, ma non sono un sostituto affidabile dello stato applicativo e non dovrebbero essere interrogati per decidere come riprendere il processo.
4. Ignorare i casi di esito incerto
Dopo un timeout, ripetere subito un comando può essere pericoloso. Prima bisogna stabilire se il sistema destinatario abbia già completato l’operazione.
5. Non prevedere un intervento manuale
Alcuni errori non possono essere risolti automaticamente. Un workflow enterprise dovrebbe prevedere stati chiari e strumenti controllati per analizzare, correggere e riprendere le elaborazioni bloccate.
FAQ
Che differenza c’è tra un workflow e una sequenza di metodi?
Una sequenza di metodi viene normalmente eseguita all’interno della stessa richiesta o dello stesso processo applicativo. Un workflow mantiene invece uno stato esplicito e può essere interrotto, ripreso o ritentato anche in esecuzioni differenti.
Che cosa significa rendere un’operazione idempotente?
Significa fare in modo che più richieste equivalenti non producano effetti duplicati dopo che la prima è stata completata.
Tutti gli errori devono essere ritentati?
No. I retry sono appropriati soprattutto per errori temporanei. Errori di validazione, autorizzazione o vincoli funzionali devono generalmente interrompere il processo.
È necessario utilizzare un workflow engine?
Non sempre. Per processi brevi e semplici può bastare una soluzione applicativa. Un motore diventa utile quando servono persistenza, attese, ripresa, retry complessi, supervisione operativa e audit.
Come si evita che lo stesso workflow venga avviato due volte?
È possibile combinare identificativi univoci, vincoli nel database, controlli di stato, lock logici e meccanismi di concorrenza ottimistica.
In sintesi
Un workflow affidabile rende esplicito ciò che normalmente resta nascosto.
Stato persistente, retry controllati, idempotenza e tracciamento permettono di riprendere i processi dopo un errore senza duplicare operazioni e senza affidarsi soltanto ai log.
Guide correlate
Hai un processo .NET difficile da controllare o da riprendere dopo un errore?
Posso supportare software house e team di sviluppo nell’analisi e nella progettazione di workflow, integrazioni API e processi backend affidabili, intervenendo anche su applicazioni .NET esistenti.