Skip to content

Commit 7382a7c

Browse files
committed
Add backoff retry which implements autorest.SendDecorator interface
1 parent 210f1a9 commit 7382a7c

File tree

6 files changed

+424
-18
lines changed

6 files changed

+424
-18
lines changed

staging/src/k8s.io/legacy-cloud-providers/azure/retry/BUILD

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,31 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
22

33
go_library(
44
name = "go_default_library",
5-
srcs = ["azure_error.go"],
5+
srcs = [
6+
"azure_error.go",
7+
"azure_retry.go",
8+
"doc.go",
9+
],
610
importmap = "k8s.io/kubernetes/vendor/k8s.io/legacy-cloud-providers/azure/retry",
711
importpath = "k8s.io/legacy-cloud-providers/azure/retry",
812
visibility = ["//visibility:public"],
9-
deps = ["//vendor/k8s.io/klog:go_default_library"],
13+
deps = [
14+
"//vendor/github.com/Azure/go-autorest/autorest:go_default_library",
15+
"//vendor/k8s.io/klog:go_default_library",
16+
],
1017
)
1118

1219
go_test(
1320
name = "go_default_test",
14-
srcs = ["azure_error_test.go"],
21+
srcs = [
22+
"azure_error_test.go",
23+
"azure_retry_test.go",
24+
],
1525
embed = [":go_default_library"],
16-
deps = ["//vendor/github.com/stretchr/testify/assert:go_default_library"],
26+
deps = [
27+
"//vendor/github.com/Azure/go-autorest/autorest/mocks:go_default_library",
28+
"//vendor/github.com/stretchr/testify/assert:go_default_library",
29+
],
1730
)
1831

1932
filegroup(

staging/src/k8s.io/legacy-cloud-providers/azure/retry/azure_error.go

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ limitations under the License.
1919
package retry
2020

2121
import (
22+
"bytes"
2223
"fmt"
24+
"io/ioutil"
2325
"net/http"
2426
"strconv"
2527
"strings"
@@ -28,6 +30,11 @@ import (
2830
"k8s.io/klog"
2931
)
3032

33+
const (
34+
// RetryAfterHeaderKey is the retry-after header key in ARM responses.
35+
RetryAfterHeaderKey = "Retry-After"
36+
)
37+
3138
var (
3239
// The function to get current time.
3340
now = time.Now
@@ -57,6 +64,15 @@ func (err *Error) Error() error {
5764
err.Retriable, err.RetryAfter.String(), err.HTTPStatusCode, err.RawError)
5865
}
5966

67+
// IsThrottled returns true the if the request is being throttled.
68+
func (err *Error) IsThrottled() bool {
69+
if err == nil {
70+
return false
71+
}
72+
73+
return err.HTTPStatusCode == http.StatusTooManyRequests || err.RetryAfter.After(now())
74+
}
75+
6076
// NewError creates a new Error.
6177
func NewError(retriable bool, err error) *Error {
6278
return &Error{
@@ -73,6 +89,20 @@ func GetRetriableError(err error) *Error {
7389
}
7490
}
7591

92+
// GetRateLimitError creates a new error for rate limiting.
93+
func GetRateLimitError(isWrite bool, opName string) *Error {
94+
opType := "read"
95+
if isWrite {
96+
opType = "write"
97+
}
98+
return GetRetriableError(fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", opType, opName))
99+
}
100+
101+
// GetThrottlingError creates a new error for throttling.
102+
func GetThrottlingError(operation, reason string) *Error {
103+
return GetRetriableError(fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", operation, reason))
104+
}
105+
76106
// GetError gets a new Error based on resp and error.
77107
func GetError(resp *http.Response, err error) *Error {
78108
if err == nil && resp == nil {
@@ -88,12 +118,8 @@ func GetError(resp *http.Response, err error) *Error {
88118
if retryAfterDuration := getRetryAfter(resp); retryAfterDuration != 0 {
89119
retryAfter = now().Add(retryAfterDuration)
90120
}
91-
rawError := err
92-
if err == nil && resp != nil {
93-
rawError = fmt.Errorf("HTTP response: %v", resp.StatusCode)
94-
}
95121
return &Error{
96-
RawError: rawError,
122+
RawError: getRawError(resp, err),
97123
RetryAfter: retryAfter,
98124
Retriable: shouldRetryHTTPRequest(resp, err),
99125
HTTPStatusCode: getHTTPStatusCode(resp),
@@ -114,6 +140,27 @@ func isSuccessHTTPResponse(resp *http.Response) bool {
114140
return false
115141
}
116142

143+
func getRawError(resp *http.Response, err error) error {
144+
if err != nil {
145+
return err
146+
}
147+
148+
if resp == nil || resp.Body == nil {
149+
return fmt.Errorf("empty HTTP response")
150+
}
151+
152+
// return the http status if unabled to get response body.
153+
defer resp.Body.Close()
154+
respBody, _ := ioutil.ReadAll(resp.Body)
155+
resp.Body = ioutil.NopCloser(bytes.NewReader(respBody))
156+
if len(respBody) == 0 {
157+
return fmt.Errorf("HTTP status code (%d)", resp.StatusCode)
158+
}
159+
160+
// return the raw response body.
161+
return fmt.Errorf("%s", string(respBody))
162+
}
163+
117164
func getHTTPStatusCode(resp *http.Response) int {
118165
if resp == nil {
119166
return -1
@@ -151,7 +198,7 @@ func getRetryAfter(resp *http.Response) time.Duration {
151198
return 0
152199
}
153200

154-
ra := resp.Header.Get("Retry-After")
201+
ra := resp.Header.Get(RetryAfterHeaderKey)
155202
if ra == "" {
156203
return 0
157204
}

staging/src/k8s.io/legacy-cloud-providers/azure/retry/azure_error_test.go

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ limitations under the License.
1919
package retry
2020

2121
import (
22+
"bytes"
2223
"fmt"
24+
"io/ioutil"
2325
"net/http"
2426
"testing"
2527
"time"
@@ -44,27 +46,27 @@ func TestGetError(t *testing.T) {
4446
},
4547
{
4648
code: http.StatusOK,
47-
err: fmt.Errorf("some error"),
49+
err: fmt.Errorf("unknown error"),
4850
expected: &Error{
4951
Retriable: true,
5052
HTTPStatusCode: http.StatusOK,
51-
RawError: fmt.Errorf("some error"),
53+
RawError: fmt.Errorf("unknown error"),
5254
},
5355
},
5456
{
5557
code: http.StatusBadRequest,
5658
expected: &Error{
5759
Retriable: false,
5860
HTTPStatusCode: http.StatusBadRequest,
59-
RawError: fmt.Errorf("HTTP response: 400"),
61+
RawError: fmt.Errorf("some error"),
6062
},
6163
},
6264
{
6365
code: http.StatusInternalServerError,
6466
expected: &Error{
6567
Retriable: true,
6668
HTTPStatusCode: http.StatusInternalServerError,
67-
RawError: fmt.Errorf("HTTP response: 500"),
69+
RawError: fmt.Errorf("some error"),
6870
},
6971
},
7072
{
@@ -83,7 +85,7 @@ func TestGetError(t *testing.T) {
8385
Retriable: true,
8486
HTTPStatusCode: http.StatusTooManyRequests,
8587
RetryAfter: now().Add(100 * time.Second),
86-
RawError: fmt.Errorf("HTTP response: 429"),
88+
RawError: fmt.Errorf("some error"),
8789
},
8890
},
8991
}
@@ -92,6 +94,7 @@ func TestGetError(t *testing.T) {
9294
resp := &http.Response{
9395
StatusCode: test.code,
9496
Header: http.Header{},
97+
Body: ioutil.NopCloser(bytes.NewReader([]byte("some error"))),
9598
}
9699
if test.retryAfter != 0 {
97100
resp.Header.Add("Retry-After", fmt.Sprintf("%d", test.retryAfter))
@@ -138,15 +141,15 @@ func TestGetStatusNotFoundAndForbiddenIgnoredError(t *testing.T) {
138141
expected: &Error{
139142
Retriable: false,
140143
HTTPStatusCode: http.StatusBadRequest,
141-
RawError: fmt.Errorf("HTTP response: 400"),
144+
RawError: fmt.Errorf("some error"),
142145
},
143146
},
144147
{
145148
code: http.StatusInternalServerError,
146149
expected: &Error{
147150
Retriable: true,
148151
HTTPStatusCode: http.StatusInternalServerError,
149-
RawError: fmt.Errorf("HTTP response: 500"),
152+
RawError: fmt.Errorf("some error"),
150153
},
151154
},
152155
{
@@ -165,7 +168,7 @@ func TestGetStatusNotFoundAndForbiddenIgnoredError(t *testing.T) {
165168
Retriable: true,
166169
HTTPStatusCode: http.StatusTooManyRequests,
167170
RetryAfter: now().Add(100 * time.Second),
168-
RawError: fmt.Errorf("HTTP response: 429"),
171+
RawError: fmt.Errorf("some error"),
169172
},
170173
},
171174
}
@@ -174,6 +177,7 @@ func TestGetStatusNotFoundAndForbiddenIgnoredError(t *testing.T) {
174177
resp := &http.Response{
175178
StatusCode: test.code,
176179
Header: http.Header{},
180+
Body: ioutil.NopCloser(bytes.NewReader([]byte("some error"))),
177181
}
178182
if test.retryAfter != 0 {
179183
resp.Header.Add("Retry-After", fmt.Sprintf("%d", test.retryAfter))
@@ -251,3 +255,38 @@ func TestIsSuccessResponse(t *testing.T) {
251255
}
252256
}
253257
}
258+
259+
func TestIsThrottled(t *testing.T) {
260+
tests := []struct {
261+
err *Error
262+
expected bool
263+
}{
264+
{
265+
err: nil,
266+
expected: false,
267+
},
268+
{
269+
err: &Error{
270+
HTTPStatusCode: http.StatusOK,
271+
},
272+
expected: false,
273+
},
274+
{
275+
err: &Error{
276+
HTTPStatusCode: http.StatusTooManyRequests,
277+
},
278+
expected: true,
279+
},
280+
{
281+
err: &Error{
282+
RetryAfter: time.Now().Add(time.Hour),
283+
},
284+
expected: true,
285+
},
286+
}
287+
288+
for _, test := range tests {
289+
real := test.err.IsThrottled()
290+
assert.Equal(t, test.expected, real)
291+
}
292+
}

0 commit comments

Comments
 (0)