-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathpolicy.go
More file actions
138 lines (115 loc) · 3.84 KB
/
Copy pathpolicy.go
File metadata and controls
138 lines (115 loc) · 3.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// Copyright (c) 2026 Onur Cinar.
// The source code is provided under MIT License.
// https://github.com/cinar/resile
package resile
import (
"context"
"errors"
"time"
"github.com/cinar/resile/circuit"
)
// fatalError is a private wrapper to indicate an error should not be retried.
type fatalError struct {
err error
}
func (f *fatalError) Error() string {
return f.err.Error()
}
func (f *fatalError) Unwrap() error {
return f.err
}
// FatalError wraps an error to indicate that the retry loop should terminate immediately.
func FatalError(err error) error {
if err == nil {
return nil
}
return &fatalError{err: err}
}
// isFatal checks if an error (or any error in its chain) is a fatal error.
func isFatal(err error) bool {
var f *fatalError
return errors.As(err, &f)
}
// RetryAfterError is implemented by errors that specify how long to wait before retrying.
// This is commonly used with HTTP 429 (Too Many Requests) or 503 (Service Unavailable)
// to respect Retry-After headers. It also supports pushback signals to cancel all retries.
type RetryAfterError interface {
error
RetryAfter() time.Duration
CancelAllRetries() bool
}
// retryPolicy defines the logic to determine if an error is retriable.
type retryPolicy struct {
retryIf error
retryIfFunc func(error) bool
}
// shouldRetry evaluates the error against the configured policy.
func (p *retryPolicy) shouldRetry(err error) bool {
if err == nil {
return false
}
// Fatal errors always terminate the loop.
if isFatal(err) {
return false
}
// Context cancellation or deadline exceeded should also terminate the loop.
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return false
}
// If no specific policy is set, we default to retrying all non-fatal errors.
if p.retryIf == nil && p.retryIfFunc == nil {
return true
}
// Check if the error matches a specific target.
if p.retryIf != nil && errors.Is(err, p.retryIf) {
return true
}
// Check if the error matches a custom function.
if p.retryIfFunc != nil && p.retryIfFunc(err) {
return true
}
return false
}
// Policy represents a composed resilience strategy.
// It is thread-safe and reusable across multiple calls.
type Policy struct {
config *Config
}
// NewPolicy creates a new Policy with the given options.
// The order of options determines the execution order from outermost to innermost.
// Example: NewPolicy(WithFallback(f), WithRetry(3)) results in Fallback(Retry(Action)).
func NewPolicy(opts ...Option) *Policy {
c := DefaultConfig()
c.pipeline = make([]middleware, 0)
for _, opt := range opts {
opt(c)
}
return &Policy{config: c}
}
// Do executes an action within the resilience policy.
func (p *Policy) Do(ctx context.Context, action func(context.Context) (any, error)) (any, error) {
var result any
err := p.config.execute(ctx, func(innerCtx context.Context, state RetryState) error {
var innerErr error
result, innerErr = action(innerCtx)
return innerErr
}, nil)
return result, err
}
// DoErr executes an action within the resilience policy.
func (p *Policy) DoErr(ctx context.Context, action func(context.Context) error) error {
return p.config.execute(ctx, nil, action)
}
// Health returns a channel that emits state change events from the underlying resilience components.
// It returns nil if no health-reporting component is configured.
// The caller should use a select with default to handle non-blocking receive.
// Note: Only circuit breaker health events are exposed through Policy.Health().
// For adaptive limiter events, access the limiter directly via AdaptiveLimiter.Health().
func (p *Policy) Health() <-chan circuit.StateEvent {
if p.config.CircuitBreaker != nil {
return p.config.CircuitBreaker.Health()
}
return nil
}
// middleware defines a function that wraps a doAction with additional resilience logic.
type middleware func(doAction) doAction