Skip to content

Commit 4fe06c5

Browse files
feat: add DRY_RUN config option (#97)
* feat: add DRY_RUN config option Add a `DRY_RUN` configuration option that controls whether the service executes operations or simply logs the received events. This is a temporary feature needed to help test the recently added logic to monitor contract events using http connections without causing any side-effects. * review: add "scheduler" and "nopScheduler" types Instead of using the `dryRun` flag to control whether or not operations are added to the (standard) scheduler, we now select the type of the scheduler pass to the timelock worker service: * if dryRun is false, use the standard scheduler * if dryRun is true, use the new "nop" scheduler, which only logs the calls but does not do anything In practice the "standard scheduler" is a new type + interface as well, since the existing implementation defined a the schedule as a simple data type which was associated with the timelock worker via implicit composition (though all the schedule related methods were defined on the timelock worker type).
1 parent 1b171e4 commit 4fe06c5

File tree

14 files changed

+472
-220
lines changed

14 files changed

+472
-220
lines changed

cmd/start.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ func startCommand() *cobra.Command {
2020

2121
nodeURL, privateKey, timelockAddress, callProxyAddress string
2222
fromBlock, pollPeriod, eventListenerPollPeriod int64
23+
dryRun bool
2324
)
2425

2526
// Initialize timelock-worker configuration.
@@ -41,6 +42,7 @@ func startCommand() *cobra.Command {
4142
startCmd.Flags().Int64Var(&fromBlock, "from-block", timelockConf.FromBlock, "Start watching from this block")
4243
startCmd.Flags().Int64Var(&pollPeriod, "poll-period", timelockConf.PollPeriod, "Poll period in seconds")
4344
startCmd.Flags().Int64Var(&eventListenerPollPeriod, "event-listener-poll-period", timelockConf.EventListenerPollPeriod, "Event Listener poll period in seconds")
45+
startCmd.Flags().BoolVar(&dryRun, "dry-run", timelockConf.DryRun, "Enable \"dry run\" mode -- monitor events but don't trigger any calls")
4446

4547
return &startCmd
4648
}
@@ -87,8 +89,13 @@ func startTimelock(cmd *cobra.Command) {
8789
logs.Fatal().Msgf("value of poll-period not set: %s", err.Error())
8890
}
8991

92+
dryRun, err := cmd.Flags().GetBool("dry-run")
93+
if err != nil {
94+
logs.Fatal().Msgf("value of dry-run not set: %s", err.Error())
95+
}
96+
9097
tWorker, err := timelock.NewTimelockWorker(nodeURL, timelockAddress, callProxyAddress, privateKey,
91-
big.NewInt(fromBlock), pollPeriod, eventListenerPollPeriod, logs)
98+
big.NewInt(fromBlock), pollPeriod, eventListenerPollPeriod, dryRun, logs)
9299
if err != nil {
93100
logs.Fatal().Msgf("error creating the timelock-worker: %s", err.Error())
94101
}

go.mod

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ go 1.22
55
require (
66
github.com/docker/go-connections v0.5.0
77
github.com/ethereum/go-ethereum v1.13.15
8+
github.com/google/go-cmp v0.6.0
89
github.com/prometheus/client_golang v1.19.1
910
github.com/rs/zerolog v1.33.0
11+
github.com/samber/lo v1.47.0
1012
github.com/smartcontractkit/ccip-owner-contracts v0.0.0-20240917103524-56f1a8d2cd4b
1113
github.com/smartcontractkit/chain-selectors v1.0.17
1214
github.com/spf13/cobra v1.8.1
@@ -95,9 +97,9 @@ require (
9597
go.uber.org/multierr v1.9.0 // indirect
9698
golang.org/x/crypto v0.22.0 // indirect
9799
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
98-
golang.org/x/sync v0.6.0 // indirect
100+
golang.org/x/sync v0.7.0 // indirect
99101
golang.org/x/sys v0.21.0 // indirect
100-
golang.org/x/text v0.15.0 // indirect
102+
golang.org/x/text v0.16.0 // indirect
101103
google.golang.org/protobuf v1.33.0 // indirect
102104
gopkg.in/ini.v1 v1.67.0 // indirect
103105
gopkg.in/yaml.v3 v3.0.1 // indirect

go.sum

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,8 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke
269269
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
270270
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
271271
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
272+
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
273+
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
272274
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
273275
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
274276
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
@@ -389,8 +391,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
389391
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
390392
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
391393
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
392-
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
393-
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
394+
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
395+
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
394396
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
395397
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
396398
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -430,8 +432,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
430432
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
431433
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
432434
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
433-
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
434-
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
435+
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
436+
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
435437
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
436438
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
437439
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

pkg/cli/config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package cli
33
import (
44
"fmt"
55
"os"
6+
"slices"
67
"strconv"
8+
"strings"
79

810
"github.com/spf13/viper"
911
)
@@ -17,6 +19,7 @@ type Config struct {
1719
FromBlock int64 `mapstructure:"FROM_BLOCK"`
1820
PollPeriod int64 `mapstructure:"POLL_PERIOD"`
1921
EventListenerPollPeriod int64 `mapstructure:"EVENT_LISTENER_POLL_PERIOD"`
22+
DryRun bool `mapstructure:"DRY_RUN"`
2023
}
2124

2225
// NewTimelockCLI return a new Timelock instance configured.
@@ -80,5 +83,10 @@ func NewTimelockCLI() (*Config, error) {
8083
c.EventListenerPollPeriod = int64(pp)
8184
}
8285

86+
if os.Getenv("DRY_RUN") != "" {
87+
trueValues := []string{"true", "yes", "on", "enabled", "1"}
88+
c.DryRun = slices.Contains(trueValues, strings.ToLower(os.Getenv("DRY_RUN")))
89+
}
90+
8391
return &c, nil
8492
}

pkg/cli/config_test.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func Test_NewTimelockCLI(t *testing.T) {
2121
name: "load from file",
2222
setup: func(t *testing.T) {
2323
unsetenvs(t, "NODE_URL", "TIMELOCK_ADDRESS", "CALL_PROXY_ADDRESS", "PRIVATE_KEY", "FROM_BLOCK",
24-
"POLL_PERIOD", "EVENT_LISTENER_POLL_PERIOD")
24+
"POLL_PERIOD", "EVENT_LISTENER_POLL_PERIOD", "DRY_RUN")
2525

2626
err := os.WriteFile(configFileName, []byte(string(
2727
"NODE_URL=wss://goerli/test\n"+
@@ -30,7 +30,8 @@ func Test_NewTimelockCLI(t *testing.T) {
3030
"PRIVATE_KEY=9876543210\n"+
3131
"FROM_BLOCK=1\n"+
3232
"POLL_PERIOD=2\n"+
33-
"EVENT_LISTENER_POLL_PERIOD=3\n",
33+
"EVENT_LISTENER_POLL_PERIOD=3\n"+
34+
"DRY_RUN=true\n",
3435
)), os.FileMode(0644))
3536
require.NoError(t, err)
3637

@@ -44,6 +45,7 @@ func Test_NewTimelockCLI(t *testing.T) {
4445
FromBlock: 1,
4546
PollPeriod: 2,
4647
EventListenerPollPeriod: 3,
48+
DryRun: true,
4749
},
4850
},
4951
{
@@ -56,7 +58,8 @@ func Test_NewTimelockCLI(t *testing.T) {
5658
"PRIVATE_KEY=9876543210\n"+
5759
"FROM_BLOCK=1\n"+
5860
"POLL_PERIOD=2\n"+
59-
"EVENT_LISTENER_POLL_PERIOD=3\n",
61+
"EVENT_LISTENER_POLL_PERIOD=3\n"+
62+
"DRY_RUN=true\n",
6063
)), os.FileMode(0644))
6164
require.NoError(t, err)
6265

@@ -67,6 +70,7 @@ func Test_NewTimelockCLI(t *testing.T) {
6770
t.Setenv("FROM_BLOCK", "4")
6871
t.Setenv("POLL_PERIOD", "5")
6972
t.Setenv("EVENT_LISTENER_POLL_PERIOD", "6")
73+
t.Setenv("DRY_RUN", "false")
7074

7175
t.Cleanup(func() { os.Remove(configFileName) })
7276
},
@@ -95,6 +99,7 @@ func Test_NewTimelockCLI(t *testing.T) {
9599
t.Setenv("FROM_BLOCK", "4")
96100
t.Setenv("POLL_PERIOD", "5")
97101
t.Setenv("EVENT_LISTENER_POLL_PERIOD", "6")
102+
t.Setenv("DRY_RUN", "yes")
98103

99104
t.Cleanup(func() { os.Remove(configFileName) })
100105
},
@@ -106,6 +111,7 @@ func Test_NewTimelockCLI(t *testing.T) {
106111
FromBlock: 4,
107112
PollPeriod: 5,
108113
EventListenerPollPeriod: 6,
114+
DryRun: true,
109115
},
110116
},
111117
{

pkg/timelock/const_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ var (
1515
testFromBlock = big.NewInt(0)
1616
testPollPeriod = 5
1717
testEventListenerPollPeriod = 0
18+
testDryRun = false
1819
testLogger = logger.Logger("info", "human")
1920
)

pkg/timelock/operations_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212

1313
func Test_isOperation(t *testing.T) {
1414
testWorker := newTestTimelockWorker(t, testNodeURL, testTimelockAddress, testCallProxyAddress, testPrivateKey,
15-
testFromBlock, int64(testPollPeriod), int64(testEventListenerPollPeriod), testLogger)
15+
testFromBlock, int64(testPollPeriod), int64(testEventListenerPollPeriod), testDryRun, testLogger)
1616

1717
var ctx context.Context
1818

@@ -56,7 +56,7 @@ func Test_isOperation(t *testing.T) {
5656

5757
func Test_isReady(t *testing.T) {
5858
testWorker := newTestTimelockWorker(t, testNodeURL, testTimelockAddress, testCallProxyAddress, testPrivateKey,
59-
testFromBlock, int64(testPollPeriod), int64(testEventListenerPollPeriod), testLogger)
59+
testFromBlock, int64(testPollPeriod), int64(testEventListenerPollPeriod), testDryRun, testLogger)
6060

6161
var ctx context.Context
6262

@@ -100,7 +100,7 @@ func Test_isReady(t *testing.T) {
100100

101101
func Test_isDone(t *testing.T) {
102102
testWorker := newTestTimelockWorker(t, testNodeURL, testTimelockAddress, testCallProxyAddress, testPrivateKey,
103-
testFromBlock, int64(testPollPeriod), int64(testEventListenerPollPeriod), testLogger)
103+
testFromBlock, int64(testPollPeriod), int64(testEventListenerPollPeriod), testDryRun, testLogger)
104104

105105
var ctx context.Context
106106

@@ -144,7 +144,7 @@ func Test_isDone(t *testing.T) {
144144

145145
func Test_isPending(t *testing.T) {
146146
testWorker := newTestTimelockWorker(t, testNodeURL, testTimelockAddress, testCallProxyAddress, testPrivateKey,
147-
testFromBlock, int64(testPollPeriod), int64(testEventListenerPollPeriod), testLogger)
147+
testFromBlock, int64(testPollPeriod), int64(testEventListenerPollPeriod), testDryRun, testLogger)
148148

149149
var ctx context.Context
150150

pkg/timelock/scheduler.go

Lines changed: 68 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,39 @@ import (
1818

1919
type operationKey [32]byte
2020

21+
type Scheduler interface {
22+
runScheduler(ctx context.Context) <-chan struct{}
23+
addToScheduler(op *contract.TimelockCallScheduled)
24+
delFromScheduler(op operationKey)
25+
dumpOperationStore(now func() time.Time)
26+
}
27+
28+
type executeFn func(context.Context, []*contract.TimelockCallScheduled)
29+
2130
// Scheduler represents a scheduler with an in memory store.
2231
// Whenever accesing the map the mutex should be Locked, to prevent
2332
// any race condition.
2433
type scheduler struct {
25-
mu sync.Mutex
26-
ticker *time.Ticker
27-
add chan *contract.TimelockCallScheduled
28-
del chan operationKey
29-
store map[operationKey][]*contract.TimelockCallScheduled
30-
busy bool
34+
mu sync.Mutex
35+
ticker *time.Ticker
36+
add chan *contract.TimelockCallScheduled
37+
del chan operationKey
38+
store map[operationKey][]*contract.TimelockCallScheduled
39+
busy bool
40+
logger *zerolog.Logger
41+
executeFn executeFn
3142
}
3243

3344
// newScheduler returns a new initialized scheduler.
34-
func newScheduler(tick time.Duration) *scheduler {
45+
func newScheduler(tick time.Duration, logger *zerolog.Logger, executeFn executeFn) *scheduler {
3546
s := &scheduler{
36-
ticker: time.NewTicker(tick),
37-
add: make(chan *contract.TimelockCallScheduled),
38-
del: make(chan operationKey),
39-
store: make(map[operationKey][]*contract.TimelockCallScheduled),
40-
busy: false,
47+
ticker: time.NewTicker(tick),
48+
add: make(chan *contract.TimelockCallScheduled),
49+
del: make(chan operationKey),
50+
store: make(map[operationKey][]*contract.TimelockCallScheduled),
51+
busy: false,
52+
logger: logger,
53+
executeFn: executeFn,
4154
}
4255

4356
return s
@@ -50,7 +63,7 @@ func newScheduler(tick time.Duration) *scheduler {
5063
// call them this way so no process is allowd to add/delete from
5164
// the store, which could cause race conditions like adding/deleting
5265
// while the operation is being executed.
53-
func (tw *Worker) runScheduler(ctx context.Context) <-chan struct{} {
66+
func (tw *scheduler) runScheduler(ctx context.Context) <-chan struct{} {
5467
done := make(chan struct{})
5568

5669
go func() {
@@ -67,7 +80,7 @@ func (tw *Worker) runScheduler(ctx context.Context) <-chan struct{} {
6780
tw.logger.Debug().Msgf("new scheduler tick: operations in store")
6881
tw.setSchedulerBusy()
6982
for _, op := range tw.store {
70-
tw.execute(ctx, op)
83+
tw.executeFn(ctx, op)
7184
}
7285
tw.setSchedulerFree()
7386
} else {
@@ -102,7 +115,7 @@ func (tw *Worker) runScheduler(ctx context.Context) <-chan struct{} {
102115
}
103116

104117
// updateSchedulerDelay updates the internal ticker delay, so it can be reconfigured while running.
105-
func (tw *Worker) updateSchedulerDelay(t time.Duration) {
118+
func (tw *scheduler) updateSchedulerDelay(t time.Duration) {
106119
if t <= 0 {
107120
tw.logger.Debug().Msgf("internal min delay not changed, invalid duration: %v", t.String())
108121
return
@@ -113,40 +126,38 @@ func (tw *Worker) updateSchedulerDelay(t time.Duration) {
113126
}
114127

115128
// addToScheduler adds a new CallSchedule operation safely to the store.
116-
func (tw *Worker) addToScheduler(op *contract.TimelockCallScheduled) {
129+
func (tw *scheduler) addToScheduler(op *contract.TimelockCallScheduled) {
117130
tw.logger.Debug().Msgf("scheduling operation: %x", op.Id)
118131
tw.add <- op
119-
tw.logger.Debug().Msgf("operations in scheduler: %v", len(tw.store))
120132
}
121133

122134
// delFromScheduler deletes an operation safely from the store.
123-
func (tw *Worker) delFromScheduler(op operationKey) {
135+
func (tw *scheduler) delFromScheduler(op operationKey) {
124136
tw.logger.Debug().Msgf("de-scheduling operation: %v", op)
125137
tw.del <- op
126-
tw.logger.Debug().Msgf("operations in scheduler: %v", len(tw.store))
127138
}
128139

129-
func (tw *Worker) setSchedulerBusy() {
140+
func (tw *scheduler) setSchedulerBusy() {
130141
tw.logger.Debug().Msgf("setting scheduler busy")
131142
tw.mu.Lock()
132143
tw.busy = true
133144
tw.mu.Unlock()
134145
}
135146

136-
func (tw *Worker) setSchedulerFree() {
147+
func (tw *scheduler) setSchedulerFree() {
137148
tw.logger.Debug().Msgf("setting scheduler free")
138149
tw.mu.Lock()
139150
tw.busy = false
140151
tw.mu.Unlock()
141152
}
142153

143-
func (tw *Worker) isSchedulerBusy() bool {
154+
func (tw *scheduler) isSchedulerBusy() bool {
144155
return tw.busy
145156
}
146157

147158
// dumpOperationStore dumps to the logger and to the log file the current scheduled unexecuted operations.
148159
// maps in go don't guarantee order, so that's why we have to find the earliest block.
149-
func (tw *Worker) dumpOperationStore(now func() time.Time) {
160+
func (tw *scheduler) dumpOperationStore(now func() time.Time) {
150161
if len(tw.store) <= 0 {
151162
tw.logger.Info().Msgf("no operations to dump")
152163
return
@@ -249,3 +260,37 @@ func toEarliestRecord(op *contract.TimelockCallScheduled) string {
249260
func toSubsequentRecord(op *contract.TimelockCallScheduled) string {
250261
return fmt.Sprintf("CallSchedule pending ID: %x\tBlock Number: %v\n", op.Id, op.Raw.BlockNumber)
251262
}
263+
264+
// ----- nop scheduler -----
265+
// nopScheduler implements the Scheduler interface but doesn't not effectively trigger any operations.
266+
type nopScheduler struct {
267+
logger *zerolog.Logger
268+
}
269+
270+
func newNopScheduler(logger *zerolog.Logger) *nopScheduler {
271+
return &nopScheduler{logger: logger}
272+
}
273+
274+
func (s *nopScheduler) runScheduler(ctx context.Context) <-chan struct{} {
275+
s.logger.Info().Msg("nop.runScheduler")
276+
ch := make(chan struct{})
277+
278+
go func() {
279+
<-ctx.Done()
280+
close(ch)
281+
}()
282+
283+
return ch
284+
}
285+
286+
func (s *nopScheduler) addToScheduler(op *contract.TimelockCallScheduled) {
287+
s.logger.Info().Any("op", op).Msg("nop.addToScheduler")
288+
}
289+
290+
func (s *nopScheduler) delFromScheduler(key operationKey) {
291+
s.logger.Info().Any("key", key).Msg("nop.delFromScheduler")
292+
}
293+
294+
func (s *nopScheduler) dumpOperationStore(now func() time.Time) {
295+
s.logger.Info().Msg("nop.dumpOperationStore")
296+
}

0 commit comments

Comments
 (0)