Le Durable Functions sono un’estensione di Azure Functions che consente di scrivere workflow stateful in un ambiente serverless. Il runtime gestisce automaticamente checkpoint e restart, sollevando lo sviluppatore dalla gestione manuale di code, tabelle di stato o timer. Il modello di consumo tipico è il piano Consumption o Flex Consumption, con un task hub che persiste lo stato su Azure Storage (queues, tables, blobs) — oppure, per throughput elevato e minori dipendenze, il backend Microsoft Netherite o MSSQL.
I tre tipi di funzione
- Orchestrator function: definisce il flusso del workflow in codice. Non esegue lavoro pesante: coordina, invoca le activity, gestisce timer ed eventi. È l’unico punto in cui vive la logica di orchestrazione.
- Activity function: è l’unità di lavoro effettivo (chiamate a database, API, elaborazioni). Viene invocata dall’orchestrator tramite
CallActivityAsynce può fallire e ritentare in modo indipendente. - Entity function (Durable Entities): gestisce piccoli pezzi di stato indirizzabili tramite un entity ID, con operazioni serializzate. Utile per contatori, aggregatori o attori con stato — ad esempio accumulare eventi da più sorgenti senza race condition.
L’ingresso avviene tipicamente da un client (starter) function, con un trigger HTTP o queue, che avvia un’istanza tramite il DurableClient binding e ne espone l’instanceId.
Event sourcing e replay
Il cuore del modello è l’event sourcing: ogni azione dell’orchestrator (chiamata a un’activity, attesa di un timer, ricezione di un evento) viene registrata in una history table. Quando un’activity completa, l’orchestrator non riprende dal punto di sospensione: viene ri-eseguito dall’inizio (replay), e il runtime “salta” le operazioni già presenti nella history restituendone il risultato memorizzato, fino a raggiungere il primo lavoro non ancora eseguito.
Questa è la ragione del vincolo fondamentale: il codice dell’orchestrator deve essere deterministico. Poiché viene rieseguito più volte, deve produrre esattamente la stessa sequenza di chiamate a ogni replay.
I pattern di orchestrazione
- Function chaining: sequenza di activity dove l’output di una alimenta la successiva (
A → B → C), con propagazione automatica degli errori. - Fan-out/fan-in: si avviano N activity in parallelo (lista di
Task), poi si attendeTask.WhenAllper aggregarne i risultati. Il fan-in — difficile da implementare a mano con code — è gestito dal runtime. - Async HTTP API: lo starter restituisce un status endpoint (
CreateCheckStatusResponse) con URL per polling, terminate e raise event. Il pattern standard per operazioni long-running invocate via HTTP 202. - Monitor: polling ricorsivo con un durable timer (
CreateTimer) per osservare uno stato finché non cambia, con intervalli e scadenza configurabili. Preferire i durable timer aTask.Delay. - Human-in-the-loop / external events: l’orchestrator si sospende su
WaitForExternalEventin attesa di un’approvazione esterna, spesso combinato con un timer di timeout (Task.WhenAnytra evento e timer) per gestire l’escalation.
Scelte architetturali tipiche
Per idempotenza e retry mirati, incapsulare le chiamate esterne nelle activity con RetryOptions. Per stato condiviso concorrente, preferire un’entity a variabili nell’orchestrator. Per workflow che superano i limiti di history, usare ContinueAsNew per resettare lo stato (pattern eternal orchestration, es. monitor perpetui).
Trappole tipiche d’esame
- Codice non deterministico nell’orchestrator → scenario: usi
DateTime.Now,Guid.NewGuid()oRandomdentro l’orchestrator e ottieni comportamenti incoerenti. Risposta giusta: usacontext.CurrentUtcDateTimeper l’ora econtext.NewGuid()per i GUID; genera valori non deterministici dentro un’activity. - I/O o chiamate di rete diretti nell’orchestrator → scenario: chiami un’API o un database dall’orchestrator. Risposta giusta: sposta ogni I/O in un’activity function; l’orchestrator deve solo coordinare.
- Aggregare risultati paralleli → scenario: devi eseguire molte attività in parallelo e attendere il completamento di tutte. Risposta giusta: fan-out/fan-in con lista di task +
Task.WhenAll, non code custom. - Attesa di approvazione con scadenza → scenario: serve un’approvazione umana entro 72 ore, altrimenti escalation. Risposta giusta:
WaitForExternalEventcombinato con un durable timer viaTask.WhenAny. Thread.Sleep/Task.Delayper attese → scenario: introduci ritardi bloccanti nell’orchestrator. Risposta giusta: usacontext.CreateTimer, l’unico meccanismo di attesa compatibile con checkpoint e replay.