Repository: ethereum-optimism/optimism (Go)

Issue:
op-batcher: DA metrics not reset when state is cleared
Under certain conditions, the batcher will clear it's state and "start fresh". However, it is not always resetting metrics which need to stay in sync with the state which is now cleared. 


https://github.com/ethereum-optimism/optimism/blob/4d832faba7d71ab883c4a6e5d65dcf0ebe925168/op-batcher/batcher/channel_manager.go#L76-L88

The channel queue length metric is correctly reset, but we need to do something similar for `pendingDABytes`, otherwise the batcher may needlessly be throttling the sequencer. 


Discussion:

Patch:
diff --git a/op-batcher/batcher/channel_manager.go b/op-batcher/batcher/channel_manager.go
index 8083cc4c82dae..acb63b74c503c 100644
--- a/op-batcher/batcher/channel_manager.go
+++ b/op-batcher/batcher/channel_manager.go
@@ -83,7 +83,10 @@ func (s *channelManager) Clear(l1OriginLastSubmittedChannel eth.BlockID) {
 	s.tip = common.Hash{}
 	s.currentChannel = nil
 	s.channelQueue = nil
-	s.metr.RecordChannelQueueLength(0)
+
+	// This is particularly important because pendingDABytes metric controls throttling:
+	s.metr.ClearAllStateMetrics()
+
 	s.txChannels = make(map[string]*channel)
 }
 
diff --git a/op-batcher/metrics/metrics.go b/op-batcher/metrics/metrics.go
index 68033f376bc00..4eee89e5186f9 100644
--- a/op-batcher/metrics/metrics.go
+++ b/op-batcher/metrics/metrics.go
@@ -44,6 +44,10 @@ type Metricer interface {
 	RecordChannelTimedOut(id derive.ChannelID)
 	RecordChannelQueueLength(len int)
 
+	// ClearAllStateMetrics resets any metrics that track current ChannelManager state
+	// It should be called when clearing the ChannelManager state.
+	ClearAllStateMetrics()
+
 	RecordBatchTxSubmitted()
 	RecordBatchTxSuccess()
 	RecordBatchTxFailed()
@@ -349,6 +353,17 @@ func (m *Metrics) RecordChannelQueueLength(len int) {
 	m.channelQueueLength.Set(float64(len))
 }
 
+// ClearAllStateMetrics clears all state metrics.
+//
+// This should cover any metric which is a Gauge and is incremented / decremented rather than "set".
+// Counter Metrics only ever go up, so they can't be reset and shouldn't be.
+// Gauge Metrics which are "set" will get the right value the next time they are updated and don't need to be reset.
+func (m *Metrics) ClearAllStateMetrics() {
+	m.RecordChannelQueueLength(0)
+	atomic.StoreInt64(&m.pendingDABytes, 0)
+	m.pendingBlocksBytesCurrent.Set(0)
+}
+
 // estimateBatchSize returns the estimated size of the block in a batch both with compression ('daSize') and without
 // ('rawSize').
 func estimateBatchSize(block *types.Block) (daSize, rawSize uint64) {
diff --git a/op-batcher/metrics/noop.go b/op-batcher/metrics/noop.go
index 9fb7b0342a56e..fc6795058f8a9 100644
--- a/op-batcher/metrics/noop.go
+++ b/op-batcher/metrics/noop.go
@@ -61,3 +61,5 @@ type ThrottlingMetrics struct {
 func (nm *ThrottlingMetrics) PendingDABytes() float64 {
 	return math.MaxFloat64
 }
+
+func (*noopMetrics) ClearAllStateMetrics() {}


Test patch:
diff --git a/op-batcher/batcher/channel_manager_test.go b/op-batcher/batcher/channel_manager_test.go
index 8dbab2f924042..2685c9359543a 100644
--- a/op-batcher/batcher/channel_manager_test.go
+++ b/op-batcher/batcher/channel_manager_test.go
@@ -122,7 +122,7 @@ func ChannelManager_Clear(t *testing.T, batchType uint) {
 	// clearing confirmed transactions, and resetting the pendingChannels map
 	cfg.ChannelTimeout = 10
 	cfg.InitRatioCompressor(1, derive.Zlib)
-	m := NewChannelManager(log, metrics.NoopMetrics, cfg, defaultTestRollupConfig)
+	m := NewChannelManager(log, metrics.NewMetrics("test"), cfg, defaultTestRollupConfig)
 
 	// Channel Manager state should be empty by default
 	require.Empty(m.blocks)
@@ -150,7 +150,6 @@ func ChannelManager_Clear(t *testing.T, batchType uint) {
 
 	// Process the blocks
 	// We should have a pending channel with 1 frame
-
 	require.NoError(m.processBlocks())
 	require.NoError(m.currentChannel.channelBuilder.co.Flush())
 	require.NoError(m.outputFrames())
@@ -174,6 +173,11 @@ func ChannelManager_Clear(t *testing.T, batchType uint) {
 	safeL1Origin := eth.BlockID{
 		Number: 123,
 	}
+
+	// Artificially pump up some metrics which need to be cleared
+	m.metr.RecordL2BlockInPendingQueue(a)
+	require.NotZero(m.metr.PendingDABytes())
+
 	// Clear the channel manager
 	m.Clear(safeL1Origin)
 
@@ -184,6 +188,7 @@ func ChannelManager_Clear(t *testing.T, batchType uint) {
 	require.Nil(m.currentChannel)
 	require.Empty(m.channelQueue)
 	require.Empty(m.txChannels)
+	require.Zero(m.metr.PendingDABytes())
 }
 
 func ChannelManager_TxResend(t *testing.T, batchType uint) {
diff --git a/op-batcher/metrics/test.go b/op-batcher/metrics/test.go
index 7e2ce2959767e..dc4c759ab5b3e 100644
--- a/op-batcher/metrics/test.go
+++ b/op-batcher/metrics/test.go
@@ -8,19 +8,29 @@ type TestMetrics struct {
 	noopMetrics
 	PendingBlocksBytesCurrent float64
 	ChannelQueueLength        int
+	pendingDABytes            float64
 }
 
 var _ Metricer = new(TestMetrics)
 
 func (m *TestMetrics) RecordL2BlockInPendingQueue(block *types.Block) {
-	_, rawSize := estimateBatchSize(block)
+	daSize, rawSize := estimateBatchSize(block)
 	m.PendingBlocksBytesCurrent += float64(rawSize)
-
+	m.pendingDABytes += float64(daSize)
 }
 func (m *TestMetrics) RecordL2BlockInChannel(block *types.Block) {
-	_, rawSize := estimateBatchSize(block)
+	daSize, rawSize := estimateBatchSize(block)
 	m.PendingBlocksBytesCurrent -= float64(rawSize)
+	m.pendingDABytes -= float64(daSize)
 }
 func (m *TestMetrics) RecordChannelQueueLength(l int) {
 	m.ChannelQueueLength = l
 }
+func (m *TestMetrics) PendingDABytes() float64 {
+	return m.pendingDABytes
+}
+func (m *TestMetrics) ClearAllStateMetrics() {
+	m.PendingBlocksBytesCurrent = 0
+	m.ChannelQueueLength = 0
+	m.pendingDABytes = 0
+}