(Durable Functions)「第22回 Azureもくもく会」に参加+LTした話

久しぶりに kingkino@マンダム (@kingkinoko) on Twitterさん主催のAzureもくもく会に参加しました。
で、LTもさせていただきました。

1. 何を もくもく+LT したの?

「Durable Functionsの基礎学習」とその発表でした。
(何かを作り上げた!みたいなものではないのだけれど、昔から技術のバックグラウンドを調べたりするの好きなので)

今年に入ってから、仕事では .NET/Azure を離れ、Java/AWS の世界に移っていましたが、ぽろぽろと趣味で.NET/Azureは やっていました。
で、先日のGlobal Azure Boot Camp 2018に参加してAzure界隈の方々とお話しさせていただいたら「Durable Functions」がホットだということで。
GW終わり辺りからドキュメントを読み、もくもく+LTをさせていただいたという感じです。

2. 発表資料

発表資料は↓↓↓です。

speakerdeck.com

なのですが、「学んだこと」「伝えたかった事」のメインはDemoをさせていただいた部分でした。
加えて、「内容がLT・プレゼン向きじゃない」+「私の説明が決して上手くない」ということで、伝わりにくかったかと思います。

ということでDemoした部分について、ブログらせて頂こうかと・・・

3. 何を学んだのか?何をDemoしたのか?

MS公式のDurable Functionsの資料を読んだ結果、以下のドキュメントがDurable Functionsが Durable である所以的な部分を指しているような気がしました。

docs.microsoft.com

しかし、
「ドキュメントに記述されているルールに従って実装すれば、いい感じにDurable Functionsは動くのだろうけど、何故上手く動くのかが腑に落ちない」
という感覚を持ち、その もやもや を払拭するための技術探索を行いました。
つまり、以下のような内容が、話としては理解するけれど、具体的にそうなる根拠となるバックエンドの知識が欲しい、と。。。

・オーケストレーター関数
 決定論的である必要がある。
 複数回実行されても結果が同じでなければならない。
  →現在日付の取得・GUIDのランダム生成の実行はNG。
  →DateTime.Now ではなく DurableOrchestrationContext.​Current​Utc​Date​Time を使う
・アクティビティ関数
 非決定論的操作が可能。
 ある責務を持ったドメイン ファンクションはここで定義。
  →つまり「DBアクセス」などの処理はアクティビティ関数に実装
・イベントソーシング(パターン)で実装されている。
・Azure Storage(キュー・テーブル)に実行履歴をチェックポイントする事で信頼性を得ている。
・awaitが呼び出されるとオーケストレーター関数をゼロから再実行する。

4. Demo内容

Durable Functionsがどのようなフローで実行されるのか?Azure Storageを使ってどのようにDurableに実行されるのか?(直訳すれば "丈夫に" "恒久的に"ですね)
ほぼVSテンプレが吐き出す以下のコードで検証します(Http TriggerによるDurable Functionsコード)。
カスタムしたのは 19 / 20行目 を追加したぐらいでしょうか。

01: using System;
02: using System.Collections.Generic;
03: using System.Net.Http;
04: using System.Threading.Tasks;
05: using Microsoft.Azure.WebJobs;
06: using Microsoft.Azure.WebJobs.Extensions.Http;
07: using Microsoft.Azure.WebJobs.Host;
08: 
09: namespace FunctionApp2
10: {
11:   public static class Function2
12:   {
13:     [FunctionName("Function2")]
14:     public static async Task<List<string>> RunOrchestrator(
15:       [OrchestrationTrigger] DurableOrchestrationContext context)
16:     {
17:       var outputs = new List<string>();
18: 
19:       var currentUtc = context.CurrentUtcDateTime;
20:       var current = DateTime.Now; // ※本当はオーケストレーター関数で実行しちゃいけないやつ
21: 
22:       outputs.Add(await context.CallActivityAsync<string>("Function2_Hello", "Tokyo"));
23: 
24:       outputs.Add(await context.CallActivityAsync<string>("Function2_Hello", "Seattle"));
25: 
26:       outputs.Add(await context.CallActivityAsync<string>("Function2_Hello", "London"));
27: 
28:       return outputs;
29:     }
30: 
31:     [FunctionName("Function2_Hello")]
32:     public static string SayHello([ActivityTrigger] string name, TraceWriter log)
33:     {
34:       log.Info($"Saying hello to {name}.");
35:       return $"Hello {name}!";
36:     }
37: 
38:     [FunctionName("Function2_HttpStart")]
39:     public static async Task<HttpResponseMessage> HttpStart(
40:       [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")]HttpRequestMessage req,
41:       [OrchestrationClient]DurableOrchestrationClient starter,
42:       TraceWriter log)
43:     {
44:       // Function input comes from the request content.
45:       string instanceId = await starter.StartNewAsync("Function2", null);
46: 
47:       log.Info($"Started orchestration with ID = '{instanceId}'.");
48: 
49:       return starter.CreateCheckStatusResponse(req, instanceId);
50:     }
51:   }

ブレークポイントは張りまくります。

f:id:daigo-knowlbo:20180512223119p:plain

テストではローカルAzure Storage(Emu)を使いますが、中身を完全に空にしておきます。

f:id:daigo-knowlbo:20180512223410p:plain

4.1. 実行

(1) デバッグ実行

プロジェクトをデバッグ実行します。
以下のような感じでローカルでAzure Functionsが実行されます。

f:id:daigo-knowlbo:20180512232907p:plain

ここで、Azure Storage ExplorerでStorageの確認を行います。

f:id:daigo-knowlbo:20180512223725p:plain

「Blob Contaners」「Queues」「Tables」に色々作成されています(中身は空)。

※上記Storage項目のそれぞれの説明はここらへんに詳細が書かれています。構成により調整が可能。

(2) HTTPトリガーをキック

HTTPトリガーの受け口である「http://localhost:7071/api/Function2_HttpStart」をポストマンでキックします。

f:id:daigo-knowlbo:20180512224654p:plain

当然、FunctionsのHTTPトリガーである「HttpStart()」が呼び出されます。

f:id:daigo-knowlbo:20180512224751p:plain

「続行(F5)」を行います。
すると、コードの実装の通り「StartNewAsync("Function2")」でFunction2オーケストレーター関数が呼び出されます。
ということで、以下の画面のように Function2オーケストレーター関数 でブレークします。

f:id:daigo-knowlbo:20180512225248p:plain

再び「続行(F5)」して「outputs.Add(await context.CallActivityAsync("Function2_Hello", "Tokyo"));」まで飛んだ状態が以下です。

f:id:daigo-knowlbo:20180512233112p:plain

日付取得結果は以下の通りでした。

context.CurrentUtcDateTimeの値 → 2018/5/12 14:30:29
DateTime.Nowの値 → 2018/5/12 23:30:46

今度は「ステップオーバー(F10)」します。
CallActivityAsync()により Function2_Hello が呼び出され、以下の場所でブレークします。

f:id:daigo-knowlbo:20180512225917p:plain

この状態でAzure Storage Explorerにて「DurableFunctionsHubHistoryテーブル」を確認すると以下のようレコードが作成されています。

f:id:daigo-knowlbo:20180512233151p:plain

※要するに「Function2オーケストレーター関数→Function2_Helloアクティビティ関数の呼び出しの履歴の保存」がAzure Storageに対して行われたということです。

で、「続行(F5)」しちゃいます。
結果、ブレークするのはココ↓↓↓↓↓。

f:id:daigo-knowlbo:20180512230343p:plain

そう、次のアクティビティ関数呼び出しの個所ではなく、オーケストレーター関数の頭のブレークに飛びます。

また、「続行(F5)」しちゃいましょう。
こんな感じになりますね↓↓↓。

f:id:daigo-knowlbo:20180512235526p:plain

context.CurrentUtcDateTimeの値 → 2018/5/12 14:30:29
DateTime.Nowの値 → 2018/5/12 23:32:31

DateTime.Nowは実行した時間そのものが取得されているのに対し、context.CurrentUtcDateTime値は「初回実行時と同じ値」が取得されています!
何度実行しても(何度再生されても)結果が同じ、つまり「決定論的動作」をしています。オーケストレーター関数の条件を満たしている!

次に「ステップオーバー(F10)」すると、先程1度実行済みの「outputs.Add(await context.CallActivityAsync("Function2_Hello", "Tokyo"));」が実行されるはず・・・
では「ステップオーバー(F10)」してみます。
Function2_Helloアクティビティ関数は呼び出されず、次のアクティビティ関数呼び出し箇所「outputs.Add(await context.CallActivityAsync("Function2_Hello", "Seattle"));」に移りました。

f:id:daigo-knowlbo:20180512231219p:plain

さらに「ステップオーバー(F10)」してみます。
今度は、Function2_Helloアクティビティ関数が呼び出されました(引数nameはもちろんSeattle)。

f:id:daigo-knowlbo:20180512231409p:plain

ここで再びAzure Storage Explorerを見てみましょう。
レコードが増えましたね。Function2_Helloアクティビティが2回呼ばれた記録が行われています。

f:id:daigo-knowlbo:20180512233357p:plain

また、Result列に「Hello Tokyo!」と、1つ目のアクティビティ関数の処理結果が保存されています。

f:id:daigo-knowlbo:20180512233518p:plain

ここでちょっといたずら的にTokyoをOsakaにUpdateしてしまいます。

f:id:daigo-knowlbo:20180512235935p:plain

再び「続行(F5)」しちゃいましょう。
想像通り、オーケストレーター関数のトップに帰ってきます。

f:id:daigo-knowlbo:20180512233721p:plain

F5を連打し、Durable Functionsの処理をすべて終了させます。

結果として処理の履歴はDurableFunctionsHubHistoryテーブルに以下のように出力されました。

f:id:daigo-knowlbo:20180513000117p:plain

DurableFunctionsHubInstancesテーブルを見ると、処理結果が保存されています。

f:id:daigo-knowlbo:20180513000203p:plain

ポストマンでstatusQueryGetUriをリクエストしてみると・・・

f:id:daigo-knowlbo:20180513000415p:plain

先程いたずらでAzure Storageデータを変更した「Osaka」の文字が。
つまり、ロジックをメモリ上で実行して終わりではなく、Azure Storageに処理状態を永続化していることが理解できました。

5. まとめ

長々と分かりにくい検証を行いましたが以下のことが明確に理解できたのではないかと思います。

  • Durable Functionsは、アクティビティ関数の実行履歴を細かくAzure Storageに保存している。
  • Durable Functionsは、オーケストレーター関数で定義されたアクティビティ関数を高い信頼性で実行するためにawaitのタイミングでAzure Storageに状態を保存し、自らをリプレイしてすべてのアクティビティ関数をワークフローとして実行している。

Durable Functionsの実装については以下のgithubで公開されているので、更にソースレベルで探索ができるのではないかと思います。

github.com

また、勉強会等でもkingkino@マンダム (@kingkinoko) on Twitterさんや(「🍖・ω・)「🍺 (@yu_ka1984) on Twitterさん などDurable Functionsマスターがいらっしゃるので、色々伺えるのではないかと思います。