Open2
golang と仲良くなる人
context.DeadlineExceeded
と context.Canceled
と仲良くなる人。
func TestRequestTimeout(t *testing.T) {
t.Parallel()
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(5 * time.Second)
})
t.Run("WithTimeout なし", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(h)
before := time.Now()
ctx := context.Background()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatal(err)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
// 今回のテストだと http handler による sleep なので err としては返らない
t.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
t.Errorf("want=http.StatusOK, got=%d", res.StatusCode)
}
// request 先の分( 5sec )経過してる
after := time.Now()
if after.Unix() != before.Add(5*time.Second).Unix() {
t.Logf("%d", after.Unix())
t.Logf("%d", before.Add(time.Second).Unix())
t.Errorf("before=%+v, after=%+v", before, after)
}
})
t.Run("WithTimeout あり", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(h)
before := time.Now()
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatal(err)
}
res, err := http.DefaultClient.Do(req)
if res != nil {
t.Fatal(res)
}
// timeout を過ぎる場合、context.DeadlineExceeded が返る
if err != nil && !errors.Is(err, context.DeadlineExceeded) {
t.Errorf("want=context.DeadlineExceeded, got=%+v", err)
}
// request 先の 5sec 分を待たず WithTimeout で指定した time.Duration 分のみ経過してる
after := time.Now()
if after.Unix() != before.Add(time.Second).Unix() {
t.Logf("%d", after.Unix())
t.Logf("%d", before.Add(time.Second).Unix())
t.Errorf("before=%+v, after=%+v", before, after)
}
})
t.Run("WithTimeout あり(途中でキャンセルする)", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(h)
before := time.Now()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
go func() {
time.Sleep(2 * time.Second)
cancel()
}()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatal(err)
}
res, err := http.DefaultClient.Do(req)
if res != nil {
t.Fatal(res)
}
if err != nil && !errors.Is(err, context.Canceled) {
t.Errorf("want=context.Canceled, got=%+v", err)
}
// request 先の 5sec 分を待たずキャンセルを行った 2sec 分のみ経過してる
after := time.Now()
if after.Unix() != before.Add(2*time.Second).Unix() {
t.Logf("%d", after.Unix())
t.Logf("%d", before.Add(time.Second).Unix())
t.Errorf("before=%+v, after=%+v", before, after)
}
})
}
結論: それぞれ timeout 時、client からの cancel 時に返るエラー。timeout は(ものによっては)exponential backoff and jitter でリトライ。cancel は適当に 4xx 系で response するなどする。
exponential backoff and jitter と仲良くなる人
func TestRetryRequest(t *testing.T) {
t.Parallel()
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(5 * time.Second)
})
t.Run("request に timeout を指定しない場合", func(t *testing.T) {
t.Parallel()
before := time.Now()
ts := httptest.NewServer(h)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
request := func(ctx context.Context) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatal(err)
}
return http.DefaultClient.Do(req)
}
actLoop := 0
policy := backoff.Exponential(
backoff.WithMinInterval(500*time.Millisecond),
backoff.WithMaxInterval(3*time.Second),
backoff.WithMaxRetries(3),
backoff.WithJitterFactor(0.05),
)
b := policy.Start(ctx)
for backoff.Continue(b) {
actLoop++
res, err := request(context.Background())
if err == nil {
if res.StatusCode != http.StatusOK {
t.Errorf("want=200, got=%d", res.StatusCode)
}
break
}
}
// request に timeout を設けていないため request 先の 5sec 経過し request としては成功しているため loop数は 1回
if actLoop != 1 {
t.Errorf("want=1, actLoop=%d", actLoop)
}
after := time.Now()
if after == before.Add(5*time.Second) {
t.Errorf("before=%+v, after=%+v", before, after)
}
})
t.Run("request に timeout を指定する場合", func(t *testing.T) {
t.Parallel()
before := time.Now()
ts := httptest.NewServer(h)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
request := func(parent context.Context) (*http.Response, error) {
ctx, cancel := context.WithTimeout(parent, time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatal(err)
}
return http.DefaultClient.Do(req)
}
actLoop := 0
var beforeInterval time.Duration
loopStart := before
policy := backoff.Exponential(
backoff.WithMinInterval(time.Second),
backoff.WithMaxInterval(3*time.Second),
backoff.WithMaxRetries(3),
backoff.WithJitterFactor(0.05),
)
b := policy.Start(ctx)
for backoff.Continue(b) {
now := time.Now()
if actLoop > 1 {
// loop 毎に interval が伸びてる( exponential backoff されてる)ことの確認
// (常に今回の interval が前回の interval を超えているか)
if beforeInterval >= now.Sub(loopStart) {
t.Errorf("beforeInterval >= now.Sub(loopStart), beforeInterval=%+v, now.Sub(loopStart)=%+v", beforeInterval, now.Sub(loopStart))
}
}
if actLoop >= 1 {
beforeInterval = now.Sub(loopStart)
loopStart = now
}
actLoop++
res, err := request(context.Background())
if err == nil {
t.Fatal("must be set err")
}
if res != nil {
t.Fatal(res)
}
if !errors.Is(err, context.DeadlineExceeded) {
t.Errorf("want=context.DeadlineExceeded, got=%+v", err)
}
}
// request に timeout がありすべての request が失敗するため上限 3 回リトライして 4loop
// 時間としては、(0.5~3)*3 の範囲で収まってるはず 5sec とか 6sec とか?最大値の 10sec は超えないはず
if actLoop != 4 {
t.Errorf("want=1, actLoop=%d", actLoop)
}
after := time.Now()
if after.Unix() >= before.Add(10*time.Second).Unix() {
t.Errorf("before=%+v, after=%+v", before, after)
}
})
t.Run("全体の最大値は超えない", func(t *testing.T) {
t.Parallel()
before := time.Now()
ts := httptest.NewServer(h)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
request := func(parent context.Context) (*http.Response, error) {
ctx, cancel := context.WithTimeout(parent, 4*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatal(err)
}
return http.DefaultClient.Do(req)
}
policy := backoff.Exponential(
backoff.WithMinInterval(time.Second),
backoff.WithMaxInterval(3*time.Second),
backoff.WithMaxRetries(3),
backoff.WithJitterFactor(0.05),
)
b := policy.Start(ctx)
for backoff.Continue(b) {
res, err := request(context.Background())
if err == nil {
t.Fatal("must be set err")
}
if res != nil {
t.Fatal(res)
}
if !errors.Is(err, context.DeadlineExceeded) {
t.Errorf("want=context.DeadlineExceeded, got=%+v", err)
}
}
// 実際は WithTimeout で設定した 10sec きっかりとならず(たぶん backoff 分待ってから context の判定に移ってる?ので 10+backoff sec 経過する)
after := time.Now()
if after.Unix() <= before.Add(10*time.Second).Unix() {
t.Errorf("before=%+v, after=%+v", before, after)
}
})
}