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 }