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  }