しばやん雑記

ASP.NET Core とメイドさんが大好きなフリーランスのプログラマーのブログ

Durable Functions のオーケストレーターが失敗したことをキャッチしたい

Durable Functions ではアクティビティが失敗した場合にはオーケストレーターで例外が投げられるので、try ~ catch を使って簡単にハンドリングしたり、アクティビティの実行に CallActivityWithRetryAsync を使えばいい感じにリトライもしてくれます。

以下のような簡単なコードで実現可能なのが Durable Functions の魅力の一つですね。

[FunctionName("Function1")]
public static async Task RunOrchestrator([OrchestrationTrigger] DurableOrchestrationContext context)
{
    try
    {
        // 失敗するかもしれない処理
        await context.CallActivityAsync("Function1_PostHttp", "kazuakix");
    }
    catch (Exception ex)
    {
        // 何かする
    }

    // 5 秒間隔で 10 回までリトライする
    await context.CallActivityWithRetryAsync("Function1_PostHttp", new RetryOptions(TimeSpan.FromSeconds(5), 10), "kazuakix");
}

このように Durable Functions を使った処理の流れで回復不能なエラーが発生した場合には、失敗としてオーケストレーターを終わらせたいです。ちなみに明示的にオーケストレーターを失敗させるには、例外を投げるしかありません。*1

割と切実にサクッとオーケストレーター自体が失敗したことをキャッチする方法で悩んだので、いろんな方法を調べた結果をまとめておきます。

全体を try ~ catch で囲む

これはアンチパターンという感じですが、単純なオーケストレーターなら許せるかなという気がします。

[FunctionName("Function1")]
public static async Task RunOrchestrator([OrchestrationTrigger] DurableOrchestrationContext context)
{
    try
    {
        // 失敗するかもしれない処理
        await context.CallActivityAsync("Function1_PostHttp", "kazuakix");

        await context.CallActivityAsync("Function1_PostHttp", "daruyanagi");
    }
    catch (Exception ex)
    {
        // 失敗したので何かする

        // オーケストレーターが失敗した扱いにするために再スロー
        throw;
    }
}

このくらい単純なら良いですけど、複雑なオーケストレーターになると現実的ではないです。catch した後に再スローしないとオーケストレーターが成功扱いになるので、Application Insights などで見た時に詰みます。

残念なコードですが、一応は手っ取り早く目的を達成できます。

Application Insights を使う

ちゃんとオーケストレーターを失敗扱いにしておくと Application Insights から簡単に追跡できます。Search から Severity level を Error でフィルタすると、失敗したログが流れてきているのを確認出来ます。

f:id:shiba-yan:20190205171654p:plain

ログは送信されているので、後はアラートを用意するだけです。新しい Azure Monitor の Alert は Classic Alert より格段に使いやすくなっているので、設定も分かりやすいです。

Metrics を選択すると、Azure Functions の場合は Function 単位で各メトリクスが存在するので、適当に Failure で絞り込んでオーケストレーターのものを選択すれば良いです。

f:id:shiba-yan:20190205172114p:plain

後はアクショングループで Webhook やメール通知の設定などをしておけば、オーケストレーターが失敗するとイベントが送信されます。Application Insights の都合上タイムラグが発生するのと、複数回エラー発生するとグルーピングされるのでそのあたりが許容できる場合は使えます。

Event Grid を使う

他にも Event Grid を使って Durable Functions のイベントを受信する方法がありますが、これは準備に手間がかかりますね。受け取る Function とかも必要なので手軽さはない感じです。

今回のサクッと扱いたいという条件から外れるので特に検証はしてないですが、ドキュメント通りならオーケストレーターが失敗した場合のイベントも飛んでくるはずです。

ETW / EventSource を使う

Durable Functions は ETW / EventSource を使ってイベントを投げてくれているので、それを受け取るようにすればエラーが発生したかどうかを簡単に確認出来ます。

実装としては EventListener を継承したクラスを作成して、有効化するぐらいです。とても簡単。

public class MyEventListener : EventListener
{
    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        var result = new StringBuilder();

        for (int i = 0; i < eventData.Payload.Count; i++)
        {
            result.Append($"{eventData.PayloadNames[i]} = {eventData.Payload[i]}, ");
        }

        Debug.WriteLine($"{eventData.EventName} : {result.ToString(0, result.Length - 2)}");
    }
}

作成した EventListener を有効化するタイミングは少しシビアで、まず WebJobsStartup を使って試したところ読み込みタイミングの問題で EventSource を取得できませんでした。

Azure Functions ではこれ以外に初期化で使えそうなタイミングがないので、仕方なく Function を実装したクラスの static コンストラクタに書きました。

public static class Function1
{
    static Function1()
    {
        var eventSource = EventSource.GetSources()
                                     .FirstOrDefault(x => x.Name == "WebJobs-Extensions-DurableTask");

        if (eventSource != null)
        {
            new MyEventListener().EnableEvents(eventSource, EventLevel.LogAlways);
        }
    }
}

とりあえずは EventLevel をほぼ無視して取得できるように LogAlways を指定しています。エラーのみ欲しい場合は適宜変えると、そのイベントだけ取得出来るので便利です。

動かして見ると、ちゃんとデバッグ出力に ETW 経由で Durable Functions のイベントが取れています。

f:id:shiba-yan:20190205163536p:plain

Payload には以下のようなデータが入っているので、オーケストレーター自体が失敗したのか、それともアクティビティが失敗したのかという判別も出来ます。

Azure 上で実行すると AppNameSlotName も入ってくると思われます。

f:id:shiba-yan:20190205163648p:plain

例えばオーケストレーターのエラーだけを受け取りたい場合には EventLevelError に変更しつつ、以下のように EventListener を用意すれば良いです。

public class MyEventListener : EventListener
{
    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        var payload = new Dictionary<string, object>();

        for (int i = 0; i < eventData.Payload.Count; i++)
        {
            payload[eventData.PayloadNames[i]] = eventData.Payload[i];
        }

        if ((string)payload["FunctionType"] != "Orchestrator")
        {
            return;
        }

        Debug.WriteLine($"Orchestrator Failed : {payload["FunctionName"]}");
    }
}

組み込みで ETW を使ったイベントを提供してくれているのは便利ですね。今回はエラーのキャッチが目的でしたが、ETW を使えばオーケストレーターが成功した時のイベントもちゃんと取れます。

*1:エラー時にはちゃんと失敗させないと Application Insights などに影響が出る