k8s.io/client-go@v0.22.2/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 "errors" 21 "net/http" 22 "reflect" 23 "testing" 24 "time" 25 26 "github.com/google/go-cmp/cmp" 27 ) 28 29 var alwaysRetryError = IsRetryableErrorFunc(func(_ *http.Request, _ error) bool { 30 return true 31 }) 32 33 func TestNextRetry(t *testing.T) { 34 fakeError := errors.New("fake error") 35 tests := []struct { 36 name string 37 attempts int 38 maxRetries int 39 request *http.Request 40 response *http.Response 41 err error 42 retryableErrFunc IsRetryableErrorFunc 43 retryExpected []bool 44 retryAfterExpected []*RetryAfter 45 }{ 46 { 47 name: "bad input, response and err are nil", 48 maxRetries: 2, 49 attempts: 1, 50 request: &http.Request{}, 51 response: nil, 52 err: nil, 53 retryExpected: []bool{false}, 54 retryAfterExpected: []*RetryAfter{nil}, 55 }, 56 { 57 name: "zero maximum retry", 58 maxRetries: 0, 59 attempts: 1, 60 request: &http.Request{}, 61 response: retryAfterResponse(), 62 err: nil, 63 retryExpected: []bool{false}, 64 retryAfterExpected: []*RetryAfter{ 65 { 66 Attempt: 1, 67 }, 68 }, 69 }, 70 { 71 name: "server returned a retryable error", 72 maxRetries: 3, 73 attempts: 1, 74 request: &http.Request{}, 75 response: nil, 76 err: fakeError, 77 retryableErrFunc: func(_ *http.Request, err error) bool { 78 if err == fakeError { 79 return true 80 } 81 return false 82 }, 83 retryExpected: []bool{true}, 84 retryAfterExpected: []*RetryAfter{ 85 { 86 Attempt: 1, 87 Wait: time.Second, 88 Reason: "retries: 1, retry-after: 1s - retry-reason: due to retryable error, error: fake error", 89 }, 90 }, 91 }, 92 { 93 name: "server returned a retryable HTTP 429 response", 94 maxRetries: 3, 95 attempts: 1, 96 request: &http.Request{}, 97 response: &http.Response{ 98 StatusCode: http.StatusTooManyRequests, 99 Header: http.Header{ 100 "Retry-After": []string{"2"}, 101 "X-Kubernetes-Pf-Flowschema-Uid": []string{"fs-1"}, 102 }, 103 }, 104 err: nil, 105 retryExpected: []bool{true}, 106 retryAfterExpected: []*RetryAfter{ 107 { 108 Attempt: 1, 109 Wait: 2 * time.Second, 110 Reason: `retries: 1, retry-after: 2s - retry-reason: due to server-side throttling, FlowSchema UID: "fs-1"`, 111 }, 112 }, 113 }, 114 { 115 name: "server returned a retryable HTTP 5xx response", 116 maxRetries: 3, 117 attempts: 1, 118 request: &http.Request{}, 119 response: &http.Response{ 120 StatusCode: http.StatusServiceUnavailable, 121 Header: http.Header{ 122 "Retry-After": []string{"3"}, 123 }, 124 }, 125 err: nil, 126 retryExpected: []bool{true}, 127 retryAfterExpected: []*RetryAfter{ 128 { 129 Attempt: 1, 130 Wait: 3 * time.Second, 131 Reason: "retries: 1, retry-after: 3s - retry-reason: 503", 132 }, 133 }, 134 }, 135 { 136 name: "server returned a non response without without a Retry-After header", 137 maxRetries: 1, 138 attempts: 1, 139 request: &http.Request{}, 140 response: &http.Response{ 141 StatusCode: http.StatusTooManyRequests, 142 Header: http.Header{}, 143 }, 144 err: nil, 145 retryExpected: []bool{false}, 146 retryAfterExpected: []*RetryAfter{ 147 { 148 Attempt: 1, 149 }, 150 }, 151 }, 152 { 153 name: "both response and err are set, err takes precedence", 154 maxRetries: 1, 155 attempts: 1, 156 request: &http.Request{}, 157 response: retryAfterResponse(), 158 err: fakeError, 159 retryableErrFunc: func(_ *http.Request, err error) bool { 160 if err == fakeError { 161 return true 162 } 163 return false 164 }, 165 retryExpected: []bool{true}, 166 retryAfterExpected: []*RetryAfter{ 167 { 168 Attempt: 1, 169 Wait: time.Second, 170 Reason: "retries: 1, retry-after: 1s - retry-reason: due to retryable error, error: fake error", 171 }, 172 }, 173 }, 174 { 175 name: "all retries are exhausted", 176 maxRetries: 3, 177 attempts: 4, 178 request: &http.Request{}, 179 response: nil, 180 err: fakeError, 181 retryableErrFunc: alwaysRetryError, 182 retryExpected: []bool{true, true, true, false}, 183 retryAfterExpected: []*RetryAfter{ 184 { 185 Attempt: 1, 186 Wait: time.Second, 187 Reason: "retries: 1, retry-after: 1s - retry-reason: due to retryable error, error: fake error", 188 }, 189 { 190 Attempt: 2, 191 Wait: time.Second, 192 Reason: "retries: 2, retry-after: 1s - retry-reason: due to retryable error, error: fake error", 193 }, 194 { 195 Attempt: 3, 196 Wait: time.Second, 197 Reason: "retries: 3, retry-after: 1s - retry-reason: due to retryable error, error: fake error", 198 }, 199 { 200 Attempt: 4, 201 }, 202 }, 203 }, 204 } 205 206 for _, test := range tests { 207 t.Run(test.name, func(t *testing.T) { 208 r := &withRetry{maxRetries: test.maxRetries} 209 210 retryGot := make([]bool, 0) 211 retryAfterGot := make([]*RetryAfter, 0) 212 for i := 0; i < test.attempts; i++ { 213 retryAfter, retry := r.NextRetry(test.request, test.response, test.err, test.retryableErrFunc) 214 retryGot = append(retryGot, retry) 215 retryAfterGot = append(retryAfterGot, retryAfter) 216 } 217 218 if !reflect.DeepEqual(test.retryExpected, retryGot) { 219 t.Errorf("Expected retry: %t, but got: %t", test.retryExpected, retryGot) 220 } 221 if !reflect.DeepEqual(test.retryAfterExpected, retryAfterGot) { 222 t.Errorf("Expected retry-after parameters to match, but got: %s", cmp.Diff(test.retryAfterExpected, retryAfterGot)) 223 } 224 }) 225 } 226 }