Skip to content

Commit 87594f7

Browse files
committed
feat: change operations retry logic to be opt-in
BREAKING CHANGE: The retry logic for operations is now opt-in. To enable the retry, the caller must now provide the `WithRetry` option to the `ExecuteOperation`. This change is intended to provide greater control over the retry behavior and to avoid unintended retries in certain scenarios.
1 parent 5ee7a55 commit 87594f7

File tree

5 files changed

+118
-24
lines changed

5 files changed

+118
-24
lines changed

.changeset/tangy-plums-draw.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": patch
3+
---
4+
5+
BREAKING: Operations retry logic is now opt in. Use the `WithRetry` method in your `ExecuteOperation` call to enable retries

operations/execute.go

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,59 @@ type ExecuteConfig[IN, DEP any] struct {
1818
type ExecuteOption[IN, DEP any] func(*ExecuteConfig[IN, DEP])
1919

2020
type RetryConfig[IN, DEP any] struct {
21-
// DisableRetry disables the retry mechanism if set to true.
22-
DisableRetry bool
21+
// Enabled dermines if the retry is enabled for the operation.
22+
Enabled bool
23+
24+
// Policy is the retry policy to control the behavior of the retry.
25+
Policy RetryPolicy
26+
2327
// InputHook is a function that returns an updated input before retrying the operation.
2428
// The operation when retried will use the input returned by this function.
2529
// This is useful for scenarios like updating the gas limit.
26-
// This will be ignored if DisableRetry is set to true.
2730
InputHook func(input IN, deps DEP) IN
2831
}
2932

30-
// WithRetryConfig is an ExecuteOption that sets the retry configuration.
33+
// newDisabledRetryConfig returns a default retry configuration that is initially disabled.
34+
func newDisabledRetryConfig[IN, DEP any]() RetryConfig[IN, DEP] {
35+
return RetryConfig[IN, DEP]{
36+
Enabled: false,
37+
Policy: RetryPolicy{
38+
MaxAttempts: 10,
39+
},
40+
}
41+
}
42+
43+
// RetryPolicy defines the arguments to control the retry behavior.
44+
type RetryPolicy struct {
45+
MaxAttempts uint
46+
}
47+
48+
// options returns the 'avast/retry' functional options for the retry policy.
49+
func (p RetryPolicy) options() []retry.Option {
50+
return []retry.Option{
51+
retry.Attempts(p.MaxAttempts),
52+
}
53+
}
54+
55+
// WithRetry is an ExecuteOption that enables the default retry for the operation.
56+
func WithRetry[IN, DEP any]() ExecuteOption[IN, DEP] {
57+
return func(c *ExecuteConfig[IN, DEP]) {
58+
c.retryConfig.Enabled = true
59+
}
60+
}
61+
62+
// WithRetryInput is an ExecuteOption that enables the default retry and provide an input
63+
// transform function which will modify the input on each retry attempt.
64+
func WithRetryInput[IN, DEP any](inputHookFunc func(IN, DEP) IN) ExecuteOption[IN, DEP] {
65+
return func(c *ExecuteConfig[IN, DEP]) {
66+
c.retryConfig.Enabled = true
67+
c.retryConfig.InputHook = inputHookFunc
68+
}
69+
}
70+
71+
// WithRetryConfig is an ExecuteOption that sets the retry configuration. This provides a way to
72+
// customize the retry behavior specific to the needs of the operation. Use this for the most
73+
// flexibility and control over the retry behavior.
3174
func WithRetryConfig[IN, DEP any](config RetryConfig[IN, DEP]) ExecuteOption[IN, DEP] {
3275
return func(c *ExecuteConfig[IN, DEP]) {
3376
c.retryConfig = config
@@ -69,39 +112,53 @@ func ExecuteOperation[IN, OUT, DEP any](
69112
return previousReport, nil
70113
}
71114

72-
executeConfig := &ExecuteConfig[IN, DEP]{retryConfig: RetryConfig[IN, DEP]{}}
115+
executeConfig := &ExecuteConfig[IN, DEP]{
116+
retryConfig: newDisabledRetryConfig[IN, DEP](),
117+
}
73118
for _, opt := range opts {
74119
opt(executeConfig)
75120
}
76121

77122
var output OUT
78123
var err error
79124

80-
if executeConfig.retryConfig.DisableRetry {
81-
output, err = operation.execute(b, deps, input)
82-
} else {
125+
if executeConfig.retryConfig.Enabled {
83126
var inputTemp = input
84-
output, err = retry.DoWithData(func() (OUT, error) {
85-
return operation.execute(b, deps, inputTemp)
86-
}, retry.OnRetry(func(attempt uint, err error) {
127+
128+
// Generate the configurable options for the retry
129+
retryOpts := executeConfig.retryConfig.Policy.options()
130+
// Use the operation context in the retry
131+
retryOpts = append(retryOpts, retry.Context(b.GetContext()))
132+
// Append the retry logic which will log the retry and attempt to transform the input
133+
// if the user provided a custom input hook.
134+
retryOpts = append(retryOpts, retry.OnRetry(func(attempt uint, err error) {
87135
b.Logger.Infow("Operation failed. Retrying...",
88136
"operation", operation.def.ID, "attempt", attempt, "error", err)
89137

90138
if executeConfig.retryConfig.InputHook != nil {
91139
inputTemp = executeConfig.retryConfig.InputHook(inputTemp, deps)
92140
}
93141
}))
142+
143+
output, err = retry.DoWithData(
144+
func() (OUT, error) {
145+
return operation.execute(b, deps, inputTemp)
146+
},
147+
retryOpts...,
148+
)
149+
} else {
150+
output, err = operation.execute(b, deps, input)
94151
}
95152

96153
if err == nil && !IsSerializable(b.Logger, output) {
97154
return Report[IN, OUT]{}, fmt.Errorf("operation %s output: %w", operation.def.ID, ErrNotSerializable)
98155
}
99156

100157
report := NewReport(operation.def, input, output, err)
101-
err = b.reporter.AddReport(genericReport(report))
102-
if err != nil {
158+
if err = b.reporter.AddReport(genericReport(report)); err != nil {
103159
return Report[IN, OUT]{}, err
104160
}
161+
105162
if report.Err != nil {
106163
return report, report.Err
107164
}
@@ -175,14 +232,15 @@ func ExecuteSequence[IN, OUT, DEP any](
175232
childReports...,
176233
)
177234

178-
err = b.reporter.AddReport(genericReport(report))
179-
if err != nil {
235+
if err = b.reporter.AddReport(genericReport(report)); err != nil {
180236
return SequenceReport[IN, OUT]{}, err
181237
}
238+
182239
executionReports, err := b.reporter.GetExecutionReports(report.ID)
183240
if err != nil {
184241
return SequenceReport[IN, OUT]{}, err
185242
}
243+
186244
if report.Err != nil {
187245
return SequenceReport[IN, OUT]{report, executionReports}, report.Err
188246
}
@@ -229,6 +287,7 @@ func loadPreviousSuccessfulReport[IN, OUT any](
229287
return typedReport, true
230288
}
231289
}
290+
232291
// No previous execution was found
233292
return Report[IN, OUT]{}, false
234293
}

operations/execute_test.go

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,52 @@ func Test_ExecuteOperation(t *testing.T) {
2424
wantErr string
2525
}{
2626
{
27-
name: "DefaultRetry",
27+
name: "no retry",
28+
wantOpCalledTimes: 1,
29+
wantErr: "test error",
30+
},
31+
{
32+
name: "with default retry",
33+
options: []ExecuteOption[int, any]{
34+
WithRetry[int, any](),
35+
},
36+
wantOpCalledTimes: 3,
37+
wantOutput: 2,
38+
},
39+
{
40+
name: "with custom retry eventual success",
41+
options: []ExecuteOption[int, any]{
42+
WithRetryConfig(RetryConfig[int, any]{
43+
Enabled: true,
44+
Policy: RetryPolicy{
45+
MaxAttempts: 10,
46+
},
47+
}),
48+
},
2849
wantOpCalledTimes: 3,
2950
wantOutput: 2,
3051
},
3152
{
32-
name: "NoRetry",
33-
options: []ExecuteOption[int, any]{WithRetryConfig[int, any](RetryConfig[int, any]{DisableRetry: true})},
53+
name: "with custom retry eventual failure",
54+
options: []ExecuteOption[int, any]{
55+
WithRetryConfig(RetryConfig[int, any]{
56+
Enabled: true,
57+
Policy: RetryPolicy{
58+
MaxAttempts: 1,
59+
},
60+
}),
61+
},
3462
wantOpCalledTimes: 1,
3563
wantErr: "test error",
3664
},
3765
{
3866
name: "NewInputHook",
39-
options: []ExecuteOption[int, any]{WithRetryConfig[int, any](RetryConfig[int, any]{InputHook: func(input int, deps any) int {
40-
// update input to 5 after first failed attempt
41-
return 5
42-
}})},
67+
options: []ExecuteOption[int, any]{
68+
WithRetryInput(func(input int, deps any) int {
69+
// update input to 5 after first failed attempt
70+
return 5
71+
}),
72+
},
4373
wantOpCalledTimes: 3,
4474
wantOutput: 6,
4575
},

operations/report.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ type SequenceReport[IN, OUT any] struct {
4444
// This is useful when we want to return the report as a generic type in the changeset.output.
4545
func (r SequenceReport[IN, OUT]) ToGenericSequenceReport() SequenceReport[any, any] {
4646
return SequenceReport[any, any]{
47-
Report: genericReport[IN, OUT](r.Report),
47+
Report: genericReport(r.Report),
4848
ExecutionReports: r.ExecutionReports,
4949
}
5050
}

operations/report_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func Test_NewReport(t *testing.T) {
6969

7070
testErr := errors.New("test error")
7171
childOperationID := uuid.New().String()
72-
report := NewReport[int, int](op.def, 1, 2, testErr, childOperationID)
72+
report := NewReport(op.def, 1, 2, testErr, childOperationID)
7373
assert.NotEmpty(t, report.ID)
7474
assert.Equal(t, op.def, report.Def)
7575
assert.Equal(t, 1, report.Input)

0 commit comments

Comments
 (0)