Repository: smartcontractkit/chainlink-ccip
Pull request #942: Commit report query limit in exec offchain config
Author: login: 0xnogo
Created: 2025-05-22T12:12:13Z | Merged: 2025-05-22T15:51:53Z

* Introducing the commit report limit in the exec offchain config
* Adding test related to the price reports spam verifying that execution is still happening.
* Reduce the limit from 2000 (1000 * 2) to 500 as the previous limits seems to have triggered CPU util on DB servers. Also it is not needed to query that much as most of the reports are going to be filtered (fully executed for example). Setting the default to 500, but we might need to update it again depending on production environment.

core ref: 055288e08c957c12cc066d087ef95217ae698221


Diff:
diff --git a/execute/factory.go b/execute/factory.go
index 9b04174072..3958a0734a 100644
--- a/execute/factory.go
+++ b/execute/factory.go
@@ -62,9 +62,6 @@ const (
 	// lenientMaxMsgsPerObs is set to the maximum number of messages that can be observed in one observation, this is a bit
 	// lenient and acts as an indicator other than a hard limit.
 	lenientMaxMsgsPerObs = 100
-
-	// maxCommitReportsToFetch is set to the maximum number of commit reports that can be fetched in each round.
-	maxCommitReportsToFetch = 1000
 )
 
 // PluginFactory implements common ReportingPluginFactory and is used for (re-)initializing commit plugin instances.
diff --git a/execute/observation.go b/execute/observation.go
index 9de3898e7d..273959c94a 100644
--- a/execute/observation.go
+++ b/execute/observation.go
@@ -189,6 +189,7 @@ func (p *Plugin) getCommitReportsObservation(
 	}
 
 	// Refresh the commit report cache first
+	// TODO: make this refresh async
 	if err := p.commitReportCache.RefreshCache(ctx); err != nil {
 		// Log error but proceed. If RefreshCache fails, GetReportsToQueryFromTimestamp
 		// will likely use a less optimal (wider) window based on its internal state or defaults,
@@ -218,6 +219,7 @@ func (p *Plugin) getCommitReportsObservation(
 		p.commitRootsCache.CanExecute,
 		fetchFrom,
 		ci.CursedSourceChains,
+		int(p.offchainCfg.MaxCommitReportsToFetch),
 		lggr,
 	)
 	if err != nil {
diff --git a/execute/plugin.go b/execute/plugin.go
index 258d440a8c..cb7faf313c 100644
--- a/execute/plugin.go
+++ b/execute/plugin.go
@@ -286,6 +286,7 @@ func getPendingReportsForExecution(
 	canExecute CanExecuteHandle,
 	fetchFrom time.Time,
 	cursedSourceChains map[cciptypes.ChainSelector]bool,
+	limit int,
 	lggr logger.Logger,
 ) (
 	groupedCommits exectypes.CommitObservations,
@@ -302,7 +303,7 @@ func getPendingReportsForExecution(
 
 	// Assuming each report can have minimum one message, max reports shouldn't exceed the max messages
 	unfinalizedIncrementReports, err := ccipReader.CommitReportsGTETimestamp(
-		ctx, queryFromTimestampForNewReports, primitives.Unconfirmed, maxCommitReportsToFetch,
+		ctx, queryFromTimestampForNewReports, primitives.Unconfirmed, limit,
 	)
 	if err != nil {
 		return nil, nil, nil, err
diff --git a/execute/plugin_test.go b/execute/plugin_test.go
index ecca8cff1d..ac71c1f26c 100644
--- a/execute/plugin_test.go
+++ b/execute/plugin_test.go
@@ -44,6 +44,7 @@ import (
 	"github.com/smartcontractkit/chainlink-ccip/pkg/consts"
 	reader2 "github.com/smartcontractkit/chainlink-ccip/pkg/reader"
 	cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3"
+	"github.com/smartcontractkit/chainlink-ccip/pluginconfig"
 )
 
 func genRandomChainReports(numReports, numMsgsPerReport int) []cciptypes.ExecutePluginReportSingleChain {
@@ -793,29 +794,140 @@ func Test_getPendingReportsForExecution(t *testing.T) {
 			wantExecutedUnfinalized: nil,
 			wantErr:                 assert.NoError,
 		},
+		{
+			name: "filters_price_reports_and_processes_merkle_report",
+			reports: []cciptypes.CommitPluginReportWithMeta{
+				// Price report 1 (no Merkle roots)
+				{
+					BlockNum:  1000,
+					Timestamp: time.UnixMilli(10101010101),
+					Report: cciptypes.CommitPluginReport{
+						BlessedMerkleRoots: nil, // Indicates a price-only report or similar non-executable
+					},
+				},
+				// Price report 2 (no Merkle roots)
+				{
+					BlockNum:  1001,
+					Timestamp: time.UnixMilli(10101010102),
+					Report: cciptypes.CommitPluginReport{
+						BlessedMerkleRoots: []cciptypes.MerkleRootChain{}, // Also indicates non-executable for messages
+					},
+				},
+				// Report with an actual Merkle root
+				{
+					BlockNum:  1002,
+					Timestamp: time.UnixMilli(10101010103),
+					Report: cciptypes.CommitPluginReport{
+						BlessedMerkleRoots: []cciptypes.MerkleRootChain{
+							{
+								ChainSel:     1, // Source chain selector
+								SeqNumsRange: cciptypes.NewSeqNumRange(1, 10),
+								MerkleRoot:   cciptypes.Bytes32{0x01}, // Example Merkle root
+							},
+						},
+					},
+				},
+			},
+			ranges:             nil, // No finalized messages for this new report
+			unfinalizedRanges:  nil, // No unfinalized messages for this new report
+			cursedSourceChains: nil,
+			fetchFrom:          time.Time{},
+			canExec:            canExecute(true), // Assume the Merkle root is executable
+			wantObs: exectypes.CommitObservations{
+				1: []exectypes.CommitData{ // Expecting only the report with the Merkle root to be processed
+					{
+						SourceChain:         1,
+						SequenceNumberRange: cciptypes.NewSeqNumRange(1, 10),
+						Timestamp:           time.UnixMilli(10101010103),
+						BlockNum:            1002,
+						MerkleRoot:          cciptypes.Bytes32{0x01},
+					},
+				},
+			},
+			wantExecutedFinalized:   nil,
+			wantExecutedUnfinalized: nil,
+			wantErr:                 assert.NoError,
+		},
+		{ // New test case for progress with low limit due to cache advancing
+			name: "progress_with_low_limit_due_to_cache_advancing",
+			// reports that the mockReader.CommitReportsGTETimestamp should return in this specific call
+			reports: []cciptypes.CommitPluginReportWithMeta{
+				{
+					BlockNum:  1002,
+					Timestamp: time.UnixMilli(10101010103), // Timestamp of the executable report
+					Report: cciptypes.CommitPluginReport{
+						BlessedMerkleRoots: []cciptypes.MerkleRootChain{
+							{
+								ChainSel:     1,
+								SeqNumsRange: cciptypes.NewSeqNumRange(1, 10),
+								MerkleRoot:   cciptypes.Bytes32{0x01},
+							},
+						},
+					},
+				},
+			},
+			ranges:             nil,
+			unfinalizedRanges:  nil,
+			cursedSourceChains: nil,
+			fetchFrom:          time.UnixMilli(10101010000), // Broader visibility window
+			canExec:            canExecute(true),
+			wantObs: exectypes.CommitObservations{
+				1: []exectypes.CommitData{
+					{
+						SourceChain:         1,
+						SequenceNumberRange: cciptypes.NewSeqNumRange(1, 10),
+						Timestamp:           time.UnixMilli(10101010103),
+						BlockNum:            1002,
+						MerkleRoot:          cciptypes.Bytes32{0x01},
+					},
+				},
+			},
+			wantExecutedFinalized:   nil,
+			wantExecutedUnfinalized: nil,
+			wantErr:                 assert.NoError,
+		},
 	}
 
 	for _, tt := range tcs {
 		t.Run(tt.name, func(t *testing.T) {
+			currentTestLogger := logger.Test(t)
 			if tt.canExec == nil {
 				tt.canExec = canExecute(true)
 			}
 
-			// Create a mock CCIPReader
 			mockReader := readerpkg_mock.NewMockCCIPReader(t)
+			var mockCache cache.CommitReportCache = &noopCommitReportCache{}
 
-			// Create a noopCommitReportCache instead of a mock
-			mockCache := &noopCommitReportCache{}
+			offchainConfigForTest := pluginconfig.ExecuteOffchainConfig{MaxCommitReportsToFetch: 10} // Default for most tests
 
-			// Setup expectations for the CCIPReader
-			mockReader.EXPECT().CommitReportsGTETimestamp(
-				mock.Anything,
-				mock.Anything, // We don't care about the exact timestamp here
-				primitives.Unconfirmed,
-				mock.Anything,
-			).Return(tt.reports, nil)
+			if tt.name == "progress_with_low_limit_due_to_cache_advancing" {
+				configurableCache := &configurableCommitReportCache{
+					reportsToQueryFrom:    time.UnixMilli(10101010102), // Advanced timestamp
+					cachedReportsToReturn: []cciptypes.CommitPluginReportWithMeta{},
+					lggr:                  currentTestLogger,
+				}
+				// For this specific test, DeduplicateReports should directly return what CommitReportsGTETimestamp returns
+				// as we assume cachedReportsToReturn is empty and the new reports are tt.reports.
+				configurableCache.deduplicateShouldReturn = tt.reports
+				mockCache = configurableCache
+				offchainConfigForTest.MaxCommitReportsToFetch = 1 // Crucial for this test case
+
+				mockReader.EXPECT().CommitReportsGTETimestamp(
+					mock.Anything,               // context
+					time.UnixMilli(10101010102), // Expecting the advanced timestamp from cache
+					primitives.Unconfirmed,
+					int(1), // Expecting the low fetch limit
+				).Return(tt.reports, nil)
+			} else {
+				// Default mock setup for other tests
+				mockReader.EXPECT().CommitReportsGTETimestamp(
+					mock.Anything,
+					mock.Anything,
+					primitives.Unconfirmed,
+					mock.Anything, // Most tests use a default MaxCommitReportsToFetch or don't care about this specific arg
+				).Return(tt.reports, nil)
+			}
 
-			// Set up finalized messages mock
 			executed := make(map[cciptypes.ChainSelector][]cciptypes.SeqNum)
 			for k, v := range tt.ranges {
 				if _, exists := executed[k]; !exists {
@@ -825,7 +937,6 @@ func Test_getPendingReportsForExecution(t *testing.T) {
 			}
 			mockReader.EXPECT().ExecutedMessages(mock.Anything, mock.Anything, primitives.Finalized).Return(executed, nil)
 
-			// Set up unfinalized messages mock
 			unfinalized := make(map[cciptypes.ChainSelector][]cciptypes.SeqNum)
 			for k, v := range tt.unfinalizedRanges {
 				if _, exists := unfinalized[k]; !exists {
@@ -842,7 +953,8 @@ func Test_getPendingReportsForExecution(t *testing.T) {
 				tt.canExec,
 				tt.fetchFrom,
 				tt.cursedSourceChains,
-				logger.Test(t),
+				int(offchainConfigForTest.MaxCommitReportsToFetch), // limit int
+				currentTestLogger, // lggr logger.Logger
 			)
 			if !tt.wantErr(t, err, "getPendingReportsForExecution(...)") {
 				return
@@ -2036,3 +2148,33 @@ func TestCommitRootsCache_SkippedRootScenario(t *testing.T) {
 			"Root3 should not be executable")
 	})
 }
+
+// configurableCommitReportCache is a mock for testing specific cache behaviors.
+// Define this near Test_getPendingReportsForExecution or in a test helper section
+type configurableCommitReportCache struct {
+	reportsToQueryFrom      time.Time
+	cachedReportsToReturn   []cciptypes.CommitPluginReportWithMeta
+	lggr                    logger.Logger
+	deduplicateShouldReturn []cciptypes.CommitPluginReportWithMeta // Control what DeduplicateReports returns
+}
+
+func (c *configurableCommitReportCache) RefreshCache(ctx context.Context) error { return nil }
+func (c *configurableCommitReportCache) GetReportsToQueryFromTimestamp() time.Time {
+	return c.reportsToQueryFrom
+}
+func (c *configurableCommitReportCache) GetCachedReports(
+	fromTimestamp time.Time) []cciptypes.CommitPluginReportWithMeta {
+	return c.cachedReportsToReturn
+}
+func (c *configurableCommitReportCache) DeduplicateReports(
+	reports []cciptypes.CommitPluginReportWithMeta,
+) []cciptypes.CommitPluginReportWithMeta {
+	if c.deduplicateShouldReturn != nil {
+		return c.deduplicateShouldReturn
+	}
+	// Fallback to real deduplication if not overridden, requires lggr to be set.
+	if c.lggr != nil {
+		return cache.DeduplicateReports(c.lggr, reports)
+	}
+	return reports // Or panic, or handle error, if lggr is essential but not provided
+}
diff --git a/execute/test_utils.go b/execute/test_utils.go
index 22ecace8e8..8c87453f07 100644
--- a/execute/test_utils.go
+++ b/execute/test_utils.go
@@ -234,6 +234,7 @@ func (it *IntTest) Start() *testhelpers.OCR3Runner[[]byte] {
 	cfg := pluginconfig.ExecuteOffchainConfig{
 		MessageVisibilityInterval: *commonconfig.MustNewDuration(8 * time.Hour),
 		BatchGasLimit:             100000000,
+		MaxCommitReportsToFetch:   10,
 	}
 	chainConfigInfos := []reader.ChainConfigInfo{
 		{
diff --git a/pluginconfig/execute.go b/pluginconfig/execute.go
index 6d3757808b..d965d16ef5 100644
--- a/pluginconfig/execute.go
+++ b/pluginconfig/execute.go
@@ -8,6 +8,10 @@ import (
 	commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config"
 )
 
+const (
+	defaultMaxCommitReportsToFetch = 250
+)
+
 // ExecuteOffchainConfig is the OCR offchainConfig for the exec plugin.
 // This is posted onchain as part of the OCR configuration process of the exec plugin.
 // Every plugin is provided this configuration in its encoded form in the NewReportingPlugin
@@ -45,6 +49,9 @@ type ExecuteOffchainConfig struct {
 	// MaxSingleChainReports is the maximum number of single chain reports that can be included in a report.
 	// When set to 0, this setting is ignored.
 	MaxSingleChainReports uint64 `json:"maxSingleChainReports"`
+
+	// MaxCommitReportsToFetch is the maximum number of commit reports that can be fetched in each round.
+	MaxCommitReportsToFetch uint64 `json:"maxCommitReportsToFetch"`
 }
 
 func (e *ExecuteOffchainConfig) ApplyDefaultsAndValidate() error {
@@ -56,6 +63,9 @@ func (e *ExecuteOffchainConfig) applyDefaults() {
 	if e.TransmissionDelayMultiplier == 0 {
 		e.TransmissionDelayMultiplier = defaultTransmissionDelayMultiplier
 	}
+	if e.MaxCommitReportsToFetch == 0 {
+		e.MaxCommitReportsToFetch = defaultMaxCommitReportsToFetch
+	}
 }
 
 func (e *ExecuteOffchainConfig) Validate() error {
@@ -78,6 +88,10 @@ func (e *ExecuteOffchainConfig) Validate() error {
 		return errors.New("MessageVisibilityInterval not set")
 	}
 
+	if e.MaxCommitReportsToFetch == 0 {
+		return errors.New("MaxCommitReportsToFetch not set")
+	}
+
 	set := make(map[string]struct{})
 	for _, ob := range e.TokenDataObservers {
 		if err := ob.Validate(); err != nil {
diff --git a/pluginconfig/execute_test.go b/pluginconfig/execute_test.go
index 0f5af9a171..e72b627770 100644
--- a/pluginconfig/execute_test.go
+++ b/pluginconfig/execute_test.go
@@ -15,6 +15,7 @@ func TestExecuteOffchainConfig_Validate(t *testing.T) {
 		RootSnoozeTime            commonconfig.Duration
 		MessageVisibilityInterval commonconfig.Duration
 		BatchingStrategyID        uint32
+		MaxCommitReportsToFetch   uint64
 	}
 	tests := []struct {
 		name    string
@@ -29,6 +30,7 @@ func TestExecuteOffchainConfig_Validate(t *testing.T) {
 				RootSnoozeTime:            *commonconfig.MustNewDuration(1),
 				MessageVisibilityInterval: *commonconfig.MustNewDuration(1),
 				BatchingStrategyID:        0,
+				MaxCommitReportsToFetch:   1,
 			},
 			false,
 		},
@@ -40,6 +42,7 @@ func TestExecuteOffchainConfig_Validate(t *testing.T) {
 				RootSnoozeTime:            *commonconfig.MustNewDuration(1),
 				MessageVisibilityInterval: *commonconfig.MustNewDuration(1),
 				BatchingStrategyID:        0,
+				MaxCommitReportsToFetch:   1,
 			},
 			true,
 		},
@@ -51,6 +54,7 @@ func TestExecuteOffchainConfig_Validate(t *testing.T) {
 				RootSnoozeTime:            *commonconfig.MustNewDuration(1),
 				MessageVisibilityInterval: *commonconfig.MustNewDuration(1),
 				BatchingStrategyID:        0,
+				MaxCommitReportsToFetch:   1,
 			},
 			true,
 		},
@@ -62,6 +66,7 @@ func TestExecuteOffchainConfig_Validate(t *testing.T) {
 				RootSnoozeTime:            *commonconfig.MustNewDuration(0),
 				MessageVisibilityInterval: *commonconfig.MustNewDuration(1),
 				BatchingStrategyID:        0,
+				MaxCommitReportsToFetch:   1,
 			},
 			true,
 		},
@@ -73,6 +78,19 @@ func TestExecuteOffchainConfig_Validate(t *testing.T) {
 				RootSnoozeTime:            *commonconfig.MustNewDuration(1),
 				MessageVisibilityInterval: *commonconfig.MustNewDuration(0),
 				BatchingStrategyID:        0,
+				MaxCommitReportsToFetch:   1,
+			},
+			true,
+		},
+		{
+			"invalid, MaxCommitReportsToFetch not set",
+			fields{
+				BatchGasLimit:             1,
+				InflightCacheExpiry:       *commonconfig.MustNewDuration(1),
+				RootSnoozeTime:            *commonconfig.MustNewDuration(1),
+				MessageVisibilityInterval: *commonconfig.MustNewDuration(1),
+				BatchingStrategyID:        0,
+				MaxCommitReportsToFetch:   0,
 			},
 			true,
 		},
@@ -85,6 +103,7 @@ func TestExecuteOffchainConfig_Validate(t *testing.T) {
 				RootSnoozeTime:            tt.fields.RootSnoozeTime,
 				MessageVisibilityInterval: tt.fields.MessageVisibilityInterval,
 				BatchingStrategyID:        tt.fields.BatchingStrategyID,
+				MaxCommitReportsToFetch:   tt.fields.MaxCommitReportsToFetch,
 			}
 			if err := e.Validate(); (err != nil) != tt.wantErr {
 				t.Errorf("ExecuteOffchainConfig.Validate() error = %v, wantErr %v", err, tt.wantErr)
@@ -101,6 +120,7 @@ func TestExecuteOffchainConfig_EncodeDecode(t *testing.T) {
 		MessageVisibilityInterval commonconfig.Duration
 		BatchingStrategyID        uint32
 		TokenDataObserver         []TokenDataObserverConfig
+		MaxCommitReportsToFetch   uint64
 	}
 	tests := []struct {
 		name   string
@@ -114,6 +134,7 @@ func TestExecuteOffchainConfig_EncodeDecode(t *testing.T) {
 				RootSnoozeTime:            *commonconfig.MustNewDuration(1),
 				MessageVisibilityInterval: *commonconfig.MustNewDuration(1),
 				BatchingStrategyID:        0,
+				MaxCommitReportsToFetch:   1,
 			},
 		},
 		{
@@ -124,6 +145,7 @@ func TestExecuteOffchainConfig_EncodeDecode(t *testing.T) {
 				RootSnoozeTime:            *commonconfig.MustNewDuration(1),
 				MessageVisibilityInterval: *commonconfig.MustNewDuration(1),
 				BatchingStrategyID:        0,
+				MaxCommitReportsToFetch:   1,
 			},
 		},
 	}
@@ -136,6 +158,7 @@ func TestExecuteOffchainConfig_EncodeDecode(t *testing.T) {
 				MessageVisibilityInterval: tt.fields.MessageVisibilityInterval,
 				BatchingStrategyID:        tt.fields.BatchingStrategyID,
 				TokenDataObservers:        tt.fields.TokenDataObserver,
+				MaxCommitReportsToFetch:   tt.fields.MaxCommitReportsToFetch,
 			}
 			encoded, err := EncodeExecuteOffchainConfig(e)
 			require.NoError(t, err)
diff --git a/pluginconfig/token_test.go b/pluginconfig/token_test.go
index 2370448385..6eef59abbb 100644
--- a/pluginconfig/token_test.go
+++ b/pluginconfig/token_test.go
@@ -21,7 +21,8 @@ func Test_TokenDataObserver_Unmarshal(t *testing.T) {
 					  "inflightCacheExpiry": "1s",
 					  "rootSnoozeTime": "1s",
 					  "messageVisibilityInterval": "1s",
-					  "batchingStrategyID": 0
+					  "batchingStrategyID": 0,
+					  "maxCommitReportsToFetch": 1
 				}`
 
 	tests := []struct {
@@ -521,6 +522,7 @@ func Test_TokenDataObserver_Validation(t *testing.T) {
 			RootSnoozeTime:            *commonconfig.MustNewDuration(1),
 			MessageVisibilityInterval: *commonconfig.MustNewDuration(1),
 			BatchingStrategyID:        0,
+			MaxCommitReportsToFetch:   1,
 			TokenDataObservers:        configs,
 		}
 	}


Reviews:
user: login: makramkd
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T13:15:00Z
user: login: 0xnogo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T13:19:49Z
user: login: makramkd
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T13:39:09Z
user: login: makramkd
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T13:39:30Z
user: login: 0xnogo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T14:01:10Z
user: login: makramkd
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T14:06:28Z
user: login: 0xnogo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T14:09:40Z
user: login: makramkd
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-22T15:51:51Z

Review comments:
diff_hunk: @@ -45,6 +45,9 @@ type ExecuteOffchainConfig struct {
 	// MaxSingleChainReports is the maximum number of single chain reports that can be included in a report.
 	// When set to 0, this setting is ignored.
 	MaxSingleChainReports uint64 `json:"maxSingleChainReports"`
+
+	// MaxCommitReportsToFetch is the maximum number of commit reports that can be fetched in each round.
+	MaxCommitReportsToFetch uint64 `json:"maxCommitReportsToFetch"`
path: pluginconfig/execute.go
user: login: makramkd
body: I'm not sure if we should add a new required field, things will start failing in production because I'm pretty sure we call `Validate` on the config in constructors.

In order to avoid failures in prod we can set config w/ this new field set explicitly, but it seems error-prone and something that can be easily lost in translation b/w dev and ops.
created_at: 2025-05-22T13:14:38Z
author_association: COLLABORATOR
diff_hunk: @@ -45,6 +45,9 @@ type ExecuteOffchainConfig struct {
 	// MaxSingleChainReports is the maximum number of single chain reports that can be included in a report.
 	// When set to 0, this setting is ignored.
 	MaxSingleChainReports uint64 `json:"maxSingleChainReports"`
+
+	// MaxCommitReportsToFetch is the maximum number of commit reports that can be fetched in each round.
+	MaxCommitReportsToFetch uint64 `json:"maxCommitReportsToFetch"`
path: pluginconfig/execute.go
user: login: 0xnogo
body: Even if it's set in the `DefaultExecuteOffChainCfg`? Chain configs are deriving their config from it.
created_at: 2025-05-22T13:19:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,6 +45,9 @@ type ExecuteOffchainConfig struct {
 	// MaxSingleChainReports is the maximum number of single chain reports that can be included in a report.
 	// When set to 0, this setting is ignored.
 	MaxSingleChainReports uint64 `json:"maxSingleChainReports"`
+
+	// MaxCommitReportsToFetch is the maximum number of commit reports that can be fetched in each round.
+	MaxCommitReportsToFetch uint64 `json:"maxCommitReportsToFetch"`
path: pluginconfig/execute.go
user: login: makramkd
body: There's already a config set in CCIPHome, so if you add this as a required field and deploy this code w/out updating that config, the plugin will just fail to start because of the validation error.
created_at: 2025-05-22T13:39:09Z
author_association: COLLABORATOR
diff_hunk: @@ -45,6 +45,9 @@ type ExecuteOffchainConfig struct {
 	// MaxSingleChainReports is the maximum number of single chain reports that can be included in a report.
 	// When set to 0, this setting is ignored.
 	MaxSingleChainReports uint64 `json:"maxSingleChainReports"`
+
+	// MaxCommitReportsToFetch is the maximum number of commit reports that can be fetched in each round.
+	MaxCommitReportsToFetch uint64 `json:"maxCommitReportsToFetch"`
path: pluginconfig/execute.go
user: login: makramkd
body: Updating config is not trivial, so IMO new fields should have defaults so that stuff doesn't break when we roll out the version.
created_at: 2025-05-22T13:39:30Z
author_association: COLLABORATOR
diff_hunk: @@ -45,6 +45,9 @@ type ExecuteOffchainConfig struct {
 	// MaxSingleChainReports is the maximum number of single chain reports that can be included in a report.
 	// When set to 0, this setting is ignored.
 	MaxSingleChainReports uint64 `json:"maxSingleChainReports"`
+
+	// MaxCommitReportsToFetch is the maximum number of commit reports that can be fetched in each round.
+	MaxCommitReportsToFetch uint64 `json:"maxCommitReportsToFetch"`
path: pluginconfig/execute.go
user: login: 0xnogo
body: Makes sense. Done!
created_at: 2025-05-22T14:01:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -8,6 +8,10 @@ import (
 	commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config"
 )
 
+const (
+	defaultMaxCommitReportsToFetch = 500
path: pluginconfig/execute.go
user: login: makramkd
body: Should the default be the same as before (1000)? Or do we actually want to drop in this PR?
created_at: 2025-05-22T14:06:25Z
author_association: COLLABORATOR
diff_hunk: @@ -8,6 +8,10 @@ import (
 	commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config"
 )
 
+const (
+	defaultMaxCommitReportsToFetch = 500
path: pluginconfig/execute.go
user: login: 0xnogo
body: I actually changed it to 250 + update the PR description

We are currently fetching 1000 * 2. I'm proposing 250 * 2.

Reason: ~750 fetched constantly was enough to trigger some CPU limit on DB side + do we need to fetch that much? Most reports are getting filtered out.

It might be too much changes in one go. So we can maybe set it to 1000.
created_at: 2025-05-22T14:09:39Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-22T14:26:21Z
body: | Metric | `ng/MaxCommitReportsToFetch` | `main` |
|--|--|--|
| **Coverage** | 72.7% | 72.2% |
author_association: NONE