retry

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 1, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

Documentation

Overview

Package retry provides retry policy, backoff math, and per-policy exit-code classification — the data primitive that engine.RunWithRetry composes with a Dispatcher + Clock into the retry loop. No engine / state / clock deps here; the package is leaf-ward by design (Phase 3 will reuse it for try/catch integration without rewiring).

Index

Constants

View Source
const MaxHonoredRetryAfter = 5 * time.Minute

MaxHonoredRetryAfter caps how long a single server-supplied Retry-After hint can stretch one sleep. Honoring a hint verbatim is correct (the server knows its own reset window) up to a point — a buggy proxy or upstream that returns "retry-after: 86400" must not be allowed to hang the pipeline for a day. Real Anthropic 429/529 hints are seconds to low minutes; 5m is a generous ceiling that rides out a true overload while bounding the worst case.

Variables

View Source
var Default = Policy{
	Attempts:              8,
	Backoff:               BackoffExp,
	Initial:               time.Second,
	Max:                   60 * time.Second,
	NonRetryableExitCodes: []int{78},
}

Default is the spec §6 default policy. The 78 sentinel is EX_CONFIG from sysexits.h — the conventional "configuration error, retry won't help" code. Treat as read-only — Merge deep-copies the NonRetryableExitCodes slice so callers can't accidentally mutate the shared default via index assignment (Revision #6 narrowed the var-vs-func footgun to the seam where it matters).

Attempts is 8 (not 3) so transient provider faults — Anthropic 429 rate_limit_error, 529 overloaded_error, 5xx, connection drops — ride out a normal rate-limit window or overload without failing the pipeline. With the exp curve capped at Max (1,2,4,8,16,32,60 → ~123s of backoff across 7 sleeps) plus any honored Retry-After hint, 8 attempts covers the common outage; a genuinely permanent fault (bad key, invalid_request, quota exhausted) is classified permanent_failure upstream and never consumes the budget.

Functions

This section is empty.

Types

type BackoffKind

type BackoffKind int

BackoffKind selects which curve BackoffFor uses to compute the sleep before attempt n. The string form is what ir.RetryPolicy.Backoff carries (spec §6 — "exp" / "linear" / ""); the typed constants here are the engine- internal form. Unknown strings are an error at Merge time (Revision #8) — slice 1.4 doesn't yet validate this field, so the runtime is the line of defense.

const (
	BackoffExp    BackoffKind = iota // default — doubles each attempt, capped at Max
	BackoffLinear                    // adds Initial each attempt, capped at Max
	BackoffNone                      // always 0 (e.g. for tests where backoff would slow things down)
)

type Policy

type Policy struct {
	Attempts              int
	Backoff               BackoffKind
	Initial               time.Duration
	Max                   time.Duration
	NonRetryableExitCodes []int
}

Policy is the merged-down retry configuration the engine actually consults at dispatch time. ir.RetryPolicy is the wire/IR form (omitempty fields, string backoff name); Merge collapses default + per-step into this typed struct.

func Merge

func Merge(def Policy, override *ir.RetryPolicy) (Policy, error)

Merge collapses def + an optional per-step override into a single Policy. Per spec §6 + plan Design question 6: per-step wins field-by-field; slice fields replace entirely (the field IS the slice, not a partial overlay).

A nil override still does work — it returns a deep-copy of def so the caller's returned Policy has its own NonRetryableExitCodes slice (Revision #6: protects against `p, _ := Merge(Default, nil); p.NonRetryableExitCodes[0] = 999` mutating Default for the next caller). A non-nil override's zero-valued fields (Attempts == 0, Initial == nil, etc.) leave the default in place — the override semantic is "set if non-zero" so the IR's `omitempty` shape doesn't silently zero a field the author didn't write.

Returns an error if override.Backoff is an unknown string (Revision #8) — callers MUST handle this; slice 2.5's interpreter halts the run with the error message at run-start, before the first dispatch.

func (Policy) BackoffFor

func (p Policy) BackoffFor(attempt int) time.Duration

BackoffFor returns the sleep duration to apply BEFORE attempt n (1-based). Attempt 1 always returns 0 (no preceding sleep). The curve is selected by p.Backoff and capped at p.Max:

  • Exp: p.Initial × 2^(n-2) (attempt 2 = Initial, attempt 3 = 2×Initial, …)
  • Linear: p.Initial × (n-1) (attempt 2 = Initial, attempt 3 = 2×Initial, …)
  • None: 0 unconditionally

The engine's retry loop calls clk.Sleep(p.BackoffFor(attempt)) between dispatch calls.

func (Policy) EffectiveBackoff

func (p Policy) EffectiveBackoff(attempt int, retryAfter time.Duration, seed string) time.Duration

EffectiveBackoff is the sleep the engine retry loop applies before an attempt (1-based). It composes three things, in order:

  1. the curve (BackoffFor) — exp/linear/none, capped at Max;
  2. an optional server retryAfter hint (from a Retry-After header or a rate-limit reset time, surfaced via DispatchResult.RetryAfter) — when it exceeds the curve the loop waits the longer of the two, so a 1s curve doesn't burn attempts hammering a window the server said resets in 30s. The honored hint is capped at MaxHonoredRetryAfter;
  3. deterministic additive jitter in [0, jitterFraction·base) keyed on (seed, attempt), so parallel retries against the same window decorrelate.

seed is the node addressing path (engine NodeIntent.Path) — a pure function of the node graph (CLAUDE.md "node addressing is one pure function"), so the jitter is deterministic and resume-safe: it perturbs only the sleep duration, never the step inputs/outputs, and sleep durations are not journaled. No time.Now()/rand — the determinism invariant holds.

Jitter is additive-only, so the result is always >= max(curve, hint): we never undershoot a server's Retry-After.

func (Policy) IsRetryableExit

func (p Policy) IsRetryableExit(exitCode int) bool

IsRetryableExit reports whether a nonzero exit code triggers a retry under this policy. exit==0 returns false (success — nothing to retry). exit in NonRetryableExitCodes returns false (permanent — declared by the author). Everything else returns true (generic failure → retry per policy).

The engine's classifier (engine.ClassifyOutcome) reads this to decide retryable_failure vs permanent_failure per spec §6.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL