Scenario di monitoraggio in Funzioni durevoli - Esempio di watcher per il meteo

Il modello di monitoraggio fa riferimento a un processo ricorrente flessibile in un flusso di lavoro, ad esempio il polling finché vengono soddisfatte determinate condizioni. Questo articolo illustra un esempio che usa Funzioni durevoli per implementare il monitoraggio.

Prerequisiti

Panoramica dello scenario

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. Un problema di questo approccio è però la gestione della durata. 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 monitoraggi vengono eseguiti a intervalli, non in base a pianificazioni: un trigger timer viene eseguito ogni ora. Un monitoraggio attende un'ora tra un'azione e l'altra. Se non specificato, le azioni di un monitoraggio non si sovrapporranno e questo può essere importante per le attività con esecuzione prolungata.
  • 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.
  • Monitoraggi possono accettare parametri. L'esempio illustra come lo stesso processo di monitoraggio del meteo possa essere applicato a qualsiasi località richiesta e numero di telefono.
  • Monitoraggi sono scalabili. Poiché ogni monitoraggio è un'istanza di orchestrazione, si possono creare più monitoraggi 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.

Configurazione

Configurazione dell'integrazione di Twilio

Questo esempio prevede l'uso del servizio Twilio per inviare messaggi SMS al telefono cellulare. Funzioni di Azure supporta già Twilio tramite l'associazione a 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 tre impostazioni all'app per le funzioni.

Nome impostazione 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 dell'integrazione di Weather Underground

Questo esempio prevede l'uso dell'API Weather Underground per controllare le condizioni meteo correnti di una località.

Prima di tutto, è necessario un account Weather Underground. È possibile crearne uno gratuitamente all'indirizzo https://www.wunderground.com/signup. Dopo avere creato l'account, sarà necessario acquisire una chiave API. A questo scopo, visitare https://www.wunderground.com/weather/api, quindi selezionare Key Settings (Impostazioni chiave). Il piano gratuito Stratus Developer è sufficiente per eseguire questo esempio.

Dopo avere acquisito la chiave API, aggiungere l'impostazione app seguente all'app per le funzioni.

Nome impostazione app Descrizione del valore
WeatherUndergroundApiKey Chiave API Weather Underground.

Funzioni

Questo articolo descrive le funzioni seguenti nell'app di esempio:

  • E3_Monitor: funzione dell'agente di orchestrazione che chiama E3_GetIsClear periodicamente. Chiama E3_SendGoodWeatherAlert se E3_GetIsClear restituisce true.
  • E3_GetIsClear: funzione di attività che controlla le condizioni meteorologiche correnti per una posizione.
  • E3_SendGoodWeatherAlert: funzione dell'attività che invia un SMS tramite Twilio.

funzione dell'agente di orchestrazione E3_Monitor

[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.");
    }
}

L'agente di orchestrazione richiede una posizione per monitorare e un numero di telefono a cui inviare un messaggio quando l'oggetto diventa chiaro nella posizione. Questi dati vengono passati all'agente di orchestrazione come oggetto fortemente tipizzato MonitorRequest .

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

  1. Ottiene MonitorRequest costituita dalla località da monitorare e dal numero di telefono al quale invierà un SMS di notifica.
  2. Determina la scadenza del monitoraggio. L'esempio usa un valore hardcoded per ragioni di brevità.
  3. Chiama E3_GetIsClear per determinare se nella località richiesta il tempo è sereno.
  4. Se il tempo è sereno, chiama E3_SendGoodWeatherAlert per inviare un SMS di notifica al numero di telefono richiesto.
  5. Crea un timer durevole per riprendere l'orchestrazione all'intervallo di polling successivo. L'esempio usa un valore hardcoded per ragioni di brevità.
  6. Continua l'esecuzione fino a quando l'ora UTC corrente supera la scadenza del monitoraggio o viene inviato un avviso SMS.

Più istanze dell'agente di orchestrazione possono essere eseguite contemporaneamente chiamando la funzione dell'agente di orchestrazione più volte. Si possono specificare la località da monitorare e il numero di telefono a cui inviare un SMS di avviso. Infine, si noti che la funzione dell'agente di orchestrazione non è in esecuzione durante l'attesa del timer, quindi non verrà addebitato alcun costo.

funzione di attività E3_GetIsClear

In modo analogo agli altri esempi, le funzioni di attività helper sono normali funzioni che usano l'associazione di trigger activityTrigger. La funzione E3_GetIsClear ottiene le condizioni meteo correnti usando l'API Weather Underground e determina se il tempo è sereno.

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

funzione di attività E3_SendGoodWeatherAlert

La funzione E3_SendGoodWeatherAlert usa l'associazione di Twilio per inviare un SMS che notifica all'utente finale che il tempo è adatto per una passeggiata.

    [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;
    }
}

Nota

Per eseguire il codice di esempio, è necessario installare il Microsoft.Azure.WebJobs.Extensions.Twilio pacchetto Nuget.

Eseguire l'esempio

Con le funzioni attivate da HTTP incluse nell'esempio, è possibile avviare l'orchestrazione inviando la richiesta HTTP POST seguente:

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 E3_Monitor viene avviata ed esegue una query delle condizioni meteo correnti per la località richiesta. Se il tempo è sereno, chiama una funzione dell'attività per inviare un avviso. In caso contrario, imposta un timer. Quando il timer scade, l'orchestrazione viene ripresa.

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

2018-03-01T01:14:41.649 Function started (Id=2d5fcadf-275b-4226-a174-f9f943c90cd1)
2018-03-01T01:14:42.741 Started orchestration with ID = '1608200bb2ce4b7face5fc3b8e674f2e'.
2018-03-01T01:14:42.780 Function completed (Success, Id=2d5fcadf-275b-4226-a174-f9f943c90cd1, Duration=1111ms)
2018-03-01T01:14:52.765 Function started (Id=b1b7eb4a-96d3-4f11-a0ff-893e08dd4cfb)
2018-03-01T01:14:52.890 Received monitor request. Location: Redmond, WA. Phone: +1425XXXXXXX.
2018-03-01T01:14:52.895 Instantiating monitor for Redmond, WA. Expires: 3/1/2018 7:14:52 AM.
2018-03-01T01:14:52.909 Checking current weather conditions for Redmond, WA at 3/1/2018 1:14:52 AM.
2018-03-01T01:14:52.954 Function completed (Success, Id=b1b7eb4a-96d3-4f11-a0ff-893e08dd4cfb, Duration=189ms)
2018-03-01T01:14:53.226 Function started (Id=80a4cb26-c4be-46ba-85c8-ea0c6d07d859)
2018-03-01T01:14:53.808 Function completed (Success, Id=80a4cb26-c4be-46ba-85c8-ea0c6d07d859, Duration=582ms)
2018-03-01T01:14:53.967 Function started (Id=561d0c78-ee6e-46cb-b6db-39ef639c9a2c)
2018-03-01T01:14:53.996 Next check for Redmond, WA at 3/1/2018 1:44:53 AM.
2018-03-01T01:14:54.030 Function completed (Success, Id=561d0c78-ee6e-46cb-b6db-39ef639c9a2c, Duration=62ms)

L'orchestrazione viene completata al raggiungimento del timeout o al rilevamento di cieli chiari. È 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 sarà simile al seguente:

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

Passaggi successivi

Questo esempio ha illustrato come usare Funzioni durevoli per monitorare lo stato di un'origine esterna usando timer durevoli e la logica condizionale. Nell'esempio successivo viene illustrato come usare eventi esterni e timer durevoli per gestire l'interazione umana.