fix(util-internal): don't wedge ingest head-polling on a hung finalized/head fetch - #544
Open
elina-chertova wants to merge 1 commit into
Open
fix(util-internal): don't wedge ingest head-polling on a hung finalized/head fetch#544elina-chertova wants to merge 1 commit into
elina-chertova wants to merge 1 commit into
Conversation
… fetch Throttler.get() returned the in-flight refresh promise whenever the cached value was older than the interval. With enablePrefetch() (used by every EVM/ Solana/Tron/Bitcoin ingest head- and finalized-head tracker), a finalized/head fetch that hangs or times out then blocks get(), which gates the ingest head-polling loop — the data-service goes silent and never recovers even after the chain resumes, without crashing. Serve the last known value while a refresh is in flight when prefetch is enabled; let the slow/hung refresh settle in the background. First read still awaits the initial fetch, and non-prefetch throttlers are unchanged. Adds throttler.test.ts (vitest) covering the wedge, first-read blocking, and non-prefetch refresh.
Contributor
|
The contract of the This PR changes the contract and I can't calculate the consequences out of my head. Depending on how relevant the AI analysis actually is, the fix should be related to either |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cause (proven)
Both
zora-mainnetEVM hotblocks pods (dwellir and uniblock — two independent providers) went silent at the identical block 49023729 at 17:28:16Z, stopped emitting head metrics (firingPortal_Hotblocks_Head_Metrics_Absent_zora_mainnet), and did not recover even though the chain kept producing blocks (verified block 49024000 exists; providers advanced past the stuck point). No crash/restart — the process was hung.Root cause is in
Throttler(util/util-internal/src/throttler.ts), the shared primitive behind every ingest head-/finalized-head tracker:lastAccessis updated only on success (execute()). When the underlying fetch hangs or times out for longer thaninterval, the nextget()falls through and returns the still-pending refresh promise. Inevm-rpc/src/data-source/ingest.tsthe hot loop doesawait finalizedHeadTracker.get()before the head poll (poll.next()), so a stalledeth_getBlockByNumber("finalized")(the pod loggedHttpTimeoutError: request timed out after 30000 mson exactly that call) blocks the whole loop. Because the loop runs in a worker, the main-threadDataServicefor awaitthen blocks forever with no further logging — matching the observed permanent silence.enablePrefetch()is used by every EVM/Solana/Tron/Bitcoin head/finalized tracker, so this can wedge any of them.Fix
Honor the prefetch contract: once a value exists,
get()returns the last known value immediately and lets the (slow/hung) refresh settle in the background, instead of blocking on it. Head polling proceeds; a stale-but-conservative finalized head is safe (only affects the finalized upper bound, which self-corrects on recovery). The first read still awaits the initial fetch, and non-prefetch throttlers are unchanged.Tests (red → green)
util/util-internal/src/throttler.test.ts(vitest, newly wired into the package'stestscript):get()must return the cached value promptly. FAILS pre-fix (returns the hung promise →'HUNG'), PASSES post-fix.get()blocks until the initial value resolves.Verified locally:
rush build --to @subsquid/util-internal✅,rush test --only @subsquid/util-internal✅. Confirmed red→green by revertingget()to the old body (the wedge test fails withexpected 'HUNG' to be 1).Scope / relation to other PRs
This is the transient-hang → permanent-wedge fatality (the process should survive a slow/unresponsive finalized endpoint and keep polling head). Distinct root cause from #513 (retry a transient
invalid block heighterror on the finalized probe) and #523 (portal-client retries transient null heads). Complementary, not a duplicate.Falsification
If a hotblocks/dumper pod still goes permanently silent (no logs, no restart) after its finalized/head RPC endpoint hangs — i.e. the head loop stops advancing while the chain keeps producing — this fix is insufficient and the block is elsewhere on the head path.