-
Notifications
You must be signed in to change notification settings - Fork 4k
/
azure_error.go
288 lines (239 loc) · 7.2 KB
/
azure_error.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
// +build !providerless
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package retry
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strconv"
"strings"
"time"
"k8s.io/klog/v2"
)
const (
// RetryAfterHeaderKey is the retry-after header key in ARM responses.
RetryAfterHeaderKey = "Retry-After"
)
var (
// The function to get current time.
now = time.Now
// StatusCodesForRetry are a defined group of status code for which the client will retry.
StatusCodesForRetry = []int{
http.StatusRequestTimeout, // 408
http.StatusInternalServerError, // 500
http.StatusBadGateway, // 502
http.StatusServiceUnavailable, // 503
http.StatusGatewayTimeout, // 504
}
)
// Error indicates an error returned by Azure APIs.
type Error struct {
// Retriable indicates whether the request is retriable.
Retriable bool
// HTTPStatusCode indicates the response HTTP status code.
HTTPStatusCode int
// RetryAfter indicates the time when the request should retry after throttling.
// A throttled request is retriable.
RetryAfter time.Time
// RetryAfter indicates the raw error from API.
RawError error
}
// Error returns the error.
// Note that Error doesn't implement error interface because (nil *Error) != (nil error).
func (err *Error) Error() error {
if err == nil {
return nil
}
// Convert time to seconds for better logging.
retryAfterSeconds := 0
curTime := now()
if err.RetryAfter.After(curTime) {
retryAfterSeconds = int(err.RetryAfter.Sub(curTime) / time.Second)
}
return fmt.Errorf("Retriable: %v, RetryAfter: %ds, HTTPStatusCode: %d, RawError: %v",
err.Retriable, retryAfterSeconds, err.HTTPStatusCode, err.RawError)
}
// IsThrottled returns true the if the request is being throttled.
func (err *Error) IsThrottled() bool {
if err == nil {
return false
}
return err.HTTPStatusCode == http.StatusTooManyRequests || err.RetryAfter.After(now())
}
// NewError creates a new Error.
func NewError(retriable bool, err error) *Error {
return &Error{
Retriable: retriable,
RawError: err,
}
}
// GetRetriableError gets new retriable Error.
func GetRetriableError(err error) *Error {
return &Error{
Retriable: true,
RawError: err,
}
}
// GetRateLimitError creates a new error for rate limiting.
func GetRateLimitError(isWrite bool, opName string) *Error {
opType := "read"
if isWrite {
opType = "write"
}
return GetRetriableError(fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", opType, opName))
}
// GetThrottlingError creates a new error for throttling.
func GetThrottlingError(operation, reason string, retryAfter time.Time) *Error {
rawError := fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", operation, reason)
return &Error{
Retriable: true,
RawError: rawError,
RetryAfter: retryAfter,
}
}
// GetError gets a new Error based on resp and error.
func GetError(resp *http.Response, err error) *Error {
if err == nil && resp == nil {
return nil
}
if err == nil && resp != nil && isSuccessHTTPResponse(resp) {
// HTTP 2xx suggests a successful response
return nil
}
retryAfter := time.Time{}
if retryAfterDuration := getRetryAfter(resp); retryAfterDuration != 0 {
retryAfter = now().Add(retryAfterDuration)
}
return &Error{
RawError: getRawError(resp, err),
RetryAfter: retryAfter,
Retriable: shouldRetryHTTPRequest(resp, err),
HTTPStatusCode: getHTTPStatusCode(resp),
}
}
// isSuccessHTTPResponse determines if the response from an HTTP request suggests success
func isSuccessHTTPResponse(resp *http.Response) bool {
if resp == nil {
return false
}
// HTTP 2xx suggests a successful response
if 199 < resp.StatusCode && resp.StatusCode < 300 {
return true
}
return false
}
func getRawError(resp *http.Response, err error) error {
if err != nil {
return err
}
if resp == nil || resp.Body == nil {
return fmt.Errorf("empty HTTP response")
}
// return the http status if unabled to get response body.
defer resp.Body.Close()
respBody, _ := ioutil.ReadAll(resp.Body)
resp.Body = ioutil.NopCloser(bytes.NewReader(respBody))
if len(respBody) == 0 {
return fmt.Errorf("HTTP status code (%d)", resp.StatusCode)
}
// return the raw response body.
return fmt.Errorf("%s", string(respBody))
}
func getHTTPStatusCode(resp *http.Response) int {
if resp == nil {
return -1
}
return resp.StatusCode
}
// shouldRetryHTTPRequest determines if the request is retriable.
func shouldRetryHTTPRequest(resp *http.Response, err error) bool {
if resp != nil {
for _, code := range StatusCodesForRetry {
if resp.StatusCode == code {
return true
}
}
// should retry on <200, error>.
if isSuccessHTTPResponse(resp) && err != nil {
return true
}
return false
}
// should retry when error is not nil and no http.Response.
if err != nil {
return true
}
return false
}
// getRetryAfter gets the retryAfter from http response.
// The value of Retry-After can be either the number of seconds or a date in RFC1123 format.
func getRetryAfter(resp *http.Response) time.Duration {
if resp == nil {
return 0
}
ra := resp.Header.Get(RetryAfterHeaderKey)
if ra == "" {
return 0
}
var dur time.Duration
if retryAfter, _ := strconv.Atoi(ra); retryAfter > 0 {
dur = time.Duration(retryAfter) * time.Second
} else if t, err := time.Parse(time.RFC1123, ra); err == nil {
dur = t.Sub(now())
}
return dur
}
// GetErrorWithRetriableHTTPStatusCodes gets an error with RetriableHTTPStatusCodes.
// It is used to retry on some HTTPStatusCodes.
func GetErrorWithRetriableHTTPStatusCodes(resp *http.Response, err error, retriableHTTPStatusCodes []int) *Error {
rerr := GetError(resp, err)
if rerr == nil {
return nil
}
for _, code := range retriableHTTPStatusCodes {
if rerr.HTTPStatusCode == code {
rerr.Retriable = true
break
}
}
return rerr
}
// GetStatusNotFoundAndForbiddenIgnoredError gets an error with StatusNotFound and StatusForbidden ignored.
// It is only used in DELETE operations.
func GetStatusNotFoundAndForbiddenIgnoredError(resp *http.Response, err error) *Error {
rerr := GetError(resp, err)
if rerr == nil {
return nil
}
// Returns nil when it is StatusNotFound error.
if rerr.HTTPStatusCode == http.StatusNotFound {
klog.V(3).Infof("Ignoring StatusNotFound error: %v", rerr)
return nil
}
// Returns nil if the status code is StatusForbidden.
// This happens when AuthorizationFailed is reported from Azure API.
if rerr.HTTPStatusCode == http.StatusForbidden {
klog.V(3).Infof("Ignoring StatusForbidden error: %v", rerr)
return nil
}
return rerr
}
// IsErrorRetriable returns true if the error is retriable.
func IsErrorRetriable(err error) bool {
if err == nil {
return false
}
return strings.Contains(err.Error(), "Retriable: true")
}