This commit introduces a resilient, configurable retry mechanism for network requests. Key changes include: - A new `pkg/retry` package with a custom `http.Transport` that implements exponential backoff and jitter. - Integration of the retry transport into the `website`, `pwa`, and `github` packages to handle transient network failures gracefully. - New persistent CLI flags (`--retries`, `--retry-backoff`, `--retry-max`, `--retry-jitter`, `--no-retry`) to allow user configuration of the retry behavior. - The flag-handling logic has been moved to a `PersistentPreRun` function to ensure user-provided values are parsed correctly. - A basic retry mechanism has been added to the `vcs` package for git clone operations. - Added unit tests for the retry transport. This work is in progress, with the next steps being to implement support for the `Retry-After` header and unify the VCS retry logic with the global configuration. Co-authored-by: Snider <631881+Snider@users.noreply.github.com>
99 lines
2.5 KiB
Go
99 lines
2.5 KiB
Go
package retry
|
|
|
|
import (
|
|
"math/rand"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// Backoff is a time.Duration that represents the backoff strategy.
|
|
type Backoff time.Duration
|
|
|
|
// Exponential returns a new Backoff duration that is the current duration
|
|
// multiplied by 2.
|
|
func (b Backoff) Exponential() Backoff {
|
|
return Backoff(time.Duration(b) * 2)
|
|
}
|
|
|
|
// Jitter returns a new Backoff duration with a random jitter added.
|
|
func (b Backoff) Jitter(factor float64) Backoff {
|
|
if factor <= 0 {
|
|
return b
|
|
}
|
|
jitter := time.Duration(rand.Float64() * factor * float64(b))
|
|
return Backoff(time.Duration(b) + jitter)
|
|
}
|
|
|
|
// Transport is an http.RoundTripper that automatically retries requests.
|
|
type Transport struct {
|
|
// Transport is the underlying http.RoundTripper to use for requests.
|
|
// If nil, http.DefaultTransport is used.
|
|
Transport http.RoundTripper
|
|
|
|
// Retries is the maximum number of retries to attempt.
|
|
Retries int
|
|
|
|
// InitialBackoff is the initial backoff duration.
|
|
InitialBackoff time.Duration
|
|
|
|
// MaxBackoff is the maximum backoff duration.
|
|
MaxBackoff time.Duration
|
|
|
|
// Jitter is the jitter factor to apply to backoff durations.
|
|
Jitter float64
|
|
}
|
|
|
|
// NewTransport creates a new Transport with default values.
|
|
func NewTransport() *Transport {
|
|
return &Transport{
|
|
Transport: http.DefaultTransport,
|
|
Retries: 3,
|
|
InitialBackoff: 1 * time.Second,
|
|
MaxBackoff: 30 * time.Second,
|
|
Jitter: 0.1,
|
|
}
|
|
}
|
|
|
|
// RoundTrip implements the http.RoundTripper interface.
|
|
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
var resp *http.Response
|
|
var err error
|
|
var backoff = Backoff(t.InitialBackoff)
|
|
|
|
for i := 0; i < t.Retries; i++ {
|
|
resp, err = t.transport().RoundTrip(req)
|
|
if err == nil && resp.StatusCode < 500 {
|
|
return resp, err
|
|
}
|
|
|
|
if i < t.Retries-1 {
|
|
time.Sleep(time.Duration(backoff))
|
|
backoff = backoff.Exponential().Jitter(t.Jitter)
|
|
if backoff > Backoff(t.MaxBackoff) {
|
|
backoff = Backoff(t.MaxBackoff)
|
|
}
|
|
}
|
|
}
|
|
return resp, err
|
|
}
|
|
|
|
func (t *Transport) transport() http.RoundTripper {
|
|
if t.Transport != nil {
|
|
return t.Transport
|
|
}
|
|
return http.DefaultTransport
|
|
}
|
|
|
|
// NewClient returns a new http.Client that uses the Transport.
|
|
func NewClient(transport *Transport) *http.Client {
|
|
return &http.Client{
|
|
Transport: transport,
|
|
}
|
|
}
|
|
|
|
var (
|
|
// DefaultTransport is the default transport with retry logic.
|
|
DefaultTransport = NewTransport()
|
|
// DefaultClient is the default client that uses the default transport.
|
|
DefaultClient = NewClient(DefaultTransport)
|
|
)
|