Skip to content

Commit 460ad0f

Browse files
committed
feat: adding additional metric to track total missing blocks
1 parent 8768dbc commit 460ad0f

File tree

2 files changed

+150
-22
lines changed

2 files changed

+150
-22
lines changed

internal/metrics/metrics.go

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import (
1212
// Metrics contains Prometheus metrics for DA verification failures
1313
type Metrics struct {
1414
// track ranges of unsubmitted blocks
15-
UnsubmittedRangeStart *prometheus.GaugeVec
16-
UnsubmittedRangeEnd *prometheus.GaugeVec
15+
UnsubmittedRangeStart *prometheus.GaugeVec
16+
UnsubmittedRangeEnd *prometheus.GaugeVec
17+
UnsubmittedBlocksTotal *prometheus.GaugeVec
1718

1819
mu sync.Mutex
1920
ranges map[string][]*blockRange // key: blobType -> sorted slice of ranges
@@ -49,14 +50,35 @@ func NewWithRegistry(namespace string, registerer prometheus.Registerer) *Metric
4950
},
5051
[]string{"chain_id", "blob_type", "range_id"},
5152
),
53+
UnsubmittedBlocksTotal: factory.NewGaugeVec(
54+
prometheus.GaugeOpts{
55+
Namespace: namespace,
56+
Name: "unsubmitted_blocks_total",
57+
Help: "total number of unsubmitted blocks",
58+
},
59+
[]string{"chain_id", "blob_type"},
60+
),
5261
ranges: make(map[string][]*blockRange),
5362
}
5463
}
5564

65+
// RecordTotalMissingBlocks updates the total count of missing blocks metric.
66+
func (m *Metrics) RecordTotalMissingBlocks(chainID, blobType string) {
67+
ranges := m.ranges[blobType]
68+
total := uint64(0)
69+
for _, r := range ranges {
70+
total += r.end - r.start + 1 // inclusive count
71+
}
72+
m.UnsubmittedBlocksTotal.WithLabelValues(chainID, blobType).Set(float64(total))
73+
}
74+
5675
// RecordMissingBlock records a block that is missing from Celestia
57-
func (m *Metrics) RecordMissingBlock(chain, blobType string, blockHeight uint64) {
76+
func (m *Metrics) RecordMissingBlock(chainID, blobType string, blockHeight uint64) {
5877
m.mu.Lock()
59-
defer m.mu.Unlock()
78+
defer func() {
79+
m.RecordTotalMissingBlocks(chainID, blobType)
80+
m.mu.Unlock()
81+
}()
6082

6183
ranges := m.ranges[blobType]
6284
if ranges == nil {
@@ -81,12 +103,12 @@ func (m *Metrics) RecordMissingBlock(chain, blobType string, blockHeight uint64)
81103
leftRange := ranges[idx-1]
82104
rightRange := ranges[idx]
83105

84-
m.deleteRange(chain, blobType, leftRange)
85-
m.deleteRange(chain, blobType, rightRange)
106+
m.deleteRange(chainID, blobType, leftRange)
107+
m.deleteRange(chainID, blobType, rightRange)
86108

87109
// extend left range to include right range
88110
leftRange.end = rightRange.end
89-
m.updateRange(chain, blobType, leftRange)
111+
m.updateRange(chainID, blobType, leftRange)
90112

91113
// remove right range from slice
92114
m.ranges[blobType] = append(ranges[:idx], ranges[idx+1:]...)
@@ -96,18 +118,18 @@ func (m *Metrics) RecordMissingBlock(chain, blobType string, blockHeight uint64)
96118
if canMergeLeft {
97119
// extend left range
98120
leftRange := ranges[idx-1]
99-
m.deleteRange(chain, blobType, leftRange)
121+
m.deleteRange(chainID, blobType, leftRange)
100122
leftRange.end = blockHeight
101-
m.updateRange(chain, blobType, leftRange)
123+
m.updateRange(chainID, blobType, leftRange)
102124
return
103125
}
104126

105127
if canMergeRight {
106128
// extend right range
107129
rightRange := ranges[idx]
108-
m.deleteRange(chain, blobType, rightRange)
130+
m.deleteRange(chainID, blobType, rightRange)
109131
rightRange.start = blockHeight
110-
m.updateRange(chain, blobType, rightRange)
132+
m.updateRange(chainID, blobType, rightRange)
111133
return
112134
}
113135

@@ -118,14 +140,17 @@ func (m *Metrics) RecordMissingBlock(chain, blobType string, blockHeight uint64)
118140
}
119141
// insert at idx
120142
ranges = append(ranges[:idx], append([]*blockRange{newRange}, ranges[idx:]...)...)
121-
m.updateRange(chain, blobType, newRange)
143+
m.updateRange(chainID, blobType, newRange)
122144
m.ranges[blobType] = ranges
123145
}
124146

125147
// RemoveVerifiedBlock removes a block from the missing ranges when it gets verified
126-
func (m *Metrics) RemoveVerifiedBlock(chain, blobType string, blockHeight uint64) {
148+
func (m *Metrics) RemoveVerifiedBlock(chainID, blobType string, blockHeight uint64) {
127149
m.mu.Lock()
128-
defer m.mu.Unlock()
150+
defer func() {
151+
m.RecordTotalMissingBlocks(chainID, blobType)
152+
m.mu.Unlock()
153+
}()
129154

130155
ranges := m.ranges[blobType]
131156
if ranges == nil {
@@ -147,7 +172,7 @@ func (m *Metrics) RemoveVerifiedBlock(chain, blobType string, blockHeight uint64
147172

148173
// range contains only this block, delete it.
149174
if r.start == r.end {
150-
m.deleteRange(chain, blobType, r)
175+
m.deleteRange(chainID, blobType, r)
151176
// remove range from slice
152177
m.ranges[blobType] = append(ranges[:idx], ranges[idx+1:]...)
153178
return
@@ -156,28 +181,28 @@ func (m *Metrics) RemoveVerifiedBlock(chain, blobType string, blockHeight uint64
156181
// block is at start of range, shrink the range
157182
if blockHeight == r.start {
158183
// remove from start of range
159-
m.deleteRange(chain, blobType, r)
184+
m.deleteRange(chainID, blobType, r)
160185
r.start++ // modify existing range
161-
m.updateRange(chain, blobType, r)
186+
m.updateRange(chainID, blobType, r)
162187
return
163188
}
164189

165190
// block is at end of range, shrink the range
166191
if blockHeight == r.end {
167192
// remove from end of range
168-
m.deleteRange(chain, blobType, r)
193+
m.deleteRange(chainID, blobType, r)
169194
r.end-- // modify existing range
170-
m.updateRange(chain, blobType, r)
195+
m.updateRange(chainID, blobType, r)
171196
return
172197
}
173198

174199
// block is in middle of range, split into two ranges
175200
oldEnd := r.end
176-
m.deleteRange(chain, blobType, r)
201+
m.deleteRange(chainID, blobType, r)
177202

178203
// update first range
179204
r.end = blockHeight - 1
180-
m.updateRange(chain, blobType, r)
205+
m.updateRange(chainID, blobType, r)
181206

182207
// create new range for the second part
183208
newRange := &blockRange{
@@ -186,7 +211,7 @@ func (m *Metrics) RemoveVerifiedBlock(chain, blobType string, blockHeight uint64
186211
}
187212
// insert after current range
188213
ranges = append(ranges[:idx+1], append([]*blockRange{newRange}, ranges[idx+1:]...)...)
189-
m.updateRange(chain, blobType, newRange)
214+
m.updateRange(chainID, blobType, newRange)
190215

191216
m.ranges[blobType] = ranges
192217
}

internal/metrics/metrics_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,20 @@ func TestMetrics_RecordMissingBlock(t *testing.T) {
118118
}
119119
require.True(t, found, "expected to find range %s [%d-%d]", expected.blobType, expected.start, expected.end)
120120
}
121+
122+
// verify total missing blocks metric for each blob type
123+
blobTypes := make(map[string]bool)
124+
for _, r := range tt.expectedRanges {
125+
blobTypes[r.blobType] = true
126+
}
127+
for blobType := range blobTypes {
128+
expectedTotal := calculateExpectedTotal(tt.expectedRanges, blobType)
129+
actualTotal := getMetricValue(t, reg, "test_unsubmitted_blocks_total", map[string]string{
130+
"chain_id": "testchain",
131+
"blob_type": blobType,
132+
})
133+
require.Equal(t, float64(expectedTotal), actualTotal, "total missing blocks for %s should be %d", blobType, expectedTotal)
134+
}
121135
})
122136
}
123137
}
@@ -256,6 +270,13 @@ func TestMetrics_RemoveVerifiedBlock(t *testing.T) {
256270
totalRanges += len(ranges)
257271
}
258272
require.Equal(t, 0, totalRanges, "expected no ranges")
273+
274+
// verify total blocks metric is 0
275+
actualTotal := getMetricValue(t, reg, "test_unsubmitted_blocks_total", map[string]string{
276+
"chain_id": tt.removeBlock.chain,
277+
"blob_type": tt.removeBlock.blobType,
278+
})
279+
require.Equal(t, float64(0), actualTotal, "total missing blocks should be 0")
259280
} else {
260281
totalRanges := 0
261282
for _, ranges := range m.ranges {
@@ -280,6 +301,20 @@ func TestMetrics_RemoveVerifiedBlock(t *testing.T) {
280301
}
281302
require.True(t, found, "expected to find range %s [%d-%d]", expected.blobType, expected.start, expected.end)
282303
}
304+
305+
// verify total missing blocks metric for each blob type
306+
blobTypes := make(map[string]bool)
307+
for _, r := range tt.expectedRanges {
308+
blobTypes[r.blobType] = true
309+
}
310+
for blobType := range blobTypes {
311+
expectedTotal := calculateExpectedTotal(tt.expectedRanges, blobType)
312+
actualTotal := getMetricValue(t, reg, "test_unsubmitted_blocks_total", map[string]string{
313+
"chain_id": tt.removeBlock.chain,
314+
"blob_type": blobType,
315+
})
316+
require.Equal(t, float64(expectedTotal), actualTotal, "total missing blocks for %s should be %d", blobType, expectedTotal)
317+
}
283318
}
284319
})
285320
}
@@ -300,6 +335,13 @@ func TestMetrics_ComplexScenario(t *testing.T) {
300335
}
301336
require.Equal(t, 1, totalRanges, "should start with one range")
302337

338+
// verify total blocks: 100-110 = 11 blocks
339+
actualTotal := getMetricValue(t, reg, "test_unsubmitted_blocks_total", map[string]string{
340+
"chain_id": "testchain",
341+
"blob_type": "header",
342+
})
343+
require.Equal(t, float64(11), actualTotal, "should have 11 total missing blocks")
344+
303345
// remove block 103 - splits into two ranges
304346
m.RemoveVerifiedBlock("testchain", "header", 103)
305347
totalRanges = 0
@@ -308,6 +350,13 @@ func TestMetrics_ComplexScenario(t *testing.T) {
308350
}
309351
require.Equal(t, 2, totalRanges, "should have two ranges after first split")
310352

353+
// verify total blocks: 100-102 (3) + 104-110 (7) = 10 blocks
354+
actualTotal = getMetricValue(t, reg, "test_unsubmitted_blocks_total", map[string]string{
355+
"chain_id": "testchain",
356+
"blob_type": "header",
357+
})
358+
require.Equal(t, float64(10), actualTotal, "should have 10 total missing blocks after removing block 103")
359+
311360
// remove block 107 - splits second range
312361
m.RemoveVerifiedBlock("testchain", "header", 107)
313362
totalRanges = 0
@@ -316,6 +365,13 @@ func TestMetrics_ComplexScenario(t *testing.T) {
316365
}
317366
require.Equal(t, 3, totalRanges, "should have three ranges after second split")
318367

368+
// verify total blocks: 100-102 (3) + 104-106 (3) + 108-110 (3) = 9 blocks
369+
actualTotal = getMetricValue(t, reg, "test_unsubmitted_blocks_total", map[string]string{
370+
"chain_id": "testchain",
371+
"blob_type": "header",
372+
})
373+
require.Equal(t, float64(9), actualTotal, "should have 9 total missing blocks after removing block 107")
374+
319375
// verify final ranges: 100-102, 104-106, 108-110
320376
expectedRanges := []expectedRange{
321377
{blobType: "header", start: 100, end: 102},
@@ -352,6 +408,13 @@ func TestMetrics_ComplexScenario(t *testing.T) {
352408
}
353409
require.Equal(t, 2, totalRanges, "should have two ranges after removing first range")
354410

411+
// verify total blocks: 104-106 (3) + 108-110 (3) = 6 blocks
412+
actualTotal = getMetricValue(t, reg, "test_unsubmitted_blocks_total", map[string]string{
413+
"chain_id": "testchain",
414+
"blob_type": "header",
415+
})
416+
require.Equal(t, float64(6), actualTotal, "should have 6 total missing blocks after removing first range")
417+
355418
// verify remaining ranges: 104-106, 108-110
356419
expectedRanges = []expectedRange{
357420
{blobType: "header", start: 104, end: 106},
@@ -395,3 +458,43 @@ type expectedRange struct {
395458
start uint64
396459
end uint64
397460
}
461+
462+
// calculateExpectedTotal calculates the total number of blocks from expected ranges
463+
func calculateExpectedTotal(ranges []expectedRange, blobType string) uint64 {
464+
total := uint64(0)
465+
for _, r := range ranges {
466+
if r.blobType == blobType {
467+
total += r.end - r.start + 1
468+
}
469+
}
470+
return total
471+
}
472+
473+
// getMetricValue retrieves the current value of a gauge metric
474+
func getMetricValue(t *testing.T, reg *prometheus.Registry, metricName string, labels map[string]string) float64 {
475+
t.Helper()
476+
metrics, err := reg.Gather()
477+
require.NoError(t, err)
478+
479+
for _, mf := range metrics {
480+
if mf.GetName() == metricName {
481+
for _, m := range mf.GetMetric() {
482+
// check if labels match
483+
match := true
484+
for _, label := range m.GetLabel() {
485+
if expectedVal, ok := labels[label.GetName()]; ok {
486+
if label.GetValue() != expectedVal {
487+
match = false
488+
break
489+
}
490+
}
491+
}
492+
if match && len(m.GetLabel()) == len(labels) {
493+
return m.GetGauge().GetValue()
494+
}
495+
}
496+
}
497+
}
498+
t.Fatalf("metric %s not found with labels %v", metricName, labels)
499+
return 0
500+
}

0 commit comments

Comments
 (0)