Skip to content

fix(portal): emit per-chain metrics file on a 0-block window (#140)#142

Merged
dzhelezov merged 1 commit into
mainfrom
fix/portal-metrics-empty-window
Jul 10, 2026
Merged

fix(portal): emit per-chain metrics file on a 0-block window (#140)#142
dzhelezov merged 1 commit into
mainfrom
fix/portal-metrics-empty-window

Conversation

@dzhelezov

Copy link
Copy Markdown
Collaborator

Fixes #140.

Mechanism

syncBlockData early-returns at the blockArr.length === 0 guard before the writeMetrics(...) call, so a chain that streams 0 blocks in a sync window never writes its portal-metrics.<chainId> file. Any consumer expecting one metrics file per configured chain then fails with metrics file not found: portal-metrics.<chainId>.

Where it shows up: running the keyless euler-multichain example, base (8453) and arbitrum (42161) stream 0 blocks in the small default window (their factory-discovered events fall outside it), so their metrics files are absent and the run is un-gate-able. All chains do backfill via the Portal (rpc_fallback=0) — only the metrics-file emission is skipped. Indexing correctness is unaffected; this is an observability/DX gap.

Fix

Extract the writeMetrics({...}) args into a single emitMetrics() closure — one source of truth for the arg shape — and invoke it on both syncBlockData exits: the normal insert path and the 0-block early return. writeMetrics overwrites ${metricsFile}.${chain.id} (last-write-wins) and reads the live cumulative stats, so the 0-block file carries the same FROZEN schema with honest zero counts (inserted.blocks=0, rpcFallbackIntervals=0). The early-out (nothing to insert for 0 blocks) is preserved. No duplication of the arg object, so the two call sites cannot diverge.

The !s path (interval not Portal-served / RPC-delegated) is deliberately untouched — it did not write metrics before and still doesn't; only the "Portal served this interval but it assembled 0 blocks" branch is fixed, which is exactly the issue's case.

Mutation-verified regression test

portal/portal.test.tsregression (#140): a chain that streams 0 blocks in its window STILL emits a per-chain metrics file (blocks=0). Drives a Portal-served in-range window [0, 5000] that assembles 0 blocks (header-only anchor, no matched data), sets PORTAL_METRICS_FILE, runs syncBlockRangeData + syncBlockData, asserts the precondition inserted.blocks == 0 (proving the buggy early-return path is reached), then asserts the per-chain file exists with the zero-window schema.

Mutation verification (in the grafted 0.16.8 tree — my repo tree stayed clean):

  • RED — removed emitMetrics() from the early-return branch (the old code): test FAILS at expect(existsSync(perChainFile)).toBe(true)expected false to be true (file absent), vitest EXIT=1.
  • GREEN — restored the fix: test PASSES, EXIT=0.

Both-version gates (scripts/sync-upstream.sh <ver> --test)

version Test Files Tests portal.test.ts EXIT
0.16.8 16 passed 313 passed 58 tests 0
0.16.6 16 passed 313 passed 58 tests 0

biome check . clean (error-level, pinned @biomejs/biome@2.5.2).

🤖 Generated with Claude Code

When a chain streams 0 blocks in a sync window, `syncBlockData` early-returns
at the `blockArr.length === 0` guard BEFORE the `writeMetrics(...)` call, so no
`portal-metrics.<chainId>` file was produced for that chain/window. Consumers
that expect one metrics file per configured chain (e.g. the keyless
euler-multichain e2e harness for base/arbitrum, whose factory-discovered events
fall outside the small default window) then fail with
`metrics file not found: portal-metrics.<chainId>`.

Indexing correctness is unaffected — this is purely an observability/gate-ability
gap. A 0-block chain still ran, and `blocks=0` is a meaningful, gate-able datapoint.

Fix: extract the `writeMetrics({...})` args into a single `emitMetrics()` closure
(one source of truth for the arg shape) and invoke it on BOTH `syncBlockData`
exits — the normal insert path AND the 0-block early return. `writeMetrics`
overwrites `${metricsFile}.${chain.id}` last-write-wins and reads the live
cumulative `stats`, so the 0-block file carries the same FROZEN schema with honest
zero counts. The early-out behavior (nothing to insert for 0 blocks) is preserved.

Regression test (portal/portal.test.ts): drives a Portal-served in-range window
that assembles 0 blocks and asserts the per-chain metrics file exists with the
zero-window schema. Mutation-verified: removing `emitMetrics()` from the
early-return branch (the old code) fails the test (file absent); with the fix it
passes. Both-version gates green (0.16.8 + 0.16.6: 16 files / 313 tests each).

Fixes #140

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dzhelezov dzhelezov marked this pull request as ready for review July 10, 2026 16:43
@dzhelezov dzhelezov merged commit 47fc710 into main Jul 10, 2026
12 checks passed
@dzhelezov dzhelezov deleted the fix/portal-metrics-empty-window branch July 10, 2026 16:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Portal: a chain that streams 0 blocks in a window emits no per-chain metrics file (writeMetrics skipped on the empty-block early return)

1 participant