Durable Functions v2 beta 2 で Durable HTTP という機能が追加されました。クリス氏が Tweet で説明しているように、アクティビティ関数無しでよい感じに 202 Accepted のポーリングを行ってくれる便利機能です。
非同期処理の開始を 202 Accepted で通知して、完了したかどうかを Location
ヘッダーで返した URL で確認するパターンを、全く意識させずに扱えます。
A quick summary of the capabilities of "Durable HTTP":
— Chris Gillum (@cgillum) 2019年8月31日
* New CallHttpAsync APIs in orchestrator function
* Automatic client-side async HTTP 202 polling
* Built-in support for Azure Managed Identities
* No activity function code required
Get the bits and give it a try! pic.twitter.com/iwE8iJXZln
しかも完了するまで単純にポーリングするのではなく、Durable Functions の Timer を使っているようなので、待機している間は CPU を使用しないので効率が良いです。とても Consumption 向きです。
非同期 HTTP API の実装
202 Accepted を使った非同期 HTTP API のパターンは一般的なのか気になったので調べてみましたが、仕様として明記されてるわけではないですが使われてはいるようです。ARM API は結構使ってました。
Template Deployment の進捗を見てると、Accepted が返ってくるので分かりやすいです。
時間がかかる処理を行う API は地味に作るのが面倒で、大抵の場合はタイムアウト長くして誤魔化しているのではないかと思いますが、Durable Functions を使えば最初から非同期な API として作られるので簡単です。
ちゃんとドキュメントにも非同期 HTTP API パターンが紹介されています。
テンプレートからオーケストレーターを作成すると CreateCheckStatusResponse
でレスポンスを作っているはずなので、非同期に対応した API がこれだけで完成です。
public class Function1 { [FunctionName("Function1")] public Task<string> RunOrchestrator([OrchestrationTrigger] IDurableOrchestrationContext context) { return Task.FromResult("Hello, world"); } [FunctionName("Function1_HttpStart")] public async Task<HttpResponseMessage> HttpStart( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestMessage req, [DurableClient] IDurableClient starter, ILogger log) { // Function input comes from the request content. string instanceId = await starter.StartNewAsync("Function1", null); log.LogInformation($"Started orchestration with ID = '{instanceId}'."); return starter.CreateCheckStatusResponse(req, instanceId); } }
適当にリクエストを投げてみると、ちゃんと 202 Accepted と Location
ヘッダーが返って来ます。
Retry-After
は何秒後に再実行するかを表していますが、Durable Functions の場合は 10 秒固定です。
Location
ヘッダーで返ってきた URL にさらにリクエストを投げると、処理が完了している場合は結果が受け取れます。終わっていない場合は 202 が返ってくるので、同じことの繰り返しです。
実行が完了していると 200 が返ってくるので、後はペイロードの値を使って良しなにするだけです。
非同期 HTTP API の利用
上の例では手動でリクエストを投げて、Durable Functions が提供する非同期 API を実行して結果を取得してみましたが、単純に実装するとループとウェイトを入れるような形になってしまいます。
実際にコンソールアプリケーションで API を実行して、結果を取得するコードを書いてみました。
class Program { static async Task Main(string[] args) { var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:7071/api/Function1_HttpStart"); var response = await _httpClient.SendAsync(request); while (response.StatusCode == System.Net.HttpStatusCode.Accepted) { await Task.Delay(response.Headers.RetryAfter.Delta.GetValueOrDefault(TimeSpan.FromSeconds(10))); var statusRequest = new HttpRequestMessage(HttpMethod.Get, response.Headers.Location); response = await _httpClient.SendAsync(statusRequest); } Console.WriteLine(await response.Content.ReadAsStringAsync()); } private static readonly HttpClient _httpClient = new HttpClient(); }
見て分かるように、202 Accepted が返ってきた場合には、何らかの方法で処理を待機させる必要があります。この場合は Task.Delay
を使っていますが、効率は良いとは言えないです。
代わりに Durable Functions v2 で追加された Durable HTTP を使うと、以下のようにシンプルに書けます。Timer を使っているので効率も良いです。
[FunctionName("Function2")] public static async Task<string> RunOrchestrator([OrchestrationTrigger] IDurableOrchestrationContext context) { var response = await context.CallHttpAsync(HttpMethod.Get, new Uri("http://localhost:7071/api/Function1_HttpStart")); return response.Content; }
制約としては Durable Functions と同じで、リクエストとレスポンスはシリアライズが可能な必要があるため、Stream
系は扱えないです。基本的には全て string
して扱われます。
同じような理由で Multipart なリクエストも扱えないので、ファイルアップロードは難しいですね。実装する場合は Blob にアップロードしてから、URL を渡す形になると思います。
実行する HTTP リクエストは DurableHttpRequest
を直接扱うことで、もう少し弄れます。この辺りは HttpClient
の HttpRequestMessage
に近いです。
[FunctionName("Function2")] public async Task<string> RunOrchestrator([OrchestrationTrigger] IDurableOrchestrationContext context) { var request = new DurableHttpRequest( HttpMethod.Get, new Uri("https://***.azurewebsites.net/api/Function1_HttpStart"), content: JsonConvert.SerializeObject(new { foo = "bar" })); request.Headers.Add("X-Function-Key", new StringValues("<functionkey>")); var response = await context.CallHttpAsync(request); return response.Content; }
コンストラクタでいろいろ指定しないといけないのが少し面倒な感じです。オブジェクト初期化子で書きたい気持ちが高まるので、暇な時にフィードバックしてみようかと思いました。
ちなみに透過的なリトライは行ってくれないので、その辺りだけは気を付ける必要があります。
元から Long-running なタスクを実行するのに適していた Durable Functions ですが、v2 で更に進化を遂げそうです。利用シーンは結構広いので、今後も積極的に使って行きます。