Modello di monitoraggio

Il modello monitor è un processo ricorrente in un flusso di lavoro che interroga periodicamente un sistema esterno finché non viene soddisfatta una condizione. Ad esempio, controlla lo stato del lavoro fino al completamento, oppure osserva i dati meteo finché il cielo non è sereno. A differenza di un trigger timer a intervallo fisso, un monitor attende tra le iterazioni (evitando sovrapposizioni), supporta intervalli dinamici e può terminare una volta soddisfatta la condizione o esaurito il tempo di timeout.

Questo articolo spiega come implementare il modello monitor utilizzando orchestrazioni durevoli.

Tip

Questo articolo illustra l'implementazione completa. Per una panoramica concettuale dei casi d'uso dell'orchestrazione durevole, vedi Cos'è il Compito Durevole?

Gli esempi Durable Functions includono uno scenario di monitoraggio meteo (C#/JavaScript) e uno scenario di monitoraggio dei problemi GitHub (Python).

Annotazioni

La versione 4 del modello di programmazione Node.js per Funzioni di Azure è disponibile a livello generale. Il modello v4 è progettato per offrire un'esperienza più flessibile e intuitiva per sviluppatori JavaScript e TypeScript. Per altre informazioni sulle differenze tra v3 e v4, vedere la guida alla migrazione.

Nei frammenti di codice seguenti JavaScript (PM4) indica il modello di programmazione v4, la nuova esperienza.

L'esempio degli SDK di Task Durable dimostra il monitoraggio dello stato del lavoro con intervalli di polling configurabili utilizzando .NET, JavaScript, Python e Java.

Prerequisiti

  • .NET 8.0 SDK o versione successiva
  • Accesso al Pianificatore di Attività Durable di Azure o all'emulatore locale

Panoramica dello scenario di monitoraggio

Questo esempio monitora le condizioni meteo correnti di una località e avvisa un utente tramite SMS quando il tempo è sereno. Per controllare il meteo e inviare gli avvisi, è possibile usare una normale funzione attivata tramite timer. Tuttavia, un problema con questo approccio è la gestione del ciclo di vita. Se deve essere inviato un solo avviso, il monitoraggio deve disabilitarsi dopo che sono state rilevate condizioni di tempo sereno.

Il modello di monitoraggio può terminare la propria esecuzione, tra gli altri vantaggi:

  • I monitor funzionano a intervalli, non secondo una pianificazione: un trigger timer viene eseguito ogni ora; un monitor attende un'ora tra le azioni. Le azioni di un monitor non si sovrappongono a meno che tu non specifichi diversamente, cosa che può essere importante per compiti di lunga durata.
  • I monitoraggi possono avere intervalli dinamici: il tempo di attesa può variare in base ad alcune condizioni.
  • I monitoraggi possono terminare quando viene soddisfatta una condizione o essere terminati da un altro processo.
  • I monitor possono prendere parametri. L'esempio mostra come applicare lo stesso processo di monitoraggio a qualsiasi posizione richiesta, numero di telefono o repository.
  • Monitoraggi sono scalabili. Poiché ogni monitor è un'istanza di orchestrazione, puoi creare più monitor senza dover creare nuove funzioni o definire altro codice.
  • I monitoraggi si integrano facilmente in flussi di lavoro più grandi. Un monitoraggio può essere una sezione di una funzione di orchestrazione più complessa o un'orchestrazione secondaria.

Questo esempio monitora lo stato di un processo a esecuzione prolungata e restituisce il risultato finale al termine o al timeout del processo. È possibile usare un normale ciclo di polling per controllare lo stato del processo, ma questo approccio presenta limitazioni per la gestione della durata e l'affidabilità.

Il modello di monitoraggio offre questi vantaggi:

  • Polling persistente: l'esecuzione dell'orchestrazione continua anche dopo i riavvii del processo e il monitoraggio continua anche dopo gli errori.
  • Intervalli configurabili: Puoi regolare dinamicamente il tempo di attesa tra i controlli di stato e l'altro.
  • Supporto del timeout: il monitoraggio può terminare quando una condizione viene soddisfatta o scade un timeout.
  • Visibilità dello stato: i client possono eseguire query sullo stato personalizzato dell'orchestrazione per visualizzare l'avanzamento del monitoraggio attuale.
  • Scalabilità: più monitoraggi possono essere eseguiti simultaneamente, ognuno dei quali tiene traccia di processi diversi.

Configurazione

Configurazione dell'integrazione di Twilio

Questo esempio prevede l'uso del servizio Twilio per inviare messaggi SMS al telefono cellulare. Funzioni di Azure ha già supporto per Twilio tramite l'associazione Twilio e l'esempio usa tale funzionalità.

È necessario per prima cosa disporre di un account Twilio. È possibile crearne uno gratuitamente all'indirizzo https://www.twilio.com/try-twilio. Dopo aver creato un account, aggiungere le seguenti tre impostazioni dell'app alla propria app per le funzioni.

Nome dell'impostazione dell'app Descrizione del valore
TwilioAccountSid SID dell'account Twilio
TwilioAuthToken Token di autenticazione per l'account Twilio
TwilioPhoneNumber Numero di telefono associato all'account Twilio usato per inviare messaggi SMS.

Configurazione di un'API meteo

Gli esempi C#/JavaScript chiamano un'API meteo per controllare le condizioni correnti. È necessario specificare la propria chiave API meteo e aggiornare di conseguenza il codice di esempio. Il codice di esempio fa riferimento a un'impostazione dell'app WeatherUndergroundApiKey : sostituisci questa chiave con quella del provider meteo scelto.

Nome dell'impostazione dell'app Descrizione del valore
WeatherUndergroundApiKey La chiave API per il meteo (sostituire con il nome della chiave fornito dal tuo provider secondo necessità).

Orchestrator


Modello isolato
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;
using Microsoft.Extensions.Logging;

namespace VSSample;

public static partial class Monitor
{
    [Function("E3_Monitor")]
    public static async Task Run(
        [OrchestrationTrigger] TaskOrchestrationContext context)
    {
        MonitorRequest input = context.GetInput<MonitorRequest>()
            ?? throw new ArgumentNullException(nameof(context), "An input object is required.");
        VerifyRequest(input);

        ILogger logger = context.CreateReplaySafeLogger("E3_Monitor");
        DateTime endTime = context.CurrentUtcDateTime.AddHours(6);
        logger.LogInformation(
            "Instantiating monitor for {Location}. Expires: {EndTime}.",
            input.Location,
            endTime);

        while (context.CurrentUtcDateTime < endTime)
        {
            logger.LogInformation(
                "Checking current weather conditions for {Location} at {CurrentTime}.",
                input.Location,
                context.CurrentUtcDateTime);

            bool isClear = await context.CallActivityAsync<bool>(
                "E3_GetIsClear",
                input.Location);

            if (isClear)
            {
                await context.CallActivityAsync(
                    "E3_SendGoodWeatherAlert",
                    input.Phone);
                break;
            }

            DateTime nextCheckpoint = context.CurrentUtcDateTime.AddMinutes(30);
            await context.CreateTimer(nextCheckpoint, CancellationToken.None);
        }

        logger.LogInformation("Monitor expiring.");
    }

    private static void VerifyRequest(MonitorRequest request)
    {
        ArgumentNullException.ThrowIfNull(request.Location);
        ArgumentException.ThrowIfNullOrEmpty(request.Phone);
    }
}

public sealed class MonitorRequest
{
    public required Location Location { get; init; }

    public required string Phone { get; init; }
}

public sealed class Location
{
    public required string State { get; init; }

    public required string City { get; init; }

    public override string ToString() => $"{City}, {State}";
}

Modello in-process
[FunctionName("E3_Monitor")]
public static async Task Run([OrchestrationTrigger] IDurableOrchestrationContext monitorContext, ILogger log)
{
    MonitorRequest input = monitorContext.GetInput<MonitorRequest>();
    if (!monitorContext.IsReplaying) { log.LogInformation($"Received monitor request. Location: {input?.Location}. Phone: {input?.Phone}."); }

    VerifyRequest(input);

    DateTime endTime = monitorContext.CurrentUtcDateTime.AddHours(6);
    if (!monitorContext.IsReplaying) { log.LogInformation($"Instantiating monitor for {input.Location}. Expires: {endTime}."); }

    while (monitorContext.CurrentUtcDateTime < endTime)
    {
        // Check the weather
        if (!monitorContext.IsReplaying) { log.LogInformation($"Checking current weather conditions for {input.Location} at {monitorContext.CurrentUtcDateTime}."); }

        bool isClear = await monitorContext.CallActivityAsync<bool>("E3_GetIsClear", input.Location);

        if (isClear)
        {
            // It's not raining! Or snowing. Or misting. Tell our user to take advantage of it.
            if (!monitorContext.IsReplaying) { log.LogInformation($"Detected clear weather for {input.Location}. Notifying {input.Phone}."); }

            await monitorContext.CallActivityAsync("E3_SendGoodWeatherAlert", input.Phone);
            break;
        }
        else
        {
            // Wait for the next checkpoint
            var nextCheckpoint = monitorContext.CurrentUtcDateTime.AddMinutes(30);
            if (!monitorContext.IsReplaying) { log.LogInformation($"Next check for {input.Location} at {nextCheckpoint}."); }

            await monitorContext.CreateTimer(nextCheckpoint, CancellationToken.None);
        }
    }

    log.LogInformation($"Monitor expiring.");
}

[Deterministic]
private static void VerifyRequest(MonitorRequest request)
{
    if (request == null)
    {
        throw new ArgumentNullException(nameof(request), "An input object is required.");
    }

    if (request.Location == null)
    {
        throw new ArgumentNullException(nameof(request.Location), "A location input is required.");
    }

    if (string.IsNullOrEmpty(request.Phone))
    {
        throw new ArgumentNullException(nameof(request.Phone), "A phone number input is required.");
    }
}

La funzione di orchestrazione richiede una posizione da monitorare e un numero di telefono a cui inviare un messaggio quando il tempo è sereno in quella posizione. Si passano questi dati alla funzione di orchestrazione come oggetto MonitorRequest fortemente tipizzato.

Le azioni di questa funzione dell'agente di orchestrazione sono le seguenti:

  1. Ottiene il MonitorRequest costituito dalla posizione da monitorare e dal numero di telefono a cui invia una notifica SMS (o repo per l'esempio di Python).
  2. Determina il tempo di scadenza del monitor. L'esempio usa un valore codificato in modo fisso per brevità.
  3. Chiama l'attività di controllo dello stato per determinare se la condizione è soddisfatta.
  4. Se la condizione viene soddisfatta, chiama l'attività di notifica per inviare una notifica.
  5. Crea un timer durevole per riprendere l'orchestrazione all'intervallo di polling successivo. L'esempio usa un valore codificato in modo fisso per brevità.
  6. Continua l'esecuzione finché l'ora UTC corrente non supera la scadenza del monitoraggio o finché non viene inviato un avviso.

Puoi eseguire più istanze di funzioni orchestrator contemporaneamente chiamando la funzione orchestrator più volte. Puoi specificare la posizione da monitorare e il numero di telefono a cui inviare un avviso. La funzione di orchestrazione non è in esecuzione mentre è in attesa del timer, quindi non ti viene addebitato alcun costo.

L'agente di orchestrazione controlla periodicamente lo stato di un processo e restituisce un risultato quando il processo viene completato o scade.

using Microsoft.DurableTask;
using System;
using System.Threading.Tasks;

[DurableTask(nameof(MonitoringJobOrchestration))]
public class MonitoringJobOrchestration : TaskOrchestrator<JobMonitorInput, JobMonitorResult>
{
    public override async Task<JobMonitorResult> RunAsync(
        TaskOrchestrationContext context, JobMonitorInput input)
    {
        var jobId = input.JobId;
        var pollingInterval = TimeSpan.FromSeconds(input.PollingIntervalSeconds);
        var expirationTime = context.CurrentUtcDateTime.AddSeconds(input.TimeoutSeconds);

        // Initialize monitoring state
        int checkCount = 0;

        while (context.CurrentUtcDateTime < expirationTime)
        {
            // Check current job status
            var jobStatus = await context.CallActivityAsync<JobStatus>(
                nameof(CheckJobStatusActivity),
                new CheckJobInput { JobId = jobId, CheckCount = checkCount });

            checkCount = jobStatus.CheckCount;

            // Make job status available via custom status
            context.SetCustomStatus(jobStatus);

            if (jobStatus.Status == "Completed")
            {
                return new JobMonitorResult
                {
                    JobId = jobId,
                    FinalStatus = "Completed",
                    ChecksPerformed = checkCount
                };
            }

            // Calculate next check time
            var nextCheck = context.CurrentUtcDateTime.Add(pollingInterval);
            if (nextCheck > expirationTime)
            {
                nextCheck = expirationTime;
            }

            // Wait until next polling interval
            await context.CreateTimer(nextCheck, default);
        }

        // Timeout reached
        return new JobMonitorResult
        {
            JobId = jobId,
            FinalStatus = "Timeout",
            ChecksPerformed = checkCount
        };
    }
}

Questo agente di orchestrazione esegue le azioni seguenti:

  1. Accetta l'ID attività, l'intervallo di polling e il timeout come parametri di input.
  2. Registra l'ora di inizio e calcola l'ora di scadenza.
  3. Entra in un ciclo di polling che controlla lo stato del processo.
  4. Aggiorna lo stato personalizzato in modo che i client possano monitorare lo stato di avanzamento.
  5. Se il lavoro viene completato, restituisce il risultato finale.
  6. Se viene raggiunto il timeout, restituisce uno stato di timeout.
  7. Utilizza CreateTimer per attendere tra i tentativi di polling senza consumare risorse.

Attività

In modo analogo agli altri esempi, le funzioni di attività helper sono normali funzioni che usano l'associazione di trigger activityTrigger.

Attività di controllo dello stato

La funzione E3_GetIsClear ottiene le condizioni meteorologiche attuali utilizzando l'API Weather Underground e determina se il cielo è sereno.


Modello isolato
using Microsoft.Azure.Functions.Worker;

namespace VSSample;

public static partial class Monitor
{
    [Function("E3_GetIsClear")]
    public static async Task<bool> GetIsClear([ActivityTrigger] Location location)
    {
        WeatherCondition currentConditions =
            await WeatherUnderground.GetCurrentConditionsAsync(location);
        return currentConditions == WeatherCondition.Clear;
    }
}

Modello in-process
[FunctionName("E3_GetIsClear")]
public static async Task<bool> GetIsClear([ActivityTrigger] Location location)
{
    var currentConditions = await WeatherUnderground.GetCurrentConditionsAsync(location);
    return currentConditions.Equals(WeatherCondition.Clear);
}

Attività di avviso

La funzione E3_SendGoodWeatherAlert utilizza Twilio per inviare un messaggio SMS che avvisa l'utente finale che è un buon momento per una passeggiata.


Modello isolato
using Microsoft.Azure.Functions.Worker;
using Twilio;
using Twilio.Rest.Api.V2010.Account;
using Twilio.Types;

namespace VSSample;

public static partial class Monitor
{
    [Function("E3_SendGoodWeatherAlert")]
    public static async Task SendGoodWeatherAlert(
        [ActivityTrigger] string phoneNumber)
    {
        string accountSid = Environment.GetEnvironmentVariable("TwilioAccountSid")
            ?? throw new InvalidOperationException("TwilioAccountSid is not configured.");
        string authToken = Environment.GetEnvironmentVariable("TwilioAuthToken")
            ?? throw new InvalidOperationException("TwilioAuthToken is not configured.");
        string fromNumber = Environment.GetEnvironmentVariable("TwilioPhoneNumber")
            ?? throw new InvalidOperationException("TwilioPhoneNumber is not configured.");

        TwilioClient.Init(accountSid, authToken);
        await MessageResource.CreateAsync(
            to: new PhoneNumber(phoneNumber),
            from: new PhoneNumber(fromNumber),
            body: "The weather's clear outside! Go take a walk!");
    }
}

Annotazioni

Per eseguire il codice di esempio del worker isolato, installa il Twilio pacchetto NuGet.


Modello in-process
    [FunctionName("E3_SendGoodWeatherAlert")]
    public static void SendGoodWeatherAlert(
        [ActivityTrigger] string phoneNumber,
        ILogger log,
        [TwilioSms(AccountSidSetting = "TwilioAccountSid", AuthTokenSetting = "TwilioAuthToken", From = "%TwilioPhoneNumber%")]
            out CreateMessageOptions message)
    {
        message = new CreateMessageOptions(new PhoneNumber(phoneNumber));
        message.Body = $"The weather's clear outside! Go take a walk!";
    }

internal class WeatherUnderground
{
    private static readonly HttpClient httpClient = new HttpClient();
    private static IReadOnlyDictionary<string, WeatherCondition> weatherMapping = new Dictionary<string, WeatherCondition>()
    {
        { "Clear", WeatherCondition.Clear },
        { "Overcast", WeatherCondition.Clear },
        { "Cloudy", WeatherCondition.Clear },
        { "Clouds", WeatherCondition.Clear },
        { "Drizzle", WeatherCondition.Precipitation },
        { "Hail", WeatherCondition.Precipitation },
        { "Ice", WeatherCondition.Precipitation },
        { "Mist", WeatherCondition.Precipitation },
        { "Precipitation", WeatherCondition.Precipitation },
        { "Rain", WeatherCondition.Precipitation },
        { "Showers", WeatherCondition.Precipitation },
        { "Snow", WeatherCondition.Precipitation },
        { "Spray", WeatherCondition.Precipitation },
        { "Squall", WeatherCondition.Precipitation },
        { "Thunderstorm", WeatherCondition.Precipitation },
    };

    internal static async Task<WeatherCondition> GetCurrentConditionsAsync(Location location)
    {
        var apiKey = Environment.GetEnvironmentVariable("WeatherUndergroundApiKey");
        if (string.IsNullOrEmpty(apiKey))
        {
            throw new InvalidOperationException("The WeatherUndergroundApiKey environment variable was not set.");
        }

        var callString = string.Format("http://api.wunderground.com/api/{0}/conditions/q/{1}/{2}.json", apiKey, location.State, location.City);
        var response = await httpClient.GetAsync(callString);
        var conditions = await response.Content.ReadAsAsync<JObject>();

        JToken currentObservation;
        if (!conditions.TryGetValue("current_observation", out currentObservation))
        {
            JToken error = conditions.SelectToken("response.error");

            if (error != null)
            {
                throw new InvalidOperationException($"API returned an error: {error}.");
            }
            else
            {
                throw new ArgumentException("Could not find weather for this location. Try being more specific.");
            }
        }

        return MapToWeatherCondition((string)(currentObservation as JObject).GetValue("weather"));
    }

    private static WeatherCondition MapToWeatherCondition(string weather)
    {
        foreach (var pair in weatherMapping)
        {
            if (weather.Contains(pair.Key))
            {
                return pair.Value;
            }
        }

        return WeatherCondition.Other;
    }
}

Annotazioni

Per eseguire il codice di esempio in corso, installa il Microsoft.Azure.WebJobs.Extensions.Twilio pacchetto NuGet.


L'attività verifica lo stato attuale del processo. In un'applicazione reale, questo passaggio chiama un'API o un servizio esterno.

using Microsoft.DurableTask;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

[DurableTask(nameof(CheckJobStatusActivity))]
public class CheckJobStatusActivity : TaskActivity<CheckJobInput, JobStatus>
{
    private readonly ILogger<CheckJobStatusActivity> _logger;

    public CheckJobStatusActivity(ILogger<CheckJobStatusActivity> logger)
    {
        _logger = logger;
    }

    public override Task<JobStatus> RunAsync(TaskActivityContext context, CheckJobInput input)
    {
        _logger.LogInformation("Checking status for job: {JobId} (check #{CheckCount})",
            input.JobId, input.CheckCount + 1);

        // Simulate job status - completes after 3 checks
        var status = input.CheckCount >= 3 ? "Completed" : "Running";

        return Task.FromResult(new JobStatus
        {
            JobId = input.JobId,
            Status = status,
            CheckCount = input.CheckCount + 1,
            LastCheckTime = DateTime.UtcNow
        });
    }
}

// Data classes
public class JobMonitorInput
{
    public string JobId { get; set; }
    public int PollingIntervalSeconds { get; set; } = 5;
    public int TimeoutSeconds { get; set; } = 30;
}

public class CheckJobInput
{
    public string JobId { get; set; }
    public int CheckCount { get; set; }
}

public class JobStatus
{
    public string JobId { get; set; }
    public string Status { get; set; }
    public int CheckCount { get; set; }
    public DateTime LastCheckTime { get; set; }
}

public class JobMonitorResult
{
    public string JobId { get; set; }
    public string FinalStatus { get; set; }
    public int ChecksPerformed { get; set; }
}

Eseguire l'esempio di monitoraggio

Utilizzando le funzioni attivate HTTP incluse nel campione, puoi avviare l'orchestrazione inviando la seguente richiesta HTTP POST:

POST https://{host}/orchestrators/E3_Monitor
Content-Length: 77
Content-Type: application/json

{ "location": { "city": "Redmond", "state": "WA" }, "phone": "+1425XXXXXXX" }
HTTP/1.1 202 Accepted
Content-Type: application/json; charset=utf-8
Location: https://{host}/runtime/webhooks/durabletask/instances/f6893f25acf64df2ab53a35c09d52635?taskHub=SampleHubVS&connection=Storage&code={SystemKey}
RetryAfter: 10

{"id": "f6893f25acf64df2ab53a35c09d52635", "statusQueryGetUri": "https://{host}/runtime/webhooks/durabletask/instances/f6893f25acf64df2ab53a35c09d52635?taskHub=SampleHubVS&connection=Storage&code={systemKey}", "sendEventPostUri": "https://{host}/runtime/webhooks/durabletask/instances/f6893f25acf64df2ab53a35c09d52635/raiseEvent/{eventName}?taskHub=SampleHubVS&connection=Storage&code={systemKey}", "terminatePostUri": "https://{host}/runtime/webhooks/durabletask/instances/f6893f25acf64df2ab53a35c09d52635/terminate?reason={text}&taskHub=SampleHubVS&connection=Storage&code={systemKey}"}

L'istanza di E3_Monitor viene avviata ed esegue una query sulle condizioni correnti. Se la condizione viene soddisfatta, chiama una funzione di attività per inviare un avviso; in caso contrario, imposta un timer. Quando il timer scade, l'orchestrazione viene riavviata.

È possibile visualizzare l'attività dell'orchestrazione esaminando i log delle funzioni nel portale di Funzioni di Azure.

L'orchestrazione termina una volta raggiunto il timeout o rilevata la condizione. È anche possibile usare l'API terminate all'interno di un'altra funzione o richiamare il webhook HTTP POST terminatePostUri a cui si fa riferimento nella risposta 202 precedente. Per usare il webhook, sostituire {text} con il motivo della terminazione anticipata. L'URL HTTP POST è simile al seguente:

POST https://{host}/runtime/webhooks/durabletask/instances/f6893f25acf64df2ab53a35c09d52635/terminate?reason=Because&taskHub=SampleHubVS&connection=Storage&code={systemKey}

Per eseguire l'esempio, è necessario:

  1. Avvia l'emulatore del pianificatore di attività durevoli (per lo sviluppo locale):

    docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dts/dts-emulator:latest
    
  2. Avviare il processo di lavoro per registrare l'agente di orchestrazione e le attività.

  3. Eseguire il client per pianificare un'orchestrazione di monitoraggio.

using System;
using System.Threading.Tasks;

var client = DurableTaskClientBuilder.UseDurableTaskScheduler(connectionString).Build();

// Schedule the monitoring orchestration
var input = new JobMonitorInput
{
    JobId = "job-" + Guid.NewGuid().ToString(),
    PollingIntervalSeconds = 5,
    TimeoutSeconds = 30
};

string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
    nameof(MonitoringJobOrchestration), input);

Console.WriteLine($"Started monitoring orchestration: {instanceId}");

// Wait for completion while checking status
while (true)
{
    var state = await client.GetInstanceMetadataAsync(instanceId, getInputsAndOutputs: true);

    if (state.RuntimeStatus == OrchestrationRuntimeStatus.Completed ||
        state.RuntimeStatus == OrchestrationRuntimeStatus.Failed)
    {
        Console.WriteLine($"Monitoring completed: {state.ReadOutputAs<JobMonitorResult>().FinalStatus}");
        break;
    }

    Console.WriteLine($"Current status: {state.ReadCustomStatusAs<JobStatus>()?.Status}");
    await Task.Delay(2000);
}

Passaggi successivi

Questo esempio dimostra come utilizzare Durable Functions per monitorare lo stato di una sorgente esterna utilizzando timer durevoli e logica condizionale. L'esempio seguente illustra come usare eventi esterni e timer durevoli per gestire l'interazione umana.

Questo esempio ha illustrato come usare gli SDK di Durable Task per implementare il modello di monitoraggio con timer durevoli e rilevamento dello stato. Per saperne di più su altri modelli e caratteristiche, consulta: