GoのHTTPライブラリにRestyというものがある。しばしばこのライブラリを使 用する機会があるため、調べたことを備忘録的に記述する。そのため網羅的な 内容ではない。

RestyでHTTPリクエスト送信のリトライ処理を行う

何度失敗すれば諦めていいのか、これは人生にとって重要な問題だ。 1度の失敗で諦めていいのか、もしくは無限に諦めず挑戦しつづけてもいいのか、これを判断するのはとても難しい。 この難しい問題を考えるため、今回はRestyのリトライ処理の実装方法を確認する。

基本的なリトライの仕方は全て https://github.com/go-resty/resty に記載されている。

ダミーサーバーを実装する

まずはリクエストを送信するためのダミーサーバーを実装する。

URL設計

ダミーサーバーはURLによって、それぞれ異なるHTTPレスポンスを返すことにする。 検証として使用するための以下のURLを実装する。

  • GET /succes 成功

  • GET /error HTTPレスポンスコードとしてInternal Server Errorを返す

  • GET /success-but-fail HTTPレスポンスコードとしてOKを返すが、 {"status": "failed"} というBODYを返す。

実装

package main

import (
	"log"
	"net/http"
)

func main () {
	http.HandleFunc("/success", func (w http.ResponseWriter, r *http.Request) {
		log.Print("Receive /success")
		w.Header().Set("Content-Type","text/plain")
		w.WriteHeader(http.StatusOK)
		w.Write([]byte(`Success!!`))
	})

	http.HandleFunc("/error", func (w http.ResponseWriter, r *http.Request) {
		log.Print("Receive /error")
		w.Header().Set("Content-Type","text/plain")
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte(`Error!!`))
	})

	http.HandleFunc("/success-but-fail", func (w http.ResponseWriter, r *http.Request) {
		log.Print("Receive /success-but-fail")
		w.Header().Set("Content-Type","application/json")
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte(`{"status": "failed"}`))
	})

	if err := http.ListenAndServe(":8000", nil); err != nil {
		log.Panic("ListenAndServe:", err)
	}
}

リクエストが受けとれることを確認する

package main

import (
	"fmt"
	"github.com/go-resty/resty/v2"
)

func main () {
	resp, err := resty.New().R().Get("http://localhost:8000/success")
	if err != nil {
		panic(err)
	}
	fmt.Println(resp)
}

リトライ処理を実装する

HTTPステータスによってリトライする

リトライする条件はAddRetryCondition関数で登録できる。 この関数がtrueを返すとリトライが実行される。 この実装ではレスポンスのステータスコードが200でなければ、1秒間隔で最大3回のリクエストを送信する。

package main

import (
	"fmt"
	"time"
	"github.com/go-resty/resty/v2"
)

func main () {
	c := resty.New()
	resp, err := c.SetRetryCount(2).
		SetRetryWaitTime(1 * time.Second).
		AddRetryCondition(func (r *resty.Response, err error) bool {
			return r.StatusCode() != 200
		}).
		R().
		Get("http://localhost:8000/error")

	if err != nil {
		panic(err)
	}
	fmt.Println(resp.StatusCode())
}

HTTPレスポンスのBODYによってリトライする

リトライする条件で扱えるものはレスポンスのステータスコードだけではなく、 他のさまざまな値を利用できる。 今度はレスポンスボディの値を元にリトライ処理を実行する。

Web APIによっては、そのWeb API呼び出しが成功しているかどうかを示す方法としてHTTPレスポンスのステータスコードを用いることもあれば、 ステータスコードは200 OKを返しレスポンスのエラーコードを含むJSONをボディ部として返すということもある。

今回は後者を想定し、レスポンスのボディ部のJSONのstatus属性がfailedであればリクエストの送信をリトライさせる。 つまり {"status": "failed"} というJSONが返されたらリトライする。

package main

import (
	"fmt"
	"time"
	"encoding/json"
	"github.com/go-resty/resty/v2"
)

type ResponseBody struct {
	Status string `json:"status,omitempty"`
}

func main () {
	c := resty.New()
	b := &ResponseBody{}
	resp, err := c.SetRetryCount(2).
		SetRetryWaitTime(1 * time.Second).
		AddRetryCondition(func (r *resty.Response, err error) bool {
			json.Unmarshal(r.Body(), b)
			return b.Status == "failed"
		}).
		R().
		Get("http://localhost:8000/success-but-fail")
	if err != nil {
		panic(err)
	}
	fmt.Println(resp.StatusCode())
	fmt.Println(b.Status)
}

Exponential Backoff

よくあるリクエストのリトライ処理のパターンとして Exponential Backoff という方法がある。 リトライするたびに次のリクエストを送信するまでの時間を長くするという方法だ。 たとえば1回目のリトライを2秒後、2回目のリトライを4秒後、3回目のリトライを8秒後といったように、指数関数的に待ち時間を増やしていく。 RestyではデフォルトのリトライのロジックにExponential Backoffアルゴリズムを採用している。

そのためリトライ回数やリトライの最大秒数などの値の設定だけでよい。

package main

import (
	"fmt"
	"time"
	"github.com/go-resty/resty/v2"
)

func main () {
	c := resty.New()
	resp, err := c.SetRetryCount(5).
		SetRetryWaitTime(1 * time.Second).
		SetRetryMaxWaitTime(20 * time.Second).
		AddRetryCondition(func (r *resty.Response, err error) bool {
			return r.StatusCode() != 200
		}).
		R().
		Get("http://localhost:8000/error")

	if err != nil {
		panic(err)
	}
	fmt.Println(resp.StatusCode())
}

Go Modules

go.mod::

module example

go 1.17

require github.com/go-resty/resty/v2 v2.7.0

require golang.org/x/net v0.0.0-20211029224645-99673261e6eb // indirect

go.sum::

github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb h1:pirldcYWx7rx7kE5r+9WsOXPXK0+WH5+uZ7uPmJ44uM=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

プロキシを中継してRestyでのリクエスト送信を行う

RestyはHTTP_PROXY環境変数やHTTPS_PROXY環境変数が設定されていれば、設定 されているプロキシにリクエストを中継させる。

export HTTP_PROXY=http://localhost:8080
export HTTPS_PROXY=http://localhost:8080
package main

import (
	"fmt"
	"github.com/go-resty/resty/v2"
)

func main() {
	c := resty.New()
	resp, err :=  c.R().Get("https://example.com")
	fmt.Println(err)
	fmt.Println(resp)
}

他にクライアントにSetProxyで設定する方法もある。

https://github.com/go-resty/resty#proxy-settings—client-as-well-as-at-request-level

まとめ

要求に応じて適切なリトライ処理を実装しよう。