mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
It includes the work over the state store, the PRC server, the HTTP server, the go API package and the CLI's command. To read more on the actuall functionality, refer to the RFCs [NMD-178] Locking with Nomad Variables and [NMD-179] Leader election using locking mechanism for the Autoscaler.
125 lines
3.8 KiB
Go
125 lines
3.8 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 := time.Duration(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.Now().Sub(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
|
|
}
|