Skip to content

feat(sync): pipelined concurrent backfill (3 → 51 blocks/sec, 17x) - #33

Merged
satyakwok merged 4 commits into
mainfrom
feat/parallel-backfill-pipeline
May 14, 2026
Merged

feat(sync): pipelined concurrent backfill (3 → 51 blocks/sec, 17x)#33
satyakwok merged 4 commits into
mainfrom
feat/parallel-backfill-pipeline

Conversation

@satyakwok

@satyakwok satyakwok commented May 14, 2026

Copy link
Copy Markdown
Member

Why

Backfill was sequential ingest_one() per block @ ~300ms each → 3 blocks/sec ceiling. With a 1.5M-block gap on mainnet (real situation 2026-05-14), ETA was 5.7 days.

What

crates/sync/src/backfill.rs:

  • futures::stream over [cursor+1..=cap] with .buffered(N) for N-concurrent fetch
  • Writes stay serial (in height order) — preserves the cursor-never-ahead-of-data invariant
  • New env INDEXER_BACKFILL_CONCURRENCY (default 50, max 500) tunes the pool
  • fetch_one() extracted to keep ingest_one() (used by tail loop) untouched
  • Progress log every 1000 blocks

Measured

Live mainnet vps4 2026-05-14:

  • concurrency=50 → 30 blocks/sec (10x)
  • concurrency=200 → 51 blocks/sec (17x — write-bound from here)
  • Original 1.5M-block backfill ETA: 138h → ~8h

Future

Write path becomes the bottleneck once fetch is parallel. Next iteration: batch writes (insert N blocks per pg tx). Out of scope — separate PR.

Risk

Low. Pipeline preserves cursor-monotonicity. Cancellation honored mid-pipeline. 404 / damaged-block path unchanged from ingest_one.

Summary by CodeRabbit

  • New Features

    • Configurable backfill concurrency via environment variable for faster, tunable syncing.
  • Improvements

    • Concurrent fetching with ordered writes to speed backfill while preserving sequence.
    • Periodic progress logging (every 1,000 blocks) and a final "caught up" message for visibility.
    • More resilient handling of missing/damaged blocks and safe mid-process cancellation that preserves progress.
  • Chores

    • Added a default repository code-owner rule.

Review Change Stack

satyakwok added 2 commits May 14, 2026 12:51
Original backfill loop awaited ingest_one() per block sequentially. With
~300ms per block (REST /chain/blocks/<n> + eth_getLogs + DB write), peak
throughput was 3 blocks/sec → 5.7 days to backfill 1.5M-block gap.

Refactor to pipelined fetch:
- futures::stream over [start..=cap]
- buffered(N) — N concurrent fetches in flight
- writes still serial in height order so the cursor never lands ahead
  of the data (sync invariant 2)
- INDEXER_BACKFILL_CONCURRENCY env var (default 50, max 500)

Measured on mainnet 2026-05-14:
- concurrency=50  → 30 blocks/sec
- concurrency=200 → 51 blocks/sec (write-bound from here)
- ETA for 1.5M-block backfill drops from 138h to ~8h

Future iteration: batch the writes (insert N blocks per tx) to lift
the write ceiling further. Out of scope for this PR.
@coderabbitai

coderabbitai Bot commented May 14, 2026

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 00b337d6-5522-4614-9103-e2f1bfd0a764

📥 Commits

Reviewing files that changed from the base of the PR and between 1e8ab5d and 00ece73.

📒 Files selected for processing (1)
  • crates/sync/src/backfill.rs

📝 Walkthrough

Walkthrough

This PR refactors the block backfill from a sequential height-by-height loop to a pipelined design: it concurrently fetches up to N block heights (configurable via INDEXER_BACKFILL_CONCURRENCY) while applying writes serially to preserve ordering. A new fetch_one helper performs fetch+conversion and returns Ok(None) for missing blocks; run_backfill logs progress, supports mid-pipeline cancellation (returning the current cursor), and advances the cursor on missing blocks. The PR also adds a repository-wide .github/CODEOWNERS entry.

Sequence Diagram

sequenceDiagram
  participant run_backfill as run_backfill
  participant fetch_queue as fetch_one queue
  participant results as results buffer
  participant write as write_block
  participant storage as cursor/block storage

  loop for each block height
    run_backfill->>fetch_queue: spawn fetch_one (bounded concurrency)
  end

  loop process results in order
    results->>run_backfill: Some(BlockBundle) or None
    alt BlockBundle
      run_backfill->>write: write_block(BlockBundle)
      write->>storage: persist block
    else None
      run_backfill->>storage: write_cursor(height, 0) + warn log
    end
    run_backfill->>run_backfill: check cancellation
    Note over run_backfill: periodic progress logging
  end

  run_backfill->>storage: return final cursor
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Sentriscloud/indexer-rs#32: Adds/updates the repository-wide .github/CODEOWNERS default owner mapping (same file changed here).
  • Sentriscloud/indexer-rs#22: Related changes to missing/404 block handling during backfill; both PRs adjust behavior to skip/advance the cursor instead of failing.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The description provides detailed technical context (Why, What, Measured, Future, Risk) but omits required template sections (Scope, Checks, Deploy impact, Linked issue). Complete the missing template sections: Scope (identify change type), Checks (confirm tests/linting pass), Deploy impact (specify deployment requirements), and Linked issue (reference any related GitHub issue).
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: implementing pipelined concurrent backfill with measured performance improvement from 3 to 51 blocks/sec.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/parallel-backfill-pipeline

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

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/sync/src/backfill.rs`:
- Around line 128-133: The code fails rustfmt: reformat the helper so it adheres
to cargo fmt style (run `cargo fmt --all`), e.g., adjust spacing and indentation
around the let bindings and match/else pattern; specifically reformat the block
containing retry_with_backoff, the let block_opt = ... line, the `let
Some(block) = block_opt else { return Ok(None); };` pattern, and the subsequent
provider.logs_in_range call (functions: retry_with_backoff, rest.block,
provider.logs_in_range, variables: block_opt, block) so the file compiles and
passes `cargo fmt -- --check`.
- Around line 70-76: Update the top-of-file/module header comments to describe
the current pipelined design: concurrent fetching of N blocks (tuned by
INDEXER_BACKFILL_CONCURRENCY and the backfill_concurrency() helper) with
sequential writes in height order so the cursor never advances past written
data; remove or change wording that says “sequential block-by-block ingest” and
that cancellation is only checked “between blocks” to instead state that
cancellation/checks occur around the fetch/write pipeline while preserving write
order. Ensure the header aligns with the inline comment and the
tracing::info!(concurrency, "backfill: pipelined fetch enabled") behavior.
- Around line 89-92: The loop in backfill.rs currently awaits
fetched.next().await before checking cancellation, which can block if an
in-flight fetch stalls; change the loop to use tokio::select! to race
cancel.cancelled() (or the cancellation future provided by your cancel token)
against fetched.next() so cancellation is observed immediately. Locate the loop
using fetched.next().await and the call to
cancel.is_cancelled()/cancel.cancelled(), replace the single await with a select
that returns early on the cancellation branch (logging and returning the cursor)
and otherwise processes the fetched (h, result) as before. Ensure you import
tokio::select! if not already available and preserve existing logging and return
semantics.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 94cffcb7-9602-4d1c-89a1-6ad55b8d2b45

📥 Commits

Reviewing files that changed from the base of the PR and between 657c2ae and 6f773ca.

📒 Files selected for processing (2)
  • .github/CODEOWNERS
  • crates/sync/src/backfill.rs

Comment thread crates/sync/src/backfill.rs
Comment thread crates/sync/src/backfill.rs Outdated
Comment thread crates/sync/src/backfill.rs Outdated
satyakwok added 2 commits May 14, 2026 15:59
CR follow-up on PR #33:
- cargo fmt: helper formatting
- cancellation race: cancel issued during a slow fetch now returns
  within the cancel timeout; was waiting for in-flight item to land
  before checking. Use tokio::select! biased on cancel side.
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.

1 participant