From d566a9ffae229bfc54b61839638ba0762e844616 Mon Sep 17 00:00:00 2001 From: Matthieu Vachon Date: Tue, 21 Jul 2026 19:51:30 -0400 Subject: [PATCH] forkable: add WithFinalizedBlockNumMetric option Reports the LIB number of the block that just became head, on the live metrics path next to the existing head block number and drift metrics. Paired with the head block number metric, it lets dashboards show how far behind finality an app's head is. The option takes a minimal Uint64Metric interface rather than a concrete type, so consumers (firehose-core) can own the metric definition without bstream depending on them. It is a separate option instead of a change to WithMetrics so existing callers keep compiling. --- CHANGELOG.md | 1 + forkable/forkable.go | 4 ++++ forkable/metrics_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ forkable/options.go | 16 ++++++++++++++++ 4 files changed, 61 insertions(+) create mode 100644 forkable/metrics_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 3af2efd..a5e3b27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `forkable.WithFinalizedBlockNumMetric`: new option reporting, on the live path, the LIB number of the block that just became head. Takes a `forkable.Uint64Metric` interface so consumers can provide their own metric implementation. - `DefaultMergedBlocksBundleSize`: new package variable (default `100`) controlling the number of blocks per merged-blocks file assumed by readers when no explicit size is given. Like `GetProtocolFirstStreamableBlock`, it is meant to be set once at process startup. - `stream.WithMergedBlocksBundleSize`: new `stream` option to set the merged-blocks bundle size for a single stream (overrides the process-wide default; used by substreams tier2 which serves multiple chains at once). - `FileSource` now fails fast with a clear error when a merged-blocks file contains a block beyond the configured bundle size (store files bigger than the configured size). diff --git a/forkable/forkable.go b/forkable/forkable.go index e6c8dc2..7922a68 100644 --- a/forkable/forkable.go +++ b/forkable/forkable.go @@ -56,6 +56,7 @@ type Forkable struct { metricsHeadBlockNum *dmetrics.HeadBlockNum metricsHeadTimeDrift *dmetrics.HeadTimeDrift metricsRelativeBlockDrift *dmetrics.HeadBlockRelativeTime + metricsFinalizedBlockNum Uint64Metric lastLongestChain []*Block } @@ -781,6 +782,9 @@ func (p *Forkable) ProcessBlock(blk *pbbstream.Block, obj any) error { if p.metricsRelativeBlockDrift != nil { p.metricsRelativeBlockDrift.SetLastBlock(blk.Time()) } + if p.metricsFinalizedBlockNum != nil { + p.metricsFinalizedBlockNum.SetUint64(blk.LibNum) + } } if p.lastBlockSent == nil { diff --git a/forkable/metrics_test.go b/forkable/metrics_test.go new file mode 100644 index 0000000..1a3333d --- /dev/null +++ b/forkable/metrics_test.go @@ -0,0 +1,40 @@ +package forkable + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type testUint64Metric struct { + value uint64 + set bool +} + +func (t *testUint64Metric) SetUint64(value uint64) { + t.value = value + t.set = true +} + +func TestForkable_FinalizedBlockNumMetric(t *testing.T) { + finalized := &testUint64Metric{} + + p := New(&testForkableSink{}, WithFinalizedBlockNumMetric(finalized), WithExclusiveLIB(bRef("00000003a"))) + p.SetLiveMetrics() + + require.NoError(t, p.ProcessBlock(tb("00000004a", "00000003a", 3), nil)) + assert.Equal(t, uint64(3), finalized.value) + + require.NoError(t, p.ProcessBlock(tb("00000005a", "00000004a", 4), nil)) + assert.Equal(t, uint64(4), finalized.value) +} + +func TestForkable_FinalizedBlockNumMetricNotSetWithoutLiveMetrics(t *testing.T) { + finalized := &testUint64Metric{} + + p := New(&testForkableSink{}, WithFinalizedBlockNumMetric(finalized), WithExclusiveLIB(bRef("00000003a"))) + + require.NoError(t, p.ProcessBlock(tb("00000004a", "00000003a", 3), nil)) + assert.False(t, finalized.set, "finalized block num metric must only be updated on the live path") +} diff --git a/forkable/options.go b/forkable/options.go index c1355a3..6d7dacf 100644 --- a/forkable/options.go +++ b/forkable/options.go @@ -42,6 +42,22 @@ func WithMetrics( } } +// Uint64Metric is the minimal interface a metric must satisfy to be updated by the +// forkable. It exists so that consumers can provide their own metric implementation +// without bstream having to depend on it. +type Uint64Metric interface { + SetUint64(value uint64) +} + +// WithFinalizedBlockNumMetric reports, on the live path, the LIB number of the block +// that just became head. Combined with the head block number metric, it tells how far +// behind finality the head is. +func WithFinalizedBlockNumMetric(finalizedBlockNum Uint64Metric) Option { + return func(f *Forkable) { + f.metricsFinalizedBlockNum = finalizedBlockNum + } +} + func WithWarnOnUnlinkableBlocks(count int) Option { return func(f *Forkable) { f.warnOnUnlinkableBlocksCount = count