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 CallActivityAsync e 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 attende Task.WhenAll per 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 a Task.Delay.
  • Human-in-the-loop / external events: l’orchestrator si sospende su WaitForExternalEvent in attesa di un’approvazione esterna, spesso combinato con un timer di timeout (Task.WhenAny tra 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() o Random dentro l’orchestrator e ottieni comportamenti incoerenti. Risposta giusta: usa context.CurrentUtcDateTime per l’ora e context.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: WaitForExternalEvent combinato con un durable timer via Task.WhenAny.
  • Thread.Sleep / Task.Delay per attese → scenario: introduci ritardi bloccanti nell’orchestrator. Risposta giusta: usa context.CreateTimer, l’unico meccanismo di attesa compatibile con checkpoint e replay.