Extending recourse¶
This document describes the extension pattern used by recourse and how to plug in custom behavior.
recourse is designed so the core stays stdlib-only and most behavior is selected by policy and resolved via registries.
Extension points¶
recourse supports (or will support) extension via these interfaces:
- Classifiers (
classify.Classifier): decide whether an attempt outcome is success, retryable, non-retryable, or abort. - Budgets (
budget.Budget): gate attempts to prevent retry/hedge storms. - Hedge triggers (
hedge.HedgeTrigger): decide when to spawn hedged attempts. - Observers (
observe.Observer): receive structured attempt/timeline events.
Registries¶
Registries are thread-safe maps from a low-cardinality name (string) to an implementation. Policies typically refer to implementations by name.
At a high level:
- Create a registry (
classify.NewRegistry(),budget.NewRegistry()). - Register your implementation under a name (e.g.
"my_classifier"). - Configure your executor to use that registry.
- Reference the name from policy (
Retry.ClassifierName, etc.).
Writing a custom classifier¶
Implement:
Classify(value any, err error) classify.Outcome
Guidelines:
- Prefer conservative behavior: if you can’t determine retry safety, return
OutcomeNonRetryable(orOutcomeAbortfor cancellation). - Avoid high-cardinality attributes.
- Treat type mismatches as configuration errors (not retryable).
Writing a custom budget¶
Implement:
AllowAttempt(ctx, key, attemptIdx, kind, ref) budget.Decision
Guidelines:
- Keep
AllowAttemptfast and concurrency-safe. - Use
ref.Costto support weighted backpressure if applicable. - If you return a
Decision.Release, it must be safe to call exactly once.
Budget decisions surface on observe.AttemptRecord as BudgetAllowed and BudgetReason. Standard reasons are:
"no_budget": no budget configured for the call."budget_not_found": policy referenced a budget name not in the registry."budget_denied": budget denied the attempt."panic_in_budget": budget panicked andRecoverPanicsconverted it to a denial.
Wiring budgets¶
Budgets are selected by policy (Retry.Budget.Name) and resolved via the executor’s Budgets registry:
budgets := budget.NewRegistry()
if err := budgets.Register("tb", budget.NewTokenBucketBudget(100, 50)); err != nil { // capacity=100, refill=50 tokens/sec
panic(err)
}
exec := retry.NewExecutor(
retry.WithProvider(provider),
retry.WithBudgetRegistry(budgets),
)
// In policy:
pol.Retry.Budget = policy.BudgetRef{Name: "tb", Cost: 1}
By default, a missing budget name is fail-closed (MissingBudgetMode=retry.FailureDeny) and records "budget_not_found" in the timeline. Use retry.WithMissingBudgetMode(retry.FailureAllowUnsafe) to opt-in to fail-open.
Writing a custom hedge trigger¶
Implement:
ShouldSpawnHedge(state hedge.HedgeState) (should bool, nextCheckIn time.Duration)
Guidelines:
- Return a sensible
nextCheckInto avoid tight polling. - Respect
MaxHedgesand don’t spawn multiple hedges in a single evaluation tick.
Versioning note¶
This extension surface is stable for the v1.x series.