最近は GitHub Copilot が色々やってくれるので助かってます。
アドベントカレンダーに何を書こうかと過去の記事を見ていたところ、以前の @gold-kou さんのアドベントカレンダー Exponential Backoff And JitterのUTを実装する で紹介されていたコードを変更する機会があったのでそちらについて書きたいと思います。
Go 1.24 へのバージョンアップ作業での出来事
Go 1.24 へバージョンアップで math/rand の Seed() を使用しているコードのテストがfailとなりました。
理由はこちらの Go 1.24 Release Notes となります。
https://go.dev/doc/go1.24#mathrandpkgmathrand
math/rand
Calls to the deprecated top-level Seed function no longer have any effect. To restore the old behavior use GODEBUG setting randseednop=0. For more background see proposal #67273.
元記事の引用
シードを1に固定しました。1に固定すると乱数値も固定できます。
top-level Seed function no longer have any effect. とのことでアプリケーションの挙動的には影響のないバージョンアップでしたが、テストコードがあったことで気づくことができました。
影響範囲もクリアだったので、仕様変更となった部分を通過できるよう修正を行いました。
コード修正
元のテスト mathRand.Seed(1) (mathRand は math/rand のaliasです) が対象の処理となります。
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")
}
}
})
}
最初の修正
func TestSleepExponentialBackoffAndJitter(t *testing.T) {
t.Run("normal", func(t *testing.T) {
baseInterval := 1 * time.Millisecond
maxInterval := 50 * time.Millisecond
for i := 0; i < 15; i++ {
expectedMaxInterval := baseInterval * time.Duration(math.Pow(2, float64(i)))
if expectedMaxInterval > maxInterval {
expectedMaxInterval = maxInterval
}
start := time.Now()
lib.SleepExponentialBackoffAndJitter(i, baseInterval, maxInterval)
sec := time.Since(start)
// 関数呼び出しや他処理のロスを考慮して+3msは許容とする
if sec > expectedMaxInterval+3*time.Millisecond {
t.Fatalf("incorrect sleep duration: %v", sec)
}
if sec > maxInterval+3*time.Millisecond {
t.Fatalf("incorrect max interval sleep duration: %v", sec)
}
}
})
}
PRの指摘
- 範囲に収まってることがテストされていない
maxInterval を越えないテストとしましたがまだまだですね。
オープンソースの実装を探してみると、calculateRetryDelay として interval の値を取得する処理を分けてるので参考に分けてみました。
https://github.com/aws/aws-sdk-js/blob/f5b1a6f0aebb477204d979091d654649f29ad9ce/lib/util.js#L884-L893
次の修正
func SleepExponentialBackoffAndJitter(tryCount int, baseInterval time.Duration, maxInterval time.Duration) {
time.Sleep(calculateExponentialBackoffAndJitter(tryCount, baseInterval, maxInterval))
}
func calculateExponentialBackoffAndJitter(tryCount int, baseInterval time.Duration, maxInterval time.Duration) time.Duration {
interval := baseInterval * time.Duration(math.Pow(2, float64(tryCount)))
if interval > maxInterval {
interval = maxInterval
}
interval = time.Duration(mathRand.Float64() * float64(interval))
return interval
}
sleep を切り離して interval の値をテストができるようになりましたが、実装の中でランダム値が生成されていることは変わらないので PR指摘のテスト修正ができていないです。
チーム相談
テストしずらいポイントをチームに相談。
- ランダムな値
- sleep処理
不確実性のあるコードをどうにかしたいですね。
そこでチームから以下のようなインターフェースの提案がありました。
type sleeper interface {
Sleep(duration time.Duration)
}
type randomizer interface {
Float64() float64
}
type ExponentialBackoff struct {
baseInterval time.Duration
maxInterval time.Duration
sleeper sleeper
randomizer randomizer
}
func NewExponentialBackoff(baseInterval, maxInterval time.Duration, sleeper sleeper, randomizer randomizer) ExponentialBackoff {
return ExponentialBackoff{
baseInterval: baseInterval,
maxInterval: maxInterval,
sleeper: sleeper,
randomizer: randomizer,
}
}
// 実装省略
randomizer と sleeper のコードの中身は golang で提供されている品質なのでテストは不要とし、interval を取得するロジックのテストはモックを利用することで検証することができるようになりました!
最後に
claude に記事のレビューをしてもらったところ以下の3点指摘をいただきました。コードに関しては生成AIでサンプル実装してくれるので興味のある方は生成してみてください。
-
GODEBUG=randseednop=0で旧動作に戻せることへの言及 - 最終的なテストコードの全体像とインターフェースを使う場合のコード側の初期化例
- cenkalti/backoffくらい見てから記事書こう
特別なインターフェースではないですが、チームでインターフェースを考えた日の紹介でした。元の記事の補足ができたことも良かったです。チームでこういういことを考えられるのもいい日々だったなと思えるくらい来年は世界が変わっていて欲しいですね。