resilience

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 8 Imported by: 0

README

resilience

CI CodeQL Coverage Mutation Documentation Go Reference Release Go License

resilience is the small composition foundation for the focused resilience libraries in golib. It provides deterministic generic policy composition, typed outcomes, caller-owned total deadlines, bounded observation, and one process-local work budget shared by retry and hedge. It does not hide a default policy stack or implement focused resilience algorithms.

Quick start

metadata, err := resilience.NewMetadata(
    requestID,
    "app.search_postal_codes",
    "postal:FI",
)
if err != nil {
    return err
}

executor, err := resilience.NewExecutor[Response](
    retryPolicy,   // logical scope
    breakerPolicy, // attempt scope
    bulkheadPolicy,
)
if err != nil {
    return err
}

result := executor.Execute(ctx, metadata,
    func(ctx context.Context, attempt resilience.Attempt) (Response, error) {
        return client.Search(ctx, request)
    },
)
return result.Err

Policies are supplied outer-to-inner. All logical policies must precede all attempt policies. This makes retries or hedges invoke the complete attempt stack for every physical attempt instead of accidentally applying an attempt-scoped policy only once.

Shared work budget

Retry and hedge must use the same WorkBudgetScope for a logical call:

budget, err := resilience.NewBudget(resilience.BudgetConfig{
    MaxResources:                 1_024,
    MaxAdditionalPerExecution:    2,
    MaxConcurrentAdditional:      100,
    MaxAdditionalPerWindow:       1_000,
    AdditionalWindow:             time.Minute,
    PermitTTL:                    30 * time.Second,
    Clock:                        clock,
})
scope, budgetContext, err := budget.Start(ctx, metadata)
defer scope.Close()

result := executor.Execute(budgetContext, metadata, operation)

The terminal stage admits every physical attempt centrally. Original work is recorded once; retry and hedge attempts draw from the same per-execution, concurrent, and rolling-window limits. A policy cannot classify budget denial as downstream failure because it receives OutcomeLocalRejection and an error matching ErrBudgetRejected.

Focused executors that do not run through Executor use AdmitAttempt before starting physical work. It allocates a unique ordinal across nested retry and hedge policies, validates parent lineage, admits the work, and returns a context carrying the current attempt. AttemptFromContext lets an inner policy reuse that current attempt instead of accounting for it twice.

The built-in budget is process-local. N additional permits on each of R pods allow up to N * R concurrent additional attempts. Use a separate, explicit distributed implementation of WorkBudget only when cluster-wide coordination is actually required.

Cancellation and ownership

The caller context is the total execution boundary. Policies may pass a shorter child context but cannot extend or detach from the caller deadline. The executor does not race operations against timers and does not create a goroutine to invoke synchronous work. If an operation ignores cancellation, the call remains blocked until that operation returns and a successful return is reported honestly.

Operation panics release acquired work permits, emit terminal observer events, and preserve the original panic. Observer panics are recovered after state changes. Bounded observer events are delivered synchronously after the outcome and accounting settle, outside policy locks. A slow observer delays return to the caller but cannot consume the operation deadline or change its result.

Outcomes

The common taxonomy is:

  • OutcomeSuccess;
  • OutcomeOperationFailure;
  • OutcomeLocalRejection;
  • OutcomeCancellation;
  • OutcomeDeadline;
  • OutcomeIgnored;
  • OutcomePolicyFailure.

Constructors preserve typed values and causes. Errors remain usable through errors.Is and errors.As; error strings contain only bounded policy, stage, and reason identifiers.

Diagnostics

Plain execution retains no timeline and currently has a zero-allocation fast path. Enable bounded events explicitly with WithTimeline or WithObserver. Events never retain operation values, arbitrary errors, context values, URLs, credentials, tenant IDs, or caller-provided maps.

Documentation

Ecosystem

Use the Golib documentation portal to choose companion packages, supported stacks, recipes, and operations guidance.

Documentation

Overview

Package resilience composes explicit synchronous resilience policies and owns process-local retry-plus-hedge amplification accounting.

The package deliberately does not implement retry, hedge, circuit-breaker, rate-limit, timeout, fallback, bulkhead, semaphore, or cache algorithms. Focused packages implement those decisions and adapt them through Policy, Stage, Execution, and WorkBudget.

Policies are ordered outer-to-inner. Logical policies must precede attempt policies so a logical policy can invoke the attempt stack repeatedly while each physical attempt remains independently observable and budgeted. Execution is synchronous and never moves an operation into a goroutine.

Index

Examples

Constants

View Source
const MaxIdentityLength = 128

MaxIdentityLength bounds identifiers retained by diagnostics and budgets.

Variables

View Source
var (
	// ErrBudgetRejected identifies local work-amplification denial.
	ErrBudgetRejected = errors.New("resilience: work budget rejected")
	// ErrBudgetClosed identifies use after a logical budget scope closed.
	ErrBudgetClosed = errors.New("resilience: budget scope closed")
	// ErrBudgetScopeMismatch identifies use of a scope for different metadata.
	ErrBudgetScopeMismatch = errors.New("resilience: budget scope mismatch")
	// ErrBudgetAlreadyAttached identifies nested scope creation on a budget context.
	ErrBudgetAlreadyAttached = errors.New("resilience: budget already attached")
	// ErrBudgetScopeRequired identifies work admission without a shared scope.
	ErrBudgetScopeRequired = errors.New("resilience: budget scope required")
	// ErrPermitCompleted identifies duplicate permit completion.
	ErrPermitCompleted = errors.New("resilience: permit already completed")
	// ErrPermitExpired identifies completion after abandoned-capacity recovery.
	ErrPermitExpired = errors.New("resilience: permit expired")
)
View Source
var (
	// ErrInvalidComposition identifies an executor policy configuration error.
	ErrInvalidComposition = errors.New("resilience: invalid composition")
	// ErrInvalidMetadata identifies missing or unbounded execution metadata.
	ErrInvalidMetadata = errors.New("resilience: invalid metadata")
	// ErrInvalidAttempt identifies inconsistent physical-attempt metadata.
	ErrInvalidAttempt = errors.New("resilience: invalid attempt")
	// ErrNilOperation identifies an execution without an operation.
	ErrNilOperation = errors.New("resilience: nil operation")
	// ErrLocalRejection identifies local policy denial without downstream work.
	ErrLocalRejection = errors.New("resilience: local rejection")
	// ErrIgnored identifies work deliberately omitted by an owning policy.
	ErrIgnored = errors.New("resilience: ignored")
	// ErrPolicyFailure identifies failure in policy logic rather than the operation.
	ErrPolicyFailure = errors.New("resilience: policy failure")
)

Functions

func AdmitAttempt

func AdmitAttempt(ctx context.Context, origin AttemptOrigin, parent uint64, startedAt time.Time) (context.Context, Attempt, Permit, error)

AdmitAttempt allocates unique physical-attempt lineage, admits it through the attached shared budget, and returns a context carrying that attempt.

func WithBudgetScope

func WithBudgetScope(ctx context.Context, scope WorkBudgetScope) (context.Context, error)

WithBudgetScope attaches a custom budget scope without replacing an existing owner.

Types

type Attempt

type Attempt struct {
	Ordinal       uint64
	Origin        AttemptOrigin
	ParentOrdinal uint64
	StartedAt     time.Time
}

Attempt describes one physical invocation within a logical execution.

func AttemptFromContext

func AttemptFromContext(ctx context.Context) (Attempt, bool)

AttemptFromContext returns the physical attempt currently invoking work.

func NewAttempt

func NewAttempt(ordinal uint64, origin AttemptOrigin, parent uint64, startedAt time.Time) (Attempt, error)

NewAttempt validates stable physical-attempt lineage.

type AttemptOrigin

type AttemptOrigin string

AttemptOrigin identifies why physical work exists.

const (
	// OriginOriginal identifies the first caller-requested operation.
	OriginOriginal AttemptOrigin = "original"
	// OriginRetry identifies sequential repeated work after a failure.
	OriginRetry AttemptOrigin = "retry"
	// OriginHedge identifies concurrent duplicate work for tail latency.
	OriginHedge AttemptOrigin = "hedge"
)

type Budget

type Budget struct {
	// contains filtered or unexported fields
}

Budget owns process-local accounting shared by retry and hedge attempts.

Example
clock := fixedExampleClock{}
budget, _ := resilience.NewBudget(resilience.BudgetConfig{
	MaxResources: 16, MaxAdditionalPerExecution: 2,
	MaxConcurrentAdditional: 1, MaxAdditionalPerWindow: 8,
	AdditionalWindow: exampleDuration, PermitTTL: exampleDuration, Clock: clock,
})
metadata, _ := resilience.NewMetadata("request-1", "postal.lookup", "postal:FI")
scope, ctx, _ := budget.Start(context.Background(), metadata)
attempt, _ := resilience.NewAttempt(1, resilience.OriginOriginal, 0, clock.Now())
permit, _ := scope.Acquire(ctx, attempt)
_ = permit.Complete()
fmt.Println(scope.Snapshot().AdditionalAdmitted)
_ = scope.Close()
Output:
0

func NewBudget

func NewBudget(config BudgetConfig) (*Budget, error)

NewBudget validates finite limits and constructs an empty process-local budget.

func (*Budget) Start

func (budget *Budget) Start(ctx context.Context, metadata Metadata) (WorkBudgetScope, context.Context, error)

Start creates one logical scope and attaches it to a derived context.

type BudgetConfig

type BudgetConfig struct {
	MaxResources              int
	MaxAdditionalPerExecution uint64
	MaxConcurrentAdditional   uint64
	MaxAdditionalPerWindow    uint64
	AdditionalWindow          time.Duration
	PermitTTL                 time.Duration
	Clock                     Clock
}

BudgetConfig defines finite process-local amplification limits.

type BudgetRejectionError

type BudgetRejectionError struct {
	Reason   RejectionReason
	Snapshot BudgetSnapshot
}

BudgetRejectionError reports local denial without classifying it as downstream failure.

func (*BudgetRejectionError) Error

func (err *BudgetRejectionError) Error() string

func (*BudgetRejectionError) Unwrap

func (err *BudgetRejectionError) Unwrap() error

type BudgetScope

type BudgetScope struct {
	// contains filtered or unexported fields
}

BudgetScope is the single retry-plus-hedge accounting owner for one logical execution.

func (*BudgetScope) Acquire

func (scope *BudgetScope) Acquire(ctx context.Context, attempt Attempt) (Permit, error)

Acquire atomically admits original, retry, or hedge work without waiting.

func (*BudgetScope) Close

func (scope *BudgetScope) Close() error

Close rejects new work and releases resource identity after active permits settle.

func (*BudgetScope) Matches

func (scope *BudgetScope) Matches(metadata Metadata) bool

Matches reports whether this scope owns the supplied immutable metadata.

func (*BudgetScope) Snapshot

func (scope *BudgetScope) Snapshot() BudgetSnapshot

Snapshot reaps expired permits and returns bounded accounting state.

type BudgetSnapshot

type BudgetSnapshot struct {
	LogicalID          string
	Resource           string
	AdditionalAdmitted uint64
	AdditionalActive   uint64
	AdditionalRecent   uint64
	Closed             bool
}

BudgetSnapshot is an immutable bounded accounting view.

type Clock

type Clock interface {
	Now() time.Time
}

Clock permits deterministic attempt and event timestamps.

type ConfigurationError

type ConfigurationError struct {
	Kind   error
	Field  string
	Reason string
}

ConfigurationError identifies a public configuration field and safe reason.

func (*ConfigurationError) Error

func (err *ConfigurationError) Error() string

func (*ConfigurationError) Unwrap

func (err *ConfigurationError) Unwrap() error

type Event

type Event struct {
	Kind      EventKind
	Policy    PolicyID
	Reason    string
	LogicalID string
	Attempt   Attempt
	At        time.Time
}

Event contains only bounded metadata and never retains operation values.

type EventKind

type EventKind string

EventKind identifies a bounded execution lifecycle event.

const (
	// EventExecutionStarted identifies logical execution admission.
	EventExecutionStarted EventKind = "execution_started"
	// EventPolicyEntered identifies entry into one composed policy.
	EventPolicyEntered EventKind = "policy_entered"
	// EventWorkAdmitted identifies budget admission of physical work.
	EventWorkAdmitted EventKind = "work_admitted"
	// EventWorkRejected identifies local budget rejection.
	EventWorkRejected EventKind = "work_rejected"
	// EventAttemptStarted identifies invocation of physical work.
	EventAttemptStarted EventKind = "attempt_started"
	// EventAttemptCompleted identifies return from physical work.
	EventAttemptCompleted EventKind = "attempt_completed"
	// EventExecutionCanceled identifies caller cancellation of an execution.
	EventExecutionCanceled EventKind = "execution_canceled"
	// EventExecutionCompleted identifies the terminal logical outcome.
	EventExecutionCompleted EventKind = "execution_completed"
)

type Execution

type Execution struct {
	Metadata Metadata
	Attempt  Attempt
	// contains filtered or unexported fields
}

Execution is immutable call metadata passed through policy stages.

func (Execution) Emit

func (execution Execution) Emit(kind EventKind, policy PolicyID, reason string)

Emit records bounded diagnostic metadata without retaining results or errors.

func (Execution) WithAttempt

func (execution Execution) WithAttempt(attempt Attempt) (Execution, error)

WithAttempt returns an execution copy for validated additional physical work.

type Executor

type Executor[T any] struct {
	// contains filtered or unexported fields
}

Executor is an immutable, reusable policy composition.

Example
package main

import (
	"context"
	"fmt"

	"github.com/faustbrian/go-resilience"
)

func main() {
	metadata, _ := resilience.NewMetadata("request-1", "postal.lookup", "postal:FI")
	executor, _ := resilience.NewExecutor[string]()
	result := executor.Execute(context.Background(), metadata,
		func(_ context.Context, attempt resilience.Attempt) (string, error) {
			return fmt.Sprintf("attempt-%d", attempt.Ordinal), nil
		},
	)
	fmt.Println(result.Value, result.Outcome.Kind)
}
Output:
attempt-1 success

func NewExecutor

func NewExecutor[T any](policies ...Policy[T]) (Executor[T], error)

NewExecutor validates policies and composes them in outer-to-inner order.

func (Executor[T]) Execute

func (executor Executor[T]) Execute(ctx context.Context, metadata Metadata, operation Operation[T]) (result Result[T])

Execute invokes synchronous work without creating an operation goroutine.

func (Executor[T]) Policies

func (executor Executor[T]) Policies() []PolicyDescriptor

Policies returns a caller-owned copy in outer-to-inner order.

func (Executor[T]) WithClock

func (executor Executor[T]) WithClock(clock Clock) (Executor[T], error)

WithClock returns a copy using clock for attempts and event timestamps.

func (Executor[T]) WithObserver

func (executor Executor[T]) WithObserver(observer Observer, maxEvents int) (Executor[T], error)

WithObserver returns a copy that sends at most maxEvents retained events to observer.

func (Executor[T]) WithTimeline

func (executor Executor[T]) WithTimeline(maxEvents int) (Executor[T], error)

WithTimeline returns a copy that retains at most maxEvents events per result.

type IgnoredError

type IgnoredError struct{ Reason string }

IgnoredError carries only a bounded reason and no application value.

func (*IgnoredError) Error

func (err *IgnoredError) Error() string

func (*IgnoredError) Unwrap

func (err *IgnoredError) Unwrap() error

type LocalRejectionError

type LocalRejectionError struct {
	Policy PolicyID
	Reason string
	Cause  error
}

LocalRejectionError carries bounded policy and reason identity with a safe cause.

func (*LocalRejectionError) Error

func (err *LocalRejectionError) Error() string

func (*LocalRejectionError) Unwrap

func (err *LocalRejectionError) Unwrap() []error

type Metadata

type Metadata struct {
	// contains filtered or unexported fields
}

Metadata is immutable logical-execution identity safe for bounded diagnostics.

func NewMetadata

func NewMetadata(logicalID, operation, resource string) (Metadata, error)

NewMetadata validates bounded, non-empty execution identifiers.

func (Metadata) LogicalID

func (metadata Metadata) LogicalID() string

LogicalID returns the stable identity shared by all attempts in an execution.

func (Metadata) Operation

func (metadata Metadata) Operation() string

Operation returns the bounded operation identity used by diagnostics.

func (Metadata) Resource

func (metadata Metadata) Resource() string

Resource returns the bounded budget resource identity.

type Observer

type Observer interface {
	Observe(Event)
}

Observer receives events after internal state changes and outside locks.

type ObserverFunc

type ObserverFunc func(Event)

ObserverFunc adapts a function to Observer.

func (ObserverFunc) Observe

func (function ObserverFunc) Observe(event Event)

Observe invokes the adapted function.

type Operation

type Operation[T any] func(context.Context, Attempt) (T, error)

Operation is caller-owned synchronous work for one physical attempt.

type Outcome

type Outcome struct {
	Kind    OutcomeKind
	Attempt Attempt
}

Outcome is the stable classification of a completed execution.

type OutcomeKind

type OutcomeKind string

OutcomeKind classifies an execution without conflating local and downstream failures.

const (
	// OutcomeSuccess identifies a completed operation without an error.
	OutcomeSuccess OutcomeKind = "success"
	// OutcomeOperationFailure identifies an error returned by downstream work.
	OutcomeOperationFailure OutcomeKind = "operation_failure"
	// OutcomeLocalRejection identifies work denied before downstream invocation.
	OutcomeLocalRejection OutcomeKind = "local_rejection"
	// OutcomeCancellation identifies cooperative caller cancellation.
	OutcomeCancellation OutcomeKind = "cancellation"
	// OutcomeDeadline identifies expiration of the caller-owned total deadline.
	OutcomeDeadline OutcomeKind = "deadline"
	// OutcomeIgnored identifies work intentionally omitted by a policy.
	OutcomeIgnored OutcomeKind = "ignored"
	// OutcomePolicyFailure identifies invalid or failed policy execution.
	OutcomePolicyFailure OutcomeKind = "policy_failure"
)

type Permit

type Permit interface {
	Complete() error
}

Permit owns completion of one admitted physical attempt.

type Policy

type Policy[T any] interface {
	Descriptor() PolicyDescriptor
	Wrap(Stage[T]) Stage[T]
}

Policy wraps the next stage without hidden registration or discovery.

type PolicyDescriptor

type PolicyDescriptor struct {
	ID         PolicyID
	Scope      Scope
	Repeatable bool
}

PolicyDescriptor makes ordering and execution scope inspectable.

type PolicyExecutionError

type PolicyExecutionError struct {
	Policy PolicyID
	Stage  string
	Cause  error
}

PolicyExecutionError identifies the policy stage that failed safely.

func (*PolicyExecutionError) Error

func (err *PolicyExecutionError) Error() string

func (*PolicyExecutionError) Unwrap

func (err *PolicyExecutionError) Unwrap() []error

type PolicyID

type PolicyID string

PolicyID is a bounded stable policy identity used by errors and diagnostics.

type RejectionReason

type RejectionReason string

RejectionReason identifies the bounded admission rule that denied work.

const (
	// ReasonExecutionLimit denies work after an execution exhausts its total allowance.
	ReasonExecutionLimit RejectionReason = "execution_limit"
	// ReasonConcurrentLimit denies work while the resource has no concurrent capacity.
	ReasonConcurrentLimit RejectionReason = "concurrent_limit"
	// ReasonWindowLimit denies work after the resource exhausts its rolling allowance.
	ReasonWindowLimit RejectionReason = "window_limit"
	// ReasonResourceLimit denies creation of a new bounded resource identity.
	ReasonResourceLimit RejectionReason = "resource_limit"
	// ReasonDuplicateWork denies a physical attempt already admitted by the scope.
	ReasonDuplicateWork RejectionReason = "duplicate_work"
	// ReasonOriginalRequired denies additional work before the original attempt.
	ReasonOriginalRequired RejectionReason = "original_required"
	// ReasonUnknownParent denies additional work whose parent was never admitted.
	ReasonUnknownParent RejectionReason = "unknown_parent"
)

func RejectionReasonOf

func RejectionReasonOf(err error) RejectionReason

RejectionReasonOf extracts a bounded reason or returns the empty value.

type Result

type Result[T any] struct {
	Value   T
	Err     error
	Outcome Outcome
	Events  []Event
}

Result preserves the typed operation value, original error, bounded events, and outcome.

func Failure

func Failure[T any](value T, err error, attempt Attempt) Result[T]

Failure preserves an operation error and classifies context causes.

func Ignored

func Ignored[T any](attempt Attempt, reason string) Result[T]

Ignored constructs a deliberate no-work result.

func LocalRejection

func LocalRejection[T any](attempt Attempt, policy PolicyID, reason string, cause error) Result[T]

LocalRejection constructs a local denial that remains distinct from operation failure.

func PolicyFailure

func PolicyFailure[T any](attempt Attempt, policy PolicyID, stage string, cause error) Result[T]

PolicyFailure constructs a failure owned by policy logic.

func Success

func Success[T any](value T, attempt Attempt) Result[T]

Success constructs a successful policy result.

type Scope

type Scope string

Scope identifies how often a policy participates in an execution.

const (
	// ScopeLogical applies once around the complete logical execution.
	ScopeLogical Scope = "logical"
	// ScopeAttempt applies independently to each physical attempt.
	ScopeAttempt Scope = "attempt"
)

type Stage

type Stage[T any] func(context.Context, Execution, Operation[T]) Result[T]

Stage is the explicit policy composition boundary.

type WorkBudget

type WorkBudget interface {
	Start(context.Context, Metadata) (WorkBudgetScope, context.Context, error)
}

WorkBudget is the shared admission contract consumed by retry and hedge policies.

type WorkBudgetScope

type WorkBudgetScope interface {
	Acquire(context.Context, Attempt) (Permit, error)
	Snapshot() BudgetSnapshot
	Matches(Metadata) bool
	Close() error
}

WorkBudgetScope owns all physical work for one logical execution.

func BudgetScopeFromContext

func BudgetScopeFromContext(ctx context.Context) (WorkBudgetScope, bool)

BudgetScopeFromContext returns the explicitly attached logical budget scope.

type WorkPermit

type WorkPermit struct {
	// contains filtered or unexported fields
}

WorkPermit owns exactly one admitted physical-work lifecycle.

func (*WorkPermit) Complete

func (permit *WorkPermit) Complete() error

Complete releases concurrent capacity exactly once.

Jump to

Keyboard shortcuts

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