Build 合わせでリリースされた Durable Functions v2 ですが、Typed Invocation が入ったバージョンを待ってたら 1 ヵ月が過ぎようとしてたので、諦めて現行バージョンで Durable Entities を学ぶことにしました。
ちなみに Durable Entities 以外に Storage Provider の分離とかありますが、Redis を試したところ PubSub じゃなくて Polling だったので割とがっかりです。Cosmos DB Provider が来るのを待ってます。
Durable Entities に関しては Chris が既にブログに書いてくれています。Typed Invocation はかなりスッキリと書けるようになりそうなので期待しています。
これまでの Durable Functions で解決できないパターンとして Aggregate が挙げられています。
名前は Actor ではないが、Actor と同じような動作をするという説明です。
ドキュメントでは近いものとして Service Fabric の Reliable Actors が挙げられています。Durable Entities を実装したのが MSR の人なので似たようなものになるのは理解できます。
今のところ例として出てくるのが Counter ぐらいなのと、Reliable Actors も楽しいサンプルが見当たらないので、実際問題としてどういう場面で使うのが良いのか難しいです。細かい粒度でのスケーリング目的なら Task / Activity で良いのではという気もしてきます。
サンプルとして用意されている RideSharing プロジェクトは中々分かりやすかったです。
思想は理解しているのですが、実際に使う場面がやはりパッと浮かばなかったので、実際に簡単なアプリケーションを書いてみることにしました。
思いついたのがメール配信管理だったので、その配信ステータスを管理する部分を今回書きました。いろんなところで話してる気がしますが、メールは API 実行して終わりという訳ではなく、正しく相手まで届いたかというトラッキングが重要なので、今回は SendGrid を使って実装しました。
作成したプロジェクト
とりあえず作成したプロジェクトは GitHub に公開しています。手元では意図したとおりに動きました。
色々と手探りな状態で書いてみたので、もっと良い設計があると思います。そもそも Durable Entities を使うより良い方法がある気もしますが勉強目的なので。
Entity の設計
Durable Entities で非常に重要となる Entity の設計は以下の通りです。
2 つの Entity が存在していて MailEntity
は 1 通のメールを表しています。そして MailStatusEntity
は現在の配信ステータス単位の一覧情報を保持しています。
メールの送信と配信ステータスの保持と更新は MailEntity
が行っています。Event Webhook からイベントが更新されたタイミングで、該当するステータスの MailStatusEntity
を実行して一覧を保持しています。
要するに MailStatusEntity
は MailEntity
にアクセスするためのインデックスです。
実装した REST API
必要最低限の REST API を以下の通り実装しました。大体は名前から想像がつくと思います。
この時、メールを識別するための ID はそのまま EntityKey
としています。
メール送信
実際に REST API を使ってメールを送信してみます。単純な API なので Body に JSON を渡すだけです。
API の実装は MailEntity
に送信を行うようにメッセージを投げているだけです。
await client.SignalEntityAsync(new EntityId(nameof(MailEntity), Guid.NewGuid().ToString()), "Send", message);
オーケストレーター以外では Fire-and-forget になるので、呼び出し自体は一瞬で返って来ます。
このメッセージを受け取る MailEntity
のエントリポイントは以下の通りです。Typed Invocation が使えるとこの辺りスッキリ書けるようになるのですが、今は switch を使って分けるしかないです。
public class MailEntity { [FunctionName(nameof(MailEntity))] public async Task EntryPoint([EntityTrigger] IDurableEntityContext context) { switch (context.OperationName) { case "Send": await Send(context); break; case "Resend": await Resend(context); break; case "UpdateStatus": await UpdateStatus(context); break; } } }
Durable Entities は EntityId 毎に別々の状態を持つので、上手く EntityId を設計する必要があるでしょう。省略しましたが MailEntity
はメールの情報以外にも送信日やステータス更新日なども持っています。
この辺りのコードを書いていて Durable Entities と名前が付けられた理由が分かってきました。
配信ステータスを更新
Event Webhook から送られてきたペイロードには EntityKey
が含まれるようにしてあるので、その値を使って MailEntity
に対して配信ステータスの更新を実行します。
foreach (var payload in eventPayloads) { await client.SignalEntityAsync(new EntityId(nameof(MailEntity), payload.EntityKey), "UpdateStatus", payload.Event); }
本来なら都度 await しない方が良いのですが、全て呼び出して Task.WhenAll
するコードを書くのが面倒だったので妥協しました。
上のメッセージを受け取った MailEntity
では状態の更新と一緒に MailStatusEntity
に対して EntityKey
を追加するようにメッセージを投げています。
if (state.Status != status) { context.SignalEntity(new EntityId(nameof(MailStatusEntity), status), "Add", context.Key); }
そして MailStatusEntity
は配信ステータス単位に作られて、それぞれが HashSet
を持っているので、そこに EntityKey
を追加しています。
現在の配信ステータスを確認
ここまでの実行によって MailStatusEntity
の状態を、読めばバウンスしたメールが存在するかといった情報を確認出来るはずです。
MailStatusEntity
の状態を読むだけの REST API を用意しているので、実行して確認してみます。
ちゃんと delivered
になっているメールの EntityKey
一覧が返って来ました。Durable Entities に送信したメッセージは 1 つずつ順に処理されるので、競合を考えなくて良いのは便利です。
返ってきた EntityKey
を適当に 1 つ選んで、メール情報を取得する API を実行すると該当する MailEntity
が保持する状態が返って来ます。
とりあえず簡単なアプリケーションでしたが、Durable Entities を実際に動かしてみることができました。
疑問点と課題
1 つは Reliable Actors の時にも思ったのですが、何処まで状態を持たせるべきなのかという点です。
Service Fabric も結局レプリケーションのためにシリアライズされて、VM のディスクに書き出されるという実装だったはずです。上の実装では無限に MailStatusEntity
の状態が大きくなっていくので、シリアライズ周りのコストが馬鹿でかくなることが容易に想像できます。
結局、妥協点を見いだせず終わった気がします。外部のストレージに書けという結果になったはずです。
もう 1 つが Aggregate を行うパターンでは Cosmos DB の Change Feed を使った方が良いのではないかという点です。多用しているパターンですが、集計を行う場合は SQL に入れた方が楽という結論が多いです。
逆に Cosmos DB の Change Feed を受け取った後の処理を Durable Entities で行うのはアリなのではという気がしています。Change Feed は PartitionKey
単位で順序保証がされているので、PartitionKey
をそのまま EntityKey
にしてしまえば、順序を保ったまま後続の処理を実装できるのではと思っています。
順番にメッセージを処理するという点では、EC サイトのカートは直列処理に出来て良いのではないかと思いましたが、結局在庫の引き当てで悩むことになりそうです。商品自体を Entity にすれば安全に在庫の引き当てが出来そうですが、スケールしないなとか色々思いました。
やっぱり結構利用する場面が難しいなと感じる結果となりました。もう少し試してみたいですね。