himanago

Azure・C#などのMS系技術やLINE関連技術など、好きな技術について書くブログ

Azure Durable Functionsでハマった話

前置き

Clovaスキルの開発でDurable Functionsを使った機能をいろいろ試しています。
そのうちのひとつで、「時間のかかる処理」をやらせるということを試していた中でちょっとハマってしまったので、記録として残しておきます。

作っていたもの

MSのかずきさんがやっていた、
blog.okazuki.jp
を発展させたものを作っていました。

作っていたのは2つ。

まずは、Clovaのオーディオ再生機能を利用した無限ループで終了を待機させるというもの(以下、待機版とよぶ)。
Durable Functionsの呼び出した処理のステータス確認をする機能を使ってループの終了判定をしています。
github.com

もうひとつは、完了したらLINE通知するというもの(以下、通知版とよぶ)。
github.com
LINE通知はClovaならではなのでおもしろいかなと思ったのですが、よく考えたら別に処理の中で待機とか状態確認とかする必要もなく、単純に終わったらLINEにPushすればいいだけ。
作っているときからDurableを使うほどのものではないな…とは思っていたものの、上記の派生でそのまま比較用に作りました。

何が起きたか

待機版を作って試して、理想通りにうまく動きました。

その後通知版を作ってデプロイして動作確認していたのですが、どうも待機版のほうが動かなくなってしまっている。
呼び出したオーケストレーター関数のステータスがPendingになることがかなり多いし、指定した待機時間より明らかに長く時間がかかっている。
しかも最終的な結果がFailed

なぜ?と思いながらもあまり深いところまで調べることができないまま、偶然?かずきさんとお会いする機会がやってきました。

スマートスピーカーを遊びたおす会にて

参加したスマートスピーカーの勉強会でかずきさんがサプライズ登壇。
まさか参考にさせていただいている方ご本人にこんなにすぐお会いできるとは思いませんでした。
blog.okazuki.jp

内容もタイムリーにDurable Functionsの話だったので、懇親会で質問させていただき、アドバイスをいただきました。

そのひとつが、↑の記事で言及いただいている、履歴削除です。
こちらを導入し、履歴を消したところ、Pendingが起きないようになったような気がします。
(細かく原因を調べてから対処したわけではないので履歴が原因だったのかははっきりしません)

ハマっているときはとにかくPendingが出まくって処理が滞っていたのでそれがなくなったのはよかったのですが、それでもまだ結果がFailedになってしまう現象は変わらずでした。

上記のほかにも、Storageに書き出されている履歴を見たり、Application Insightsで調べてみたりするとよい、というアドバイスをかずきさんよりいただいたので、そのあたりを調べてみることにしました。

エラー内容と発生場所

Storageをみてみると、こんなエラーが出ていました。

Orchestrator function 'LongTimeOrchestrationFunction' failed: Value cannot be null.Parameter name: basePath

basePath…????

見覚えがありました。これは通知版のほうの

// 結果をLINEで通知
var config = new ConfigurationBuilder()
                .SetBasePath(context.GetInput<string>())    // FunctionAppDirectory
                .AddJsonFile("local.settings.json", true)
                .AddEnvironmentVariables()
                .Build();

の処理で出そうなエラーです。
basePathをオーケストレーターのinputで渡していますが、それが渡ってきていないときのエラーと同じです。

ところがいま動かないのは、待機版。
なぜ通知版のようなエラーが…?

そう、待機版から、通知版のオーケストレーター関数が呼ばれてしまっていたのです。

再現手順と原因

結論からいうと、原因は同一タスクハブ内に同一名称のオーケストレーター関数を置いたことが原因でした。
docs.microsoft.com

今回の再現手順

同一のストレージアカウントを使い、Durable Functionsを使用した以下の2つのFunction Appを作成。

  • Function App①(待機版)
    実行しているオーケストレーターの名称は「LongTimeOrchestrationFunction」

  • Function App②(通知版)
    実行しているオーケストレーターの名称は「LongTimeOrchestrationFunction」
    ※①と同じ名前だが、実行する内容は異なる。

まず①待機版をデプロイし、②通知版をデプロイしてそれぞれを実行すると、
結果は①成功、②失敗

次に①待機版をデプロイしなおすと、
結果は①失敗、②成功

そして②通知版を再度デプロイしなおすと、
なんと①成功、②失敗

エラー内容などから見て、どうも①②のどちらを実行しても、前にデプロイしたほうのオーケストレーター関数が呼ばれていることがわかりました。

最新のものが呼ばれるのではなく、最新じゃないほうが呼ばれるという動きでした。

なぜこうなったか

Durable Functions はストレージアカウント内に作られた「タスクハブ」の中で動作をします。

https://docs.microsoft.com/ja-jp/azure/azure-functions/durable/media/durable-functions-task-hubs/task-hubs-storage.png

Durable Functionsのタスクハブ名は、デフォルトで DurableFunctionsTaskHub です。
2つのアプリを作ったとしても、同じストレージアカウントの中に作ると、デフォルトのタスクハブに両方のアプリが入ることになってしまう、ということでした。

Durable Functionsは複雑な処理を普通のプログラムのように記述できる点が魅力ですが、だからといって完全に普通のプログラミングと完全に同じ感覚でやってはいけませんね。
たしかに同じ場所に同じ名前のもの入れるなって話ですが、普通の感覚だと別アプリ/別プロジェクト/別namespaceだったら同じ名前でも問題ない…となりますよね。

でもDurable Functionsはそもそも関数間のコールを関数名の文字列で行うので、そりゃ衝突してしまいます…。

対処法

ということなので、タスクハブが共通にならないよう、片方(今回は通知版)のタスクハブ名をhost.jsonで変更しました。
これで、どちらも動くようになりました!
(もちろん、別のストレージアカウントを使うのでもOK)

{
  "version": "2.0",
  "extensions": {
    "durableTask": {
      "hubName": "ClovaLongRunCmplLinePushNotificationSampleTaskHub"
    }
  }
}

でも、だったら最初からタスクハブ名はアプリごとの固有の名前になってほしいな…と思ったのですが、逆に別のプロジェクトで作った関数を同じタスクハブ内で回して相互に連携…とかも多いのかな…?

「デフォルトをアプリ名にしてくれ!」っていうissueも上がっているみたいなので、そのうち対応されるのかもしれません。 github.com

ということで。
なんとか対処することができました。本当によかった…。

心残り

Application Insightsでいろいろ見たかったのですが、なぜか最初の画面ですぐクラッシュしてしまい見れませんでした…。
後日再挑戦したいと思います。

お礼

かずきさんには勉強会のあとも、メールやTwitterでフォローいただきました。
ありがとうございました!!

おわりに

Durable Functions、楽しいです。

これまで雰囲気で使ってましたが、内部の動作を考えたりドキュメントをじっくり読んだり、ソースを追ったり…ということをすると理解が深まりより楽しいですね。そんなことを再認識しました。

スマスピとの組み合わせも現在いろいろ試しているところなので、どこかでまとめて報告できたらなと思います。