github.com/hashicorp/nomad/api@v0.0.0-20240306165712-3193ac204f65/retry_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package api 5 6 import ( 7 "context" 8 "encoding/json" 9 "math" 10 "net/http" 11 "net/http/httptest" 12 "testing" 13 "time" 14 15 "github.com/shoenig/test/must" 16 ) 17 18 type mockHandler struct { 19 callsCounter []time.Time 20 } 21 22 func (mh *mockHandler) Handle(rw http.ResponseWriter, req *http.Request) { 23 mh.callsCounter = append(mh.callsCounter, time.Now()) 24 25 // return a populated meta after 7 tries to test he retries stops after a 26 // successful call 27 if len(mh.callsCounter) < 7 { 28 http.Error(rw, http.StatusText(http.StatusBadGateway), http.StatusBadGateway) 29 return 30 } 31 32 rw.WriteHeader(http.StatusOK) 33 rw.Header().Set("Content-Type", "application/json") 34 35 resp := &WriteMeta{} 36 jsonResp, _ := json.Marshal(resp) 37 38 rw.Write(jsonResp) 39 return 40 } 41 42 func Test_RetryPut_multiple_calls(t *testing.T) { 43 t.Run("successfully retries until no error, delayed capped to 100ms", func(t *testing.T) { 44 mh := mockHandler{ 45 callsCounter: []time.Time{}, 46 } 47 48 server := httptest.NewServer(http.HandlerFunc(mh.Handle)) 49 cm, err := NewClient(&Config{ 50 Address: server.URL, 51 retryOptions: &retryOptions{ 52 delayBase: 10 * time.Millisecond, 53 maxRetries: 10, 54 maxBackoffDelay: 100 * time.Millisecond, 55 }, 56 }) 57 must.NoError(t, err) 58 59 md, err := cm.retryPut(context.TODO(), "/endpoint", nil, nil, &WriteOptions{}) 60 must.NoError(t, err) 61 62 must.Len(t, 7, mh.callsCounter) 63 64 must.NotNil(t, md) 65 must.Greater(t, 10*time.Millisecond, mh.callsCounter[1].Sub(mh.callsCounter[0])) 66 must.Greater(t, 20*time.Millisecond, mh.callsCounter[2].Sub(mh.callsCounter[1])) 67 must.Greater(t, 40*time.Millisecond, mh.callsCounter[3].Sub(mh.callsCounter[2])) 68 must.Greater(t, 80*time.Millisecond, mh.callsCounter[4].Sub(mh.callsCounter[3])) 69 must.Greater(t, 100*time.Millisecond, mh.callsCounter[5].Sub(mh.callsCounter[4])) 70 must.Greater(t, 100*time.Millisecond, mh.callsCounter[6].Sub(mh.callsCounter[5])) 71 }) 72 } 73 74 func Test_RetryPut_one_call(t *testing.T) { 75 t.Run("successfully retries until no error, delayed capped to 100ms", func(t *testing.T) { 76 mh := mockHandler{ 77 callsCounter: []time.Time{}, 78 } 79 80 server := httptest.NewServer(http.HandlerFunc(mh.Handle)) 81 cm, err := NewClient(&Config{ 82 Address: server.URL, 83 retryOptions: &retryOptions{ 84 delayBase: 10 * time.Millisecond, 85 maxRetries: 1, 86 }, 87 }) 88 must.NoError(t, err) 89 90 md, err := cm.retryPut(context.TODO(), "/endpoint/", nil, nil, &WriteOptions{}) 91 must.Error(t, err) 92 must.Nil(t, md) 93 94 must.Len(t, 2, mh.callsCounter) 95 }) 96 } 97 98 func Test_RetryPut_capped_base_too_big(t *testing.T) { 99 t.Run("successfully retries until no error, delayed capped to 100ms", func(t *testing.T) { 100 mh := mockHandler{ 101 callsCounter: []time.Time{}, 102 } 103 104 server := httptest.NewServer(http.HandlerFunc(mh.Handle)) 105 cm, err := NewClient(&Config{ 106 Address: server.URL, 107 retryOptions: &retryOptions{ 108 delayBase: math.MaxInt64 * time.Nanosecond, 109 maxRetries: 3, 110 maxBackoffDelay: 200 * time.Millisecond, 111 }, 112 }) 113 must.NoError(t, err) 114 115 md, err := cm.retryPut(context.TODO(), "/endpoint", nil, nil, &WriteOptions{}) 116 must.Error(t, err) 117 118 must.Len(t, 4, mh.callsCounter) 119 120 must.Nil(t, md) 121 must.Greater(t, cm.config.retryOptions.maxBackoffDelay, mh.callsCounter[1].Sub(mh.callsCounter[0])) 122 must.Greater(t, cm.config.retryOptions.maxBackoffDelay, mh.callsCounter[2].Sub(mh.callsCounter[1])) 123 }) 124 }