retryablehttp の exponential backoff の挙動を検証

## retryablehttp とは
[公式ドキュメント](https://pkg.go.dev/github.com/hashicorp/go-retryablehttp)
> The retryablehttp package provides a familiar HTTP client interface with automatic retries and exponential backoff. It is a thin wrapper over the standard net/http client library and exposes nearly the same public API. This makes retryablehttp very easy to drop into existing programs.

**自動リトライ**と **exponential backoff** を備えた HTTP クライアントインターフェースを提供するパッケージです。

retryablehttp は標準バッケージである [net/http](https://pkg.go.dev/net/http) のラッパーであり、ほぼ同じ API を公開しているので既存のコードに簡単に取り込むことができます。

```main.go
// 例）retryablehttp を使って作成した client で http リクエストを送る
package main

import (
	"io"
	"log"
	"net/http"
	"time"

	"github.com/hashicorp/go-retryablehttp"
)

func main() {
    // サーバーを立てる
	go serve()
    // サーバーが起動するのを待つためスリープ
	time.Sleep(1 * time.Second)

	retryClient := retryablehttp.NewClient()

	standardClient := retryClient.StandardClient() // *http.Client
	resp, err := standardClient.Get("http://localhost:8080/hello")
	if err != nil {
		log.Println(err)
		return
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	log.Println(string(body))
}

func serve() {
	helloHandler := func(w http.ResponseWriter, req *http.Request) {
		io.WriteString(w, "Hello, world!\n")
	}
	http.HandleFunc("/hello", helloHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

```
出力
```
2025/02/08 13:06:01 [DEBUG] GET http://localhost:8080/hello
2025/02/08 13:06:01 Hello, world!
```

## retryablehttp の exponential backoff を試してみる
exponential backoff とは、リクエストが失敗した際に行うリトライで、待機時間を徐々に延ばしていく手法のことです。
```main.go
package main

import (
	"io"
	"log"
	"net/http"
	"time"

	"github.com/hashicorp/go-retryablehttp"
)

func main() {
    // サーバーを立てる
	go serve()
    // サーバーが起動するのを待つためスリープ
	time.Sleep(1 * time.Second)

	retryClient := retryablehttp.NewClient()
	retryClient.RetryMax = 5 // リトライ回数を 5 回に指定
	retryClient.RetryWaitMin = 1 * time.Second // 最小リトライ間隔を 1 秒に設定

	standardClient := retryClient.StandardClient() // *http.Client
	resp, err := standardClient.Get("http://localhost:8080/hello")
	if err != nil {
		log.Println(err)
		return
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	log.Println(string(body))
}

func serve() {
	helloHandler := func(w http.ResponseWriter, req *http.Request) {
		w.WriteHeader(http.StatusInternalServerError) // status 500
	}
	http.HandleFunc("/hello", helloHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

```
出力
```
2025/02/08 13:53:16 [DEBUG] GET http://localhost:8080/hello
2025/02/08 13:53:16 [DEBUG] GET http://localhost:8080/hello (status: 500): retrying in 1s (5 left)
2025/02/08 13:53:17 [DEBUG] GET http://localhost:8080/hello (status: 500): retrying in 2s (4 left)
2025/02/08 13:53:19 [DEBUG] GET http://localhost:8080/hello (status: 500): retrying in 4s (3 left)
2025/02/08 13:53:23 [DEBUG] GET http://localhost:8080/hello (status: 500): retrying in 8s (2 left)
2025/02/08 13:53:31 [DEBUG] GET http://localhost:8080/hello (status: 500): retrying in 16s (1 left)
2025/02/08 13:53:47 Get "http://localhost:8080/hello": GET http://localhost:8080/hello giving up after 6 attempt(s)
```
`RetryWaitMin` に設定した 1 秒からはじまり、
1秒, 2秒, 4秒, 8秒... と間隔を延ばしながらリトライが行われていることがわかります

また、 `RetryWaitMax` を設定することでリトライ間隔の上限も設定できます
```main.go
package main

import (
	"io"
	"log"
	"net/http"
	"time"

	"github.com/hashicorp/go-retryablehttp"
)

func main() {
    // サーバーを立てる
	go serve()
    // サーバーが起動するのを待つためスリープ
	time.Sleep(1 * time.Second)

	retryClient := retryablehttp.NewClient()
	retryClient.RetryMax = 5
	retryClient.RetryWaitMin = 1 * time.Second
	retryClient.RetryWaitMax = 10 * time.Second // 最大リトライ間隔を 10 秒に設定

	standardClient := retryClient.StandardClient() // *http.Client
	resp, err := standardClient.Get("http://localhost:8080/hello")
	if err != nil {
		log.Println(err)
		return
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	log.Println(string(body))
}

func serve() {
	helloHandler := func(w http.ResponseWriter, req *http.Request) {
		w.WriteHeader(http.StatusInternalServerError) // status 500
	}
	http.HandleFunc("/hello", helloHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}
```
出力
```
2025/02/08 13:57:23 [DEBUG] GET http://localhost:8080/hello
2025/02/08 13:57:23 [DEBUG] GET http://localhost:8080/hello (status: 500): retrying in 1s (5 left)
2025/02/08 13:57:24 [DEBUG] GET http://localhost:8080/hello (status: 500): retrying in 2s (4 left)
2025/02/08 13:57:26 [DEBUG] GET http://localhost:8080/hello (status: 500): retrying in 4s (3 left)
2025/02/08 13:57:30 [DEBUG] GET http://localhost:8080/hello (status: 500): retrying in 8s (2 left)
2025/02/08 13:57:38 [DEBUG] GET http://localhost:8080/hello (status: 500): retrying in 10s (1 left)
2025/02/08 13:57:48 Get "http://localhost:8080/hello": GET http://localhost:8080/hello giving up after 6 attempt(s)
```
5回目のリトライが 16 秒ではなく 10 秒になっている

## retryablehttp のリトライ間隔の算出ロジックを確認してみる
[Client](https://pkg.go.dev/github.com/hashicorp/go-retryablehttp#Client) 構造体に `Backoff` のフィールドがあり、特に指定しなければ [DefaultBackoff](https://pkg.go.dev/github.com/hashicorp/go-retryablehttp#DefaultBackoff) がリトライ間隔の算出をします。

その実装は github から確認することができます。

> ```go
> // DefaultBackoff provides a default callback for Client.Backoff which
> // will perform exponential backoff based on the attempt number and limited
> // by the provided minimum and maximum durations.
> //
> // It also tries to parse Retry-After response header when a http.StatusTooManyRequests
> // (HTTP Code 429) is found in the resp parameter. Hence it will return the number of
> // seconds the server states it may be ready to process more requests from this client.
> func DefaultBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
> 	if resp != nil {
> 		if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode == http.StatusServiceUnavailable {
> 			if sleep, ok := parseRetryAfterHeader(resp.Header["Retry-After"]); ok {
> 				return sleep
> 			}
> 		}
> 	}
> 
> 	mult := math.Pow(2, float64(attemptNum)) * float64(min)
> 	sleep := time.Duration(mult)
> 	if float64(sleep) != mult || sleep > max {
> 		sleep = max
> 	}
> 	return sleep
> }
> ```
[hashicorp/go-retryablehttp](https://github.com/hashicorp/go-retryablehttp/blob/1542b31176d3973a6ecbc06c05a2d0df89b59afb/client.go#L544-L566) より引用

## まとめ
- retryablehttp を使って、リトライ間隔をリトライの度に延ばす exponential backoff が実現できる
    - `RetryMax` を設定して、最大リトライ回数を制限できる
    - `RetryWaitMin` を設定して、リトライ間隔の下限を決められる
    - `RetryWaitMax` を設定して、リトライ間隔の上限を決められる
- リトライ間隔を算出する `DefaultBackoff` について、実装が GitHub で確認できる