LoginSignup
4
1

More than 1 year has passed since last update.

Exponential Backoff And JitterのUTを実装する

Last updated at Posted at 2022-12-02

はじめに

ZOZOではAPI Gatewayを内製しています。API GatewayによるAPIリクエストのリトライ時には、Exponential Backoff And Jitterによる待ち時間を経た後にリトライする仕様となっています。

Exponential Backoff And Jitterとは

Exponential Backoff And Jitterとは、リトライ回数が増えるごとに待ち時間を指数関数的に増やし、かつランダム性も加えることで輻輳を防ぐための考えです。一定間隔での待ち時間を経たリトライに比べて、リクエストが詰まりにくくなるという利点があります。

Goでのコードは以下です。引数tryCountは、何回目のリクエストかを意味しています。0の場合は初回のリクエストです。引数baseIntervalは、基本となるインターバルです。引数maxIntervalは、最大のインターバルです。ベースインターバルを使って指数関数的に計算した結果が最大インターバルを上回る場合は最大インターバルを使用します。底は2に固定しています。

func SleepExponentialBackoffAndJitter(tryCount int, baseInterval time.Duration, maxInterval time.Duration) {
	interval := baseInterval * time.Duration(math.Pow(2, float64(tryCount)))
	if interval > maxInterval {
		interval = maxInterval
	}

	interval = time.Duration(mathRand.Float64() * float64(interval))
	time.Sleep(interval)
}

UTどうしよう

関数を実装した以上、UTも実装したくなるものです。しかし、これのUTって何を実装すればよいの?、と自分は少し手が止まってしまいました。

何をチェックするかを考える

今回のテスト対象の関数(SleepExponentialBackoffAndJitter)はreturnで何かを返す関数ではありません。では、何をチェックすれば良いのでしょうか。それは、「テスト対象関数の処理時間」と「期待するスリープ時間」の比較かと考えました。

テスト対象関数の処理時間

start := time.Now()
lib.SleepExponentialBackoffAndJitter(i, baseInterval, maxInterval)
sec := time.Since(start)

このような感じで、テスト対象関数を実行する前に現在時刻を取得し、テスト対象関数の実行完了後に time.Since 関数を実行することで、経過時間を取得することができます。

期待するスリープ時間

テスト対象関数内では、 ベースインターバル * 2^試行回数 を算出し、それが最大インターバルより大きければ最大インターバルを、そうでなければ算出結果に乱数を乗算した結果をインターバル(スリープ時間)としています。テスト関数側でもそれと同じ方法で計算します。
ただし、乱数によってインターバルの値が変動してしまうため、シードを1に固定しました。1に固定すると乱数値も固定できます。

// fix random for test
mathRand.Seed(1)

expectedInterval := baseInterval * time.Duration(math.Pow(2, float64(i)))
if expectedInterval > maxInterval {
	expectedInterval = maxInterval
}
// seedが1のrand.Float64()の1回目の呼び出し結果は0.6046602879796196
expectedInterval = time.Duration(0.6046602879796196 * float64(expectedInterval))

厳密にはチェックできない

計算結果はあくまでスリープだけの時間であり、当然ながら関数全体の処理時間ではありません。ある程度の誤差は必ず発生します。今回はざっくりと誤差を50msとして定めました。

if sec < expectedInterval || sec > expectedInterval+50*time.Millisecond {
	t.Fatalf("incorrect sleep duration")
}

stack overflowで調べてみると1.05倍を誤差として考えるアイディアもあります。今回は、後述する10回実行により、1.05倍に収まらなかったため、この方法は採用しませんでした。

最終形態

最終的に実装したUTは以下です。for分で10回実行しているのは、指数関数的に待ち時間が増える過程もテストするためです。

func TestSleepExponentialBackoffAndJitter(t *testing.T) {
	t.Run("normal", func(t *testing.T) {
		baseInterval := 1 * time.Millisecond
		maxInterval := 500 * time.Millisecond

		for i := 0; i < 10; i++ {
			// fix random for test
			mathRand.Seed(1)

			expectedInterval := baseInterval * time.Duration(math.Pow(2, float64(i)))
			if expectedInterval > maxInterval {
				expectedInterval = maxInterval
			}
			// seedが1のrand.Float64()の1回目の呼び出し結果は0.6046602879796196
			expectedInterval = time.Duration(0.6046602879796196 * float64(expectedInterval))

			start := time.Now()
			lib.SleepExponentialBackoffAndJitter(i, baseInterval, maxInterval)
			sec := time.Since(start)

			// 関数呼び出しや他処理のロスを考慮して+50msは許容とする
			if sec < expectedInterval || sec > expectedInterval+50*time.Millisecond {
				t.Fatalf("incorrect sleep duration")
			}
		}
	})
}
4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1