BEACHSIDE BLOG

Azure とか C# 好きなエンジニアの個人メモ ( ・ㅂ・)و ̑̑

Durable Functions: Human interaction pattern ( 人による操作 pattern )

Azure Functions の拡張機能である Durable Functions にはいくつかの実装パターンがありますが、ここでは Human interaction pattern を取り上げます。

今年の1月頃に書きかけてから、書くほどのことでもないなーと放置してたブログを更新しました...あの頃書いておけばよかった。

Human interactionパターンの活用例

用途として、以下のような雑なストーリーが想像できます。

  • なんらかの処理が終わったのをトリガーに Durable Functions を起動して、承認者へ通知する。
  • 承認/却下されるかタイムアウトが来るまで待ち続ける
  • 承認/却下/タイムアウトに応じて処理を行う

Human interaction パターンを実装してみる

上記の雑なストーリーに合わせて実装してみます。

準備

前提としては、開発は C# で実装します。
Visual Studio 2019 で Functions のプロジェクトを作ってから、プロジェクトを右クリック > 追加 > 新しい Azure 関数 して Durable Functions Orchestration を一つ追加しました。

あと、SendGrid を利用したメールの送信と、CosmosDB へのデータ更新をアウトバインドで実行しようとかなーくらいを思ったので、以下の nuget パッケージをインストールしています。

  • Microsoft.Azure.WebJobs.Extensions.CosmosDB
  • Microsoft.Azure.WebJobs.Extensions.SendGrid

Durabule の起動

まず一つめのサンプルコードは起動のところですが、いつも通りの [OrchestrationClient] があるメソッドから起動です。ここを呼び出すのはHTTP の POST で、Body に必要なデータを渡してあげます。渡すデータは Todo って class を想定しています(これも含めた全コードはブログの最後に付いてます)。
31-32行目で REST で受け取った Body の情報を Todo 型に Deserialize しています。

また、承認の Event 発火のための URL を渡したいので、34行目で取得しています。そして37行目の後続のオーケストレーターのメソッドに渡しています。Todo も一緒に渡したかったので、 ValueTuple 型で渡しました。

オーケストレーター

先ほどのコードの続きでオーケストレーター部分になります。

6行目で、先ほど渡した ValueTuple を受け取ります。
そして8行目で承認者へ通知を行います。Retry 付きなのはただの遊び心です。"RequestApproval" の Activity のメソッドは後で説明します。

10行目以降が Human interaction パターンの基本的なところですね。タイマー(正確には durable timer)で時間制限を設定しつつ、15行目が承認者からのレスポンスを待っている部分です。

承認者のレスポンスとタイマーのタイムアウトでどちらかのレスポンスが返ってきたら処理再開するってのが、16行目までのところですね。 その後は、レスポンスの内容に応じて処理をする(18-19行目)、またはタイムアウトの処理をする(20行目)です。

(自分が完全に忘れてしまっていたので)ここで使ってるものをもうちょっと補足を書いておくと、

CallActivityAsync メソッド (または CallActivityWithRetryAsync メソッド) は、[ActivityTrigger] の処理を発火します。

WaitForExternalEvent メソッドは、ExternalEvent の起動を待ちます。具体的に何を待っているかというと、 1つめのサンプルコードで取得した eventUrl 変数の値の URL に {EventName} に値をセットしたURL へのリクエストです。例えばローカルデバッグ時にはこんな URL が取得できます。

"http://localhost:7071/runtime/webhooks/durabletask/instances/1/raiseEvent/{eventName}?taskHub=DurableFunctionsHub&connection=Storage&code=PgkERnWbe2L5JQuCHgCecEampCTroRXgKxm0vyhyEej2fmlVoF43DQ=="

15行目のように書くと、URLの {eventName} の部分に、今回は ApprovalEventName 変数の値である「ApprovalEvent」が入った URL が叩かれるとここが発火します。19行目で approvalTask.Result を取得しています。これは、このURLをRESTで送信する際に body に 0 or 1 を入れることで取得可能にしてます。これを後続の処理(後ほど登場する ProcessApprovalAsync メソッド)の中で承認/拒否の判断フラグに使います。

承認者の通知処理

2つめのコードサンプルの8行目の中身です。SendGrid の出力バインダーで適当にメール投げてるだけです。

URL をぶん投げてるだけという雑さです。ここら辺は、承認/拒否の判断を受け取る専用の Functions の URL を渡してあげて、その処理に応じてこの URL を叩くように隠蔽してあげるとか、なんぼでも簡単にできるので今回は雑に終わらせました。

メールが届くと URL が本文に入ってくるので、POSTMAN とかで Body に 0 とか 1 をセットして送信しています。

f:id:beachside:20190920043219p:plain

承認後の処理

今回は CosmosDB の出力バインダーでを使って Todo データの Status プロパティを更新しただけですがシンプルに書けます。

実用的にするなら、承認の結果を TODO の作成者に送信しろよって話になりそうですが、さっきのメール送信の ActivityTrigger をちょっと工夫すれば応用効くので省略しました。

環境変数の設定

ローカルデバッグ時なら local.settings.json に CosmosDB の接続文字列や SendGrid の API キーを入れる必要がありますね。

終わりに

数日検証した後、9カ月くらい Durable Functions を触ってなくて全く記憶に残ってなかったのでメモを残してみました。

結局のところ、C# の非同期メソッドによるactivity trigger の function や external event とかをコントロールしていい感じにワークフローを構成するっていう点を抑えておけば、あとはドキュメントみれば何とかなりそうですね。

ただ、最近でた Actor パターン系のやつは全然見てないので、そのうち整理しておきたいところです。

雑なコードの全体を貼っておきます。

あー Json の Serializer 回りの処理が雑に attribute で済ませてるのは多少心残りですね(笑)