mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
125 lines
3.7 KiB
Go
125 lines
3.7 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package api
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
defaultNumberOfRetries = 5
|
|
defaultDelayTimeBase = time.Second
|
|
defaultMaxBackoffDelay = 5 * time.Minute
|
|
)
|
|
|
|
type retryOptions struct {
|
|
maxRetries int64 // Optional, defaults to 5
|
|
// maxBackoffDelay sets a capping value for the delay between calls, to avoid it growing infinitely
|
|
maxBackoffDelay time.Duration // Optional, defaults to 5 min
|
|
// maxToLastCall sets a capping value for all the retry process, in case there is a deadline to make the call.
|
|
maxToLastCall time.Duration // Optional, defaults to 0, meaning no time cap
|
|
// fixedDelay is used in case an uniform distribution of the calls is preferred.
|
|
fixedDelay time.Duration // Optional, defaults to 0, meaning Delay is exponential, starting at 1sec
|
|
// delayBase is used to calculate the starting value at which the delay starts to grow,
|
|
// When left empty, a value of 1 sec will be used as base and then the delays will
|
|
// grow exponentially with every attempt: starting at 1s, then 2s, 4s, 8s...
|
|
delayBase time.Duration // Optional, defaults to 1sec
|
|
|
|
// maxValidAttempt is used to ensure that a big attempts number or a big delayBase number will not cause
|
|
// a negative delay by overflowing the delay increase. Every attempt after the
|
|
// maxValid will use the maxBackoffDelay if configured, or the defaultMaxBackoffDelay if not.
|
|
maxValidAttempt int64
|
|
}
|
|
|
|
func (c *Client) retryPut(ctx context.Context, endpoint string, in, out any, q *WriteOptions) (*WriteMeta, error) {
|
|
var err error
|
|
var wm *WriteMeta
|
|
|
|
attemptDelay := 100 * time.Second // Avoid a tick before starting
|
|
startTime := time.Now()
|
|
|
|
t := time.NewTimer(attemptDelay)
|
|
defer t.Stop()
|
|
|
|
for attempt := int64(0); attempt < c.config.retryOptions.maxRetries+1; attempt++ {
|
|
attemptDelay = c.calculateDelay(attempt)
|
|
|
|
t.Reset(attemptDelay)
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
case <-t.C:
|
|
|
|
}
|
|
|
|
wm, err = c.put(endpoint, in, out, q)
|
|
|
|
// Maximum retry period is up, don't retry
|
|
if c.config.retryOptions.maxToLastCall != 0 && time.Since(startTime) > c.config.retryOptions.maxToLastCall {
|
|
break
|
|
}
|
|
|
|
// The put function only returns WriteMetadata if the call was successful
|
|
// don't retry
|
|
if wm != nil {
|
|
break
|
|
}
|
|
|
|
// If WriteMetadata is nil, we need to process the error to decide if a retry is
|
|
// necessary or not
|
|
var callErr UnexpectedResponseError
|
|
ok := errors.As(err, &callErr)
|
|
|
|
// If is not UnexpectedResponseError, it is an error while performing the call
|
|
// don't retry
|
|
if !ok {
|
|
break
|
|
}
|
|
|
|
// Only 500+ or 429 status calls may be retried, otherwise
|
|
// don't retry
|
|
if !isCallRetriable(callErr.StatusCode()) {
|
|
break
|
|
}
|
|
}
|
|
|
|
return wm, err
|
|
}
|
|
|
|
// According to the HTTP protocol, it only makes sense to retry calls
|
|
// when the error is caused by a temporary situation, like a server being down
|
|
// (500s+) or the call being rate limited (429), this function checks if the
|
|
// statusCode is between the errors worth retrying.
|
|
func isCallRetriable(statusCode int) bool {
|
|
return statusCode > http.StatusInternalServerError &&
|
|
statusCode < http.StatusNetworkAuthenticationRequired ||
|
|
statusCode == http.StatusTooManyRequests
|
|
}
|
|
|
|
func (c *Client) calculateDelay(attempt int64) time.Duration {
|
|
if c.config.retryOptions.fixedDelay != 0 {
|
|
return c.config.retryOptions.fixedDelay
|
|
}
|
|
|
|
if attempt == 0 {
|
|
return 0
|
|
}
|
|
|
|
if attempt > c.config.retryOptions.maxValidAttempt {
|
|
return c.config.retryOptions.maxBackoffDelay
|
|
}
|
|
|
|
newDelay := c.config.retryOptions.delayBase << (attempt - 1)
|
|
if c.config.retryOptions.maxBackoffDelay != defaultMaxBackoffDelay &&
|
|
newDelay > c.config.retryOptions.maxBackoffDelay {
|
|
return c.config.retryOptions.maxBackoffDelay
|
|
}
|
|
|
|
return newDelay
|
|
}
|