かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Azure Functions の Durable Functions をクロスデバイスアプリケーションのバックエンドとして使おう

個人的に一番好きな Azure の機能の中に Azure Functions があります。 その中でも特に Durable Functions は強い。

複数の処理が組み合わさって長時間にわたって動く可能性のある処理を簡単にコードで書けるのが特徴です。

例えば以下のドキュメントからの引用ですが…

Durable Functions のパターンおよび技術的概念 - Azure | Microsoft Docs

以下のような複数の関数が、連続して動くようなケースですね。

f:id:okazuki:20190303233327p:plain

もし、これを特に Azure Functions や Durable Functions にあるような機能を使わないで書くとこんなイメージになると思います。

// F1 のコード

var result = DoSomething();

var queue = new QueueClient("F2Queue");
queue.PutMessage(result);
// F2 のコード
var inputQueue = new QueueClient("F2Queue");
var outputQueue = new QueueClient("F3Queue");
while (true)
{
  var input = inputQueue.GetMessage();
  var result = DoSomething2(input);
  outputQueue.PutMessage(result);
}

F3 と F4 も同じ感じですね。

F4 だけは処理結果を最終的に DB なり何らかのストレージに書き込むかもしれません。

// F4 のコード
var inputQueue = new QueueClient("F2Queue");
var db = new Database("接続文字列");
while (true)
{
  var input = inputQueue.GetMessage();
  var result = DoSomething2(input);
  db.Insert(result);
}

んで、この関数チェーンを読んで結果を受け取る人は…

var body = ...;
var client = new HttpClient();
awaut client.PostAsync("http://example.com/api/F1", body);

var db = new Database("接続文字列");
Result result = null;
do 
{
  result = db.Get();
  Thread.Sleep(3000);
}
while (result != null);

DoSomething(result);

非常に単純に書きましたが、こんな単純な処理でも足りてないものがあります。処理の呼び出しと結果のデータを紐づけるためのキー情報が必要ですね。あとはエラーが出たときはどうするの?とか、分岐したらどうなるの?とかループがあると…と色々考えると出来なくはないけど、かなりめんどくさいレベルになります。

Durable Functions

まぁそこらへんをこういう風に書けるようにしてくれるやつ。

var x = await context.CallActivityAsync("F1", null);
var y = await context.CallActivityAsync("F2", x);
var z = await context.CallActivityAsync("F3", y);
return await context.CallActivityAsync("F4", z);

F1, F2, F3, F4 の処理も普通に関数みたいに書けます。

// F1
[FunctionName("F1")]
public static async Task<string> F1([ActivityTrigger] string arg)
{
  return DoSomething();
}

F2, F3, F4 とかも同じノリでいけます。

さて、ここで気付いてほしいのは F1, F2, F3, F4 の中には単純に各々の処理でやりたいことを書けます。そして、それを呼び出す側では処理の流れを書けます。

Durable Functions を使わないケースのコードは F1, F2, F3, F4 の中に処理の全体の流れの制御が分散していて見通しが悪いものでした。さらにフローが断片化したものとビジネスロジックが混ざってあまりいい感じではないものでした。

こんな風にフローを定義するところがオーケストレーター関数で、個別のロジックを書くところがアクティビティ関数と呼ばれたりします。

このような非常に単純なケースでも見た目がすっきりします。複雑なケースだと推して知るべし。

その他にもタイマーで一定時間待ったり、外部イベントという機能を使うと外から特定の処理が呼ばれるまでオーケストレーター関数を途中で待たせるとか色々できます。

クロスデバイスアプリの裏側として

クロスデバイスアプリケーションって複数デバイスで 1 つのタスクを最適なデバイスでやれるようにしてるものだと思ってるのですが、そういうものを作る場合には、処理がどこまで進んだのかとか何処かでみんながアクセスできるように管理してやる必要があります。

多分単純に作ると DB に進捗状況を記録しておいて、それを参照しつつ、その時にする処理を進めるとかっていった感じで実装することが出来るんじゃないかなって思います。

例えば、処理A、処理B、処理Cを順番にするけど 処理A はスマートスピーカーで起動して、次の処理はパソコンでやって、最後の処理はスマホでやるとか。

そんな作業ってどんな作業だよ!?って思うかもしれないけど、あくまでたとえってことで。 そういう場合にはこんな感じで Durable Functions のオーケストレーター関数が書けるかな?

[FunctionName("SomeTask")]
public static async Task<COutput> Run([OrchestrationTrigger] DurableOrchestrationContext context)
{
  context.SetCustomStatus("処理Aの途中");
  var a = await context.CallActivityAsync("処理A", null);
  context.SetCustomStatus(new { result = a, status = "処理Aの終わり" });
  // 外から B トリガーが呼ばれるのを待つ
  var bInput = await context.WaitForExternalEvent<BInput>("BTrigger");
  var b = await context.CallActivityAsync<BOutput>("処理B", bInput);
  context.SetCustomStatus(new { result = b, status = "処理Bの終わり" });
  var cInput = await context.WiatForExternalEvent<CInput>("CTrigger");
  return await context.CallActivityAsync<COutput>("処理C", cInput);
}

Durable Functions には実行したオーケストレーター関数の処理状況を確認するための API があるので、それを使えば今自分がやらないといけない所まで処理が進んでるのか?とかがチェックできます。

もうちょっとクライアント側にやさしく作るとすると、各処理の終わりとかのタイミングでプッシュ通知のメカニズムを使って通知してあげると親切っぽいですね。

まとめ

つらつらとしたメモですが、こんな感じで処理状態を記録しつつ処理を進めていくようなワークフロー的なものを簡単に書けるのでマルチデバイスアプリケーションや、まぁ別にマルチデバイスじゃなくても 1 つのワークフローが長い処理を進めていくようなものを作るのには良さそうですね。

もちろんプログラムコードで書けるので分岐、ループ、例外処理などが普通に書けて強い。