デュラブってる?(激寒)

◆前史(飛ばしてよし)

AzureでFunctionを実行したい時に使えるのがAzure Functionsです。
FunctionっていうのはつまりC#だので書くサーバー処理の話だ。多分。そのFunctionはバッチ処理をしたりとかファイルを生成したりメールを送ったりとかする(小学生並みの説明)
Functionのなにが凄いかっていうと、普段は寝てるのに処理を走らせたいときだけ起きてくれる。つまり、インスタンスをずっと起こしておく必要がなくなるわけで、正しく使えばお金の節約ができる。(App Serviceの上にFunctionを配置することで、不眠なFunctionを作ることもできるよ。App Serviceの負荷が小さい時間帯にリソースを有効活用できたりするし、10分の処理時間制限が撤廃されたりするよ。)

Functionは何らかのきっかけをトリガして動く。トリガできるイベントは

  • Blobと呼ばれるAzureのストレージにファイルが配置された
  • HTTPリクエスト
  • Queueストレージにメッセージが配置された
  • あらかじめ設定された時間が来た

とかだ。さまざまだ。

でね?例えば

「Blobに配置されたCSVを加工してSQL DatabaseにINSERT」
した後に
「そのデータを元に集計バッチ処理を走らせる」

ってことをしたくなるのが男のサガじゃないですか。
しかし、それをFunction一本糞にしてしまうのはおかしい。単一責任の原則とかでね。それゆえ、Functionは2つになる。

そうすると「Function1が終わるのを待ってFunction2を蹴る」という同期的な操作をしたくなる。そこでどうするかというと

Function1の最後で「Queueストレージ」にメッセージを格納する処理を実行し、Queueのメッセージ配置をFunction2でトリガする

という話になってくるでしょう。
でも、ちょっとそれは微妙だわな。Functionの純粋性がなんとなく損なわれるような気分だ。

あとは、非同期で、つまり並列でFunction1とFunction2とFunction3を動かして、そいつらの処理が終わるのを待つ、すなわち同期してからFunction4を動かしたい。ってのもなんか込み入った処理を書かねばならぬ気配がするよね。

そこで今回ご紹介させて頂くのがDurable Functionsです。

◆Durable Functions

  • durable – 永続性のある、恒久性の、もちのよい、丈夫な

どういうことなの・・・
しゃあねぇ。公式の解説を見てみよう。お前らはとりあえず図だけでも見てくれればいいよ。

参考:Durable Functions の概要

つまり、Functionの実行をオーケストレーションするのがDurable Functionsだ。
ここで言うオーケストレーションはつまり複数のFunctionどもの動作を調整するみてぇな意味で、オーケストラの指揮者みたいなノリかと思う。

Durabli FunctionsはC#とかの適当な言語で記述する。そして、今までのFunctionみたいに任意のイベントをトリガして実行が開始される。

◆準備

どこから説明したもんかわからんが、Functionの開発経験がある人向けに話す。

Durable Functionsは(2018年現在)新しめの機能であるから、Visual Studioの更新やらプロジェクトのDLLの更新やらが必要だ。「動かねぇ」とか「エラー吐きやがった」と思ったら以下を確認するべし。また、Visual Studio自体のバージョンが古いと動かないかもしれない。

以下を更新すれば大体オッケーと思われるが、なんか憶測でしかないから各自調べてうまくやって。(調査放棄)

  • Visual Studio
    ⇒ ツール ⇒ 拡張機能と更新プログラム ⇒ 更新プログラム
    ⇒「Microsoft Azure WebJobs ツール」とか
  • プロジェクト
    ⇒ ソリューションエクスプローラーでプロジェクトを開く
    ⇒ 「依存関係」を右クリックし、「NuGetパッケージの管理」
     ⇒ 「Microsoft.Azure.WebJobs.Extensions.DurableTask」とか
     ⇒ 「Microsoft.Azure.WebJobs.Extensions.Storage」とか
     ⇒ 「Microsoft.NET.Sdk.Functions」とか

◆作業内容

▼Durable Functionsの実体的なアレを作成

クラスが一つ増える。その中には

  • DurableOrchestrationClientを蹴る処理
  • Orchestrator

のメソッドが記載される。概要は下記を参照。

参考:Durable Functions を始めるときに知っていると幸せになれる7つの Tip – Tip 1. クラスと責務

新しいDurable Functionsを作る際、Visual Studioがテンプレートを作ってくれる。

  • ソリューションエクスプローラーにて、適当なフォルダとかをクリックして「追加」
  • 「新しい Azure 関数」 ⇒ 「Azure 関数」
  • 「Durable Functions Orchestration」

じゃあ見ていこう。

●HttpStartメソッド

HTTPリクエストをきっかけに実行される。そして、Orchestration Clientを蹴る。
StartNewAsyncメソッドの第二引数はnullになっているが、適当なObjectを引数にすることでOrchestratorに値を渡すことができる。Tupleを使用すれば2つの値を渡すことも可能。

参考:Durable Functions のバインド – トリガーの使用方法

参考リンクに記載されている「JSON にシリアル化できる型にする必要があります。」ってのがキモ。~Context系で受け取る引数はシリアライズ可能なオブジェクトでなければいけない。簡単に言うと「インスタンスをテキストファイルに変換し、それをインスタンスに変換しなおせる」という性質を持ったオブジェクトだ。詳しくはググれ。

Q:なんでシリアライザブルなオブジェクトじゃなきゃだめなん?
A:Durable Functionって内部的にはQueueストレージを使ってるらしい。Queueストレージに入るのはテキストファイルであるから、そういうことだろう。

参考:Durable Functions のパフォーマンスとスケーリング – 内部キュー トリガー

●RunOrchestratorメソッド

Orchestrator。Orchestration Clientから「DurableOrchestrationContext」型のコンテキスト(情報の塊的なアレ)を受け取る。
「DurableOrchestrationContext.GetInput<T>()」メソッドを使用することで、Tに指定した型の引数を取り出すことができる。Tuple使っても同じ。

outputsはただの「Activity Functionから戻り値受け取れますよ」アピールであるから、awaitから記述しても普通に動く。

●SayHelloメソッド

Orchestrateされる対象のActivity Functionだ。その証拠に引数に[ActivityTrigger]アノテーションが記載されている。こいつは「DurableActivityContext」とかいうコンテストを受け取ることもできる。

参考:Durable Functions のバインド – トリガー サンプル

Activity Functionは処理の主役で、いわゆる「ビジネスロジック」とか呼ばれがちなアイツはすべてここに乗るんじゃなかろうか。

Activityメソッドは非同期で動かされたり同期的に動かされたりする。また、このメソッドはstringの戻り値で定義されているが別にvoidでも大丈夫だし、async Task<T>でも大丈夫だ。

▼旧Functionを修正

いままでは、各FunctionがそれぞれBlobやらQueueストレージの入力をトリガして動いていたんだけど、Durable Functionsさんで制御をするから、各FunctionはActivity Functionというものになり、引数としてOrchestratorからコンテキストを受け取るのみになる。だからFunctionからトリガハンドルの記述はなくなる。

◆Blobトリガ始動での実装

これが悩ませてきた。Blob始動のDurable Functionのサンプルが少ない。
まぁ、だったら俺が作ってやるよ(天下無双)

機能としては

  • Blobの「foo」に「bar/{name}.csv」が連携された際
  • CSVを一行ずつ読み込みListに格納したあとで
  • SayHelloする

って感じ。この場合、{name}のところは任意の文字で良い。

やってることは割と単純で、気合でコードを読み込めばわかるはず。だから解説はしない。ウソ。ちょっとする。ここに至るまでの変遷が知りたい人は続けて読み進めてくれ。

▼ちょっと解説

  1. Activity Functionの置き場はこのクラス内ではないはず。新しくクラスファイルを作成し、そこにクラスとFunctionを記述しよう。
  2. ReadBlobStreamメソッドの引数が不思議なことになっている。何をしているのかっていうと、「fileName」をOrchestratorから受け取りながら
    [Blob(foo/bar/{fileName}.csv, FileAccess.Read)]
    の{fileName}にバインドしている。そのパス情報からStreamを読み込んでいる。すごい
  3. [FunctionName(nameof(RunOrchestrator))]

    とかやってるけど、FunctionNameが被るとまずい。それぞれのOrchestrator特有の名前を付けてやるのが良かろう。

◆うま味

うーーーーまーーーーあーーーーじーーーー!

うまあじは沢山あるけど、いま思うのは

  1. Queueへのメッセージ送信処理を書かなくてもよくなった。Activity Functionには処理のことだけ書かれている状態になった。さらに言うと、次に実行したいFunctionを選択するためのswitch文を書かなくて済む。
  2. どういう順番でFunctionたちが実行されていくのかが視認しやすい。
  3. 一度書いたFunctionを簡単に使いまわせる。
  4. Functionをいじることなく実行順序の変更ができる。同じFunctionを何回も動かしたりするのも得意だし、並列実行だってこなせるぜ。
  5. Functionの10分制限も各Function毎だからヘーキ。FunctionをオーケストレーションするためのFunctionを自作しようとすると時間制限とか面倒だね。WebApp作るのもおかしな話だし。
  6. いままでよりサーバーレスっぽい気がする。Functionが次のFunctionを呼び出すために必要なQueueストレージを作らなくてもよい。(不要とは言っていない
  7. エラー発生時のリトライポリシーもコード上で明示できる。
    Durable Functions のエラー処理 – エラー発生時の自動再試行
  8. エラーのハンドリング処理をOrchestratorに書けちゃう。だからロジックだけをActivity Functionに書くという選択肢を採用してもいいし、しなくてもいい。
  9. 現場での評価が上がって、おちんぎんがおっきくなり、彼女ができる。

とかだろうか。

◆まず味

  1. Queueストレージが汚れる。
    内部的にQueueを使っているって話をしたけど、Durable Functionsが動いた後で「durablefunctionshub-control-xx」とかいうQueueが沢山できる。それも、消えないで残る。隠蔽して💛
  2. ログ見づらい。少なくとも見やすくはない。テキストエディタかなんかに貼ってから確認してる。

◆詰まりポインツ

▼いろいろ更新してなかった

System.Private.CoreLib: Exception while executing function: ImportStore. System.Private.CoreLib: One or more errors occurred. (This operation is not permitted because the blob has snapshots.). Microsoft.WindowsAzure.Storage: This operation is not permitted because the blob has snapshots.

とか

Could not load type ‘Microsoft.Azure.WebJobs.ExecutionContext’ from assembly ‘Microsoft.Azure.WebJobs.Extensions, Version=3.0.0.0, Culture=neutral, PublicKeyToken=null’.

のエラーが出た。更新をかけよう。

参考:Common errors when upgrading to 2.0.12050 (or newer)

▼いままでBlob TriggerでBindしてたStream

もともと、あるFunctionがBlobTriggerでStreamをバインドして、そいつを処理していた。が、スターター(例でいうとBlobStartメソッド)でバインドしたStreamをContext経由で渡そうとすると

Timeouts are not supported on this stream.

とか言って怒られる。

CloudBlockBlobをContext経由で渡そうとすると

Newtonsoft.Json.JsonSerializationException: Unable to find a constructor to use for type Microsoft.WindowsAzure.Storage.Blob.CloudBlockBlob. A class should either have a default constructor, one constructor with arguments or a constructor marked with the JsonConstructor attribute.

とかいって怒られる。CloudBlockBlobはシリアライズできないのだ。

「System.IO.Stream.get_ReadTimeout()」の結果を取ろうとしてExceptionを吐いたりもしていたな。

だから、UriをBlobから取得して、そのUriを持ちまわそうと思った。そして、Activityメソッド内でUriからBlobをコンストラクトした。したらそのCloudBlockBlobのDownloadToStreamAsyncメソッドが not working で、

The specified resource does not exist.

とか言われた。

これは別記事で解消方法を紹介する。が、結局つかわなかったな。

最終的に
Support for declarative binding expressions
から
Support for binding expressions in activity functions
をみて、例に挙げた解法を導出した。

◆結論

わしが作成したサンプルをいじれば何となく使えるものができるんじゃあるまいか。「直したほうがいい」とか「ここ解り辛い」とか「参考になりましたぁ」とかあったらコメントしてくれな。

そして、Durable Functionはなかなか使えると思うぜ。上でも紹介したけど、理解するにあたりこのページはだいぶ役立った。家に来て妹をファックしていいぞ。

また、Durable Functionの作者であるクリス兄貴がde:code 2018において日本語で紹介プレゼンをしてくれたそうだ。下記動画だ。なかなかに面白かった。