resiliency
API
resiliency
packageAPI reference for the resiliency
package.
Imports
(6)
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)
}
}
Fields
| Name | Type | Description |
|---|---|---|
| mu | sync.RWMutex | |
| state | State | |
| failures | int | |
| threshold | int | |
| timeout | time.Duration | |
| lastFailure | time.Time | |
| onStateChange | func(from, to State) |
Uses
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
t
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
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
initial
max
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
t
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)
}
})
}