Share via


지속성 함수의 모니터 시나리오 - 날씨 관찰 앱 샘플

모니터링 패턴은 워크플로의 유연한 되풀이(예: 특정 조건이 충족될 때까지 폴링) 프로세스를 말합니다. 이 문서에서는 지속성 함수를 사용하여 모니터링을 구현하는 샘플에 대해 설명합니다.

필수 조건

시나리오 개요

이 샘플은 특정 위치의 현재 기상 조건을 모니터링하고 하늘이 맑으면 SMS로 사용자에게 알려줍니다. 타이머로 트리거되는 일반 함수를 사용하여 날씨를 확인하고 알림을 보낼 수 있습니다. 단, 이러한 방식의 한 가지 문제점은 수명 관리입니다. 알림을 하나만 보내야 하는 경우에는 맑은 날씨가 감지된 후 모니터를 비활성화해야 합니다. 모니터링 패턴은 다른 이점과 함께 자체 실행을 종료 할 수 있습니다.

  • 모니터는 간격을 두고 실행되며 일정에 따라 실행되는 것이 아닙니다. 타이머 트리거는 1시간마다 실행(run)되고 모니터는 작업 사이에 한 시간을 대기합니다(wait). 모니터의 작업은 지정되지 않는 한 겹치지 않습니다. 이는 장기 실행되는 작업에 중요합니다.
  • 모니터에 동적 간격을 설정할 수 있으며, 대기 시간은 조건에 따라 변경될 수 있습니다.
  • 모니터는 조건이 충족되면 종료되거나 다른 프로세스에 의해 종료될 수 있습니다.
  • 모니터에 매개 변수를 사용할 수 있습니다. 이 샘플에서는 요청 받은 위치와 전화 번호에 동일한 기상 모니터링 프로세스를 적용하는 방법을 보여줍니다.
  • 모니터는 확장성이 있습니다. 각 모니터는 오케스트레이션 인스턴스이기 때문에 새 함수를 만들거나 코드를 더 정의하지 않고도 다수의 모니터를 만들 수 있습니다.
  • 모니터는 보다 큰 워크플로에 쉽게 통합됩니다. 모니터는 더 복잡한 오케스트레이션 함수 또는 하위 오케스트레이션의 한 섹션이 될 수 있습니다.

구성

Twilio 통합 구성

이 샘플에서는 Twilio 서비스를 사용하여 SMS 메시지를 휴대폰으로 보냅니다. Azure Functions는 이미 Twilio 바인딩을 통해 Twilio를 지원하며, 샘플에서 이 기능을 사용합니다.

가장 먼저 필요한 것은 Twilio 계정입니다. https://www.twilio.com/try-twilio에서 무료로 만들 수 있습니다. 계정이 있으면 다음 세 가지 앱 설정을 함수 앱에 추가합니다.

앱 설정 이름 값 설명
TwilioAccountSid Twilio 계정의 SID
TwilioAuthToken Twilio 계정의 인증 토큰
TwilioPhoneNumber Twilio 계정과 연결되는 전화 번호이며, SMS 메시지를 보내는 데 사용됩니다.

Weather Underground 통합 구성

이 샘플에는 Weather Underground API를 사용하여 특정 위치의 현재 기상 조건을 확인하는 작업이 포함됩니다.

가장 먼저 필요한 것은 Weather Underground 계정입니다. https://www.wunderground.com/signup에서 무료로 만들 수 있습니다. 계정이 있으면 API 키를 확보해야 합니다. https://www.wunderground.com/weather/api에 방문하여 Key Settings(키 설정)를 선택하면 합니다. Stratus Developer 계획은 무료이며 이 샘플을 실행하기에 충분합니다.

API 키가 확보되면 함수 앱에 다음 앱 설정을 추가합니다.

앱 설정 이름 값 설명
WeatherUndergroundApiKey Weather Underground API 키입니다.

함수

이 문서에서는 샘플 앱의 다음 함수에 대해 설명합니다.

  • E3_Monitor: E3_GetIsClear를 주기적으로 호출하는 오케스트레이터 함수입니다. E3_GetIsClear가 true를 반환하면 E3_SendGoodWeatherAlert를 호출합니다.
  • E3_GetIsClear: 특정 위치의 현재 기상 조건을 확인하는 작업 함수입니다.
  • E3_SendGoodWeatherAlert: Twilio를 통해 SMS 메시지를 보내는 작업 함수입니다.

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. 모니터링할 위치와 SMS 알림을 보낼 전화 번호로 구성된 MonitorRequest를 가져옵니다.
  2. 모니터의 만료 시간을 결정합니다. 간결함을 위해 이 샘플에서는 하드 코드된 값을 사용합니다.
  3. E3_GetIsClear를 호출하여 요청 받은 위치에 하늘이 맑은지 확인합니다.
  4. 날씨가 맑으면 E3_SendGoodWeatherAlert를 호출하여 요청 받은 전화 번호로 SMS 알림을 보냅니다.
  5. 다음 폴링 간격에서 오케스트레이션을 다시 시작하도록 지속성 타이머를 만듭니다. 간결함을 위해 이 샘플에서는 하드 코드된 값을 사용합니다.
  6. 현재 UTC 시간이 모니터의 만료 시간을 지나거나 SMS 경고가 전송될 때까지 계속 실행합니다.

여러 오케스트레이터 인스턴스는 오케스트레이터 함수를 여러 번 호출하여 동시에 실행할 수 있습니다. 모니터링할 위치와 SMS 알림을 보낼 전화 번호를 지정할 수 있습니다. 마지막으로, 타이머를 기다리는 동안 오케스트레이터 함수가 실행 되고 있지 않으므로 요금이 청구되지 않습니다.

E3_GetIsClear 작업 함수

다른 샘플과 마찬가지로 도우미 작업 함수는 activityTrigger 트리거 바인딩을 사용하는 일반 함수입니다. E3_GetIsClear 함수는 Weather Underground API를 사용하여 현재 기상 조건을 가져와서 하늘이 맑은지를 판단합니다.

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

참고 항목

샘플 코드를 실행하려면 Microsoft.Azure.WebJobs.Extensions.Twilio Nuget 패키지를 설치해야 합니다.

샘플 실행

샘플에 포함된 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 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)

시간 제한에 도달하거나 맑은 하늘이 감지되면 오케스트레이션이 완료됩니다. 다른 함수 내에서 terminate API를 사용하거나 위의 202 응답에서 참조된 terminatePostUri HTTP POST 웹후크를 호출할 수도 있습니다. 웹후크를 사용하려면 {text}를 초기 종료의 원인으로 바꿉니다. HTTP POST URL은 대략 다음과 같이 표시됩니다.

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

다음 단계

이 샘플은 지속성 함수가 지속성 타이머와 조건부 논리를 사용하여 외부 소스의 상태를 모니터링하는 방법을 보여줍니다. 다음 샘플에서는 외부 이벤트 및 지속형 타이머를 사용하여 인간 상호 작용을 처리하는 방법을 보여 줍니다.