Skip to content

fix(indexer): keep one flush timer per BufferedTask and defer days without blocks - #7

Merged
romanornr merged 1 commit into
masterfrom
fix/via-indexer-idle-timers
Oct 21, 2026
Merged

romanornr merged 1 commit into
masterfrom
fix/via-indexer-idle-timers

Conversation

@romanornr

@romanornr romanornr commented Oct 21, 2026 •

Copy link
Copy Markdown

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 BufferedTask

Most 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:

uptime        timers per worker   flush wake-ups per worker
10 minutes    2                   ~7 per second
1 day         ~145                ~480 per second
42 days       ~6,000              ~20,000 per second

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

  • BufferedTask keeps one flush timer per worker: cancels previous timer before starting new one.
  • Hot contracts recheck day without indexed blocks once per day instead of every 10 s.

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-formatted pass. mix credo: three existing findings in unchanged Via zkSync files. Earlier full indexer suite passed with same production code. Upstream PRs not opened yet.

Copilot AI balanced review requested due to automatic review settings October 21, 2026 13:23
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T14:00:41.576806Z 95eef6e New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8c027e6f-bbe9-489c-8e1e-4976775d091a
📥 Commits

Reviewing files that changed from the base of the PR and between 232da46 and f8517d4.

📒 Files selected for processing: 4
  • apps/indexer/lib/indexer/buffered_task.ex
  • apps/indexer/lib/indexer/fetcher/stats/hot_smart_contracts.ex
  • apps/indexer/test/indexer/buffered_task_test.exs
  • apps/indexer/test/indexer/fetcher/stats/hot_smart_contracts_test.exs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Walkthrough

The indexer now cancels an existing finite flush timer before scheduling a replacement. Hot-contract fetching handles unavailable dates separately, retries eligible dates the following day, and continues daily scheduling for new-day requests.

Changes: Buffered flush timers

Layer / File(s) Summary
Flush timer replacement
apps/indexer/lib/indexer/buffered_task.ex, apps/indexer/test/indexer/buffered_task_test.exs
Finite-interval scheduling cancels an existing timer before replacing it. Tests cover repeated scheduling, expired timers, and the :infinity interval.

Hot-contract date handling

Layer / File(s) Summary
Unavailable-date handling and retries
apps/indexer/lib/indexer/fetcher/stats/hot_smart_contracts.ex, apps/indexer/test/indexer/fetcher/stats/hot_smart_contracts_test.exs
The handler retries unavailable dates younger than 30 days on the following day, continues daily scheduling for new-day requests, and logs when indexed blocks are unavailable. Tests cover the retry boundary and later processing.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to f8517

Deferred dates continue to be checked daily, and no issue requiring resolution before merge was identified.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 4 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title concisely identifies both main changes: keeping one flush timer per BufferedTask and deferring hot-contract dates without indexed blocks.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The pull request description directly explains both changes: preventing BufferedTask flush-timer accumulation and deferring hot-contract statistics retries for days without indexed blocks. It also d…
Full details: Description check

Explanation

The pull request description directly explains both changes: preventing BufferedTask flush-timer accumulation and deferring hot-contract statistics retries for days without indexed blocks. It also describes validation and deployment impact.

✨ Finishing Touches: 💡 2
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit watches timers glide,
One flush stays ready by its side.
No blocks today? The date can wait,
Tomorrow brings another date.
The contracts bloom when records grow.

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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:

  • BufferedTask cancels the previous flush timer before starting a new one so each worker keeps a single timer.
  • HotSmartContracts defers 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 :infinity no-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

@romanornr
romanornr force-pushed the fix/via-indexer-idle-timers branch from f8517d4 to 7792474 Compare October 21, 2026 13:51
…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).
@romanornr
romanornr force-pushed the fix/via-indexer-idle-timers branch from 7792474 to 95eef6e Compare October 21, 2026 13:58
@romanornr
romanornr merged commit 64a8b2b into master Oct 21, 2026
17 of 20 checks passed
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.

2 participants