こんにちは、サイオステクノロジー武井です。今日は超絶便利なDurable Functionsのリトライ機能についてちょっとお話してみたいと思います。
リトライ機能
クラウド上のAPIを叩く場合は特にリトライ機能は必須かと思います。クラウドデザインパターンにも書かれています。
https://docs.microsoft.com/ja-jp/azure/architecture/patterns/retry
クラウド上のAPIに限らずですが、ネットワーク上の問題だったり、APIのスロットル制限に引っかかったり、その他謎な理由でも、結構APIは失敗します。そんな場合に短期的なりトライも長期的なリトライも必須になってきます。
ただ、このリトライ機能、実装しようと思うとかなり厄介ですよね。
Durable Functionsにはこのリトライを簡単にできる機能があります。これが超絶便利なので、使い方や注意点も含めて説明してみようと思います。
ちなみに、Microsoftに限らずですが、SDK側でリトライを実装してくれているものもあります。ただ、それも完全というわけではなく、例えば、Graph SDKでは、HTTPステータスが異常な値(500とか)の場合はSDK側でリトライしてくれますが、ネットワーク的に到達できず、HTTPレスポンスがそもそも返ってこない場合は例外を吐いて終わりです。意外にそういうエラーのほうが圧倒的に多いです。そんなときにDurable Functionsのリトライは役に立ちます。SDK側とDurable Functionsの両方でリトライかけておけば、さらに安心ですよね。
やってみよう
超カンタンで、アクティビティ関数を実行するときにパラメーター渡してあげるだけです。以下は、Java Scriptの場合の実装例です。callActivityWithRetryでアクティビティ関数を呼んで上げて、その引数にリトライのオプションを渡してあげます。この例は、1秒おきに100回リトライする例です。
const firstRetryIntervalInMilliseconds = 1000; const maxNumberOfAttempts = 100; const retryOptions = new df.RetryOptions(firstRetryIntervalInMilliseconds, maxNumberOfAttempts); yield context.df.callActivityWithRetry("SayHello", retryOptions, null);
バックオフ係数とかも指定できます。大抵、失敗した後にすぐにリトライしてもまた失敗するパティーンが多いです。なので、バックオフ係数で再試行間隔をどんどん時間をあけて行うのが一般的です。
例えば、1回目のリトライ間隔は2秒、次のリトライまでの間隔は4秒、次は8秒、16秒みたいな感じでやりとリトライの成功率が高くなります。こんな感じで実装します。
const firstRetryIntervalInMilliseconds = 1000; const maxNumberOfAttempts = 100; const retryOptions = new df.RetryOptions(firstRetryIntervalInMilliseconds, maxNumberOfAttempts); retryOptions.backoffCoefficient = 2; yield context.df.callActivityWithRetry("SayHello", retryOptions, null);
つまりリトライ間隔は以下の式で計算されることになります。
firstRetryIntervalInMilliseconds * backoffCoefficient ^ n(n はリトライ回数)
アクティビティ関数の冪等性担保
リトライはアクティビティ関数単位で行われるので、アクティビティ関数は何度も何度も実行されます。なので、なるべくアクティビティ関数で行う処理は最小限にして冪等性を担保すると幸せになれるかと思います。
冪等性が担保されていないと、リトライされるたびに違う実行結果になってしまうので、そもそもリトライの意味がありません。
リトライを実行させたくない場合
リトライを実行させたくない場合もあると思います。例えばHTTPステータス500系のエラーではサーバー側が不調なのでリトライさせても意味はあると思いますが、400系のエラーはそもそも入力値に誤りがあったりとかなので、リトライさせてもあまり意味がありません。
Durable Functionsにはこんなときのために、リトライオプションにリトライを実行させるかどうかを決定するためのコールバック関数を定義できます。以下の例はC#ですが、例外のメッセージにhogeが含まれる場合のみリトライする例です。
var result = await ctx.CallActivityWithRetryAsync<string>("SayHello", new RetryOptions(TimeSpan.FromSeconds(5),4) { Handle = ex => ex.InnerException.Message == "hoge" }, input);
これも超絶便利な機能なんですが、Java Scriptではまだ実装されていません。早く実装されないかな。。。
不具合(2021年11月30日時点)
使ってみて、なんか一部の処理で想定外の動作をすることがあるようです。Issueもすでに上げました。
https://github.com/Azure/azure-functions-durable-js/issues/291
次のようなケースで変な動きをします。
- アクティビティ関数Aをリトライで並列実行をする。
1のアクティビティ関数がリトライされた後に正常に終了する。 - アクティビティ関数Bを実行する。
const df = require("durable-functions"); module.exports = df.orchestrator(function* (context) { const firstRetryIntervalInMilliseconds = 1000; const maxNumberOfAttempts = 10; const retryOptions = new df.RetryOptions(firstRetryIntervalInMilliseconds, maxNumberOfAttempts); const tasks = []; for (let i = 0; i < 5; i++) { tasks.push(context.df.callActivityWithRetry("ActivityA", retryOptions)); } yield context.df.Task.all(tasks); const results = yield context.df.callActivityWithRetry("ActivityB", retryOptions); });
module.exports = async function (context) { // 一定確率で例外発生 const min = 1; const max = 3; const a = Math.floor(Math.random() * (max + 1 - min)) + min; if (a == 1) throw Error('hoge'); return "hoge"; };
module.exports = async function (context) { context.log('ActivityB start'); context.log('Do something...'); context.log('ActivityB End'); };
早く治らないかしら。。。