Compartilhar via


Cenário do Monitor em Funções Duráveis - Exemplo de observador meteorológico

O padrão de monitoramento refere-se a um processo recorrente flexível em um fluxo de trabalho - por exemplo, sondagem até que determinadas condições sejam atendidas. Este artigo explica um exemplo que usa as Funções Duráveis para implementar o monitoramento.

Pré-requisitos

Visão geral do cenário

Este exemplo monitora as condições meteorológicas atuais de um local e alerta um usuário por SMS quando as condições climáticas melhoram. Você poderia usar uma função disparada por temporizador para verificar a meteorologia e enviar alertas. No entanto, um problema com essa abordagem é o gerenciamento do tempo de vida. Se apenas um alerta for enviado, o monitor deverá desabilitar-se após a detecção das condições climáticas. O padrão de monitoramento pode encerrar sua própria execução, entre outros benefícios:

  • Os monitores são executados em intervalos, não em agendamentos: um disparador com timer executa a cada hora; um monitor aguarda uma hora entre as ações. As ações de um monitor não se sobrepõem, a menos que seja especificado, o que pode ser importante para tarefas de longa duração.
  • Os monitores podem ter intervalos dinâmicos: o tempo de espera pode mudar com base em alguma condição.
  • Os monitores poderão terminar quando alguma condição for atendida ou terminada por outro processo.
  • Monitores podem receber parâmetros. O exemplo mostra como o mesmo processo de monitoramento meteorológico pode ser aplicado a qualquer número do telefone e localização solicitados.
  • Monitores são escalonáveis. Como cada monitor é uma instância de orquestração, vários monitores podem ser criados sem a necessidade de criar novas funções ou definir mais código.
  • Os monitores integram-se facilmente em fluxos de trabalho maiores. Um monitor pode ser uma seção de uma função de orquestração mais complexa ou uma sub-orquestração.

Configuração

Configurando a integração com o Twilio

Este exemplo envolve o uso do serviço Twilio para enviar mensagens SMS a um telefone celular. O Azure Functions já tem suporte para Twilio por meio da Associação com o Twilio e o exemplo usa esse recurso.

A primeira coisa de que você precisa é uma conta do Twilio. É possível criar uma gratuitamente em https://www.twilio.com/try-twilio. Quando tiver uma conta, adicione as três seguintes configurações de aplicativo ao seu aplicativo de função.

Nome da configuração do aplicativo Descrição do valor
TwilioAccountSid A SID de sua conta do Twilio
TwilioAuthToken O token de autenticação de sua conta do Twilio
TwilioPhoneNumber O número de telefone associado à sua conta do Twilio. Ele é usado para enviar mensagens SMS.

Configurar a integração do Weather Underground

Este exemplo envolve o uso da API do Weather Underground para verificar as condições climáticas atuais de um local.

A primeira coisa que você precisa é de uma conta do Weather Underground. É possível criar uma gratuitamente em https://www.wunderground.com/signup. Depois de ter uma conta, você precisa adquirir uma chave API. Você pode fazer isso, visitando https://www.wunderground.com/weather/api e selecionando Configurações de Chaves. O plano do Desenvolvedor Stratus é gratuito e suficiente para executar este exemplo.

Após ter uma chave de API, adicione a configuração de aplicativo a seguir no seu aplicativo de funções.

Nome da configuração do aplicativo Descrição do valor
WeatherUndergroundApiKey A chave de API do Weather Underground.

As funções

Este artigo explica as seguintes funções no aplicativo de exemplo:

  • E3_Monitor: uma função do orquestrador que chama E3_GetIsClear periodicamente. Chamará E3_SendGoodWeatherAlert se E3_GetIsClear retornar verdadeiro.
  • E3_GetIsClear: uma função de atividade que verifica as condições meteorológicas atuais de um local.
  • E3_SendGoodWeatherAlert: uma função de atividade que envia uma mensagem SMS via Twilio.

Função de orquestrador 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.");
    }
}

O orquestrador requer um local para monitorar e um número de telefone para enviar uma mensagem quando o tempo ficar bom no local. Esses dados são passados para o orquestrador como objeto MonitorRequest com tipagem forte.

Essa função de orquestrador executa as ações a seguir:

  1. Obtém o MonitorRequest que consiste no local a ser monitorado e no número de telefone para o qual envia uma notificação por SMS.
  2. Determina o tempo de expiração do monitor. O exemplo usa um valor embutido em código para brevidade.
  3. Chama E3_GetIsClear para determinar se há boas condições climáticas na localização solicitada.
  4. Se o houver boas condições climáticas, chamará E3_SendGoodWeatherAlert para enviar uma notificação de SMS para o número do telefone solicitado.
  5. Cria um temporizador durável para retomar a orquestração no próximo intervalo de sondagem. O exemplo usa um valor embutido em código para brevidade.
  6. Continua em execução até que o horário UTC atual passe o tempo de expiração do monitor ou um alerta por SMS seja enviado.

É possível executar várias instâncias de orquestrador ao mesmo tempo. Para isso, basta chamar a função de orquestrador várias vezes. A localização a ser monitorada e o número do telefone para envio de um alerta por SMS podem ser excluídos. Por fim, observe que a função orquestrador não está* em execução enquanto aguarda o cronômetro, portanto você não será cobrado por isso.

Função de atividade E3_GetIsClear

Como com outros exemplo, as funções de atividade auxiliares são funções regulares que usam a associação de gatilho activityTrigger. A função E3_GetIsClear função obtém as condições meteorológicas atuais, utilizando a API do Weather Underground e determina se as condições estão boas.

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

Função de atividade E3_SendGoodWeatherAlert

A função E3_SendGoodWeatherAlert usa a associação do Twilio para enviar uma mensagem SMS notificando o usuário final de que as condições climáticas estão favoráveis para uma caminhada.

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

Observação

Você precisará instalar o Microsoft.Azure.WebJobs.Extensions.Twilio pacote do NuGet para executar o código de exemplo.

Execute o exemplo

Usando as funções disparadas por HTTP incluídas no exemplo, você pode iniciar a orquestração enviando a seguinte solicitação 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}"}

A instância E3_Monitor inicia e consulta as condições meteorológicas atuais para a localização solicitada. Se as condições climáticas estiverem boas, a instância chama uma função de atividade para enviar um alerta; caso contrário, define um temporizador. Quando o cronômetro expirar, a orquestração será retomada.

É possível visualizar a atividade da orquestração, observando os logs de função no Portal do Azure Functions.

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)

A orquestração é concluída quando o tempo limite é alcançado ou as condições climáticas favoráveis são detectadas. Você também pode usar a API terminate dentro de outra função ou invocar o webhook terminaPostUri HTTP POST referenciado na resposta 202 anterior. Para usar o webhook, substitua {text} pelo motivo do encerramento antecipado. O URL HTTP POST é mais ou menos assim:

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

Próximas etapas

Esse exemplo demonstra como usar Durable Functions ​​para monitorar o status de uma fonte externa usando temporizadores duráveis e lógica condicional. O próximo exemplo mostra como usar eventos externos e temporizadores duráveis para lidar com interação humana.