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  }