Repository navigation
fix(indexer): keep one flush timer per BufferedTask and defer days without blocks - #7
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
There was a problem hiding this comment.
🔵 Needs a closer look
It changes flush-timer lifecycle logic in BufferedTask, core infrastructure shared by every indexer fetcher, so the large blast radius warrants human sign-off despite no defects found.
No open issues 🎉
What changed in this PR
This PR fixes two resource-leak/noise bugs in the indexer that cause elevated CPU and log spam on idle chains over long uptimes.
The first fix targets Indexer.BufferedTask, which underpins all indexer fetchers. Previously, each idle re-check (schedule_next_buffer_flush) started a fresh repeating flush timer while overwriting the stored reference, leaking the old timer. Over days of uptime these accumulate, so workers spend most of their time waking up only to hibernate again. The fix cancels any live flush_timer before scheduling a new one, keeping exactly one timer per worker.
The second fix targets Indexer.Fetcher.Stats.HotSmartContracts. A day with no indexed blocks previously fell into the generic error branch, retrying every 10 seconds and logging an error each time. The new {:error, {:error, :not_found}} clause reschedules the recheck once on the next day, keeps the daily sequence progressing, stops at the 30‑day window edge, and logs at info level instead of error.
Changes:
BufferedTaskcancels the previous flush timer before starting a new one so each worker keeps a single timer.HotSmartContractsdefers a block-less day to the next day (bounded by the 30-day window), preserves the daily fetch chain, and logs at info instead of retrying every 10s with an error.- Adds focused tests covering the single-timer invariant, the
:infinityno-timer case, and the deferral/daily-sequence/window-edge behavior.
| File | Description |
|---|---|
apps/indexer/lib/indexer/buffered_task.ex |
Cancels a live flush_timer reference before scheduling a new flush, preventing timer accumulation. |
apps/indexer/lib/indexer/fetcher/stats/hot_smart_contracts.ex |
Adds a not_found branch that defers block-less days once per day within the 30-day window and logs at info level. |
apps/indexer/test/indexer/buffered_task_test.exs |
Tests the single-timer invariant across repeated polls, timer replacement after an expired tick, and the :infinity no-timer case. |
apps/indexer/test/indexer/fetcher/stats/hot_smart_contracts_test.exs |
Adds trace-based tests for deferral scheduling, daily-sequence continuation, window-edge stop, and eventual fetch once blocks exist. |
Review effort: Balanced
f8517d4 to
7792474
Compare
…hot-contract days daily BufferedTask: every idle re-check started new flush timer and overwrote stored reference without stopping old timer, so timers piled up with uptime (idle explorer at ~1.1 cores after 42 days, 0.04 after restart). Cancel stored timer before starting new one. Same cancel-then-reschedule order as Broadway RateLimiter. Hot contracts: day without indexed blocks no longer retried every 10 s with error log. Info log, recheck next UTC day within existing 30-day window, daily sequence continues. Other errors keep 10 s retry. Regression tests: existing flush test starts with live timer and checks cancellation; new hot-contracts test checks info log, no error. Upstream-ready branches on blockscout/blockscout 6275a9c: fix/buffered-task-single-flush-timer (fa32904a9, e8aef41ab), fix/hot-contracts-empty-day-retry (890008a67, 9f923a724).
7792474 to
95eef6e
Compare
Motivation
Testnet explorer climbed to ~1.1 CPU cores on idle chain after 42 days uptime. Restart on 2026-10-21 dropped it to 0.04 cores. Left alone, keeps climbing until server runs out of CPU. PR carries two upstream-ready fixes, so fork ships them before upstream merges them.
1. Flush timers pile up in
BufferedTaskMost indexer fetchers (token info, token balances, coin balances, others) run on shared worker
Indexer.BufferedTask. Worker collects entries, processes them in batches. Repeating flush timer wakes it every 300 ms to move waiting entries into processing. With no work, worker re-checks database roughly every 10 minutes.Should be: one flush timer per worker. Since upstream #14018: every idle re-check starts new flush timer, code overwrites stored timer reference without stopping old timer. Old timer keeps firing, keeps rescheduling itself. Each worker gains one timer per re-check:
Timer counts derived from code, not counted live. Measured: bounded live profile put ~96% of VM work in 13 such workers, all waking only to hibernate again.
Fix: cancel stored timer before starting new one. Every path that starts timer now replaces it instead of adding one. Timer already fired: its message runs once, that flush replaces current timer, no extra chain survives. Same cancel-then-reschedule order as Broadway RateLimiter (L74-L75). Broadway's drain-or-raise step (L112-L124) left out: here cancel runs inside flush handler, fired message already consumed, so step would raise on every normal flush.
2. Hot-contracts statistics retry missing days every 10 s
Hot-contracts fetcher builds daily per-contract statistics. Paused chain (last block 2026-10-05): every later day has no blocks. Fetcher treated "no blocks for day" as failure, retried every 10 s, logged error each time. After restart: 15 missing days retried, ~90 error lines per minute. Now: info log, same day checked again next UTC day via module's existing next-day scheduler. Days older than existing 30-day window: no more checks. Other errors keep 10 s retry.
Changelog
Bug Fixes
BufferedTaskkeeps one flush timer per worker: cancels previous timer before starting new one.Incompatible Changes
None. No config, schema, migration or dependency changes. Four files: 17 code lines, 18 test lines.
Upgrading
Normal image upgrade. After merge: run manual "Publish Via backend" workflow with merged master SHA. Deploy tracked in vianetwork/via-infra#117.
Validation
Each fix: regression test fails on old code, passes with fix. Timer: existing flush test now starts with live timer, checks it gets cancelled. Hot contracts: one new test, day without blocks logs info, not error. One commit, same content as upstream-ready branches on blockscout/blockscout master 6275a9c (there: test commit then fix commit). Fork: focused tests (29) and
mix format --check-formattedpass.mix credo: three existing findings in unchanged Via zkSync files. Earlier full indexer suite passed with same production code. Upstream PRs not opened yet.