Getting started¶
Install¶
The simplest path (facade)¶
Use recourse.Do / recourse.DoValue for the "zero config" path.
If you do not call recourse.Init, recourse lazily creates the default executor and uses policy.DefaultPolicyFor(key).
That default policy is bounded and retries transient errors, so a first run can show the retry path and the timeline:
package main
import (
"context"
"errors"
"fmt"
"github.com/aponysus/recourse/observe"
"github.com/aponysus/recourse/recourse"
)
type User struct{ ID string }
func main() {
ctx, capture := observe.RecordTimeline(context.Background())
attempts := 0
user, err := recourse.DoValue[User](ctx, "user-service.GetUser", func(ctx context.Context) (User, error) {
attempts++
if attempts == 1 {
return User{}, errors.New("temporary upstream error")
}
return User{ID: "123"}, nil
})
fmt.Printf("user=%s err=%v attempts=%d\n", user.ID, err, attempts)
for _, attempt := range capture.Timeline().Attempts {
fmt.Printf("attempt=%d reason=%s err=%v\n", attempt.Attempt, attempt.Outcome.Reason, attempt.Err)
}
}
Keys must be low-cardinality (stable across requests). Good: "payments.Charge". Bad: "GET /users/123".
Getting a timeline¶
// Start timeline capture
ctx, capture := observe.RecordTimeline(ctx)
user, err := recourse.DoValue(ctx, "user-service.GetUser", op)
// Access timeline (safe even after function returns)
tl := capture.Timeline()
for _, a := range tl.Attempts {
// inspect outcome, error, backoff, budget gating, timings
}
Standard usage (custom defaults)¶
For most applications, you want standard defaults (auto classification, an unlimited budget, and latency-based hedging triggers) but with your own instance.
// Create a pre-configured executor
exec := retry.NewDefaultExecutor()
// Use it
user, err := retry.DoValue[User](ctx, exec, "user-service.GetUser", op)
NewDefaultExecutor comes with:
- Classifiers:
AutoClassifier(HTTPError-aware; otherwise usesAlwaysRetryOnError) - Budgets:
UnlimitedBudgetregistered as"unlimited" - Hedging:
FixedDelayandLatency(p90,p95,p99) triggers registered
Explicit wiring (advanced)¶
If you want to supply policies, classifiers, and budgets explicitly, build a retry.Executor and either use it directly or initialize the facade:
budgets := budget.NewRegistry()
if err := budgets.Register("global", budget.NewTokenBucketBudget(100, 50)); err != nil { // capacity=100, refill=50 tokens/sec
panic(err)
}
pol := policy.New("user-service.GetUser",
policy.MaxAttempts(3),
policy.Classifier(classify.ClassifierHTTP),
policy.Budget("global"),
// Enable Hedging (fixed delay)
policy.EnableHedging(),
policy.HedgeDelay(10*time.Millisecond),
policy.HedgeMaxAttempts(2),
)
// Enable Circuit Breaking
pol.Circuit = policy.CircuitPolicy{
Enabled: true,
Threshold: 5,
Cooldown: 10 * time.Second,
}
key := pol.Key
provider := &controlplane.StaticProvider{
Policies: map[policy.PolicyKey]policy.EffectivePolicy{
pol.Key: pol,
},
}
exec := retry.NewExecutor(
retry.WithProvider(provider),
retry.WithBudgetRegistry(budgets),
)
recourse.Init(exec)
user, err := recourse.DoValue[User](ctx, key.String(), op)
// Or call retry directly when you want to pass the executor explicitly:
// user, err := retry.DoValue[User](ctx, exec, key, op)