Skip to content

Commit e1da9af

Browse files
[CLD-150]: fix: migration operations package (#4)
Migrate the operations package from chainlink to here. JIRA: https://smartcontract-it.atlassian.net/browse/CLD-150
1 parent 439d8f2 commit e1da9af

File tree

17 files changed

+2772
-11
lines changed

17 files changed

+2772
-11
lines changed

Taskfile.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# https://taskfile.dev
2+
3+
version: "3"
4+
5+
includes:
6+
lint:
7+
taskfile: ./taskfiles/lint/Taskfile.yml
8+
9+
test:
10+
taskfile: ./taskfiles/test/Taskfile.yml

go.mod

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,79 @@ go 1.24.1
44

55
require (
66
github.com/Masterminds/semver/v3 v3.3.1
7+
github.com/avast/retry-go/v4 v4.6.1
78
github.com/ethereum/go-ethereum v1.15.7
9+
github.com/google/uuid v1.6.0
810
github.com/pkg/errors v0.9.1
911
github.com/smartcontractkit/chain-selectors v1.0.49
12+
github.com/smartcontractkit/chainlink-common v0.6.0
13+
github.com/smartcontractkit/mcms v0.16.1
1014
github.com/stretchr/testify v1.10.0
15+
go.uber.org/zap v1.27.0
1116
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0
1217
)
1318

1419
require (
15-
github.com/davecgh/go-spew v1.1.1 // indirect
20+
filippo.io/edwards25519 v1.0.0-rc.1 // indirect
21+
github.com/Microsoft/go-winio v0.6.2 // indirect
22+
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
23+
github.com/bits-and-blooms/bitset v1.17.0 // indirect
24+
github.com/blendle/zapdriver v1.3.1 // indirect
25+
github.com/consensys/bavard v0.1.22 // indirect
26+
github.com/consensys/gnark-crypto v0.14.0 // indirect
27+
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
28+
github.com/crate-crypto/go-kzg-4844 v1.1.0 // indirect
29+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
30+
github.com/deckarep/golang-set/v2 v2.6.0 // indirect
31+
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
32+
github.com/ethereum/c-kzg-4844 v1.0.3 // indirect
33+
github.com/ethereum/go-verkle v0.2.2 // indirect
34+
github.com/fatih/color v1.17.0 // indirect
35+
github.com/fsnotify/fsnotify v1.7.0 // indirect
36+
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
37+
github.com/gagliardetto/binary v0.8.0 // indirect
38+
github.com/gagliardetto/solana-go v1.12.0 // indirect
39+
github.com/gagliardetto/treeout v0.1.4 // indirect
40+
github.com/go-ole/go-ole v1.3.0 // indirect
41+
github.com/go-playground/locales v0.14.1 // indirect
42+
github.com/go-playground/universal-translator v0.18.1 // indirect
43+
github.com/go-playground/validator/v10 v10.24.0 // indirect
44+
github.com/gorilla/websocket v1.5.0 // indirect
1645
github.com/holiman/uint256 v1.3.2 // indirect
46+
github.com/json-iterator/go v1.1.12 // indirect
47+
github.com/karalabe/hid v1.0.1-0.20240306101548-573246063e52 // indirect
48+
github.com/klauspost/compress v1.17.11 // indirect
49+
github.com/leodido/go-urn v1.4.0 // indirect
50+
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
51+
github.com/mattn/go-colorable v0.1.13 // indirect
52+
github.com/mattn/go-isatty v0.0.20 // indirect
53+
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
54+
github.com/mmcloughlin/addchain v0.4.0 // indirect
55+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
56+
github.com/modern-go/reflect2 v1.0.2 // indirect
57+
github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect
1758
github.com/mr-tron/base58 v1.2.0 // indirect
18-
github.com/pmezard/go-difflib v1.0.0 // indirect
19-
golang.org/x/crypto v0.35.0 // indirect
20-
golang.org/x/sys v0.30.0 // indirect
59+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
60+
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
61+
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250226104101-11778f2ead98 // indirect
62+
github.com/smartcontractkit/libocr v0.0.0-20250220133800-f3b940c4f298 // indirect
63+
github.com/spf13/cast v1.7.1 // indirect
64+
github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect
65+
github.com/supranational/blst v0.3.14 // indirect
66+
github.com/tklauser/go-sysconf v0.3.12 // indirect
67+
github.com/tklauser/numcpus v0.6.1 // indirect
68+
github.com/yusufpapurcu/wmi v1.2.3 // indirect
69+
go.mongodb.org/mongo-driver v1.12.2 // indirect
70+
go.uber.org/multierr v1.11.0 // indirect
71+
go.uber.org/ratelimit v0.2.0 // indirect
72+
golang.org/x/crypto v0.37.0 // indirect
73+
golang.org/x/net v0.39.0 // indirect
74+
golang.org/x/sync v0.13.0 // indirect
75+
golang.org/x/sys v0.32.0 // indirect
76+
golang.org/x/term v0.31.0 // indirect
77+
golang.org/x/text v0.24.0 // indirect
78+
golang.org/x/time v0.9.0 // indirect
79+
golang.org/x/tools v0.32.0 // indirect
2180
gopkg.in/yaml.v3 v3.0.1 // indirect
81+
rsc.io/tmplfunc v0.0.3 // indirect
2282
)

go.sum

Lines changed: 369 additions & 7 deletions
Large diffs are not rendered by default.

operations/execute.go

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
package operations
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/avast/retry-go/v4"
8+
)
9+
10+
var ErrNotSerializable = errors.New("data cannot be safely written to disk without data lost, " +
11+
"avoid type that can't be serialized")
12+
13+
// ExecuteConfig is the configuration for the ExecuteOperation function.
14+
type ExecuteConfig[IN, DEP any] struct {
15+
retryConfig RetryConfig[IN, DEP]
16+
}
17+
18+
type ExecuteOption[IN, DEP any] func(*ExecuteConfig[IN, DEP])
19+
20+
type RetryConfig[IN, DEP any] struct {
21+
// DisableRetry disables the retry mechanism if set to true.
22+
DisableRetry bool
23+
// InputHook is a function that returns an updated input before retrying the operation.
24+
// The operation when retried will use the input returned by this function.
25+
// This is useful for scenarios like updating the gas limit.
26+
// This will be ignored if DisableRetry is set to true.
27+
InputHook func(input IN, deps DEP) IN
28+
}
29+
30+
// WithRetryConfig is an ExecuteOption that sets the retry configuration.
31+
func WithRetryConfig[IN, DEP any](config RetryConfig[IN, DEP]) ExecuteOption[IN, DEP] {
32+
return func(c *ExecuteConfig[IN, DEP]) {
33+
c.retryConfig = config
34+
}
35+
}
36+
37+
// ExecuteOperation executes an operation with the given input and dependencies.
38+
// Execution will return the previous successful execution result and skip execution if there was a
39+
// previous successful run found in the Reports.
40+
// If previous unsuccessful execution was found, the execution will not be skipped.
41+
//
42+
// Note:
43+
// Operations that were skipped will not be added to the reporter.
44+
//
45+
// Retry:
46+
// By default, it retries the operation up to 10 times with exponential backoff if it fails.
47+
// Use WithRetryConfig to customize the retry behavior.
48+
// To cancel the retry early, return an error with NewUnrecoverableError.
49+
//
50+
// Input & Output:
51+
// The input and output must be JSON serializable. If the input is not serializable, it will return an error.
52+
// To be serializable, the input and output must be json.marshalable, or it must implement json.Marshaler and json.Unmarshaler.
53+
// IsSerializable can be used to check if the input or output is serializable.
54+
func ExecuteOperation[IN, OUT, DEP any](
55+
b Bundle,
56+
operation *Operation[IN, OUT, DEP],
57+
deps DEP,
58+
input IN,
59+
opts ...ExecuteOption[IN, DEP],
60+
) (Report[IN, OUT], error) {
61+
if !IsSerializable(b.Logger, input) {
62+
return Report[IN, OUT]{}, fmt.Errorf("operation %s input: %w", operation.def.ID, ErrNotSerializable)
63+
}
64+
65+
if previousReport, found := loadPreviousSuccessfulReport[IN, OUT](b, operation.def, input); found {
66+
b.Logger.Infow("Operation already executed. Returning previous result", "id", operation.def.ID,
67+
"version", operation.def.Version, "description", operation.def.Description)
68+
return previousReport, nil
69+
}
70+
71+
executeConfig := &ExecuteConfig[IN, DEP]{retryConfig: RetryConfig[IN, DEP]{}}
72+
for _, opt := range opts {
73+
opt(executeConfig)
74+
}
75+
76+
var output OUT
77+
var err error
78+
79+
if executeConfig.retryConfig.DisableRetry {
80+
output, err = operation.execute(b, deps, input)
81+
} else {
82+
var inputTemp = input
83+
output, err = retry.DoWithData(func() (OUT, error) {
84+
return operation.execute(b, deps, inputTemp)
85+
}, retry.OnRetry(func(attempt uint, err error) {
86+
b.Logger.Infow("Operation failed. Retrying...",
87+
"operation", operation.def.ID, "attempt", attempt, "error", err)
88+
89+
if executeConfig.retryConfig.InputHook != nil {
90+
inputTemp = executeConfig.retryConfig.InputHook(inputTemp, deps)
91+
}
92+
}))
93+
}
94+
95+
if err == nil && !IsSerializable(b.Logger, output) {
96+
return Report[IN, OUT]{}, fmt.Errorf("operation %s output: %w", operation.def.ID, ErrNotSerializable)
97+
}
98+
99+
report := NewReport(operation.def, input, output, err)
100+
err = b.reporter.AddReport(genericReport(report))
101+
if err != nil {
102+
return Report[IN, OUT]{}, err
103+
}
104+
if report.Err != nil {
105+
return report, report.Err
106+
}
107+
return report, nil
108+
}
109+
110+
// ExecuteSequence executes a Sequence and returns a SequenceReport.
111+
// The SequenceReport contains a report for the Sequence and also the execution reports which are all
112+
// the operations that were executed as part of this sequence.
113+
// The latter is useful when we want to return all the executed reports to the changeset output.
114+
// Execution will return the previous successful execution result and skip execution if there was a
115+
// previous successful run found in the Reports.
116+
// If previous unsuccessful execution was found, the execution will not be skipped.
117+
//
118+
// Note:
119+
// Sequences or Operations that were skipped will not be added to the reporter.
120+
// The ExecutionReports do not include Sequences or Operations that were skipped.
121+
//
122+
// Input & Output:
123+
// The input and output must be JSON serializable. If the input is not serializable, it will return an error.
124+
// To be serializable, the input and output must be json.marshalable, or it must implement json.Marshaler and json.Unmarshaler.
125+
// IsSerializable can be used to check if the input or output is serializable.
126+
func ExecuteSequence[IN, OUT, DEP any](
127+
b Bundle, sequence *Sequence[IN, OUT, DEP], deps DEP, input IN,
128+
) (SequenceReport[IN, OUT], error) {
129+
if !IsSerializable(b.Logger, input) {
130+
return SequenceReport[IN, OUT]{}, fmt.Errorf("sequence %s input: %w", sequence.def.ID, ErrNotSerializable)
131+
}
132+
133+
if previousReport, found := loadPreviousSuccessfulReport[IN, OUT](b, sequence.def, input); found {
134+
executionReports, err := b.reporter.GetExecutionReports(previousReport.ID)
135+
if err != nil {
136+
return SequenceReport[IN, OUT]{}, err
137+
}
138+
b.Logger.Infow("Sequence already executed. Returning previous result", "id", sequence.def.ID,
139+
"version", sequence.def.Version, "description", sequence.def.Description)
140+
return SequenceReport[IN, OUT]{previousReport, executionReports}, nil
141+
}
142+
143+
b.Logger.Infow("Executing sequence", "id", sequence.def.ID,
144+
"version", sequence.def.Version, "description", sequence.def.Description)
145+
recentReporter := NewRecentMemoryReporter(b.reporter)
146+
newBundle := Bundle{
147+
Logger: b.Logger,
148+
GetContext: b.GetContext,
149+
reporter: recentReporter,
150+
reportHashCache: b.reportHashCache,
151+
}
152+
ret, err := sequence.handler(newBundle, deps, input)
153+
if errors.Is(err, ErrNotSerializable) {
154+
return SequenceReport[IN, OUT]{}, err
155+
}
156+
157+
if err == nil && !IsSerializable(b.Logger, ret) {
158+
return SequenceReport[IN, OUT]{}, fmt.Errorf("sequence %s output: %w", sequence.def.ID, ErrNotSerializable)
159+
}
160+
161+
recentReports := recentReporter.GetRecentReports()
162+
childReports := make([]string, 0, len(recentReports))
163+
for _, rep := range recentReports {
164+
childReports = append(childReports, rep.ID)
165+
}
166+
167+
report := NewReport(
168+
sequence.def,
169+
input,
170+
ret,
171+
err,
172+
childReports...,
173+
)
174+
175+
err = b.reporter.AddReport(genericReport(report))
176+
if err != nil {
177+
return SequenceReport[IN, OUT]{}, err
178+
}
179+
executionReports, err := b.reporter.GetExecutionReports(report.ID)
180+
if err != nil {
181+
return SequenceReport[IN, OUT]{}, err
182+
}
183+
if report.Err != nil {
184+
return SequenceReport[IN, OUT]{report, executionReports}, report.Err
185+
}
186+
return SequenceReport[IN, OUT]{report, executionReports}, nil
187+
}
188+
189+
// NewUnrecoverableError creates an error that indicates an unrecoverable error.
190+
// If this error is returned inside an operation, the operation will no longer retry.
191+
// This allows the operation to fail fast if it encounters an unrecoverable error.
192+
func NewUnrecoverableError(err error) error {
193+
return retry.Unrecoverable(err)
194+
}
195+
196+
func loadPreviousSuccessfulReport[IN, OUT any](
197+
b Bundle, def Definition, input IN,
198+
) (Report[IN, OUT], bool) {
199+
prevReports, err := b.reporter.GetReports()
200+
if err != nil {
201+
b.Logger.Errorw("Failed to get reports", "error", err)
202+
return Report[IN, OUT]{}, false
203+
}
204+
currentHash, err := constructUniqueHashFrom(b.reportHashCache, def, input)
205+
if err != nil {
206+
b.Logger.Errorw("Failed to construct unique hash", "error", err)
207+
return Report[IN, OUT]{}, false
208+
}
209+
210+
for _, report := range prevReports {
211+
// Check if operation/sequence was run previously and return the report if successful
212+
reportHash, err := constructUniqueHashFrom(b.reportHashCache, report.Def, report.Input)
213+
if err != nil {
214+
b.Logger.Errorw("Failed to construct unique hash for previous report", "error", err)
215+
continue
216+
}
217+
if reportHash == currentHash && report.Err == nil {
218+
typedReport, ok := typeReport[IN, OUT](report)
219+
if !ok {
220+
b.Logger.Debugw(fmt.Sprintf("Previous %s execution found but couldn't find its matching Report", def.ID), "report_id", report.ID)
221+
continue
222+
}
223+
b.Logger.Debugw(fmt.Sprintf("Previous %s execution found. Returning its result from Report storage", def.ID), "report_id", report.ID)
224+
return typedReport, true
225+
}
226+
}
227+
// No previous execution was found
228+
return Report[IN, OUT]{}, false
229+
}

0 commit comments

Comments
 (0)