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  }