Поделиться через


Сценарий монитора в устойчивых функциях — пример наблюдателя за погодой

Шаблон монитора представляет собой гибкий повторяющийся процесс в рабочем процессе. Например, повторение опроса, пока не будут выполнены определенные условия. В этой статье приведен пример использования устойчивых функций для реализации мониторинга.

Необходимые компоненты

Обзор сценария

Этот пример отслеживает текущие погодные условия местоположения и с помощью SMS оповещает пользователя, когда небо ясное. Для проверки погоды и отправки оповещений можно использовать обычную функцию, активируемую по таймеру. Однако одной из проблем, связанной с этим подходом, является управление жизненным циклом. Если необходимо отправить только одно оповещение, монитор должен отключиться после обнаружения ясной погоды. Возможность завершить свое выполнение является одним из преимуществ шаблона мониторинга, среди которых также:

  • Мониторы работают с интервалами, а не по расписанию: триггер таймера запускается каждый час, а монитор ожидает один час между действиями. Действия монитора не будут перекрываться, если не указано, что может быть важно для длительных задач.
  • Мониторы могут иметь динамические интервалы: время ожидания может меняться в зависимости от некоторого состояния.
  • Мониторы могут завершить работу, когда какое-либо условие выполняется, или быть завершены другим процессом.
  • Мониторы могут принимать параметры. Пример показывает, как один и тот же процесс мониторинга погоды может применяться к любому запрашиваемому местоположению и номеру телефона.
  • Мониторы можно масштабировать. Та как каждый монитор является экземпляром оркестрации, можно создавать несколько мониторов без необходимости создавать новые функции или писать новый код.
  • Мониторы легко интегрируются в более крупные рабочие процессы. Монитором может быть один раздел более сложной функции оркестрации или суборкестрация.

Настройка

Настройка интеграции Twilio

Для отправки SMS-сообщений на мобильный телефон в этом примере используется служба Twilio. В Функциях Azure уже реализована поддержка Twilio через привязку Twilio. В этом образце используется эта функция.

Первое, что вам нужно — это учетная запись Twilio. Вы можете создать бесплатную учетную запись здесь: https://www.twilio.com/try-twilio. Получив учетную запись, добавьте следующие три параметра приложения в приложение-функцию.

Имя параметра приложения Описание значения
TwilioAccountSid Идентификатор безопасности вашей учетной записи в Twilio.
TwilioAuthToken Маркер проверки подлинности вашей учетной записи в Twilio.
TwilioPhoneNumber Номер телефона, связанный с вашей учетной записью в Twilio. Используется для отправки SMS-сообщений.

Настройка интеграции Weather Underground

Этот пример включает в себя использование API Weather Underground для проверки текущих погодных условий для местоположения.

Для начала потребуется учетная запись Weather Underground. Вы можете создать бесплатную учетную запись, перейдя по этому адресу https://www.wunderground.com/signup. После получения учетной записи необходимо получить ключ API. Для этого откройте страницу https://www.wunderground.com/weather/api, а затем выберите параметры ключа. Бесплатного плана Stratus Developer достаточно для запуска этого примера.

Получив ключ API, добавьте следующие параметры приложения в приложение-функцию.

Имя параметра приложения Описание значения
WeatherUndergroundApiKey Ваш ключ API Weather Underground.

Функции

В этой статье описаны следующие функции в примере приложения:

  • E3_Monitor: функция оркестрации, которая периодически вызывает E3_GetIsClear. Она вызывает E3_SendGoodWeatherAlert, если E3_GetIsClear возвращает значение true.
  • E3_GetIsClear: функция действия, которая проверяет текущие погодные условия для местоположения.
  • E3_SendGoodWeatherAlert: функция действия, которая отправляет SMS-сообщение через Twilio.

Функция оркестратора 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.");
    }
}

Оркестратору требуется расположение для отслеживания и номера телефона, чтобы отправить сообщение, когда погода становится ясной в расположении. Эти данные передаются оркестратору в виде строго типизированного объекта MonitorRequest.

Эта функция оркестратора выполняет следующие задачи:

  1. Получает MonitorRequest, состоящий из расположения для отслеживания и номера телефона, на который он отправляет SMS-уведомление.
  2. Задает срок действия монитора. Для краткости в образце используется жестко заданное значение.
  3. Вызывает E3_GetIsClear, чтобы определить, ясное ли небо в запрашиваемом местоположении.
  4. Если погода хорошая, вызывается E3_SendGoodWeatherAlert, чтобы отправить SMS-уведомление на запрошенный номер телефона.
  5. Создает устойчивый таймер для возобновления оркестрации во время следующего интервала опроса. Для краткости в образце используется жестко заданное значение.
  6. Продолжает работать до тех пор, пока текущее время в формате UTC не превысит срок действия монитора или пока не будет отправлено SMS-оповещение.

Несколько экземпляров оркестратора могут выполняться одновременно, неоднократно вызывая функцию оркестратора. Можно указать местоположение для мониторинга и номер телефона для отправки SMS-оповещения. Наконец, обратите внимание, что функция оркестратора не выполняется во время ожидания таймера, поэтому плата за нее не будет взиматься.

Функция действия E3_GetIsClear

Вспомогательные функции действий, как и другие примеры, являются обычными функциями, которые используют привязку триггера activityTrigger. Функция E3_GetIsClear получает текущие погодные условия с использованием API Weather Underground и определяет, чистое ли небо.

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

Функция действия E3_SendGoodWeatherAlert

Функция E3_SendGoodWeatherAlert использует привязку Twilio для отправки SMS-сообщения, уведомляющего пользователя, что это подходящее время для прогулки.

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

Примечание.

Чтобы запустить пример кода, потребуется установить пакет NuGet Microsoft.Azure.WebJobs.Extensions.Twilio.

Запуск примера

С помощью функций, активируемых по HTTP (перечисленных в примере), можно запустить оркестрацию, отправив следующий запрос 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}"}

Экземпляр E3_Monitor запускается и запрашивает текущие погодные условия для запрашиваемого местоположения. Если погода хорошая, он вызывает функцию действия для отправки оповещения. В противном случае он устанавливает таймер. По истечении срока действия таймера оркестрация возобновляется.

Результат действия оркестрации можно просмотреть в журналах функций на портале Функций 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)

Оркестрация завершается по достижении времени ожидания или в случае обнаружения прояснения. Вы также можете использовать terminate API внутри другой функции или вызвать веб-перехватчик HTTPPostUri HTTP POST, на который ссылается в ответе 202. Чтобы использовать веб-перехватчик, замените {text} на причину досрочного завершения. URL-адрес HTTP POST выглядит примерно следующим образом:

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

Следующие шаги

В этом примере показано, как использовать Устойчивые функции для отслеживания состояния внешнего источника с помощью устойчивых таймеров и условной логики. В приведенном ниже примере показано, как использовать внешние события и устойчивые таймеры для обработки действий человека.