Documentation
¶
Overview ¶
Package gotry provides an http.RoundTripper that adds retries with backoff, Retry-After handling, retry budgets and request hedging to outbound HTTP calls. It has no dependencies beyond the standard library.
Because it is a RoundTripper, existing call sites keep using client.Get and client.Do unchanged, and any SDK that accepts an *http.Client picks up the behaviour automatically.
client := &http.Client{
Transport: gotry.NewTransport(
http.DefaultTransport,
gotry.WithMaxAttempts(4),
gotry.WithBackoff(gotry.ExponentialJitter(100*time.Millisecond, 5*time.Second)),
gotry.WithBudget(0.1, 10),
gotry.WithHedging(50*time.Millisecond, 2),
),
}
resp, err := client.Get("https://api.example.com/users/1") // unchanged call site
// per-request override via context
ctx = gotry.WithPolicy(ctx, gotry.NoRetry())
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
resp, err = client.Do(req)
Index ¶
- Variables
- func WithPolicy(ctx context.Context, p Policy) context.Context
- type Attempt
- type Backoff
- type BackoffFunc
- type BreakerConfig
- type BreakerState
- type Classifier
- type Decision
- type Error
- type HedgeEvent
- type Hooks
- type Option
- func WithBackoff(b Backoff) Option
- func WithBudget(ratio float64, minPerSec int) Option
- func WithCircuitBreaker(cfg BreakerConfig) Option
- func WithClassifier(cl Classifier) Option
- func WithHedging(delay time.Duration, maxHedges int) Option
- func WithHooks(h Hooks) Option
- func WithIdempotencyKeyHeaders(names ...string) Option
- func WithMaxAttempts(n int) Option
- func WithMaxBufferedBody(n int64) Option
- func WithMaxRetryAfter(d time.Duration) Option
- func WithNonIdempotentRetries(allow bool) Option
- func WithPerAttemptTimeout(d time.Duration) Option
- func WithRetryableStatuses(codes ...int) Option
- type Policy
- type RetryEvent
- type Transport
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrCircuitOpen = errors.New("gotry: circuit open")
ErrCircuitOpen is returned when a host's circuit breaker rejects a request.
Functions ¶
func WithPolicy ¶
WithPolicy returns a context carrying a per-request Policy.
Example ¶
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"time"
"github.com/MrVSiK/gotry"
)
func main() {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
}))
defer srv.Close()
client := &http.Client{Transport: gotry.NewTransport(nil,
gotry.WithBackoff(gotry.Constant(time.Millisecond)))}
ctx := gotry.WithPolicy(context.Background(), gotry.NoRetry())
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL, nil)
resp, err := client.Do(req) // one attempt only: the 503 is returned as is
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close()
fmt.Println(resp.StatusCode)
}
Output: 503
Types ¶
type Backoff ¶
Backoff computes how long to wait before a retry.
retry is 1 for the first retry. prev is the previous delay, or 0 when there was none. Implementations must be safe for concurrent use.
func DecorrelatedJitter ¶
DecorrelatedJitter returns a Backoff that waits a random duration in [base, min(max, 3*prev)], so consecutive delays are decorrelated.
func Exponential ¶
Exponential returns a Backoff that waits base, 2*base, 4*base, ... capped at max.
func ExponentialJitter ¶
ExponentialJitter is like Exponential but picks a uniformly random delay in [0, exp) where exp is the capped exponential value ("full jitter").
type BackoffFunc ¶
BackoffFunc adapts a function to the Backoff interface.
type BreakerConfig ¶
BreakerConfig configures per-host circuit breakers. Zero fields take the defaults: FailureThreshold 5, OpenDuration 10s, HalfOpenProbes 1.
A half-open probe holds its slot until its single attempt completes, so a probe that hangs keeps the breaker half-open with that slot taken. Give requests a context deadline or set a per-attempt timeout when the breaker is enabled.
type BreakerState ¶
type BreakerState int
BreakerState is the state of a per-host circuit breaker.
const ( // Closed lets all requests through. Closed BreakerState = iota // Open rejects requests until OpenDuration has elapsed. Open // HalfOpen lets a limited number of probe requests through. HalfOpen )
func (BreakerState) String ¶
func (s BreakerState) String() string
type Classifier ¶
Classifier decides whether an attempt should be retried.
Example ¶
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"sync/atomic"
"time"
"github.com/MrVSiK/gotry"
)
func main() {
var hits atomic.Int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if hits.Add(1) == 1 {
w.Header().Set("X-Retryable", "true")
w.WriteHeader(http.StatusConflict)
}
}))
defer srv.Close()
var retryConflict gotry.Classifier = func(a gotry.Attempt) gotry.Decision {
if a.Response != nil && a.Response.Header.Get("X-Retryable") == "true" {
return gotry.Retry
}
return gotry.Default // defer to the built-in rules
}
client := &http.Client{Transport: gotry.NewTransport(nil,
gotry.WithClassifier(retryConflict),
gotry.WithBackoff(gotry.Constant(time.Millisecond)),
)}
resp, err := client.Get(srv.URL)
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close()
fmt.Println(resp.StatusCode)
}
Output: 200
type Decision ¶
type Decision int
Decision is a Classifier's verdict on an attempt.
func DefaultClassifier ¶
DefaultClassifier applies the built-in rules with the default retryable status set {502, 503, 504}, plus 429.
type Error ¶
type Error struct {
// Attempts is the number of attempts made.
Attempts int
// Err is the last attempt's error.
Err error
}
Error is returned when gotry gives up after two or more attempts with an error. When the final attempt produced a response, that response is returned with a nil error instead. Single-attempt errors are returned unwrapped.
type HedgeEvent ¶
HedgeEvent describes a hedged request that is about to be launched.
type Hooks ¶
type Hooks struct {
OnRetry func(RetryEvent)
OnHedge func(HedgeEvent)
OnBreakerStateChange func(host string, from, to BreakerState)
}
Hooks are optional observability callbacks. Every field may be nil.
Hooks run synchronously on the request goroutine, so they must be fast. Events carry no response, only the status code; the Request they carry is the caller's original request, which hooks must not modify. A panicking hook is not recovered: the panic propagates out of RoundTrip. Calls to OnBreakerStateChange are not strictly ordered under contention: concurrent requests may observe and report transitions out of order.
type Option ¶
type Option func(*config)
Option configures a Transport.
func WithBudget ¶
WithBudget configures the retry budget; ratio <= 0 disables it.
func WithCircuitBreaker ¶
func WithCircuitBreaker(cfg BreakerConfig) Option
WithCircuitBreaker enables per-host circuit breaking.
Example ¶
package main
import (
"errors"
"fmt"
"net/http"
"net/http/httptest"
"time"
"github.com/MrVSiK/gotry"
)
func main() {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
}))
defer srv.Close()
client := &http.Client{Transport: gotry.NewTransport(nil,
gotry.WithMaxAttempts(1),
gotry.WithCircuitBreaker(gotry.BreakerConfig{FailureThreshold: 2, OpenDuration: time.Minute}),
)}
for range 2 {
resp, err := client.Get(srv.URL)
if err != nil {
fmt.Println(err)
return
}
resp.Body.Close()
fmt.Println(resp.StatusCode)
}
_, err := client.Get(srv.URL)
fmt.Println(errors.Is(err, gotry.ErrCircuitOpen))
}
Output: 503 503 true
func WithClassifier ¶
func WithClassifier(cl Classifier) Option
WithClassifier sets a custom classifier consulted alongside the built-in one.
func WithHedging ¶
WithHedging enables hedged requests: after delay with no response, launch up to maxHedges extra attempts. Hedging is on only when both delay and maxHedges are positive. Requests with an Upgrade header are never hedged.
Example ¶
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"sync/atomic"
"time"
"github.com/MrVSiK/gotry"
)
func main() {
var hits atomic.Int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if hits.Add(1) == 1 {
<-r.Context().Done() // slow first attempt; cancelled once the hedge wins
}
}))
defer srv.Close()
client := &http.Client{Transport: gotry.NewTransport(nil,
gotry.WithHedging(20*time.Millisecond, 1))}
resp, err := client.Get(srv.URL)
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close()
fmt.Println(resp.StatusCode)
}
Output: 200
func WithIdempotencyKeyHeaders ¶
WithIdempotencyKeyHeaders sets the headers whose presence marks a request as safe to retry.
func WithMaxAttempts ¶
WithMaxAttempts sets the total attempts including the first; n < 1 means 1.
func WithMaxBufferedBody ¶
WithMaxBufferedBody sets the largest request body buffered for replay; n < 0 means 0.
func WithMaxRetryAfter ¶
WithMaxRetryAfter sets the largest Retry-After honoured; larger values make the transport give up.
func WithNonIdempotentRetries ¶
WithNonIdempotentRetries allows retrying non-idempotent requests.
func WithPerAttemptTimeout ¶
WithPerAttemptTimeout bounds each attempt; 0 means no limit.
func WithRetryableStatuses ¶
WithRetryableStatuses replaces the default retryable set {502, 503, 504}. 429 is handled separately.
type Policy ¶
type Policy struct {
MaxAttempts int
Backoff Backoff
PerAttemptTimeout time.Duration
Classifier Classifier
AllowNonIdempotent *bool // nil = inherit
HedgeDelay time.Duration // hedging needs a positive delay, here or from WithHedging
MaxHedges int // -1 = disable hedging for this request
}
Policy overrides transport settings for a single request. Zero-valued fields inherit the transport's setting. Budget, circuit breaker and hooks are transport-wide only.
type RetryEvent ¶
type RetryEvent struct {
Request *http.Request
Attempt int
Delay time.Duration
StatusCode int
Err error
RetryAfter bool
}
RetryEvent describes a retry that is about to happen.
type Transport ¶
type Transport struct {
// contains filtered or unexported fields
}
Transport is an http.RoundTripper that retries failed attempts. It is safe for concurrent use.
func NewTransport ¶
func NewTransport(base http.RoundTripper, opts ...Option) *Transport
NewTransport wraps base, which defaults to http.DefaultTransport when nil.
Example ¶
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"sync/atomic"
"time"
"github.com/MrVSiK/gotry"
)
func main() {
var hits atomic.Int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if hits.Add(1) == 1 {
w.WriteHeader(http.StatusServiceUnavailable)
}
}))
defer srv.Close()
client := &http.Client{Transport: gotry.NewTransport(nil,
gotry.WithMaxAttempts(3),
gotry.WithBackoff(gotry.Constant(time.Millisecond)),
)}
resp, err := client.Get(srv.URL)
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close()
fmt.Println(resp.StatusCode)
}
Output: 200
func (*Transport) CloseIdleConnections ¶
func (t *Transport) CloseIdleConnections()
CloseIdleConnections forwards to the base RoundTripper when it supports it.