Durable Functionsで学ぶクラウドデザインパターン -Idempotency Key-
ごきげんよう、みなさま。
いかがお過ごしでしょうか?
2019年になりました。
Azure FunctionsはすでにV2が基本となり、Durable Functionsのバージョンは1.7.1へ、以前に比べてパフォーマンスもだいぶ良くなり、ますます使うしかないDurable Functions。
みなさま、張り切ってでゅらぶって参りましょう!!
という事で、今回は私自身に学びがあった内容の記録としてDurable Functionsで学ぶシリーズをやっていこうと思います。
とは言いましても今回はタイトルにはクラウドデザインパターンとありますがクラウドデザインパターンとは少し違います。
SOAとかそっちよりなんですかね、実際。あんまりよくわからないですが、でも必要な箇所は多いパターンだと思います。
じゃあなんでクラウドデザインパターンってタイトルにしているんだって?
それは、統一性というやつですよ(^_^;)?
あとSEO的なね。
さて、本シリーズ第4弾となります今回は「Idempotency Key」というパターンについて学んでいきたいと思います。
まえおき
みなさん、APIにPOSTやらPUSHやらPUTなんかの登録・更新リクエストとかって投げまくってますよね?
その時に1回しかリクエストしてはいけない。みたいなケースってないですか?
まあ世の中の登録や更新リクエストは1回しか投げちゃいけないことが一般的だと思います。
でも実際に本当に必要なリクエストを1回しか投げないというのは難しかったりします。
そんなの簡単でしょ?って。
じゃあローカルでDurable Functionsのデバックを行なってActivity内でAPIをコールした後にブレークポイントを仕掛けて止まったらそのままプロセスを終了してみてください。
そしてもう1度デバッグを開始して放置するのです・・・・・・2回目、投げられちゃいますよね?
無理やりじゃなくても起こり得ます。
例えばAPIリクエスト中に突然のネットワーク切断、なんて起こったらAPI側は処理を実行してくれているのに結果を受け取れません。
それが処理されたのか、処理される前にネットワーク切断が起きたのか?
そうなったらもうわかりません。闇の中です。
絶対に2回要求を投げないという設計は結構無理があるのです。
では、それに対応するためのデザインパターンは?ってなった時にIdempotency Key パターンが出てくるのです。
ただしこのパターン。API側のデザインパターンであって要求を出す側のデザインパターンではありません。
なので、世の中のAPIがIdempotency keyを採用してくれるといいなぁということで今回はAPI側の実装を行なってみました。
実際Durable FunctionsのOrchestrator起動にAPIを公開するようなケースでは用意しておいた方が良いと思います。
Idempotency Key
Idempotencyは日本語で冪等性になります。
冪等性、Durable Functionsを扱う上では大変重要な言葉です。
Wikipediaによれば
冪等性 = "大雑把に言って、ある操作を1回行っても複数回行っても結果が同じであることをいう概念である"
らしいです。
このIdempotency Key パターンを大雑把に言ってしまえば
要求の中にKeyを指定すると、同じキーのリクエストの処理は1度しか実行せず返答結果は常に同じになる。
というものです。
要求を出す側で要求を出す前にリクエスト用のキーを生成して保存しておいてね。
もし要求したか分からなくなっちゃても、同じキーをつけてリクエストしてくれればこっちで返すべき返答をちゃんとコントロールしておくよ。
ってな感じです。
このデザインパターンが採用されている有名どころというとStripeという決済サービスのAPIが有名です。
私もStripeの実装を調べている中で、このパターンを学習しました。
日本の決済系のサービスも是非採用していただきたい😤パターンです。
サンプル
OrchestratorとかActivityは特に中身のないものをになっています。
Orchestratorを起動するAPIに工夫が色々盛り込まれている感じです。
解説
では早速解説に参りましょう。
まずはクライアントが送信するリクエスト内容
public class RequestModel { public string UserId { get; set; } public string IdempotencyKey { get; set; } public string ProductId { get; set; } public long Amount { get; set; } public bool GetValidate() { return !string.IsNullOrEmpty(UserId) && !string.IsNullOrEmpty(IdempotencyKey) && !string.IsNullOrEmpty(ProductId) && Amount > 0; } }
まぁなんかそれっぽくしてあります。
そしてforループに入ります。
このループはIdempotency keyの一意性を確保するためのリトライループです。
ループ内ではまずStorage tableからUserId, IdempotencyKeyによってTableEntityを取得しています。Line49
このTableEntityは以下のような定義です。
public class IdempotencyKeyTableEntity : TableEntity { public string TaskHubName { get; set; } public string InstanceId { get; set; } }
PartitionKeyにはUserId、RowKeyにはIdempotencyKeyを指定しいます。
ここでわかるように今回の実装ではUserId + IdempotencyKeyによって冪等性が保証される設計になっています。
場合よってはIdempotencyKeyのみで保証するパターンもあり得ますしリクエスト内容全てをハッシュにしてキーとして使用することでリクエスト内容 + IdempotencyKeyで冪等性を保証する設計にするパターンもあるかと思います。
そしてこのテーブルにはTaskHubNameとInstanceIdのフィールドがあります。
InstanceIdはわかりますがなぜTaskHubNameを保存するのでしょう?
それはAPIの更新によってTaskHubNameが変更された場合にも冪等性を確保するためです。
では過去のTaskhubのDurable Functionsの結果をどのように呼び出すのでしょうか?
それにはBinderを使用します。
Binderは関数の引数に設定することで取得でき、 Binderを使用すると関数内にて入出力バインドを得ることができます。
今回は保存されているTaskHubNameで作成されたDurableOrchestrationClientBaseを取得しています。Line58
有効なIdempotencyKeyが見つかった場合にはTaskHubNameとInstanceIdを元にCreateCheckStatusResponseにて結果を返答しています。
そして見つからなかった場合、まずIdempotencyKeyTableEntityをInsertしに行きます。
ここで大切なことはInsertの結果、409(コンフリクト)レスポンスが起き得るということです。
これをキャッチした場合は並列に同じリクエストを受け付けたようなケースが想定されます。
409をキャッチしたらcontinueしてループな最初から実行し直して行きます。
その後はOrchestratorを起動し作成されたInstanceIdをIdempotencyKeyの保存情報に更新して返答をします。
今回はInstanceIdの作成をDurable Functionsに任せていますが自分で作成してしまえば、最後のInsertMergeは必要なくなります。
終わりに
いかがでしたでしょうか。
割と簡単にIdempotency key パターンが提供できたかと思います。
もちろん超大規模な環境だったりした場合にはもっと速度を意識した実装にする必要があると思います。
世の中のAPIたちが色々といい感じになる世の中が早く来ると良いですね。2019年ですがcsv連携とかやりたくないものですね。
それではまた次回をお楽しみに、サヨナラ、サヨナラ、サヨナラ!