Skip to content

refactor: parallelize collect_event for tipset range#6881

Merged
hanabi1224 merged 6 commits intomainfrom
hm/speed-up-collect-events-for-range
Apr 10, 2026
Merged

refactor: parallelize collect_event for tipset range#6881
hanabi1224 merged 6 commits intomainfrom
hm/speed-up-collect-events-for-range

Conversation

@hanabi1224
Copy link
Copy Markdown
Contributor

@hanabi1224 hanabi1224 commented Apr 9, 2026

Summary of changes

To mitigate #6879
(Perf is still worse than Lotus due to lack of SQL index)

Test result with https://github.com/hugomrdias/foxer
Command: bun --filter foc-api dev

# pr
 09:36:15.504 INFO  migrations applied driver=pglite (360ms)
│ 09:36:15.531 INFO  api server listening port=4200
│ 09:36:15.810 INFO  startup sanity check completed (299ms)
│ 09:36:15.812 INFO  starting backfill fromBlock=3610265 toBlock=3612375 batchSize=500
│ 09:36:20.876 INFO  backfill batch completed indexedUpTo=3610764 contracts=4 throughput=98.76 (5.063s)
│ 09:36:29.081 INFO  backfill batch completed indexedUpTo=3611264 contracts=4 throughput=60.94 (8.205s)
│ 09:36:31.061 INFO  backfill batch completed indexedUpTo=3611764 contracts=4 throughput=252.53 (1.98s)
│ 09:36:35.972 INFO  backfill batch completed indexedUpTo=3612264 contracts=4 throughput=101.81 (4.911s)
│ 09:36:36.364 INFO  backfill batch completed indexedUpTo=3612375 contracts=4 throughput=283.16 (392ms)
│ 09:36:36.364 INFO  backfill completed blocks=2111 (23.052s)

# main
 08:36:02.140 INFO  migrations applied driver=pglite (967ms)
│ 08:36:02.173 INFO  api server listening port=4200
│ 08:36:02.659 INFO  startup sanity check completed (508ms)
│ 08:36:02.662 INFO  starting backfill fromBlock=3610265 toBlock=3612255 batchSize=500
│ 08:36:13.538 INFO  backfill batch completed indexedUpTo=3610764 contracts=4 throughput=45.97 (10.876s)
│ 08:36:29.144 INFO  backfill batch completed indexedUpTo=3611264 contracts=4 throughput=32.04 (15.605s)
│ 08:36:36.933 INFO  backfill batch completed indexedUpTo=3611764 contracts=4 throughput=64.2 (7.788s)
│ 08:36:43.285 INFO  backfill batch completed indexedUpTo=3612255 contracts=4 throughput=77.3 (6.352s)
│ 08:36:43.285 INFO  backfill completed blocks=1991 (43.119s)

# Lotus
│ 08:35:22.289 INFO  migrations applied driver=pglite (396ms)
│ 08:35:22.322 INFO  api server listening port=4200
│ 08:35:22.759 INFO  startup sanity check completed (458ms)
│ 08:35:22.762 INFO  starting backfill fromBlock=3610265 toBlock=3612253 batchSize=500
│ 08:35:24.706 INFO  backfill batch completed indexedUpTo=3610764 contracts=4 throughput=257.2 (1.944s)
│ 08:35:25.978 INFO  backfill batch completed indexedUpTo=3611264 contracts=4 throughput=393.39 (1.271s)
│ 08:35:26.438 INFO  backfill batch completed indexedUpTo=3611764 contracts=4 throughput=1089.32 (459ms)
│ 08:35:26.708 INFO  backfill batch completed indexedUpTo=3612253 contracts=4 throughput=1811.11 (270ms)
│ 08:35:26.709 INFO  backfill completed blocks=1989 (3.95s)

Changes introduced in this pull request:

Reference issue to close (if applicable)

Closes

Other information and links

Change checklist

  • I have performed a self-review of my own code,
  • I have made corresponding changes to the documentation. All new code adheres to the team's documentation standards,
  • I have added tests that prove my fix is effective or that my feature works (if possible),
  • I have made sure the CHANGELOG is up-to-date. All user-facing changes should be reflected in this document.

Outside contributions

  • I have read and agree to the CONTRIBUTING document.
  • I have read and agree to the AI Policy document. I understand that failure to comply with the guidelines will lead to rejection of the pull request.

Summary by CodeRabbit

  • Refactor
    • Collects events across block ranges concurrently, reducing query latency and making event searches more responsive.
  • Bug Fixes
    • Enforces the overall maximum result limit when merging batched event results and returns a clearer "maximum allowed" error message if exceeded.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 9, 2026

Walkthrough

Adds concurrent per-tipset event collection via a new collect_events_for_tipsets method, merges per-tipset results while enforcing max_filter_results, updates collect_events checks/messages, and switches the range filter path to use the new concurrent collector.

Changes

Cohort / File(s) Summary
Event filter concurrency
src/rpc/methods/eth/filter/mod.rs
Added EthEventHandler::collect_events_for_tipsets to schedule collect_events for multiple Tipsets using FuturesOrdered, merge results, and enforce max_filter_results. Cached max_filter_results in collect_events, tightened the overflow check and error message. Replaced sequential loop in get_events_for_parsed_filter (Range) with the new concurrent collector. Also added futures::stream::{FuturesOrdered, TryStreamExt} imports and minor formatting changes.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Handler as EthEventHandler
    participant Tasks as FuturesOrdered(TaskQueue)
    participant DB as Blockstore/DB

    Client->>Handler: get_events_for_parsed_filter(range)
    Handler->>Tasks: spawn collect_events for each Tipset
    Tasks->>DB: collect_events(Tipset) [concurrent]
    DB-->>Tasks: Vec<CollectedEvent> (per task)
    Tasks-->>Handler: task results (ordered/completed)
    Handler->>Handler: merge results, enforce max_filter_results
    Handler-->>Client: combined Vec<CollectedEvent>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • akaladarshi
  • LesnyRumcajs
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: parallelizing the collect_event operation for tipset ranges, which matches the primary refactoring work documented in the changeset.

✏️ 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 hm/speed-up-collect-events-for-range
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch hm/speed-up-collect-events-for-range

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

@hanabi1224 hanabi1224 marked this pull request as ready for review April 9, 2026 01:39
@hanabi1224 hanabi1224 requested a review from a team as a code owner April 9, 2026 01:39
@hanabi1224 hanabi1224 requested review from LesnyRumcajs and akaladarshi and removed request for a team April 9, 2026 01:39
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/rpc/methods/eth/filter/mod.rs (1)

415-425: Add tipset context to task failures.

This new async boundary will otherwise surface a range error without saying which tipset failed. Wrapping the await with tipset context keeps the failure actionable.

Suggested fix
                 for tipset in max_tipset
                     .chain(&ctx.store())
                     .take_while(|ts| ts.epoch() >= *range.start())
                 {
                     tasks.push_back(async move {
+                        let epoch = tipset.epoch();
                         let mut collected_events = vec![];
                         Self::collect_events(
                             ctx,
                             &tipset,
                             Some(pf),
                             skip_event,
                             &mut collected_events,
                         )
-                        .await?;
+                        .await
+                        .with_context(|| format!("collecting events for tipset {epoch}"))?;
                         anyhow::Ok(collected_events)
                     });
                 }

As per coding guidelines, Use anyhow::Result<T> for most operations and add context with .context() when errors occur.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/rpc/methods/eth/filter/mod.rs` around lines 415 - 425, The task closure
pushed via tasks.push_back that calls Self::collect_events currently returns raw
errors without tipset info; modify the awaited call to add context on failure
(use anyhow::Result and .context()) so failures include which tipset failed—wrap
the .await result of Self::collect_events(...) with
.context(format!("collect_events failed for tipset {:?}", tipset)) (or
equivalent) inside the async move block so any error surfaced from
collect_events includes the tipset identifier.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/rpc/methods/eth/filter/mod.rs`:
- Around line 410-429: The parallel tipset loop can accumulate more than the
global max_filter_results because each task enforces the cap locally; modify the
drain loop that iterates over tasks.try_next() to enforce the global cap when
extending collected_events from each task: after receiving events from
Self::collect_events, append only up to (max_filter_results -
collected_events.len()) items and break out when the global cap is reached. Also
bound concurrent in-flight tipset tasks (the FuturesOrdered producer over
max_tipset.chain(&ctx.store())) by adding a semaphore or a limited buffer so you
don’t spawn tasks for the entire range at once.

---

Nitpick comments:
In `@src/rpc/methods/eth/filter/mod.rs`:
- Around line 415-425: The task closure pushed via tasks.push_back that calls
Self::collect_events currently returns raw errors without tipset info; modify
the awaited call to add context on failure (use anyhow::Result and .context())
so failures include which tipset failed—wrap the .await result of
Self::collect_events(...) with .context(format!("collect_events failed for
tipset {:?}", tipset)) (or equivalent) inside the async move block so any error
surfaced from collect_events includes the tipset identifier.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c72a743d-348e-4aae-92a2-9c4da8c30951

📥 Commits

Reviewing files that changed from the base of the PR and between a738e2a and 24d381b.

📒 Files selected for processing (1)
  • src/rpc/methods/eth/filter/mod.rs

Comment thread src/rpc/methods/eth/filter/mod.rs Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 9, 2026

Codecov Report

❌ Patch coverage is 91.66667% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.01%. Comparing base (a6a523f) to head (7a0feec).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/rpc/methods/eth/filter/mod.rs 91.66% 0 Missing and 3 partials ⚠️
Additional details and impacted files
Files with missing lines Coverage Δ
src/rpc/methods/eth/filter/mod.rs 88.54% <91.66%> (+0.21%) ⬆️

... and 7 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update a6a523f...7a0feec. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread src/rpc/methods/eth/filter/mod.rs Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
src/rpc/methods/eth/filter/mod.rs (3)

266-272: Narrow or document this new helper API.

collect_events_for_tipsets looks like an internal implementation detail in this file, but it is exposed as pub and has no rustdoc. If it is not meant for external callers, please keep it private; otherwise add a doc comment before exporting it.

As per coding guidelines, **/*.rs: Document public functions and structs with doc comments.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/rpc/methods/eth/filter/mod.rs` around lines 266 - 272, The function
collect_events_for_tipsets is currently public without documentation; either
mark it private (remove pub) if it's an internal helper, or add a Rust doc
comment describing its purpose, parameters (ctx, tipsets, spec, skip_event,
collected_events), behavior, return type, and thread-safety assumptions before
exporting it so it conforms to the project's public API docs requirement for
collect_events_for_tipsets.

275-278: Add tipset context before propagating task errors.

A bare error from collect_events is harder to triage once this runs through a concurrent queue, because the failing tipset is no longer obvious at the call site.

Suggested change
         for tipset in tipsets {
             tasks.push_back(async move {
                 let mut events = vec![];
-                Self::collect_events(ctx, &tipset, spec, skip_event, &mut events).await?;
+                Self::collect_events(ctx, &tipset, spec, skip_event, &mut events)
+                    .await
+                    .with_context(|| {
+                        format!("collecting events for tipset at epoch {}", tipset.epoch())
+                    })?;
                 anyhow::Ok(events)
             });
         }

As per coding guidelines, **/*.rs: Use anyhow::Result<T> for most operations and add context with .context() when errors occur.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/rpc/methods/eth/filter/mod.rs` around lines 275 - 278, The task closure
pushing to tasks (the async block calling Self::collect_events) currently
returns raw errors from collect_events, making it hard to know which tipset
failed; update the error propagation to add context that includes the tipset
info before returning (use anyhow::Context/.context()). Specifically wrap the
await call to Self::collect_events(ctx, &tipset, spec, skip_event, &mut
events).await with .context(format!(...)) or similar so the resulting
anyhow::Result from the closure includes the tipset identifier, ensuring the
closure still returns anyhow::Result<Vec<...>> but with added context for easier
triage.

420-449: Please add a regression test for the concurrent Range path.

This branch changed the collection strategy while keeping the same external contract. A focused test spanning multiple tipsets should assert stable event ordering/no duplication and merged max_filter_results enforcement so this path stays Lotus-compatible as it evolves.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/rpc/methods/eth/filter/mod.rs` around lines 420 - 449, Add a regression
test that exercises the ParsedFilterTipsets::Range branch and the concurrent
collection behavior of Self::collect_events_for_tipsets: build a fake chain with
multiple sequential tipsets (including a heaviest tipset to trigger the
heaviest_epoch logic and ResolveNullTipset::TakeOlder path), install events
across tipsets, then call the RPC/filter-range code path to collect events;
assert the returned events have stable deterministic ordering, no duplicates,
and that the merged max_filter_results limit is enforced across tipsets. Ensure
the test hits the code that computes max_height (including the -1 heaviest case)
and uses ctx.chain_index().tipset_by_height(...) so the new Range collection
strategy is exercised end-to-end.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/rpc/methods/eth/filter/mod.rs`:
- Around line 266-272: The function collect_events_for_tipsets is currently
public without documentation; either mark it private (remove pub) if it's an
internal helper, or add a Rust doc comment describing its purpose, parameters
(ctx, tipsets, spec, skip_event, collected_events), behavior, return type, and
thread-safety assumptions before exporting it so it conforms to the project's
public API docs requirement for collect_events_for_tipsets.
- Around line 275-278: The task closure pushing to tasks (the async block
calling Self::collect_events) currently returns raw errors from collect_events,
making it hard to know which tipset failed; update the error propagation to add
context that includes the tipset info before returning (use
anyhow::Context/.context()). Specifically wrap the await call to
Self::collect_events(ctx, &tipset, spec, skip_event, &mut events).await with
.context(format!(...)) or similar so the resulting anyhow::Result from the
closure includes the tipset identifier, ensuring the closure still returns
anyhow::Result<Vec<...>> but with added context for easier triage.
- Around line 420-449: Add a regression test that exercises the
ParsedFilterTipsets::Range branch and the concurrent collection behavior of
Self::collect_events_for_tipsets: build a fake chain with multiple sequential
tipsets (including a heaviest tipset to trigger the heaviest_epoch logic and
ResolveNullTipset::TakeOlder path), install events across tipsets, then call the
RPC/filter-range code path to collect events; assert the returned events have
stable deterministic ordering, no duplicates, and that the merged
max_filter_results limit is enforced across tipsets. Ensure the test hits the
code that computes max_height (including the -1 heaviest case) and uses
ctx.chain_index().tipset_by_height(...) so the new Range collection strategy is
exercised end-to-end.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 183e8980-3221-4597-b51d-948c6d8e7786

📥 Commits

Reviewing files that changed from the base of the PR and between db51085 and 15dc5b3.

📒 Files selected for processing (1)
  • src/rpc/methods/eth/filter/mod.rs

@hanabi1224 hanabi1224 requested a review from LesnyRumcajs April 9, 2026 14:06
Comment thread src/rpc/methods/eth/filter/mod.rs
Comment thread src/rpc/methods/eth/filter/mod.rs Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/rpc/methods/eth/filter/mod.rs (1)

371-375: ⚠️ Potential issue | 🟠 Major

Reject the max + 1th event here.

Because this ensure! runs before push, len() == max_filter_results still passes and the next event is appended. The Range path re-checks during merge, but the Hash/Key branches in this file and the direct EthEventHandler::collect_events caller in src/rpc/methods/eth.rs:1311 can still return one more event than configured, which breaks the max-results contract.

Suggested fix
                         let ce = CollectedEvent {
                             entries,
                             emitter_addr: resolved,
                             event_idx,
                             reverted: false,
                             height,
                             tipset_key: tipset_key.clone(),
                             msg_idx: msg_idx as u64,
                             msg_cid: message.cid(),
                         };
                         ensure!(
-                            collected_events.len() <= max_filter_results,
+                            collected_events.len() < max_filter_results,
                             "filter matches too many events (maximum {max_filter_results} allowed), try a more restricted filter"
                         );
                         collected_events.push(ce);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/rpc/methods/eth/filter/mod.rs` around lines 371 - 375, The ensure! check
allows one extra event because it runs before pushing; update the guard in the
collector so it rejects the (max+1)th item—either change the condition to
require collected_events.len() < max_filter_results or use
collected_events.len().saturating_add(1) <= max_filter_results (or move the
check to after push and test > max_filter_results) in the block that contains
ensure! and collected_events.push(ce); make this change in the same code that
implements EthEventHandler::collect_events and the Hash/Key/Range branches so
the max_filter_results contract is enforced everywhere.
🧹 Nitpick comments (1)
src/rpc/methods/eth/filter/mod.rs (1)

275-279: Add tipset context to per-task failures.

If one branch fails, try_next() only surfaces the inner error, so you lose which tipset caused the failure. Wrapping the await with the epoch keeps range-scan failures diagnosable.

Suggested fix
         for tipset in tipsets {
+            let epoch = tipset.epoch();
             tasks.push_back(async move {
                 let mut events = vec![];
-                Self::collect_events(ctx, &tipset, spec, skip_event, &mut events).await?;
+                Self::collect_events(ctx, &tipset, spec, skip_event, &mut events)
+                    .await
+                    .with_context(|| format!("collecting events for epoch {epoch}"))?;
                 anyhow::Ok(events)
             });
         }

As per coding guidelines, "Use anyhow::Result for most operations and add context with .context() when errors occur".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/rpc/methods/eth/filter/mod.rs` around lines 275 - 279, The per-task
closure pushing to tasks uses Self::collect_events(ctx, &tipset, spec,
skip_event, &mut events).await but loses which tipset failed; wrap the await
result in an anyhow context that includes the tipset identifier (e.g., epoch or
cid) so failures report the tipset that caused them. Update the async closure
(the task created in tasks.push_back with Self::collect_events) to call
.context(...) on the awaited Result to append the tipset context before
returning anyhow::Ok(events).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/rpc/methods/eth/filter/mod.rs`:
- Around line 371-375: The ensure! check allows one extra event because it runs
before pushing; update the guard in the collector so it rejects the (max+1)th
item—either change the condition to require collected_events.len() <
max_filter_results or use collected_events.len().saturating_add(1) <=
max_filter_results (or move the check to after push and test >
max_filter_results) in the block that contains ensure! and
collected_events.push(ce); make this change in the same code that implements
EthEventHandler::collect_events and the Hash/Key/Range branches so the
max_filter_results contract is enforced everywhere.

---

Nitpick comments:
In `@src/rpc/methods/eth/filter/mod.rs`:
- Around line 275-279: The per-task closure pushing to tasks uses
Self::collect_events(ctx, &tipset, spec, skip_event, &mut events).await but
loses which tipset failed; wrap the await result in an anyhow context that
includes the tipset identifier (e.g., epoch or cid) so failures report the
tipset that caused them. Update the async closure (the task created in
tasks.push_back with Self::collect_events) to call .context(...) on the awaited
Result to append the tipset context before returning anyhow::Ok(events).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5328e85e-5ee8-488a-b197-292d426bbad4

📥 Commits

Reviewing files that changed from the base of the PR and between 15dc5b3 and 7a0feec.

📒 Files selected for processing (1)
  • src/rpc/methods/eth/filter/mod.rs

@LesnyRumcajs LesnyRumcajs added the RPC requires calibnet RPC checks to run on CI label Apr 10, 2026
@hanabi1224 hanabi1224 enabled auto-merge April 10, 2026 10:39
@hanabi1224 hanabi1224 added this pull request to the merge queue Apr 10, 2026
Merged via the queue into main with commit 6fab16d Apr 10, 2026
71 checks passed
@hanabi1224 hanabi1224 deleted the hm/speed-up-collect-events-for-range branch April 10, 2026 11:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

RPC requires calibnet RPC checks to run on CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants