🎉

Durable Functions で ContinueAsNew を使っているオーケストレーター関数の戻り値

2024/10/10に公開

Durable Functions で、そこそこ長い処理をする可能性があるオーケストレーター関数では適宜 ContinueAsNew を使って新しいインプットと共に自分自身を再起動することがあります。この場合、オーケストレーター関数の戻り値はどうなるのか気になったので調べてみました。

Durable Functions を新規作成したプロジェクトでおなじみの SayHello を 3 都市に対して呼び出すオーケストレーター関数を以下のように書き換えてみました。

using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.Extensions.Logging;
using System.Runtime.InteropServices;

namespace FunctionApp1
{
    public static class Function1
    {
        [Function(nameof(Function1))]
        public static async Task<string> RunOrchestrator(
            [OrchestrationTrigger] TaskOrchestrationContext context)
        {
            ILogger logger = context.CreateReplaySafeLogger(nameof(Function1));

            // 引数で受け取ったリストの最初の要素に対して SayHello アクティビティを呼び出す
            var input = context.GetInput<string[]>();
            ArgumentNullException.ThrowIfNull(input);
            if (input.Length == 0) throw new ArgumentException("Please pass a list with at least one item in it", nameof(input));

            var message = await context.CallActivityAsync<string>(nameof(SayHello), input[0]);
            logger.LogInformation(message);

            // まだ引数に後続の値がある場合は ContinueAsNew を呼び出して再実行
            // そうじゃない場合は終了
            if (input.Length == 1)
            {
                return "Done!!";
            }
            else
            {
                context.ContinueAsNew(input[1..]);
                return "Continuing...";
            }
        }

        [Function(nameof(SayHello))]
        public static string SayHello([ActivityTrigger] string name, FunctionContext executionContext)
        {
            ILogger logger = executionContext.GetLogger("SayHello");
            logger.LogInformation("Saying hello to {name}.", name);
            return $"Hello {name}!";
        }

        [Function("Function1_HttpStart")]
        public static async Task<HttpResponseData> HttpStart(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
            [DurableClient] DurableTaskClient client,
            FunctionContext executionContext)
        {
            ILogger logger = executionContext.GetLogger("Function1_HttpStart");

            // Tokyo, Seattle, London を渡してオーケストレーター関数を起動
            var instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
                nameof(Function1),
                input: (string[])["Tokyo", "Seattle", "London"]);

            logger.LogInformation("Started orchestration with ID = '{instanceId}'.", instanceId);

            // オーケストレーター関数の完了を待機
            var metadata = await client.WaitForInstanceCompletionAsync(instanceId, getInputsAndOutputs: true);

            // 結果を返す
            var res = HttpResponseData.CreateResponse(req);
            await res.WriteAsJsonAsync(metadata);
            return res;
        }
    }
}

RunOrchestrator メソッドのコメントに書いてある通り ContinueAsNew を使うように変更しています。ContinueAsNew を呼び出す時には Continuing... という文字列を返して、本当に終了する時には Done!! という文字列を返すようにしています。

HttpStarter メソッドではオーケストレーター関数を起動したあとに完了を待って、その結果を JSON で返すようにしています。

この状態で実行してみると、以下のような結果が返ってきました。

{
  "Name": "Function1",
  "InstanceId": "8d46ce370f9349b5ad665b9ec10b77bc",
  "DataConverter": {},
  "RuntimeStatus": 1,
  "CreatedAt": "2024-10-10T01:35:02.5404041+00:00",
  "LastUpdatedAt": "2024-10-10T01:35:02.6202305+00:00",
  "SerializedInput": "[\"London\"]",
  "SerializedOutput": "\"Done!!\"",
  "SerializedCustomStatus": "null",
  "FailureDetails": null,
  "IsRunning": false,
  "IsCompleted": true
}

SerializedOutput にオーケストレーター関数の戻り値が入るのですが、ちゃんと終了した時に返される Done!! が入っていました。ということは ContinueAsNew を呼んだケースでの戻り値は無視されるようです。

まとめ

ContinueAsNew を呼ぶオーケストレーター関数で戻り値を返す際には、ダミーの戻り値を返すので OK。ContinueAsNew を呼ばないルートの本当に終了するケースでの戻り値が最終的なアウトプットとして扱われる。

Microsoft (有志)

Discussion