😋

Goで作る!リトライ処理

2022/12/26に公開

概要

サービス間の通信でリトライ処理を行うと思うが、Goの標準パッケージで実装はされていない。

自前でリトライの間隔幅を設定したり、リトライの上限を設けたりして実装するのは結構大変。

良いライブラリがあった

cenkalti/backoffというライブラリが、上記要件を満たすような実装になっていた。
早速使ってみた。

https://github.com/cenkalti/backoff

指数バックオフ

実装的にはこんな感じ。

func main() {
	if err := exampleRetry(); err != nil {
		fmt.Println(err)
	}
}

func exampleRetry() error {
	now := time.Now()
	
	// リトライ上限、リトライ間隔の設定
	b := backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 5)
	operation := func() error {
		elapsedSeconds := time.Since(now).Seconds()
		fmt.Println(elapsedSeconds)
                
		// わざとエラーにする
		return errors.New("hoge")
	}
	
	// 実行
	if err := backoff.Retry(operation, b); err != nil {
		return err
	}

	return nil
}

しっかり5回までリトライされて、指数バックオフ出来てる!

出力
0
0.552330144 ← ここからリトライ処理
1.63271196
2.94284202
4.5252347109999995
6.865723375 ← 最後のリトライ処理
hoge

Program exited.

backoff.NewExponentialBackOff()の中身を見ると、ライブラリ側のデフォルトの設定で返してくれている。

ExponentialBackOffがパブリックな構造体なので、InitialIntervalを1秒にしたりとかしてカスタマイズも出来そう。

cenkalti/backoff/exponential.go
// NewExponentialBackOff creates an instance of ExponentialBackOff using default values.
func NewExponentialBackOff() *ExponentialBackOff {
	b := &ExponentialBackOff{
		InitialInterval:     DefaultInitialInterval,
		RandomizationFactor: DefaultRandomizationFactor,
		Multiplier:          DefaultMultiplier,
		MaxInterval:         DefaultMaxInterval,
		MaxElapsedTime:      DefaultMaxElapsedTime,
		Stop:                Stop,
		Clock:               SystemClock,
	}
	b.Reset()
	return b
}

定数バックオフ

下記の部分を、NewConstantBackOffに変えてあげるだけで同じ間隔幅でバックオフが出来るようになる。

- b := backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 5)

+ duration := 100 * time.Millisecond
+ b := backoff.WithMaxRetries(backoff.NewConstantBackOff(duration), 5)

リトライ可否の判定も可能

backoff.Permanent()を使うと、リトライを即終了出来るので、例えばタイムアウトした時のみリトライしたいみたいな事も出来る。

func main() {
	if err := exampleRetry(); err != nil {
		fmt.Println(err)
	}
}

func exampleRetry() error {
	b := backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 5)
	operation := func() error {
		err := getRecord()
		if err == nil {
			return nil
		}

		errCode := extractErrCode(err)
		if errCode == codes.DeadlineExceeded {
			// タイムアウトならリトライを行う
			return err
		}

		// それ以外は即終了
		return backoff.Permanent(err)
	}

	// 実行
	if err := backoff.Retry(operation, b); err != nil {
		return err
	}

	return nil
}

まとめ

  • Goで自前でリトライ処理を行うのは結構大変だが、cenkalti/backoffを使えば簡単に実装出来る
  • 指数バックオフや定数バックオフも出来るかつ、特定の場合のみリトライを行うなど小回りも効くのオススメ👍

Discussion