|
| 1 | +package exec |
| 2 | + |
| 3 | +import ( |
| 4 | + "errors" |
| 5 | + "fmt" |
| 6 | + "strings" |
| 7 | + "time" |
| 8 | +) |
| 9 | + |
| 10 | +//go:generate mockery --name RetryableExecutor --structname RetryableExecutorMock --filename exec_mock.go --inpackage |
| 11 | + |
| 12 | +// RetryableExecutor is the interface used for executing a closure and its fallback if the initial execution fails. |
| 13 | +// |
| 14 | +// The interface is generic over type T which represents the return value of the closure. |
| 15 | +type RetryableExecutor[T any] interface { |
| 16 | + ExecWithFallback(execFn func() (T, error), fallbackFn func() error) (T, error) |
| 17 | +} |
| 18 | + |
| 19 | +// retryableExecutor implements RetryableExecutor. |
| 20 | +// |
| 21 | +// It can be configured via the provided OptsFn(s). |
| 22 | +type retryableExecutor[T any] struct { |
| 23 | + opts *Opts |
| 24 | +} |
| 25 | + |
| 26 | +// NewRetryableExecutor creates a new RetryableExecutor. |
| 27 | +func NewRetryableExecutor[T any](opts ...OptsFn) RetryableExecutor[T] { |
| 28 | + execOpts := NewDefaultExecOpts() |
| 29 | + |
| 30 | + for _, opt := range opts { |
| 31 | + opt(execOpts) |
| 32 | + } |
| 33 | + |
| 34 | + return &retryableExecutor[T]{ |
| 35 | + execOpts, |
| 36 | + } |
| 37 | +} |
| 38 | + |
| 39 | +// ExecWithFallback will attempt to execute the provided execFn and, in the case of failure, it will execute |
| 40 | +// the fallbackFn and retry execution of execFn. |
| 41 | +func (r *retryableExecutor[T]) ExecWithFallback(execFn func() (T, error), fallbackFn func() error) (res T, err error) { |
| 42 | + if execFn == nil { |
| 43 | + return res, ErrMissingExecFn |
| 44 | + } |
| 45 | + |
| 46 | + if fallbackFn == nil { |
| 47 | + return res, ErrMissingFallbackFn |
| 48 | + } |
| 49 | + |
| 50 | + execErr := &Error{} |
| 51 | + |
| 52 | + retryCount := uint(0) |
| 53 | + |
| 54 | + for { |
| 55 | + res, err = execFn() |
| 56 | + |
| 57 | + if err == nil { |
| 58 | + return res, nil |
| 59 | + } |
| 60 | + |
| 61 | + execErr.AddErr(fmt.Errorf("exec function error: %w", err)) |
| 62 | + |
| 63 | + if retryCount == r.opts.maxRetryCount { |
| 64 | + return res, execErr |
| 65 | + } |
| 66 | + |
| 67 | + if err = fallbackFn(); err != nil && !r.opts.retryOnFallbackError { |
| 68 | + execErr.AddErr(fmt.Errorf("fallback function error: %w", err)) |
| 69 | + |
| 70 | + return res, execErr |
| 71 | + } |
| 72 | + |
| 73 | + retryCount++ |
| 74 | + |
| 75 | + time.Sleep(r.opts.retryTimeout) |
| 76 | + } |
| 77 | +} |
| 78 | + |
| 79 | +var ( |
| 80 | + ErrMissingExecFn = errors.New("no exec function provided") |
| 81 | + ErrMissingFallbackFn = errors.New("no fallback function provided") |
| 82 | +) |
| 83 | + |
| 84 | +const ( |
| 85 | + defaultMaxRetryCount = 3 |
| 86 | + defaultErrTimeout = 0 * time.Second |
| 87 | + defaultRetryOnFallbackError = true |
| 88 | +) |
| 89 | + |
| 90 | +// Opts holds the configurable options for a RetryableExecutor. |
| 91 | +type Opts struct { |
| 92 | + // maxRetryCount holds maximum number of retries in the case of failure. |
| 93 | + maxRetryCount uint |
| 94 | + |
| 95 | + // retryTimeout holds the timeout between retries. |
| 96 | + retryTimeout time.Duration |
| 97 | + |
| 98 | + // retryOnFallbackError specifies whether a retry will be done in the case of |
| 99 | + // failure of the fallback function. |
| 100 | + retryOnFallbackError bool |
| 101 | +} |
| 102 | + |
| 103 | +// NewDefaultExecOpts creates the default Opts. |
| 104 | +func NewDefaultExecOpts() *Opts { |
| 105 | + return &Opts{ |
| 106 | + maxRetryCount: defaultMaxRetryCount, |
| 107 | + retryTimeout: defaultErrTimeout, |
| 108 | + retryOnFallbackError: defaultRetryOnFallbackError, |
| 109 | + } |
| 110 | +} |
| 111 | + |
| 112 | +// OptsFn is function that operate on Opts. |
| 113 | +type OptsFn func(opts *Opts) |
| 114 | + |
| 115 | +// WithMaxRetryCount sets the max retry count. |
| 116 | +// |
| 117 | +// Note that a default value is provided if the provided count is 0. |
| 118 | +func WithMaxRetryCount(maxRetryCount uint) OptsFn { |
| 119 | + return func(opts *Opts) { |
| 120 | + if maxRetryCount == 0 { |
| 121 | + maxRetryCount = defaultMaxRetryCount |
| 122 | + } |
| 123 | + |
| 124 | + opts.maxRetryCount = maxRetryCount |
| 125 | + } |
| 126 | +} |
| 127 | + |
| 128 | +// WithRetryTimeout sets the retry timeout. |
| 129 | +func WithRetryTimeout(retryTimeout time.Duration) OptsFn { |
| 130 | + return func(opts *Opts) { |
| 131 | + opts.retryTimeout = retryTimeout |
| 132 | + } |
| 133 | +} |
| 134 | + |
| 135 | +// WithRetryOnFallBackError sets the retryOnFallbackError flag. |
| 136 | +func WithRetryOnFallBackError(retryOnFallbackError bool) OptsFn { |
| 137 | + return func(opts *Opts) { |
| 138 | + opts.retryOnFallbackError = retryOnFallbackError |
| 139 | + } |
| 140 | +} |
| 141 | + |
| 142 | +// Error holds none or multiple errors that can happen during execution. |
| 143 | +type Error struct { |
| 144 | + errs []error |
| 145 | +} |
| 146 | + |
| 147 | +// AddErr appends an error to the error slice of Error. |
| 148 | +func (e *Error) AddErr(err error) { |
| 149 | + e.errs = append(e.errs, err) |
| 150 | +} |
| 151 | + |
| 152 | +// Error implements the standard error interface. |
| 153 | +func (e *Error) Error() string { |
| 154 | + sb := strings.Builder{} |
| 155 | + |
| 156 | + for i, err := range e.errs { |
| 157 | + sb.WriteString(fmt.Sprintf("error %d: %s\n", i, err)) |
| 158 | + } |
| 159 | + |
| 160 | + return sb.String() |
| 161 | +} |
0 commit comments