Skip to content

Commit e1f5485

Browse files
authored
Add a new RateLimitLinearJitterBackoff policy
1 parent b0cac1e commit e1f5485

File tree

2 files changed

+124
-0
lines changed

2 files changed

+124
-0
lines changed

client.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,23 @@ func LinearJitterBackoff(min, max time.Duration, attemptNum int, resp *http.Resp
638638
return time.Duration(jitterMin * int64(attemptNum))
639639
}
640640

641+
// RateLimitLinearJitterBackoff wraps the retryablehttp.LinearJitterBackoff.
642+
// It first checks if the response status code is http.StatusTooManyRequests
643+
// (HTTP Code 429) or http.StatusServiceUnavailable (HTTP Code 503). If it is
644+
// and the response contains a Retry-After response header, it will wait the
645+
// amount of time specified by the header. Otherwise, this calls
646+
// LinearJitterBackoff.
647+
func RateLimitLinearJitterBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
648+
if resp != nil {
649+
if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode == http.StatusServiceUnavailable {
650+
if sleep, ok := parseRetryAfterHeader(resp.Header["Retry-After"]); ok {
651+
return sleep
652+
}
653+
}
654+
}
655+
return LinearJitterBackoff(min, max, attemptNum, resp)
656+
}
657+
641658
// PassthroughErrorHandler is an ErrorHandler that directly passes through the
642659
// values from the net/http library for the final request. The body is not
643660
// closed.

client_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,6 +1143,113 @@ func TestClient_PostForm(t *testing.T) {
11431143
resp.Body.Close()
11441144
}
11451145

1146+
func TestBackoff_RateLimitLinearJitterBackoff(t *testing.T) {
1147+
testCases := []struct {
1148+
name string
1149+
min time.Duration
1150+
max time.Duration
1151+
headers http.Header
1152+
responseCode int
1153+
expect time.Duration
1154+
}{
1155+
{
1156+
name: "429 no retry header",
1157+
min: time.Second,
1158+
max: time.Second,
1159+
headers: http.Header{},
1160+
responseCode: http.StatusTooManyRequests,
1161+
expect: time.Second,
1162+
},
1163+
{
1164+
name: "503 no retry header",
1165+
min: time.Second,
1166+
max: time.Second,
1167+
headers: http.Header{},
1168+
responseCode: http.StatusServiceUnavailable,
1169+
expect: time.Second,
1170+
},
1171+
{
1172+
name: "429 retry header",
1173+
min: time.Second,
1174+
max: time.Second,
1175+
headers: http.Header{
1176+
"Retry-After": []string{"2"},
1177+
},
1178+
responseCode: http.StatusTooManyRequests,
1179+
expect: 2 * time.Second,
1180+
},
1181+
{
1182+
name: "503 retry header",
1183+
min: time.Second,
1184+
max: time.Second,
1185+
headers: http.Header{
1186+
"Retry-After": []string{"2"},
1187+
},
1188+
responseCode: http.StatusServiceUnavailable,
1189+
expect: 2 * time.Second,
1190+
},
1191+
{
1192+
name: "502 ignore retry header",
1193+
min: time.Second,
1194+
max: time.Second,
1195+
headers: http.Header{
1196+
"Retry-After": []string{"2"},
1197+
},
1198+
responseCode: http.StatusBadGateway,
1199+
expect: time.Second,
1200+
},
1201+
{
1202+
name: "502 no retry header",
1203+
min: time.Second,
1204+
max: time.Second,
1205+
headers: http.Header{},
1206+
responseCode: http.StatusBadGateway,
1207+
expect: time.Second,
1208+
},
1209+
{
1210+
name: "429 retry header with jitter",
1211+
min: time.Second,
1212+
max: 5 * time.Second,
1213+
headers: http.Header{
1214+
"Retry-After": []string{"2"},
1215+
},
1216+
responseCode: http.StatusTooManyRequests,
1217+
expect: 2 * time.Second,
1218+
},
1219+
{
1220+
name: "429 retry header less than min",
1221+
min: 5 * time.Second,
1222+
max: 10 * time.Second,
1223+
headers: http.Header{
1224+
"Retry-After": []string{"2"},
1225+
},
1226+
responseCode: http.StatusTooManyRequests,
1227+
expect: 2 * time.Second,
1228+
},
1229+
{
1230+
name: "429 retry header in range",
1231+
min: time.Second,
1232+
max: 10 * time.Second,
1233+
headers: http.Header{
1234+
"Retry-After": []string{"2"},
1235+
},
1236+
responseCode: http.StatusTooManyRequests,
1237+
expect: 2 * time.Second,
1238+
},
1239+
}
1240+
for _, tc := range testCases {
1241+
t.Run(tc.name, func(t *testing.T) {
1242+
got := RateLimitLinearJitterBackoff(tc.min, tc.max, 0, &http.Response{
1243+
StatusCode: tc.responseCode,
1244+
Header: tc.headers,
1245+
})
1246+
if got != tc.expect {
1247+
t.Fatalf("expected %s, got %s", tc.expect, got)
1248+
}
1249+
})
1250+
}
1251+
}
1252+
11461253
func TestBackoff(t *testing.T) {
11471254
type tcase struct {
11481255
min time.Duration

0 commit comments

Comments
 (0)