github.com/hashicorp/nomad/api@v0.0.0-20240306165712-3193ac204f65/retry.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package api
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"net/http"
    10  	"time"
    11  )
    12  
    13  const (
    14  	defaultNumberOfRetries = 5
    15  	defaultDelayTimeBase   = time.Second
    16  	defaultMaxBackoffDelay = 5 * time.Minute
    17  )
    18  
    19  type retryOptions struct {
    20  	maxRetries int64 // Optional, defaults to 5
    21  	// maxBackoffDelay  sets a capping value for the delay between calls, to avoid it growing infinitely
    22  	maxBackoffDelay time.Duration // Optional, defaults to 5 min
    23  	// maxToLastCall sets a capping value for all the retry process, in case there is a deadline to make the call.
    24  	maxToLastCall time.Duration // Optional, defaults to 0, meaning no time cap
    25  	// fixedDelay is used in case an uniform distribution of the calls is preferred.
    26  	fixedDelay time.Duration // Optional, defaults to 0, meaning Delay is exponential, starting at 1sec
    27  	// delayBase is used to calculate the starting value at which the delay starts to grow,
    28  	// When left empty, a value of 1 sec will be used as base and then the delays will
    29  	// grow exponentially with every attempt: starting at 1s, then 2s, 4s, 8s...
    30  	delayBase time.Duration // Optional, defaults to 1sec
    31  
    32  	// maxValidAttempt is used to ensure that a big attempts number or a big delayBase number will not cause
    33  	// a negative delay by overflowing the delay increase. Every attempt after the
    34  	// maxValid will use the maxBackoffDelay if configured, or the defaultMaxBackoffDelay if not.
    35  	maxValidAttempt int64
    36  }
    37  
    38  func (c *Client) retryPut(ctx context.Context, endpoint string, in, out any, q *WriteOptions) (*WriteMeta, error) {
    39  	var err error
    40  	var wm *WriteMeta
    41  
    42  	attemptDelay := 100 * time.Second // Avoid a tick before starting
    43  	startTime := time.Now()
    44  
    45  	t := time.NewTimer(attemptDelay)
    46  	defer t.Stop()
    47  
    48  	for attempt := int64(0); attempt < c.config.retryOptions.maxRetries+1; attempt++ {
    49  		attemptDelay = c.calculateDelay(attempt)
    50  
    51  		t.Reset(attemptDelay)
    52  
    53  		select {
    54  		case <-ctx.Done():
    55  			return nil, ctx.Err()
    56  		case <-t.C:
    57  
    58  		}
    59  
    60  		wm, err = c.put(endpoint, in, out, q)
    61  
    62  		// Maximum retry period is up, don't retry
    63  		if c.config.retryOptions.maxToLastCall != 0 && time.Since(startTime) > c.config.retryOptions.maxToLastCall {
    64  			break
    65  		}
    66  
    67  		// The put function only returns WriteMetadata if the call was successful
    68  		// don't retry
    69  		if wm != nil {
    70  			break
    71  		}
    72  
    73  		// If WriteMetadata is nil, we need to process the error to decide if a retry is
    74  		// necessary or not
    75  		var callErr UnexpectedResponseError
    76  		ok := errors.As(err, &callErr)
    77  
    78  		// If is not UnexpectedResponseError, it is an error while performing the call
    79  		// don't retry
    80  		if !ok {
    81  			break
    82  		}
    83  
    84  		// Only 500+ or 429 status calls may be retried, otherwise
    85  		// don't retry
    86  		if !isCallRetriable(callErr.StatusCode()) {
    87  			break
    88  		}
    89  	}
    90  
    91  	return wm, err
    92  }
    93  
    94  // According to the HTTP protocol, it only makes sense to retry calls
    95  // when the error is caused by a temporary situation, like a server being down
    96  // (500s+) or the call being rate limited (429), this function checks if the
    97  // statusCode is between the errors worth retrying.
    98  func isCallRetriable(statusCode int) bool {
    99  	return statusCode > http.StatusInternalServerError &&
   100  		statusCode < http.StatusNetworkAuthenticationRequired ||
   101  		statusCode == http.StatusTooManyRequests
   102  }
   103  
   104  func (c *Client) calculateDelay(attempt int64) time.Duration {
   105  	if c.config.retryOptions.fixedDelay != 0 {
   106  		return c.config.retryOptions.fixedDelay
   107  	}
   108  
   109  	if attempt == 0 {
   110  		return 0
   111  	}
   112  
   113  	if attempt > c.config.retryOptions.maxValidAttempt {
   114  		return c.config.retryOptions.maxBackoffDelay
   115  	}
   116  
   117  	newDelay := c.config.retryOptions.delayBase << (attempt - 1)
   118  	if c.config.retryOptions.maxBackoffDelay != defaultMaxBackoffDelay &&
   119  		newDelay > c.config.retryOptions.maxBackoffDelay {
   120  		return c.config.retryOptions.maxBackoffDelay
   121  	}
   122  
   123  	return newDelay
   124  }