Ignite のタイミングで Flex Consumption が GA したこともあり、そろそろ .NET Isolated 向けの Durable Functions を使いたい気持ちが高まってきたのでしっかりと確認しておくことにしました。以前調べたときは対応していない機能も多かった気がするのですが、今では API も整理されて完成度がかなり高まっています。
詳細は説明しないので以下の公式ドキュメントにも目を通しておくと吉です。
.NET Isolated 向けの Durable Functions を使いたいモチベーションの中でも最大なものとして、これも Ignite で発表された Durable Task Scheduler の存在があります。Durable Task Scheduler はこれまでの Durable Functions に対してスケーラビリティとオブザーバビリティを提供してくれます。
まだ絶賛 Preview 中ですが、自分が触っている感じではかなり Durable Functions の運用が行いやすくなっているので、早く GA して欲しいという気持ちしかありません。
Durable Task Scheduler のブログには書かれていませんが、Private Preview に参加している人間向けのドキュメントには .NET Isolated などの分離ワーカー向けのみ導入方法が書かれていました。将来的には .NET Isolated などの分離ワーカーに一本化されるので、何処かのタイミングで In-Process からの移行が必要なので、この機会にやっておくのは一つの手です。
今回 DTS を利用するために .NET Isolated 向けの Durable Functions を気合いを入れて試したという話になります。後述しますが DTS は In-Process でも利用できると知ったのはこのブログをほぼ書いた後でした。
.NET Isolated 版の Durable Functions は In-Process からクラスやインターフェース名が変更されたぐらいで正直大きな違いはありません。内部的には In-Process と .NET Isolated で同じライブラリが使われているので挙動についても一貫性があるはずです。
とりあえず Visual Studio を使って新しい Durable Functions のプロジェクトを作成すると、テンプレートで以下のようなオーケストレーターとアクティビティが生成されます。
[Function(nameof(Function1))] public static async Task<List<string>> RunOrchestrator( [OrchestrationTrigger] TaskOrchestrationContext context) { ILogger logger = context.CreateReplaySafeLogger(nameof(Function1)); logger.LogInformation("Saying hello."); var outputs = new List<string>(); // Replace name and input with values relevant for your Durable Functions Activity outputs.Add(await context.CallActivityAsync<string>(nameof(SayHello), "Tokyo")); outputs.Add(await context.CallActivityAsync<string>(nameof(SayHello), "Seattle")); outputs.Add(await context.CallActivityAsync<string>(nameof(SayHello), "London")); // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"] return outputs; } [Function(nameof(SayHello))] public static string SayHello([ActivityTrigger] string name, FunctionContext executionContext) { ILogger logger = executionContext.GetLogger("SayHello"); logger.LogInformation("Saying hello to {name}.", name); return $"Hello {name}!"; }
正直なところ、このテンプレートを見るだけで .NET Isolated 版の Durable Functions については大体理解出来るはずです。多少メソッドの引数で取るクラスの型が変わっていますが、アクティビティの呼び出しについてはこれまで通りなので悩むことは無いでしょう。
既存の Durable Functions の移行については後述しますが、大体は .NET Upgrade Assistant を使ってコンパイルエラーを潰していくという流れで大体問題ありません。現時点では Durable Functions には未対応となるため、規模によりますが多少の手作業は発生します。
テンプレートで生成されたレベルのコードでは .NET Isolated へ移行したメリットがあまりないので、次は Source Generator を使って Durable Functions のよくあるミスをコンパイル時に検出できるようにします。
Source Generator を有効化する
実は In-Process の Durable Functions でもプレビューで公開されていた Source Generator ですが、今回 .NET Isolated では公式ドキュメントで扱われるレベルになりました。この Source Generator はアクティビティ毎に専用の拡張メソッドを生成してくれます。
使い方はドキュメントに書いてある通りですが、以下の NuGet パッケージをインストールするだけなので非常にお手軽です。まだプレビューなのが少し不安になりますが、複雑なコード生成を行っているわけではないので問題なさそうです。
Durable Functions を使った開発でよくあるミスは、アクティビティのシグネチャを変更しても呼び出し側の修正を忘れてしまい、実行時に型エラーになるというものがあります。これは Durable Functions のアクティビティ実行はキューを用いた間接呼び出しなことに起因します。Source Generator が出るまでは回避が難しい問題でしたが、ようやく安全にアクティビティを呼び出せるようになります。
関数ベースで利用する
Durable Functions の Source Generator には 2 つの機能が用意されていて、1 つは前述したようにアクティビティ毎に専用の拡張メソッドを作成する機能です。
Source Generator の NuGet パッケージをインストールしてビルドすると、以下のように IntelliSense でアクティビティ用の拡張メソッドが表示されるようになります。
重要なのは Source Generator がアクティビティのシグネチャを読み取って、適切な型の拡張メソッドを生成してくれるという点です。これによりアクティビティの型を変えた場合には拡張メソッドの型も変更されるので、もちろんコンパイルエラーになるため安全です。
生成された拡張メソッドは以下のようにシンプルなものですが、自動で生成されるというのが最高です。
Source Generator によって生成された拡張メソッドを使うと、以下のように非常にすっきりとオーケストレーターを書くことが出来ます。拡張メソッドによって型安全なだけではなく、アクティビティの名前を間違えることもなくなるので積極的に使っていきたい機能です。
[Function(nameof(Function1))] public static async Task<List<string>> RunOrchestrator( [OrchestrationTrigger] TaskOrchestrationContext context) { ILogger logger = context.CreateReplaySafeLogger(nameof(Function1)); logger.LogInformation("Saying hello."); var outputs = new List<string>(); // Replace name and input with values relevant for your Durable Functions Activity outputs.Add(await context.CallSayHelloAsync("Tokyo")); outputs.Add(await context.CallSayHelloAsync("Seattle")); outputs.Add(await context.CallSayHelloAsync("London")); // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"] return outputs; }
既存の In-Process な Durable Functions から .NET Isolated へ移行する際には確実に使っておきたい機能の一つです。このように拡張メソッドが提供されるだけなので、アプリケーションの構造を大きく変えることなく組み込めるのもメリットです。
クラスベースで利用する
もう一つ Source Generator に用意されている機能が、オーケストレーターとアクティビティをクラスベースで定義する機能です。個人的にはこの機能が一番気に入っています。
正直あまりイメージ出来ないと思いますが、要するにオーケストレーターとアクティビティを以下のように TaskActivity<TInput, TOutput>
や TaskOrchestrator<TInput, TOutput>
を実装したクラスとして定義できると言うものです。独立したクラスとして実装できるの DI 周りもシンプルに出来るのと、実装をクラスに閉じ込めることが出来るので扱いやすいです。
[DurableTask(nameof(Orchestration))] public class Orchestration : TaskOrchestrator<string, List<string>> { public override async Task<List<string>> RunAsync(TaskOrchestrationContext context, string input) { var logger = context.CreateReplaySafeLogger<Orchestration>(); logger.LogInformation("Saying hello."); var outputs = new List<string>(); // Replace name and input with values relevant for your Durable Functions Activity outputs.Add(await context.CallSayHelloAsync("Tokyo")); outputs.Add(await context.CallSayHelloAsync("Seattle")); outputs.Add(await context.CallSayHelloAsync("London")); // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"] return outputs; } } [DurableTask(nameof(SayHello))] public class SayHello(ILogger<SayHello> logger) : TaskActivity<string, string> { public override async Task<string> RunAsync(TaskActivityContext context, string input) { logger.LogInformation("Saying hello to {name}.", input); return $"Hello {input}!"; } }
オーケストレーターとアクティビティはそれぞれ独立して動作するので、実装クラス自体も分かれていた方が管理しやすくなります。依存関係もシンプルに出来るのでかなりお気に入りの機能です。
このクラスベースの Durable Functions ですが残念ながら Azure Functions SDK の最適化によって、現在のバージョンではデフォルトの設定では正しく動作しなくなってしまいました。現時点での最新バージョンをインストールして実行してみると、以下のようにオーケストレーターとアクティビティが Azure Functions Runtime によって認識されていないことが分かります。
これは少し前の Azure Functions SDK のアップデートで組み込まれた Source Generator の影響です。以下の Issue で色々と話がされていますが、現時点では SDK が複数の Source Generator を受け付けられるような拡張ポイントを用意するという流れになっています。
SDK に組み込まれた Source Generator の影響で利用できなくなっているので、以下のような設定を csproj
ファイルに追加して Source Generator を無効化してしまえば動作するようになります。
<PropertyGroup> <FunctionsEnableWorkerIndexing>false</FunctionsEnableWorkerIndexing> <FunctionsEnableExecutorSourceGen>false</FunctionsEnableExecutorSourceGen> </PropertyGroup>
上の設定を csproj
に追加して再度実行してみると、今度は正しくオーケストレーターとアクティビティが認識されています。この状態で HttpTrigger を実行するとクラスベースで定義したオーケストレーターとアクティビティが実行されます。
実際に使っていますがクラスベースで書けるのはかなり快適なので、Azure Functions SDK の Source Generator を無効化しなくても使えるようになって欲しいですが、拡張ポイントの実装にそれなりに時間が掛かりそうです。回避策はあるので気長に待とうと思います。
既存のプロジェクトを移行する
最後に In-Process の Durable Functions から .NET Isolated の Durable Functions への移行について簡単に書いておくことにします。基本的には以下の公式ドキュメントに基づいて修正して貰えれば問題ないはずです。前述したように .NET Upgrade Assistant は Durable Functions に対応していないので、クラス名の修正などは自分で行う必要がありますが、そこまで難しいものではありません。
Durable Entities についても同様で、クラス名が変わっているぐらいなので対応は簡単です。
.NET Isolated 版に移行しても内部で使われている実装は In-Process と同じですので、Azure Storage や Task Hub はそのまま使いまわすことが出来ます。心配であれば変更しても問題ないですが、実装からして互換性には問題ないと考えています。
移行する最大のモチベーションだった Durable Task Scheduler ですが、ドキュメントには .NET Isolated などの分離ワーカーへの導入方法しか書かれていませんでしたが、先日 DTS を作っている Chris に直接聞いたところ実は In-Process 向けのパッケージも用意されているらしく、それを使うと DTS を既存の In-Process な Durable Functions でも利用できることまで確認しました。
従って Durable Task Scheduler を使うためのモチベーションとしては消滅してしまいましたが、インターフェースが再設計されて分かりやすくなっているので移行する価値はあると考えています。