Documentation
¶
Overview ¶
Package kretry provides retry-with-backoff for flaky operations, with jitter and cancellation built in.
Two things worth knowing that aren't separate API surface because they're already covered by what's here:
- A per-attempt timeout, distinct from the overall ctx passed to Do, is just context.WithTimeout(ctx, ...) inside f — f already gets ctx and can derive its own bound from it.
- Retrying at more than one layer of a call chain (client, load balancer, gateway all retrying the same request) multiplies load on a struggling downstream service instead of backing off it — retry at one layer, or share a retry budget across them.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Do ¶
Do calls f, retrying it with b's delays for as long as f returns an error wrapped with RetryableError. It returns nil on the first success; the most recent error once f returns a non-retryable error or b stops retrying; or ctx's error if ctx is canceled before an attempt or during a retry wait (waiting never blocks past ctx's cancellation). If f panics, the panic is not recovered — it propagates to Do's caller, same as kevent.Bus.Publish: a panic means something in f is actually broken, not a condition to retry past.
Example ¶
package main
import (
"context"
"fmt"
"time"
"github.com/Trxncoo/kinetic/pkg/kretry"
)
func main() {
backoff := kretry.NewExponential(time.Millisecond).
WithMaxRetries(5).
WithCappedDuration(10 * time.Millisecond).
WithFullJitter()
attempts := 0
err := kretry.Do(context.Background(), backoff, func(ctx context.Context) error {
attempts++
if attempts < 3 {
return kretry.RetryableError(fmt.Errorf("attempt %d failed", attempts))
}
fmt.Println("succeeded on attempt", attempts)
return nil
})
if err != nil {
fmt.Println("failed:", err)
}
}
Output: succeeded on attempt 3
func RetryableError ¶
RetryableError marks err as worth retrying. Do and DoValue treat any error NOT wrapped this way as permanent and stop immediately — opt-in to retry, not opt-out, so a caller can't accidentally retry a non-idempotent or genuinely permanent failure just because they forgot to special-case it. errors.Is and errors.As still reach the original err through the wrapper.
Example ¶
package main
import (
"context"
"errors"
"fmt"
"time"
"github.com/Trxncoo/kinetic/pkg/kretry"
)
func main() {
backoff := kretry.NewConstant(time.Millisecond).WithMaxRetries(3)
attempts := 0
err := kretry.Do(context.Background(), backoff, func(ctx context.Context) error {
attempts++
// A 4xx-shaped error: not wrapped with RetryableError, so Do
// stops immediately instead of retrying it.
return errors.New("400 bad request")
})
fmt.Println("attempts:", attempts)
fmt.Println("error:", err)
}
Output: attempts: 1 error: 400 bad request
Types ¶
type Backoff ¶
type Backoff struct {
// contains filtered or unexported fields
}
Backoff computes the delay before each retry attempt. It's a concrete struct, not an interface — unlike kevent.Bus or kcache.Cache, there's no plausible second "backend" to swap in here, just algorithm composition, so a closure-wrapped struct with chainable With* methods gets simpler call sites for free.
A Backoff is stateful: each instance (and each link in a chain) closes over its own attempt count. Build one fresh per Do/DoValue call — it's not safe to share or reuse across concurrent retry loops.
func NewConstant ¶
NewConstant returns a Backoff with a fixed delay that never stops on its own — pair it with WithMaxRetries if you want a bound.
func NewExponential ¶
NewExponential returns a Backoff that doubles the delay on every call: base, 2*base, 4*base, and so on. Like NewConstant, it never stops on its own — pair it with WithCappedDuration too, not just WithMaxRetries: time.Duration is an int64 count of nanoseconds, and doubling indefinitely will eventually overflow it (~63 doublings from 1ms) if nothing caps the delay first. WithFullJitter guards against the resulting non-positive delay rather than panicking, but backoff that's silently stopped backing off is still a real degradation, not a safe substitute for capping.
func (Backoff) WithCappedDuration ¶
WithCappedDuration clamps every delay from b to at most limit.
func (Backoff) WithFullJitter ¶
WithFullJitter replaces every delay from b with a random duration in [0, delay) — AWS's "Full Jitter" formula, which their own simulations found gives the lowest server load and lowest completion time among the common jitter strategies. Put this last in a chain, after any WithCappedDuration, so the cap bounds what gets jittered.
func (Backoff) WithMaxRetries ¶
WithMaxRetries allows at most n retries through b, not counting the first attempt — paired with Do, WithMaxRetries(2) means up to 2 retries after the first attempt fails, 3 total calls to the retried function.