k8s.io/client-go@v0.22.2/rest/with_retry.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 "fmt" 22 "io" 23 "io/ioutil" 24 "net/http" 25 "time" 26 27 "k8s.io/klog/v2" 28 ) 29 30 // IsRetryableErrorFunc allows the client to provide its own function 31 // that determines whether the specified err from the server is retryable. 32 // 33 // request: the original request sent to the server 34 // err: the server sent this error to us 35 // 36 // The function returns true if the error is retryable and the request 37 // can be retried, otherwise it returns false. 38 // We have four mode of communications - 'Stream', 'Watch', 'Do' and 'DoRaw', this 39 // function allows us to customize the retryability aspect of each. 40 type IsRetryableErrorFunc func(request *http.Request, err error) bool 41 42 func (r IsRetryableErrorFunc) IsErrorRetryable(request *http.Request, err error) bool { 43 return r(request, err) 44 } 45 46 var neverRetryError = IsRetryableErrorFunc(func(_ *http.Request, _ error) bool { 47 return false 48 }) 49 50 // WithRetry allows the client to retry a request up to a certain number of times 51 // Note that WithRetry is not safe for concurrent use by multiple 52 // goroutines without additional locking or coordination. 53 type WithRetry interface { 54 // SetMaxRetries makes the request use the specified integer as a ceiling 55 // for retries upon receiving a 429 status code and the "Retry-After" header 56 // in the response. 57 // A zero maxRetries should prevent from doing any retry and return immediately. 58 SetMaxRetries(maxRetries int) 59 60 // NextRetry advances the retry counter appropriately and returns true if the 61 // request should be retried, otherwise it returns false if: 62 // - we have already reached the maximum retry threshold. 63 // - the error does not fall into the retryable category. 64 // - the server has not sent us a 429, or 5xx status code and the 65 // 'Retry-After' response header is not set with a value. 66 // 67 // if retry is set to true, retryAfter will contain the information 68 // regarding the next retry. 69 // 70 // request: the original request sent to the server 71 // resp: the response sent from the server, it is set if err is nil 72 // err: the server sent this error to us, if err is set then resp is nil. 73 // f: a IsRetryableErrorFunc function provided by the client that determines 74 // if the err sent by the server is retryable. 75 NextRetry(req *http.Request, resp *http.Response, err error, f IsRetryableErrorFunc) (*RetryAfter, bool) 76 77 // BeforeNextRetry is responsible for carrying out operations that need 78 // to be completed before the next retry is initiated: 79 // - if the request context is already canceled there is no need to 80 // retry, the function will return ctx.Err(). 81 // - we need to seek to the beginning of the request body before we 82 // initiate the next retry, the function should return an error if 83 // it fails to do so. 84 // - we should wait the number of seconds the server has asked us to 85 // in the 'Retry-After' response header. 86 // 87 // If BeforeNextRetry returns an error the client should abort the retry, 88 // otherwise it is safe to initiate the next retry. 89 BeforeNextRetry(ctx context.Context, backoff BackoffManager, retryAfter *RetryAfter, url string, body io.Reader) error 90 } 91 92 // RetryAfter holds information associated with the next retry. 93 type RetryAfter struct { 94 // Wait is the duration the server has asked us to wait before 95 // the next retry is initiated. 96 // This is the value of the 'Retry-After' response header in seconds. 97 Wait time.Duration 98 99 // Attempt is the Nth attempt after which we have received a retryable 100 // error or a 'Retry-After' response header from the server. 101 Attempt int 102 103 // Reason describes why we are retrying the request 104 Reason string 105 } 106 107 type withRetry struct { 108 maxRetries int 109 attempts int 110 } 111 112 func (r *withRetry) SetMaxRetries(maxRetries int) { 113 if maxRetries < 0 { 114 maxRetries = 0 115 } 116 r.maxRetries = maxRetries 117 } 118 119 func (r *withRetry) NextRetry(req *http.Request, resp *http.Response, err error, f IsRetryableErrorFunc) (*RetryAfter, bool) { 120 if req == nil || (resp == nil && err == nil) { 121 // bad input, we do nothing. 122 return nil, false 123 } 124 125 r.attempts++ 126 retryAfter := &RetryAfter{Attempt: r.attempts} 127 if r.attempts > r.maxRetries { 128 return retryAfter, false 129 } 130 131 // if the server returned an error, it takes precedence over the http response. 132 var errIsRetryable bool 133 if f != nil && err != nil && f.IsErrorRetryable(req, err) { 134 errIsRetryable = true 135 // we have a retryable error, for which we will create an 136 // artificial "Retry-After" response. 137 resp = retryAfterResponse() 138 } 139 if err != nil && !errIsRetryable { 140 return retryAfter, false 141 } 142 143 // if we are here, we have either a or b: 144 // a: we have a retryable error, for which we already 145 // have an artificial "Retry-After" response. 146 // b: we have a response from the server for which we 147 // need to check if it is retryable 148 seconds, wait := checkWait(resp) 149 if !wait { 150 return retryAfter, false 151 } 152 153 retryAfter.Wait = time.Duration(seconds) * time.Second 154 retryAfter.Reason = getRetryReason(r.attempts, seconds, resp, err) 155 return retryAfter, true 156 } 157 158 func (r *withRetry) BeforeNextRetry(ctx context.Context, backoff BackoffManager, retryAfter *RetryAfter, url string, body io.Reader) error { 159 // Ensure the response body is fully read and closed before 160 // we reconnect, so that we reuse the same TCP connection. 161 if ctx.Err() != nil { 162 return ctx.Err() 163 } 164 165 if seeker, ok := body.(io.Seeker); ok && body != nil { 166 if _, err := seeker.Seek(0, 0); err != nil { 167 return fmt.Errorf("can't Seek() back to beginning of body for %T", r) 168 } 169 } 170 171 klog.V(4).Infof("Got a Retry-After %s response for attempt %d to %v", retryAfter.Wait, retryAfter.Attempt, url) 172 if backoff != nil { 173 backoff.Sleep(retryAfter.Wait) 174 } 175 return nil 176 } 177 178 // checkWait returns true along with a number of seconds if 179 // the server instructed us to wait before retrying. 180 func checkWait(resp *http.Response) (int, bool) { 181 switch r := resp.StatusCode; { 182 // any 500 error code and 429 can trigger a wait 183 case r == http.StatusTooManyRequests, r >= 500: 184 default: 185 return 0, false 186 } 187 i, ok := retryAfterSeconds(resp) 188 return i, ok 189 } 190 191 func getRetryReason(retries, seconds int, resp *http.Response, err error) string { 192 // priority and fairness sets the UID of the FlowSchema 193 // associated with a request in the following response Header. 194 const responseHeaderMatchedFlowSchemaUID = "X-Kubernetes-PF-FlowSchema-UID" 195 196 message := fmt.Sprintf("retries: %d, retry-after: %ds", retries, seconds) 197 198 switch { 199 case resp.StatusCode == http.StatusTooManyRequests: 200 // it is server-side throttling from priority and fairness 201 flowSchemaUID := resp.Header.Get(responseHeaderMatchedFlowSchemaUID) 202 return fmt.Sprintf("%s - retry-reason: due to server-side throttling, FlowSchema UID: %q", message, flowSchemaUID) 203 case err != nil: 204 // it's a retryable error 205 return fmt.Sprintf("%s - retry-reason: due to retryable error, error: %v", message, err) 206 default: 207 return fmt.Sprintf("%s - retry-reason: %d", message, resp.StatusCode) 208 } 209 } 210 211 func readAndCloseResponseBody(resp *http.Response) { 212 if resp == nil { 213 return 214 } 215 216 // Ensure the response body is fully read and closed 217 // before we reconnect, so that we reuse the same TCP 218 // connection. 219 const maxBodySlurpSize = 2 << 10 220 defer resp.Body.Close() 221 222 if resp.ContentLength <= maxBodySlurpSize { 223 io.Copy(ioutil.Discard, &io.LimitedReader{R: resp.Body, N: maxBodySlurpSize}) 224 } 225 } 226 227 func retryAfterResponse() *http.Response { 228 return &http.Response{ 229 StatusCode: http.StatusInternalServerError, 230 Header: http.Header{"Retry-After": []string{"1"}}, 231 } 232 }