-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathretry.go
129 lines (110 loc) · 4.8 KB
/
retry.go
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
package retrier
import (
"context"
"time"
"github.com/hueristiq/hq-go-retrier/backoff"
)
// Operation is a function type that represents an operation that can be retried.
// The operation returns an error, which indicates whether the operation failed or succeeded.
type Operation func() (err error)
// withEmptyData wraps an Operation function to convert it into an OperationWithData that
// returns an empty struct. This is used for cases where the operation does not return any data
// but can be retried with the same mechanism as data-returning operations.
//
// Returns:
// - operationWithData: An OperationWithData function that returns an empty struct and error,
// allowing non-data-returning operations to be handled by the RetryWithData function.
func (o Operation) withEmptyData() (operationWithData OperationWithData[struct{}]) {
operationWithData = func() (struct{}, error) {
return struct{}{}, o()
}
return
}
// OperationWithData is a function type that represents an operation that returns data along with an error.
// The generic type T allows the operation to return any type of data, making the retrier versatile for operations
// that may return results along with a possible error.
type OperationWithData[T any] func() (data T, err error)
// Retry attempts to execute the provided operation with a retry mechanism, using the provided options.
// If the operation continues to fail, it will retry based on the configuration, which may include max retries,
// backoff strategies, and min/max delay between retries.
//
// Parameters:
// - ctx: A context to control the lifetime of the retry operation. If the context is canceled or times out,
// the retry operation will stop and return the context's error.
// - operation: The operation to be retried.
// - opts: Optional configuration options that can adjust max retries, backoff strategy, or delay intervals.
//
// Returns:
// - err: The error returned by the last failed attempt, or the context's error if the operation is canceled.
//
// Example:
//
// err := retrier.Retry(ctx, someOperation, retrier.WithMaxRetries(5), retrier.WithBackoff(backoff.Exponential()))
// // Retries 'someOperation' up to 5 times with exponential backoff.
func Retry(ctx context.Context, operation Operation, opts ...Option) (err error) {
// Use RetryWithData with an empty struct as a workaround for non-data-returning operations.
_, err = RetryWithData(ctx, operation.withEmptyData(), opts...)
return
}
// RetryWithData attempts to execute the provided operation, which returns data along with an error, using the retry mechanism.
// It retries the operation based on the configuration and returns the result data if successful, or an error if the retries fail.
//
// Parameters:
// - ctx: A context to control the lifetime of the retry operation. If the context is canceled or times out,
// the retry operation will stop and return the context's error.
// - operation: The operation to be retried, which returns a value of type T and an error.
// - opts: Optional configuration options that can adjust max retries, backoff strategy, or delay intervals.
//
// Returns:
// - result: The result of the operation if it succeeds within the allowed retry attempts.
// - err: The error returned by the last failed attempt, or the context's error if the operation is canceled.
//
// Example:
//
// result, err := retrier.RetryWithData(ctx, fetchData, retrier.WithMaxRetries(5), retrier.WithBackoff(backoff.Exponential()))
// // Retries 'fetchData' up to 5 times with exponential backoff.
func RetryWithData[T any](ctx context.Context, operation OperationWithData[T], opts ...Option) (result T, err error) {
cfg := &Configuration{
maxRetries: 3,
maxDelay: 1000 * time.Millisecond,
minDelay: 100 * time.Millisecond,
backoff: backoff.Exponential(),
}
for _, opt := range opts {
opt(cfg)
}
for attempt := range cfg.maxRetries {
select {
case <-ctx.Done():
// If the context is done, return the context's error.
err = ctx.Err()
return
default:
// Execute the operation and check for success.
result, err = operation()
if err == nil {
// Operation succeeded, return the result.
return
}
// If the operation fails, calculate the backoff delay.
b := cfg.backoff(cfg.minDelay, cfg.maxDelay, attempt)
// Trigger notifier if configured, providing feedback on the error and backoff duration.
if cfg.notifier != nil {
cfg.notifier(err, b)
}
// Wait for the backoff period before the next retry attempt.
ticker := time.NewTicker(b)
select {
case <-ticker.C:
// Backoff delay is over, stop the ticker and proceed to the next retry attempt.
ticker.Stop()
case <-ctx.Done():
// If the context is done, stop the ticker and return the context's error.
ticker.Stop()
err = ctx.Err()
return
}
}
}
return
}