resiliency API

resiliency

package

API reference for the resiliency package.

T
type

State

pkg/resiliency/breaker.go:11-11
type State int
S
struct

CircuitBreaker

CircuitBreaker protects a caller from repeated failures.

It implements a state machine with Closed, Open, and Half-Open states.

pkg/resiliency/breaker.go:29-37
type CircuitBreaker struct

Example

cb := resiliency.NewCircuitBreaker(3, time.Minute)
err := cb.Execute(func() error {
	return doRiskyOperation()
})

Methods

OnStateChange
Method

OnStateChange registers a callback for state transitions.

Parameters

fn func(from, to State)
func (*CircuitBreaker) OnStateChange(fn func(from, to State))
{
	cb.mu.Lock()
	defer cb.mu.Unlock()
	cb.onStateChange = fn
}
Execute
Method

Execute wraps a function call with circuit breaker logic.

Parameters

fn func() error

Returns

error
func (*CircuitBreaker) Execute(fn func() error) error
{
	if !cb.allow() {
		return ErrCircuitOpen
	}

	err := fn()
	if err != nil {
		cb.onFailure()
		return err
	}

	cb.onSuccess()
	return nil
}
allow
Method

Returns

bool
func (*CircuitBreaker) allow() bool
{
	cb.mu.Lock()
	defer cb.mu.Unlock()

	switch cb.state {
	case StateClosed:
		return true
	case StateOpen:
		if time.Since(cb.lastFailure) > cb.timeout {
			cb.changeState(StateHalfOpen)
			return true
		}
		return false
	case StateHalfOpen:
		// In half-open, we only allow one request to probe the system.
		// For simplicity in this foundation version, we'll allow it.
		return true
	}
	return false
}
onSuccess
Method
func (*CircuitBreaker) onSuccess()
{
	cb.mu.Lock()
	defer cb.mu.Unlock()

	if cb.state == StateHalfOpen {
		cb.changeState(StateClosed)
	}
	cb.failures = 0
}
onFailure
Method
func (*CircuitBreaker) onFailure()
{
	cb.mu.Lock()
	defer cb.mu.Unlock()

	cb.failures++
	cb.lastFailure = time.Now()

	if cb.state == StateClosed && cb.failures >= cb.threshold {
		cb.changeState(StateOpen)
	} else if cb.state == StateHalfOpen {
		cb.changeState(StateOpen)
	}
}
changeState
Method

Parameters

to State
func (*CircuitBreaker) changeState(to State)
{
	from := cb.state
	cb.state = to
	if cb.onStateChange != nil {
		cb.onStateChange(from, to)
	}
}
State
Method

State returns the current state of the circuit breaker.

Returns

func (*CircuitBreaker) State() State
{
	cb.mu.RLock()
	defer cb.mu.RUnlock()
	return cb.state
}

Fields

Name Type Description
mu sync.RWMutex
state State
failures int
threshold int
timeout time.Duration
lastFailure time.Time
onStateChange func(from, to State)
F
function

NewCircuitBreaker

NewCircuitBreaker creates a new circuit breaker.

Parameters

threshold
int
timeout

Returns

pkg/resiliency/breaker.go:40-46
func NewCircuitBreaker(threshold int, timeout time.Duration) *CircuitBreaker

{
	return &CircuitBreaker{
		threshold: threshold,
		timeout:   timeout,
		state:     StateClosed,
	}
}
F
function

TestCircuitBreaker

Parameters

pkg/resiliency/breaker_test.go:9-44
func TestCircuitBreaker(t *testing.T)

{
	cb := NewCircuitBreaker(2, 50*time.Millisecond)

	// State: Closed
	err := cb.Execute(func() error { return nil })
	if err != nil || cb.State() != StateClosed {
		t.Error("Circuit should be closed and return nil")
	}

	// First failure
	_ = cb.Execute(func() error { return errors.New("fail1") })
	if cb.State() != StateClosed {
		t.Error("Circuit should still be closed after 1 failure")
	}

	// Second failure -> State: Open
	_ = cb.Execute(func() error { return errors.New("fail2") })
	if cb.State() != StateOpen {
		t.Error("Circuit should be open after reaching threshold")
	}

	// While Open
	err = cb.Execute(func() error { return nil })
	if err != ErrCircuitOpen {
		t.Errorf("Execute should return ErrCircuitOpen when open, got %v", err)
	}

	// Wait for timeout -> State: Half-Open (on next Execute)
	time.Sleep(60 * time.Millisecond)

	// First success in Half-Open -> State: Closed
	err = cb.Execute(func() error { return nil })
	if err != nil || cb.State() != StateClosed {
		t.Errorf("Circuit should be closed after success in half-open, got state %v", cb.State())
	}
}
S
struct

RetryOptions

RetryOptions configures retry behavior.

pkg/resiliency/retry.go:10-15
type RetryOptions struct

Fields

Name Type Description
Attempts int
InitialDelay time.Duration
MaxDelay time.Duration
Factor float64
F
function

Retry

Retry executes fn up to Attempts times with exponential backoff.

Parameters

fn
func() error
opts
...func(*RetryOptions)

Returns

error
pkg/resiliency/retry.go:32-67
func Retry(ctx context.Context, fn func() error, opts ...func(*RetryOptions)) error

{
	o := DefaultRetryOptions
	for _, opt := range opts {
		opt(&o)
	}

	var lastErr error
	for i := 0; i < o.Attempts; i++ {
		if err := ctx.Err(); err != nil {
			return err
		}

		if err := fn(); err == nil {
			return nil
		} else {
			lastErr = err
		}

		if i < o.Attempts-1 {
			delay := time.Duration(float64(o.InitialDelay) * math.Pow(o.Factor, float64(i)))
			if delay > o.MaxDelay {
				delay = o.MaxDelay
			}

			timer := time.NewTimer(delay)
			select {
			case <-ctx.Done():
				timer.Stop()
				return ctx.Err()
			case <-timer.C:
			}
		}
	}

	return lastErr
}

Example

err := resiliency.Retry(ctx, func() error {
	return doNetworkCall()
}, resiliency.WithAttempts(5))
F
function

WithAttempts

WithAttempts sets the maximum number of retry attempts.

Parameters

n
int

Returns

func(*RetryOptions)
pkg/resiliency/retry.go:70-72
func WithAttempts(n int) func(*RetryOptions)

{
	return func(o *RetryOptions) { o.Attempts = n }
}
F
function

WithDelay

WithDelay sets the initial and max delay for backoff.

Parameters

Returns

func(*RetryOptions)
pkg/resiliency/retry.go:75-80
func WithDelay(initial, max time.Duration) func(*RetryOptions)

{
	return func(o *RetryOptions) {
		o.InitialDelay = initial
		o.MaxDelay = max
	}
}
F
function

WithFactor

WithFactor sets the backoff factor.

Parameters

f
float64

Returns

func(*RetryOptions)
pkg/resiliency/retry.go:83-85
func WithFactor(f float64) func(*RetryOptions)

{
	return func(o *RetryOptions) { o.Factor = f }
}
F
function

TestRetry

Parameters

pkg/resiliency/retry_test.go:10-64
func TestRetry(t *testing.T)

{
	t.Run("SuccessFirstTry", func(t *testing.T) {
		calls := 0
		err := Retry(context.Background(), func() error {
			calls++
			return nil
		})
		if err != nil || calls != 1 {
			t.Errorf("Retry failed: %v, calls=%d", err, calls)
		}
	})

	t.Run("SuccessAfterRetries", func(t *testing.T) {
		calls := 0
		err := Retry(context.Background(), func() error {
			calls++
			if calls < 3 {
				return errors.New("fail")
			}
			return nil
		}, WithAttempts(5), WithDelay(1*time.Millisecond, 10*time.Millisecond))

		if err != nil || calls != 3 {
			t.Errorf("Retry failed: %v, calls=%d", err, calls)
		}
	})

	t.Run("FailureAllAttempts", func(t *testing.T) {
		calls := 0
		targetErr := errors.New("permanent fail")
		err := Retry(context.Background(), func() error {
			calls++
			return targetErr
		}, WithAttempts(3), WithDelay(1*time.Millisecond, 1*time.Millisecond))

		if err != targetErr || calls != 3 {
			t.Errorf("Retry should return last error: %v, calls=%d", err, calls)
		}
	})

	t.Run("ContextCancellation", func(t *testing.T) {
		ctx, cancel := context.WithCancel(context.Background())
		cancel()

		calls := 0
		err := Retry(ctx, func() error {
			calls++
			return errors.New("fail")
		})

		if err != context.Canceled || calls != 0 {
			t.Errorf("Retry should stop on context cancel: %v", err)
		}
	})
}