Skip to content

Commit bd9c24a

Browse files
with timeout
1 parent 2240e92 commit bd9c24a

File tree

3 files changed

+110
-12
lines changed

3 files changed

+110
-12
lines changed

engine/cld/mcms/proposalanalysis/engine.go

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"io"
77
"maps"
88
"slices"
9+
"time"
910

1011
"github.com/samber/lo"
1112
"github.com/smartcontractkit/mcms"
@@ -34,12 +35,13 @@ type analyzerEngine struct {
3435
solanaRegistry experimentalanalyzer.SolanaDecoderRegistry
3536
executionContext types.ExecutionContext // Store for formatters
3637
logger logger.Logger
38+
analyzerTimeout time.Duration
3739
}
3840

3941
var _ types.AnalyzerEngine = &analyzerEngine{}
4042

4143
// NewAnalyzerEngine creates a new analyzer engine
42-
// Options can be provided to customize the engine behavior, such as injecting registries and logger
44+
// Options can be provided to customize the engine behavior, such as injecting registries, logger, and timeouts
4345
func NewAnalyzerEngine(opts ...EngineOption) types.AnalyzerEngine {
4446
// Apply options to get configuration
4547
cfg := ApplyEngineOptions(opts...)
@@ -50,6 +52,7 @@ func NewAnalyzerEngine(opts ...EngineOption) types.AnalyzerEngine {
5052
evmRegistry: cfg.GetEVMRegistry(),
5153
solanaRegistry: cfg.GetSolanaRegistry(),
5254
logger: cfg.GetLogger(),
55+
analyzerTimeout: cfg.GetAnalyzerTimeout(),
5356
}
5457
return engine
5558
}
@@ -262,9 +265,17 @@ func (ae *analyzerEngine) analyzeProposal(
262265
continue
263266
}
264267

265-
annotations, err := proposalAnalyzer.Analyze(ctx, req, decodedProposal)
268+
// Execute analyzer with timeout
269+
analyzerCtx, cancel := context.WithTimeout(ctx, ae.analyzerTimeout)
270+
annotations, err := proposalAnalyzer.Analyze(analyzerCtx, req, decodedProposal)
271+
cancel() // Always cancel to free resources
272+
266273
if err != nil {
267-
ae.logger.Errorw("Proposal analyzer failed", "analyzerID", proposalAnalyzer.ID(), "error", err)
274+
if analyzerCtx.Err() == context.DeadlineExceeded {
275+
ae.logger.Errorw("Proposal analyzer timed out", "analyzerID", proposalAnalyzer.ID(), "timeout", ae.analyzerTimeout)
276+
} else {
277+
ae.logger.Errorw("Proposal analyzer failed", "analyzerID", proposalAnalyzer.ID(), "error", err)
278+
}
268279
continue
269280
}
270281
// Track which analyzer created the annotations
@@ -332,9 +343,17 @@ func (ae *analyzerEngine) analyzeBatchOperation(
332343
continue
333344
}
334345

335-
annotations, err := batchOpAnalyzer.Analyze(ctx, req, decodedBatchOperation)
346+
// Execute analyzer with timeout
347+
analyzerCtx, cancel := context.WithTimeout(ctx, ae.analyzerTimeout)
348+
annotations, err := batchOpAnalyzer.Analyze(analyzerCtx, req, decodedBatchOperation)
349+
cancel() // Always cancel to free resources
350+
336351
if err != nil {
337-
ae.logger.Errorw("Batch operation analyzer failed", "analyzerID", batchOpAnalyzer.ID(), "chainSelector", decodedBatchOperation.ChainSelector(), "error", err)
352+
if analyzerCtx.Err() == context.DeadlineExceeded {
353+
ae.logger.Errorw("Batch operation analyzer timed out", "analyzerID", batchOpAnalyzer.ID(), "chainSelector", decodedBatchOperation.ChainSelector(), "timeout", ae.analyzerTimeout)
354+
} else {
355+
ae.logger.Errorw("Batch operation analyzer failed", "analyzerID", batchOpAnalyzer.ID(), "chainSelector", decodedBatchOperation.ChainSelector(), "error", err)
356+
}
338357
continue
339358
}
340359
trackedAnnotations := trackAnnotations(annotations, batchOpAnalyzer.ID())
@@ -413,9 +432,17 @@ func (ae *analyzerEngine) analyzeCall(
413432
continue
414433
}
415434

416-
annotations, err := callAnalyzer.Analyze(ctx, req, decodedCall)
435+
// Execute analyzer with timeout
436+
analyzerCtx, cancel := context.WithTimeout(ctx, ae.analyzerTimeout)
437+
annotations, err := callAnalyzer.Analyze(analyzerCtx, req, decodedCall)
438+
cancel() // Always cancel to free resources
439+
417440
if err != nil {
418-
ae.logger.Errorw("Call analyzer failed", "analyzerID", callAnalyzer.ID(), "callName", decodedCall.Name(), "error", err)
441+
if analyzerCtx.Err() == context.DeadlineExceeded {
442+
ae.logger.Errorw("Call analyzer timed out", "analyzerID", callAnalyzer.ID(), "callName", decodedCall.Name(), "timeout", ae.analyzerTimeout)
443+
} else {
444+
ae.logger.Errorw("Call analyzer failed", "analyzerID", callAnalyzer.ID(), "callName", decodedCall.Name(), "error", err)
445+
}
419446
continue
420447
}
421448
trackedAnnotations := trackAnnotations(annotations, callAnalyzer.ID())
@@ -467,9 +494,17 @@ func (ae *analyzerEngine) analyzeParameter(
467494
continue
468495
}
469496

470-
annotations, err := paramAnalyzer.Analyze(ctx, req, decodedParameter)
497+
// Execute analyzer with timeout
498+
analyzerCtx, cancel := context.WithTimeout(ctx, ae.analyzerTimeout)
499+
annotations, err := paramAnalyzer.Analyze(analyzerCtx, req, decodedParameter)
500+
cancel() // Always cancel to free resources
501+
471502
if err != nil {
472-
ae.logger.Errorw("Parameter analyzer failed", "analyzerID", paramAnalyzer.ID(), "paramName", decodedParameter.Name(), "paramType", decodedParameter.Type(), "error", err)
503+
if analyzerCtx.Err() == context.DeadlineExceeded {
504+
ae.logger.Errorw("Parameter analyzer timed out", "analyzerID", paramAnalyzer.ID(), "paramName", decodedParameter.Name(), "paramType", decodedParameter.Type(), "timeout", ae.analyzerTimeout)
505+
} else {
506+
ae.logger.Errorw("Parameter analyzer failed", "analyzerID", paramAnalyzer.ID(), "paramName", decodedParameter.Name(), "paramType", decodedParameter.Type(), "error", err)
507+
}
473508
continue
474509
}
475510
trackedAnnotations := trackAnnotations(annotations, paramAnalyzer.ID())

engine/cld/mcms/proposalanalysis/engine_options.go

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
package proposalanalysis
22

33
import (
4+
"time"
5+
46
"github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger"
57
experimentalanalyzer "github.com/smartcontractkit/chainlink-deployments-framework/experimental/analyzer"
68
)
79

10+
// Default timeout for analyzer execution
11+
const DefaultAnalyzerTimeout = 5 * time.Minute
12+
813
// EngineOption configures the analyzer engine using the functional options pattern
914
type EngineOption func(*engineConfig)
1015

1116
// engineConfig holds configuration for the analyzer engine
1217
type engineConfig struct {
13-
evmRegistry experimentalanalyzer.EVMABIRegistry
14-
solanaRegistry experimentalanalyzer.SolanaDecoderRegistry
15-
logger logger.Logger
18+
evmRegistry experimentalanalyzer.EVMABIRegistry
19+
solanaRegistry experimentalanalyzer.SolanaDecoderRegistry
20+
logger logger.Logger
21+
analyzerTimeout time.Duration
1622
}
1723

1824
// WithEVMRegistry allows injecting an EVM ABI registry into the analyzer engine
@@ -87,3 +93,28 @@ func (cfg *engineConfig) GetLogger() logger.Logger {
8793
}
8894
return cfg.logger
8995
}
96+
97+
// WithAnalyzerTimeout allows configuring the timeout for analyzer execution
98+
// Each analyzer will be given this amount of time to complete before being cancelled
99+
// This is important for analyzers that make network calls or other long-running operations
100+
// Default is 5 minutes if not specified
101+
//
102+
// Example:
103+
//
104+
// engine := proposalanalysis.NewAnalyzerEngine(
105+
// proposalanalysis.WithAnalyzerTimeout(2 * time.Minute),
106+
// )
107+
func WithAnalyzerTimeout(timeout time.Duration) EngineOption {
108+
return func(cfg *engineConfig) {
109+
cfg.analyzerTimeout = timeout
110+
}
111+
}
112+
113+
// GetAnalyzerTimeout returns the analyzer timeout from the config
114+
// Returns DefaultAnalyzerTimeout (5 minutes) if none was provided
115+
func (cfg *engineConfig) GetAnalyzerTimeout() time.Duration {
116+
if cfg.analyzerTimeout == 0 {
117+
return DefaultAnalyzerTimeout
118+
}
119+
return cfg.analyzerTimeout
120+
}

engine/cld/mcms/proposalanalysis/engine_options_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package proposalanalysis
22

33
import (
44
"testing"
5+
"time"
56

67
"github.com/stretchr/testify/assert"
78

@@ -38,4 +39,35 @@ func TestEngineOptions(t *testing.T) {
3839
assert.Nil(t, cfg.GetEVMRegistry())
3940
assert.Nil(t, cfg.GetSolanaRegistry())
4041
})
42+
43+
t.Run("WithAnalyzerTimeout option sets timeout", func(t *testing.T) {
44+
customTimeout := 2 * time.Minute
45+
cfg := ApplyEngineOptions(WithAnalyzerTimeout(customTimeout))
46+
47+
assert.Equal(t, customTimeout, cfg.GetAnalyzerTimeout())
48+
})
49+
50+
t.Run("GetAnalyzerTimeout returns default when not set", func(t *testing.T) {
51+
cfg := ApplyEngineOptions()
52+
53+
timeout := cfg.GetAnalyzerTimeout()
54+
assert.Equal(t, DefaultAnalyzerTimeout, timeout)
55+
assert.Equal(t, 5*time.Minute, timeout)
56+
})
57+
58+
t.Run("all options can be combined including timeout", func(t *testing.T) {
59+
lggr := logger.Test(t)
60+
customTimeout := 1 * time.Minute
61+
cfg := ApplyEngineOptions(
62+
WithLogger(lggr),
63+
WithAnalyzerTimeout(customTimeout),
64+
WithEVMRegistry(nil),
65+
WithSolanaRegistry(nil),
66+
)
67+
68+
assert.NotNil(t, cfg.GetLogger())
69+
assert.Equal(t, customTimeout, cfg.GetAnalyzerTimeout())
70+
assert.Nil(t, cfg.GetEVMRegistry())
71+
assert.Nil(t, cfg.GetSolanaRegistry())
72+
})
4173
}

0 commit comments

Comments
 (0)