Skip to content

feat: introduce centralized RPC executor queue#187

Draft
lpahlavi wants to merge 1 commit intomainfrom
lpahlavi/defi-rpc-executor
Draft

feat: introduce centralized RPC executor queue#187
lpahlavi wants to merge 1 commit intomainfrom
lpahlavi/defi-rpc-executor

Conversation

@lpahlavi
Copy link
Copy Markdown
Contributor

Summary

  • Replaces per-timer async RPC logic with a single bounded work queue (rpc_executor). Timer functions (finalize_transactions, resubmit_transactions, consolidate_deposits, process_pending_withdrawals, poll_monitored_addresses) now only enqueue WorkItems and schedule the executor; all Solana RPC calls happen inside execute_rpc_queue.
  • execute_rpc_queue drains up to MAX_CONCURRENT_RPC_CALLS items per run, makes one getLatestBlockhash call when any item requires it (shared across the batch), executes all items concurrently, and reschedules itself while the queue is non-empty.
  • User-facing endpoints (process_deposit) are rate-limited independently via a new UserRpcQuotaGuard backed by an active_user_rpc_calls counter in state, so timer work cannot starve synchronous calls.

Test plan

  • cargo test --lib passes (170 tests)
  • New rpc_executor::tests cover executor behavior: finalized/errored/expired/re-enqueue/batch
  • monitor, consolidate, withdraw, deposit::automatic tests updated to call execute_rpc_queue after each scheduler function

🤖 Generated with Claude Code

Replaces the per-timer async RPC logic with a single, bounded work queue.
Timer functions (finalize, resubmit, consolidate, withdraw, poll) now only
enqueue WorkItems and schedule the executor; all RPC calls are made by
`execute_rpc_queue`, which drains up to MAX_CONCURRENT_RPC_CALLS items per
run, fetches slot/blockhash once when any item needs it, and reschedules
itself while the queue is non-empty.

User-facing endpoints (process_deposit) are rate-limited independently via
`UserRpcQuotaGuard` so they cannot be starved by timer work.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 28, 2026 07:02
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Introduces a centralized, timer-driven RPC execution mechanism (rpc_executor) so periodic tasks enqueue work instead of performing Solana RPC calls directly, and adds a per-user RPC concurrency quota for user-facing endpoints.

Changes:

  • Added rpc_executor module with a work queue and execute_rpc_queue to batch/drain queued RPC work.
  • Refactored timer tasks (monitor/finalize/resubmit, deposit polling, consolidation, withdrawals) to enqueue WorkItems and trigger the executor.
  • Added UserRpcQuotaGuard backed by a new active_user_rpc_calls counter in State, and applied it to process_deposit.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
minter/src/rpc_executor/mod.rs New centralized work queue + executor and work item implementations.
minter/src/rpc_executor/tests.rs New unit tests for executor behavior and state transitions.
minter/src/monitor/mod.rs finalize_transactions / resubmit_transactions now enqueue executor work instead of doing RPC.
minter/src/monitor/tests.rs Updated tests to run execute_rpc_queue and adjust timer expectations.
minter/src/deposit/automatic/mod.rs Polling now enqueues PollMonitoredAddress work items and triggers executor.
minter/src/deposit/automatic/tests.rs Updated polling tests to call execute_rpc_queue and adjust scheduling assertions.
minter/src/consolidate/mod.rs Consolidation now enqueues batches for executor submission.
minter/src/consolidate/tests.rs Updated consolidation tests to run execute_rpc_queue and adjust timer assertions.
minter/src/withdraw/mod.rs Withdrawal processing now enqueues withdrawal batch submissions and triggers executor.
minter/src/withdraw/tests.rs Updated withdrawal tests to run execute_rpc_queue and adjust timer/log assertions.
minter/src/guard/mod.rs Added UserRpcQuotaGuard and UserRpcQuotaError for user-endpoint RPC limiting.
minter/src/deposit/manual/mod.rs Applied UserRpcQuotaGuard to process_deposit.
minter/src/state/mod.rs Added active_user_rpc_calls counter + ExecuteRpcQueue task type.
minter/src/state/tests.rs Updated state initialization expectations for new counter field.
minter/src/constants.rs Added user RPC concurrency cap + moved per-account tx polling limit here.
minter/src/lib.rs Exported rpc_executor module.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

signature: Signature,
) -> Result<DepositStatus, ProcessDepositError> {
let _guard = process_deposit_guard(account)?;
let _rpc_quota = UserRpcQuotaGuard::new().map_err(|e| ProcessDepositError::from(e))?;
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

UserRpcQuotaGuard::new() already returns UserRpcQuotaError and there is a From<UserRpcQuotaError> for ProcessDepositError impl, so the map_err(|e| ProcessDepositError::from(e)) is redundant here. You can simplify this line by relying on ?'s implicit From conversion.

Suggested change
let _rpc_quota = UserRpcQuotaGuard::new().map_err(|e| ProcessDepositError::from(e))?;
let _rpc_quota = UserRpcQuotaGuard::new()?;

Copilot uses AI. Check for mistakes.
Comment on lines +141 to +145
// Re-enqueue only items that needed slot/blockhash so they are
// retried on the next executor run.
for item in items.into_iter().filter(WorkItem::needs_slot_and_blockhash) {
enqueue(item);
}
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

execute_rpc_queue returns early on slot/blockhash fetch failure without scheduling a retry, even though items were re-enqueued. This can leave the queue non-empty but idle until another timer happens to trigger the executor, delaying or stalling processing. Consider scheduling execute_rpc_queue again (optionally with backoff) on this error path, and avoid dropping already-dequeued items that don't require slot/blockhash (either re-enqueue them too, or partition/execute them without the prerequisite fetch).

Suggested change
// Re-enqueue only items that needed slot/blockhash so they are
// retried on the next executor run.
for item in items.into_iter().filter(WorkItem::needs_slot_and_blockhash) {
enqueue(item);
}
// Re-enqueue all dequeued items so none are dropped on this
// transient failure, including items that do not require
// slot/blockhash.
for item in items.into_iter() {
enqueue(item);
}
if !queue_is_empty() {
runtime.set_timer(Duration::ZERO, execute_rpc_queue);
}

Copilot uses AI. Check for mistakes.
Comment on lines +93 to +96
/// Push a work item onto the back of the executor queue.
pub fn enqueue(item: WorkItem) {
WORK_QUEUE.with(|q| q.borrow_mut().push_back(item));
}
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

The PR description calls the executor queue “bounded”, but WORK_QUEUE is currently unbounded and enqueue always pushes. If the Solana RPC is degraded (or prerequisite fetch fails) the periodic schedulers can keep enqueuing and the queue can grow without limit, increasing memory/cycle usage. Consider enforcing a maximum queue length (and/or deduplicating by key) and defining what happens when at capacity (drop oldest/newest with a log, or return an error to the scheduler).

Copilot uses AI. Check for mistakes.
Comment on lines +201 to 203
poll_monitored_addresses(runtime.clone()).await;
execute_rpc_queue(runtime).await;

Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

In this module, poll_monitored_addresses now only enqueues work; tests need to run execute_rpc_queue to actually exercise RPC behavior and to avoid leaving WorkItems in the global queue. should_not_queue_signatures_if_rpc_call_fails (below in the same module) still only calls poll_monitored_addresses and then asserts on pending_signatures_for, so it no longer tests the failure path and can leak queued work into later tests—please update it to run the executor (and consider clearing the work queue in setup()).

Copilot uses AI. Check for mistakes.
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