Quantcast
Channel: かずきのBlog@hatena
Viewing all articles
Browse latest Browse all 1387

Durable Functions の Entity のクラススタイルプログラミングモデルを試してみよう

$
0
0

Durable Functions の Entity のプログラミング体験が割と辛いのですがクラススタイルのプログラミングモデルを使うと既存のプログラミングと同じような感じでいけて素敵です!

クラススタイルのプログラミングモデルは、2019/08/05 時点では Durable Functions v2.0.0-beta1 で試すことが出来ます。

github.com

試してみよう!

早速 Azure Functions のプロジェクトを作ります。そして Microsoft.Azure.WebJobs.Extensions.DurableTaskの 2.0.0-beta1 を追加します。

クラススタイルのエンテティは、普通のクラス!違う点があるとすると、クラス名の FunctionName属性のついた EntityTriggerの関数があるところですね。

using Microsoft.Azure.WebJobs;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Threading.Tasks;

namespace FunctionApp1
{
    publicinterface IPerson
    {
        void SetName(string name);
        Task CurseAsync();
    }

    publicclass Person : IPerson
    {
        [JsonProperty("name")]
        publicstring Name { get; set; }
        [JsonProperty("age")]
        publicint Age { get; set; }

        publicvoid SetName(string name) => Name = name;
        // 適当な時間経過後に加齢する呪いpublic async Task CurseAsync()
        {
            var r = new Random();
            await Task.Delay(TimeSpan.FromSeconds(r.Next(10)));
            Age += r.Next(10);
        }

        [FunctionName(nameof(Person))]
        publicstatic Task Run([EntityTrigger] IDurableEntityContext ctx)
            => ctx.DispatchAsync<Person>();
    }
}

この EntityTriggerのついた関数の DispatchAsyncが凄くいい仕事します。Person クラスの関数をいい感じに呼び出してくれます。

HttpTrigger などから呼び出してみよう

HttpTrigger などから Entity を使うには、IDurableOrchestrationClient#SignalEntityAsyncでメソッドを呼び出して、IDurableOrchestrationClient#ReadEntityStateAsyncで Entity のステータスを取得します。ステータスは、先ほど定義した Person クラスのプロパティに値が入ってる感じのものが返ってきます。

例えば…適当に SetName を呼んでステータスを読み取ってみましょう。

[FunctionName("CreatePerson")]
publicstatic async Task<IActionResult> CreatePersonAsync(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post")]HttpRequest req,
    [OrchestrationClient] IDurableOrchestrationClient client)
{
    var id = CreateEntityIdFrom(req);
    if (!id.HasValue)
    {
        returnnew BadRequestResult();
    }

    await client.SignalEntityAsync<IPerson>(id.Value, proxy => proxy.SetName(id.Value.EntityKey));
    returnnew OkObjectResult(await client.ReadEntityStateAsync<Person>(id.Value));
}

privatestatic EntityId? CreateEntityIdFrom(HttpRequest req)
{
    var name = req.Query["name"];
    if (string.IsNullOrEmpty(name))
    {
        returnnull;
    }

    returnnew EntityId(nameof(Person), name);
}

EntityIdの最初の引数が Person クラスに定義した関数の名前です。そして、EntityIdの第二引数に Entity の識別子みたいな感じですね。今回は HttpTrigger の URL のクエリーパラメータの name でを使う感じにしました。

POST http://localhost:7071/api/CreatePerson?name=kazuakix HTTP/1.1こんな感じのURL を叩くと下のような結果が返ってきます。

HTTP/1.1 200 OK
Connection: close
Date: Mon, 05 Aug 2019 11:30:47 GMT
Content-Type: application/json; charset=utf-8
Server: Kestrel
Content-Length: 41

{
  "entityExists": false,
  "entityState": null
}

entityStateが null なのが気になりますが、これは SignalEntityAsyncは処理の完了を待たずに、処理の要求がキューに入ったら戻ってくる感じです。ReadEntityStateAsyncも別に SignalEntityAsyncで何か処理が行われるか気にせず結果を返すので、こんな結果になります。

なので、もう一度同じ HttpTrigger の関数を叩くと以下のような結果になります。

HTTP/1.1 200 OK
Connection: close
Date: Mon, 05 Aug 2019 11:34:36 GMT
Content-Type: application/json; charset=utf-8
Server: Kestrel
Content-Length: 63

{
  "entityExists": true,
  "entityState": {
    "name": "kazuakix",
    "age": 0
  }
}

他に、Person#CurseAsyncを呼ぶ場合は以下のようなコードになります。

[FunctionName("Curse")]
publicstatic async Task<IActionResult> CurseAsync(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post")]HttpRequest req,
    [OrchestrationClient] IDurableOrchestrationClient client)
{
    var id = CreateEntityIdFrom(req);
    if (!id.HasValue)
    {
        returnnew BadRequestResult();
    }

    await client.SignalEntityAsync<IPerson>(id.Value, proxy => proxy.CurseAsync());
    returnnew OkObjectResult(await client.ReadEntityStateAsync<Person>(id.Value));
}

この例でも CurseAsyncを処理するという要求をするだけなので、最後の結果の ReadEntityStateAsyncの結果は加齢前のものになります。試しに呼んでみると…

HTTP/1.1 200 OK
Connection: close
Date: Mon, 05 Aug 2019 11:36:45 GMT
Content-Type: application/json; charset=utf-8
Server: Kestrel
Content-Length: 63

{
  "entityExists": true,
  "entityState": {
    "name": "kazuakix",
    "age": 0
  }
}

年齢は変わりません。ちょっと何もせずステータスを返す以下のような関数を作って…

[FunctionName("GetPerson")]
publicstatic async Task<IActionResult> GetPersonAsync(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post")]HttpRequest req,
    [OrchestrationClient] IDurableOrchestrationClient client)
{
    var id = CreateEntityIdFrom(req);
    if (!id.HasValue)
    {
        returnnew BadRequestResult();
    }

    returnnew OkObjectResult(await client.ReadEntityStateAsync<Person>(id.Value));
}

適当な時間経過後に呼んでみましょう。

HTTP/1.1 200 OK
Connection: close
Date: Mon, 05 Aug 2019 11:37:46 GMT
Content-Type: application/json; charset=utf-8
Server: Kestrel
Content-Length: 63

{
  "entityExists": true,
  "entityState": {
    "name": "kazuakix",
    "age": 4
  }
}

年齢増えてますね。

処理結果を待ちつつ結果を取りたい

そういうときは OrchestrationTriggerの中で Entity を使うと良さそう。

[FunctionName("CurseAndWait")]
publicstatic async Task<IActionResult> CurseAndWaitAsync(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post")]HttpRequest req,
    [OrchestrationClient] IDurableOrchestrationClient client)
{
    var id = CreateEntityIdFrom(req);
    if (!id.HasValue)
    {
        returnnew BadRequestResult();
    }

    var instanceId = await client.StartNewAsync("CurseAndWaitOrchestrator", id.Value);
    while((await client.GetStatusAsync(instanceId)).RuntimeStatus != OrchestrationRuntimeStatus.Completed)
    {
        await Task.Delay(5000);
    }

    returnnew OkObjectResult(await client.ReadEntityStateAsync<Person>(id.Value));
}

[FunctionName("CurseAndWaitOrchestrator")]
publicstatic async Task CurseAndWaitOrchestratorAsync(
    [OrchestrationTrigger]IDurableOrchestrationContext context)
{
    var id = context.GetInput<EntityId>();

    var proxy = context.CreateEntityProxy<IPerson>(id);
    await proxy.CurseAsync();
}

オーケストレーター関数の終了待つのに、WaitForCompletionOrCreateCheckStatusResponseAsync使えばいいやって思ったけど、この関数は、あくまでレスポンス受け取った先の人のためのメソッドであって、HttpTrigger の中でこの関数を await しても必ずしも完了まで待ってくれないんですね。勉強になった。 ということでステータスが完了になるまで自分で待ってます。

そうすると、関数を呼ぶと加齢が完了して、結果に加齢後のオブジェクトが入ってます。 呼び出し前の状態を確認するために GetPerson 関数を呼んで結果を見てます。

HTTP/1.1 200 OK
Connection: close
Date: Mon, 05 Aug 2019 12:02:23 GMT
Content-Type: application/json; charset=utf-8
Server: Kestrel
Content-Length: 64

{
  "entityExists": true,
  "entityState": {
    "name": "kazuakix",
    "age": 47
  }
}

47 歳ですね。では先ほど作った CurseAndWait関数をこんな感じ( GET http://localhost:7071/api/CurseAndWait?name=kazuakix HTTP/1.1 )で呼んでみます。

HTTP/1.1 200 OK
Connection: close
Date: Mon, 05 Aug 2019 12:02:50 GMT
Content-Type: application/json; charset=utf-8
Server: Kestrel
Content-Length: 64

{
  "entityExists": true,
  "entityState": {
    "name": "kazuakix",
    "age": 53
  }
}

53 歳になりましたね。あってるのかな。

まとめ

Durable Functions の Entity が本当に Entity 書いてるみたいになるのでとてもいい。Durable Functions v2 のリリースが楽しみになりますね。


Viewing all articles
Browse latest Browse all 1387

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>