k8s.io/client-go@v0.31.1/rest/with_retry_test.go (about) 1 /* 2 Copyright 2021 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package rest 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "io" 24 "net/http" 25 "net/url" 26 "reflect" 27 "strings" 28 "testing" 29 "time" 30 31 "github.com/google/go-cmp/cmp" 32 ) 33 34 var alwaysRetryError = IsRetryableErrorFunc(func(_ *http.Request, _ error) bool { 35 return true 36 }) 37 38 func TestIsNextRetry(t *testing.T) { 39 fakeError := errors.New("fake error") 40 tests := []struct { 41 name string 42 attempts int 43 maxRetries int 44 request *http.Request 45 response *http.Response 46 err error 47 retryableErrFunc IsRetryableErrorFunc 48 retryExpected []bool 49 retryAfterExpected []*RetryAfter 50 }{ 51 { 52 name: "bad input, response and err are nil", 53 maxRetries: 2, 54 attempts: 1, 55 request: &http.Request{}, 56 response: nil, 57 err: nil, 58 retryExpected: []bool{false}, 59 retryAfterExpected: []*RetryAfter{nil}, 60 }, 61 { 62 name: "zero maximum retry", 63 maxRetries: 0, 64 attempts: 1, 65 request: &http.Request{}, 66 response: retryAfterResponse(), 67 err: nil, 68 retryExpected: []bool{false}, 69 retryAfterExpected: []*RetryAfter{ 70 { 71 Attempt: 1, 72 }, 73 }, 74 }, 75 { 76 name: "server returned a retryable error", 77 maxRetries: 3, 78 attempts: 1, 79 request: &http.Request{}, 80 response: nil, 81 err: fakeError, 82 retryableErrFunc: func(_ *http.Request, err error) bool { 83 if err == fakeError { 84 return true 85 } 86 return false 87 }, 88 retryExpected: []bool{true}, 89 retryAfterExpected: []*RetryAfter{ 90 { 91 Attempt: 1, 92 Wait: time.Second, 93 Reason: "retries: 1, retry-after: 1s - retry-reason: due to retryable error, error: fake error", 94 }, 95 }, 96 }, 97 { 98 name: "server returned a retryable HTTP 429 response", 99 maxRetries: 3, 100 attempts: 1, 101 request: &http.Request{}, 102 response: &http.Response{ 103 StatusCode: http.StatusTooManyRequests, 104 Header: http.Header{ 105 "Retry-After": []string{"2"}, 106 "X-Kubernetes-Pf-Flowschema-Uid": []string{"fs-1"}, 107 }, 108 }, 109 err: nil, 110 retryExpected: []bool{true}, 111 retryAfterExpected: []*RetryAfter{ 112 { 113 Attempt: 1, 114 Wait: 2 * time.Second, 115 Reason: `retries: 1, retry-after: 2s - retry-reason: due to server-side throttling, FlowSchema UID: "fs-1"`, 116 }, 117 }, 118 }, 119 { 120 name: "server returned a retryable HTTP 5xx response", 121 maxRetries: 3, 122 attempts: 1, 123 request: &http.Request{}, 124 response: &http.Response{ 125 StatusCode: http.StatusServiceUnavailable, 126 Header: http.Header{ 127 "Retry-After": []string{"3"}, 128 }, 129 }, 130 err: nil, 131 retryExpected: []bool{true}, 132 retryAfterExpected: []*RetryAfter{ 133 { 134 Attempt: 1, 135 Wait: 3 * time.Second, 136 Reason: "retries: 1, retry-after: 3s - retry-reason: 503", 137 }, 138 }, 139 }, 140 { 141 name: "server returned a non response without without a Retry-After header", 142 maxRetries: 1, 143 attempts: 1, 144 request: &http.Request{}, 145 response: &http.Response{ 146 StatusCode: http.StatusTooManyRequests, 147 Header: http.Header{}, 148 }, 149 err: nil, 150 retryExpected: []bool{false}, 151 retryAfterExpected: []*RetryAfter{ 152 { 153 Attempt: 1, 154 }, 155 }, 156 }, 157 { 158 name: "both response and err are set, err takes precedence", 159 maxRetries: 1, 160 attempts: 1, 161 request: &http.Request{}, 162 response: retryAfterResponse(), 163 err: fakeError, 164 retryableErrFunc: func(_ *http.Request, err error) bool { 165 if err == fakeError { 166 return true 167 } 168 return false 169 }, 170 retryExpected: []bool{true}, 171 retryAfterExpected: []*RetryAfter{ 172 { 173 Attempt: 1, 174 Wait: time.Second, 175 Reason: "retries: 1, retry-after: 1s - retry-reason: due to retryable error, error: fake error", 176 }, 177 }, 178 }, 179 { 180 name: "all retries are exhausted", 181 maxRetries: 3, 182 attempts: 4, 183 request: &http.Request{}, 184 response: nil, 185 err: fakeError, 186 retryableErrFunc: alwaysRetryError, 187 retryExpected: []bool{true, true, true, false}, 188 retryAfterExpected: []*RetryAfter{ 189 { 190 Attempt: 1, 191 Wait: time.Second, 192 Reason: "retries: 1, retry-after: 1s - retry-reason: due to retryable error, error: fake error", 193 }, 194 { 195 Attempt: 2, 196 Wait: time.Second, 197 Reason: "retries: 2, retry-after: 1s - retry-reason: due to retryable error, error: fake error", 198 }, 199 { 200 Attempt: 3, 201 Wait: time.Second, 202 Reason: "retries: 3, retry-after: 1s - retry-reason: due to retryable error, error: fake error", 203 }, 204 { 205 Attempt: 4, 206 }, 207 }, 208 }, 209 } 210 211 for _, test := range tests { 212 t.Run(test.name, func(t *testing.T) { 213 restReq := &Request{ 214 bodyBytes: []byte{}, 215 c: &RESTClient{ 216 base: &url.URL{}, 217 }, 218 } 219 r := &withRetry{maxRetries: test.maxRetries} 220 221 retryGot := make([]bool, 0) 222 retryAfterGot := make([]*RetryAfter, 0) 223 for i := 0; i < test.attempts; i++ { 224 retry := r.IsNextRetry(context.TODO(), restReq, test.request, test.response, test.err, test.retryableErrFunc) 225 retryGot = append(retryGot, retry) 226 retryAfterGot = append(retryAfterGot, r.retryAfter) 227 } 228 229 if !reflect.DeepEqual(test.retryExpected, retryGot) { 230 t.Errorf("Expected retry: %t, but got: %t", test.retryExpected, retryGot) 231 } 232 if !reflect.DeepEqual(test.retryAfterExpected, retryAfterGot) { 233 t.Errorf("Expected retry-after parameters to match, but got: %s", cmp.Diff(test.retryAfterExpected, retryAfterGot)) 234 } 235 }) 236 } 237 } 238 239 func TestWrapPreviousError(t *testing.T) { 240 const ( 241 attempt = 2 242 previousAttempt = 1 243 containsFormatExpected = "- error from a previous attempt: %s" 244 ) 245 var ( 246 wrappedCtxDeadlineExceededErr = &url.Error{ 247 Op: "GET", 248 URL: "http://foo.bar", 249 Err: context.DeadlineExceeded, 250 } 251 wrappedCtxCanceledErr = &url.Error{ 252 Op: "GET", 253 URL: "http://foo.bar", 254 Err: context.Canceled, 255 } 256 urlEOFErr = &url.Error{ 257 Op: "GET", 258 URL: "http://foo.bar", 259 Err: io.EOF, 260 } 261 ) 262 263 tests := []struct { 264 name string 265 previousErr error 266 currentErr error 267 expectedErr error 268 wrapped bool 269 contains string 270 }{ 271 { 272 name: "current error is nil, previous error is nil", 273 }, 274 { 275 name: "current error is nil", 276 previousErr: errors.New("error from a previous attempt"), 277 }, 278 { 279 name: "previous error is nil", 280 currentErr: urlEOFErr, 281 expectedErr: urlEOFErr, 282 wrapped: false, 283 }, 284 { 285 name: "both current and previous errors represent the same error", 286 currentErr: urlEOFErr, 287 previousErr: &url.Error{Op: "GET", URL: "http://foo.bar", Err: io.EOF}, 288 expectedErr: urlEOFErr, 289 }, 290 { 291 name: "current and previous errors are not same", 292 currentErr: urlEOFErr, 293 previousErr: errors.New("unknown error"), 294 expectedErr: urlEOFErr, 295 wrapped: true, 296 contains: fmt.Sprintf(containsFormatExpected, "unknown error"), 297 }, 298 { 299 name: "current error is context.Canceled", 300 currentErr: context.Canceled, 301 previousErr: io.EOF, 302 expectedErr: context.Canceled, 303 wrapped: true, 304 contains: fmt.Sprintf(containsFormatExpected, io.EOF.Error()), 305 }, 306 { 307 name: "current error is context.DeadlineExceeded", 308 currentErr: context.DeadlineExceeded, 309 previousErr: io.EOF, 310 expectedErr: context.DeadlineExceeded, 311 wrapped: true, 312 contains: fmt.Sprintf(containsFormatExpected, io.EOF.Error()), 313 }, 314 { 315 name: "current error is a wrapped context.DeadlineExceeded", 316 currentErr: wrappedCtxDeadlineExceededErr, 317 previousErr: io.EOF, 318 expectedErr: wrappedCtxDeadlineExceededErr, 319 wrapped: true, 320 contains: fmt.Sprintf(containsFormatExpected, io.EOF.Error()), 321 }, 322 { 323 name: "current error is a wrapped context.Canceled", 324 currentErr: wrappedCtxCanceledErr, 325 previousErr: io.EOF, 326 expectedErr: wrappedCtxCanceledErr, 327 wrapped: true, 328 contains: fmt.Sprintf(containsFormatExpected, io.EOF.Error()), 329 }, 330 { 331 name: "previous error should be unwrapped if it is url.Error", 332 currentErr: urlEOFErr, 333 previousErr: &url.Error{Err: io.ErrUnexpectedEOF}, 334 expectedErr: urlEOFErr, 335 wrapped: true, 336 contains: fmt.Sprintf(containsFormatExpected, io.ErrUnexpectedEOF.Error()), 337 }, 338 { 339 name: "previous error should not be unwrapped if it is not url.Error", 340 currentErr: urlEOFErr, 341 previousErr: fmt.Errorf("should be included in error message - %w", io.EOF), 342 expectedErr: urlEOFErr, 343 wrapped: true, 344 contains: fmt.Sprintf(containsFormatExpected, "should be included in error message - EOF"), 345 }, 346 } 347 348 for _, test := range tests { 349 t.Run(test.name, func(t *testing.T) { 350 retry := &withRetry{ 351 previousErr: test.previousErr, 352 } 353 354 err := retry.WrapPreviousError(test.currentErr) 355 switch { 356 case test.expectedErr == nil: 357 if err != nil { 358 t.Errorf("Expected a nil error, but got: %v", err) 359 return 360 } 361 case test.expectedErr != nil: 362 // make sure the message from the returned error contains 363 // message from the "previous" error from retries. 364 if !strings.Contains(err.Error(), test.contains) { 365 t.Errorf("Expected error message to contain %q, but got: %v", test.contains, err) 366 } 367 368 currentErrGot := err 369 if test.wrapped { 370 currentErrGot = errors.Unwrap(err) 371 } 372 if test.expectedErr != currentErrGot { 373 t.Errorf("Expected current error %v, but got: %v", test.expectedErr, currentErrGot) 374 } 375 } 376 }) 377 } 378 379 t.Run("Before should track previous error", func(t *testing.T) { 380 retry := &withRetry{ 381 currentErr: io.EOF, 382 } 383 384 ctx, cancel := context.WithCancel(context.Background()) 385 cancel() 386 387 // we pass zero Request object since we expect 'Before' 388 // to check the context for error at the very beginning. 389 err := retry.Before(ctx, &Request{}) 390 if err != context.Canceled { 391 t.Errorf("Expected error: %v, but got: %v", context.Canceled, err) 392 } 393 if retry.currentErr != context.Canceled { 394 t.Errorf("Expected current error: %v, but got: %v", context.Canceled, retry.currentErr) 395 } 396 if retry.previousErr != io.EOF { 397 t.Errorf("Expected previous error: %v, but got: %v", io.EOF, retry.previousErr) 398 } 399 }) 400 }