Skip to content

fix: reduce forester load and update proving key downloads#2382

Merged
ananas-block merged 16 commits into
mainfrom
sergey/forester-rpc-efficiency
Jul 21, 2026
Merged

fix: reduce forester load and update proving key downloads#2382
ananas-block merged 16 commits into
mainfrom
sergey/forester-rpc-efficiency

Conversation

@sergeytimoshin

@sergeytimoshin sergeytimoshin commented Jun 8, 2026

Copy link
Copy Markdown
Contributor
  • Add adaptive V2 polling and reduce the default RPC pool size.
  • Disable V1 presort by default and fail fast on unsupported indexer methods.
  • Move proving-key downloads to CloudFront with checksum verification.

The V1 send path's optional get_queue_leaf_indices "presort" was effectively
always on (gated only by enable_v1_multi_nullify). On an indexer that does not
implement the endpoint it returns 404, which the Photon client treated as a
retryable ApiError and backed off ~44s (10 retries) per send. That consumed the
entire per-slot send budget, so the chunk aborted at the deadline guard before
building/sending any transaction — V1 queues never drained.

Changes:
- photon_indexer: treat an ApiError whose message contains "method not found"
  or "status 404" as non-retryable (fail fast instead of ~44s backoff).
- forester: add `--enable-v1-presort` (env ENABLE_V1_PRESORT), default off, and
  gate the get_queue_leaf_indices presort path on it.
- forester: replace the fixed 200ms V2 queue poll with adaptive backoff
  (200ms -> 10s cap, reset to 200ms when work is found) to stop idle V2 trees
  from re-fetching the queue ~5x/sec for the whole eligible window.
- forester: lower default --rpc-pool-size 100 -> 32.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a CLI/config toggle for optional V1 presort and lowers the RPC pool default to 32; introduces a process-wide semaphore to limit V1 send concurrency, adaptive backoff for V2 polling, conditional compressible-provider spawn, smart-transaction resend rate-limiting, and indexer 404/method-not-found non-retry classification.

Changes

Forester runtime and send-path updates

Layer / File(s) Summary
V1 Presort CLI and Configuration Wiring
forester/src/cli.rs, forester/src/config.rs
Reduces RPC pool default from 100→32, adds --enable-v1-presort/ENABLE_V1_PRESORT, and wires ForesterConfig.enable_v1_presort into new_for_start, new_for_status (false), and Clone.
Photon indexer: non-retryable 404/method-not-found
sdk-libs/client/src/indexer/photon_indexer.rs
Classifies IndexerError::ApiError messages containing “method not found” or “status 404” as non-retryable and logs accordingly; other API errors continue to be retried.
Conditional compressible provider spawn
forester/src/api_server.rs
run_compressible_provider is spawned only if in-memory trackers exist or config.forester_api_urls is non-empty.
EpochManager: semaphore, presort toggle, adaptive polling
forester/src/epoch_manager.rs
Adds process-wide v1_send_permits: Arc<Semaphore> initialized from transaction_config.max_concurrent_sends, passes it into V1 batching, sets enable_presort from config, and replaces fixed 200ms idle sleep with exponential-backoff polling (200ms doubling to 10s, reset on work).
V1 send: chunking, permit acquisition, bounded concurrency
forester/src/processor/v1/send_transaction.rs
Introduces shared send_permits semaphore, computes effective batch sizes, pre-acquires per-chunk permits with deadline-bound acquisition, limits concurrent chunks, uses buffer_unordered for results, and maps permit timeouts/closed-limiter to explicit send result variants.
Smart-transaction resend rate-limiting
forester/src/smart_transaction.rs
Adds MIN_TRANSACTION_RESEND_INTERVAL, computes resend interval from poll interval (×4 with minimum), and only invokes send when scheduled next_send_at is reached; advances schedule after each attempt.

Sequence Diagram

sequenceDiagram
  participant EpochManager
  participant SendBatchedTransactions
  participant V1_Send_Semaphore
  participant RPCNode
  EpochManager->>SendBatchedTransactions: request send batch (v1)
  SendBatchedTransactions->>V1_Send_Semaphore: try acquire N permits (deadline-bound)
  V1_Send_Semaphore-->>SendBatchedTransactions: permits granted / timeout / closed
  SendBatchedTransactions->>RPCNode: submit transactions (bounded concurrency)
  RPCNode-->>SendBatchedTransactions: send result / error
  SendBatchedTransactions-->>EpochManager: aggregated chunk results
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

ai-review

Suggested reviewers

  • SwenSchaeferjohann
  • ananas-block

Poem

Semaphore guards the V1 parade,
Polling learns to pause, then trade,
Presort waits behind a flag so small,
404s no longer call for thrall,
RPC trimmed down — a leaner hall.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 70.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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title captures the main change of reducing forester load, though the proving key downloads clause is unrelated noise.
✨ 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 sergey/forester-rpc-efficiency

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

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 412b3356de

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2347 to +2348
tokio::time::sleep(poll_interval).await;
poll_interval = (poll_interval * 2).min(POLL_INTERVAL_MAX);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Cap idle V2 polling to the light-slot budget

When a V2 tree is idle early in its eligible light slot, this sleep backs off without considering how much of the light slot remains. I checked the protocol defaults (programs/registry/src/protocol_config/state.rs): local/default slot_length is 10 Solana slots (~4s) and testnet is 60 (~24s), so after several empty polls the forester can sleep 6.4s/10s, wake after forester_slot_details.end_solana_slot, and skip work that arrived during the sleep until a later eligibility window. Cap the sleep/backoff by the remaining light-slot time or keep it well below the slot length so late-arriving V2 queue items can still be processed in the current slot.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@forester/src/epoch_manager.rs`:
- Around line 2268-2275: POLL_INTERVAL_MIN and POLL_INTERVAL_MAX are hardcoded;
make them configurable via CLI and environment variables and use those values
wherever the constants are referenced (e.g., POLL_INTERVAL_MIN,
POLL_INTERVAL_MAX, and the local poll_interval variable in epoch_manager.rs and
the other occurrences you noted). Add config fields (e.g.,
v2_poll_interval_min_ms and v2_poll_interval_max_ms) to the application
Config/opts (supporting both CLI flags and env vars), parse them into Durations
(defaulting to Duration::from_millis(200) and Duration::from_secs(10) when
unspecified), and replace the constants/inline literals with values from that
Config (ensure validation: min <= max and reasonable bounds). Ensure all uses
(including the other instances at the referenced locations) read from the same
config struct so deployments can tune RPC pressure without code changes.
🪄 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

Run ID: 22decbc8-303a-44d0-aa19-6feb45d77c75

📥 Commits

Reviewing files that changed from the base of the PR and between 5d58e11 and 412b335.

📒 Files selected for processing (4)
  • forester/src/cli.rs
  • forester/src/config.rs
  • forester/src/epoch_manager.rs
  • sdk-libs/client/src/indexer/photon_indexer.rs

Comment thread forester/src/epoch_manager.rs
sergeytimoshin and others added 3 commits June 10, 2026 11:00
The api_server unconditionally spawned run_compressible_provider, which with no
in-memory trackers falls back to a full paginated getProgramAccounts scan every
30s. Even with compressible tracking disabled (the default), that heavy scan ties
up RPC pool connections and triggers "Failed to get RPC connection" pool failures
on mainnet.

Only spawn the provider when there is a cheap source — in-memory trackers
(compression enabled) or upstream forester APIs to aggregate. Otherwise skip it;
the dashboard simply reports no compressible counts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
forester/src/processor/v1/send_transaction.rs (1)

216-233: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reserve send permits by transaction count, not work-item count.

permits_to_reserve is derived from work_chunk.len(), but build_signed_transaction_batch(..., build_config) can collapse multiple work items into a single transaction when build_config.batch_size / legacy_ixs_per_tx is greater than 1. That over-reserves the process-wide semaphore, strands unused permits, and under contention can make a chunk return Ok(()) with 0 sends even though there was enough capacity for the chunk’s actual transaction count. Base the reservation on the number of transactions this chunk can emit (or at least div_ceil(work_chunk.len(), batch_size)) so the limiter matches real send concurrency.

Suggested direction
-                let permits_to_reserve = work_chunk.len().min(effective_max_concurrent_sends).max(1);
+                let tx_batch_size = usize::try_from(build_config.batch_size)
+                    .unwrap_or(usize::MAX)
+                    .max(1);
+                let permits_to_reserve = work_chunk
+                    .len()
+                    .div_ceil(tx_batch_size)
+                    .min(effective_max_concurrent_sends)
+                    .max(1);

Also applies to: 248-257

🤖 Prompt for 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.

In `@forester/src/processor/v1/send_transaction.rs` around lines 216 - 233, The
current code computes permits_to_reserve from work_chunk.len(), which
over-reserves when build_signed_transaction_batch (and its
build_config.batch_size / legacy_ixs_per_tx) can combine multiple work items
into fewer transactions; change the reservation logic used before calling
acquire_send_permits (and the identical logic in the other chunk where permits
are reserved) to compute the number of transactions instead, e.g.
div_ceil(work_chunk.len(), effective_batch_size) where effective_batch_size =
build_config.batch_size * legacy_ixs_per_tx (or the exact contraction logic used
by build_signed_transaction_batch), then
.min(effective_max_concurrent_sends).max(1) to produce permits_to_reserve so the
semaphore matches actual transaction concurrency.
🤖 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.

Outside diff comments:
In `@forester/src/processor/v1/send_transaction.rs`:
- Around line 216-233: The current code computes permits_to_reserve from
work_chunk.len(), which over-reserves when build_signed_transaction_batch (and
its build_config.batch_size / legacy_ixs_per_tx) can combine multiple work items
into fewer transactions; change the reservation logic used before calling
acquire_send_permits (and the identical logic in the other chunk where permits
are reserved) to compute the number of transactions instead, e.g.
div_ceil(work_chunk.len(), effective_batch_size) where effective_batch_size =
build_config.batch_size * legacy_ixs_per_tx (or the exact contraction logic used
by build_signed_transaction_batch), then
.min(effective_max_concurrent_sends).max(1) to produce permits_to_reserve so the
semaphore matches actual transaction concurrency.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0c062dbc-9528-4a41-9dde-0f98638a1d58

📥 Commits

Reviewing files that changed from the base of the PR and between 50271a5 and 2627db3.

📒 Files selected for processing (1)
  • forester/src/processor/v1/send_transaction.rs

@sergeytimoshin sergeytimoshin changed the title forester: fix V1 nullify presort retry storm and reduce RPC pressure fix(forester): bound RPC load and isolate prover queues Jul 20, 2026
@sergeytimoshin sergeytimoshin changed the title fix(forester): bound RPC load and isolate prover queues fix: bound forester RPC load and isolate prover queues Jul 20, 2026
@sergeytimoshin sergeytimoshin changed the title fix: bound forester RPC load and isolate prover queues fix: reduce forester load and update proving key downloads Jul 21, 2026
@ananas-block
ananas-block merged commit ad5964f into main Jul 21, 2026
49 checks passed
@ananas-block
ananas-block deleted the sergey/forester-rpc-efficiency branch July 21, 2026 14:55
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