Skip to content

Reduce RPC hot-path requests#1259

Merged
GabrielePicco merged 9 commits into
masterfrom
perf/cache-rpc-slot-blockhash
May 29, 2026
Merged

Reduce RPC hot-path requests#1259
GabrielePicco merged 9 commits into
masterfrom
perf/cache-rpc-slot-blockhash

Conversation

@GabrielePicco
Copy link
Copy Markdown
Collaborator

@GabrielePicco GabrielePicco commented May 28, 2026

Summary

  • Cache latest blockhashes and recent slots in the shared MagicblockRpcClient to avoid repeated hot-path RPC calls.
  • Seed the slot cache from getLatestBlockhash responses and serialize cache refreshes to avoid request stampedes.
  • Replace the TableMania ALT finalized-readiness slot polling loop with timed sleeps while keeping finalized account fetches as the readiness check.

Validation

  • cargo check -p magicblock-rpc-client -p magicblock-table-mania
  • cargo check -p magicblock-committor-service
  • cargo test -p magicblock-rpc-client -p magicblock-table-mania

Summary by CodeRabbit

  • Performance Improvements

    • Added in-memory caching for RPC blockhashes and slots to reduce requests and improve latency.
    • Optimized remote-table polling with timed, fixed-interval checks for smoother synchronization.
  • Reliability

    • New explicit cache invalidation ensures retries use fresh blockhashes, reducing transient failures.
    • Services now accept an optional shared chain-slot signal to improve coordination across components.
  • Logging

    • Reduced noisy wait-loop logs by emitting periodic status messages less frequently.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

Warning

Review limit reached

@GabrielePicco, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 52 minutes and 13 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9b779a86-bd9a-4966-991d-da6457359e9e

📥 Commits

Reviewing files that changed from the base of the PR and between d9be235 and 7f63d87.

📒 Files selected for processing (1)
  • magicblock-chainlink/src/remote_account_provider/mod.rs
📝 Walkthrough

Walkthrough

This PR adds per-client in-memory caching for latest blockhash and slot (with TTLs, guarded refresh, and invalidate API), changes the blockhash RPC call to RpcRequest::GetLatestBlockhash with JSON params, threads an optional shared Arc chain-slot through validator/chainlink/remote-provider/committor wiring, refactors remote-table wait loop logging to a time-based rate limiter, and propagates cache invalidation calls into intent executors and delivery preparator retry paths.

Suggested reviewers

  • thlorenz
  • bmuddha
  • snawaz
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/cache-rpc-slot-blockhash

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 and usage tips.

@GabrielePicco GabrielePicco marked this pull request as ready for review May 28, 2026 08:27
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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: 22db84c8e2

ℹ️ 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 thread magicblock-rpc-client/src/lib.rs Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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: 01e4190d9c

ℹ️ 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 thread magicblock-rpc-client/src/lib.rs Outdated
Comment on lines +328 to +329
if let Some(slot) = self.cached_slot().await {
return Ok(slot);
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 Bypass the slot cache for freshness-sensitive callers

When a slot was cached shortly before a transaction (including via get_latest_blockhash() caching the response context), this branch can return the pre-transaction slot for up to 400ms. LookupTableRc::deactivate immediately stores rpc_client.get_slot() as the table's deactivation_slot, and is_deactivated_on_chain derives the close threshold from that value; on the repo's 50ms local validator this can make table GC/close() run several slots early and fail until the real deactivation window catches up. Keep get_slot() fresh, or split out a cached accessor only for polling paths that tolerate staleness.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

How was this resolved? Which commit?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 2151ff9. get_slot() now bypasses the cache and fetches from RPC every time, then updates the cache opportunistically.

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

🤖 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 `@magicblock-rpc-client/src/lib.rs`:
- Around line 346-349: clear_cached_blockhash currently only writes to
self.cache.blockhash, allowing a concurrent get_latest_blockhash refresh to
repopulate an older value immediately after clear; fix by acquiring the same
refresh mutex used by get_latest_blockhash (the cache refresh lock) before
setting *cached = None so the clear is serialized with refreshes—update
clear_cached_blockhash to lock that refresh mutex, then write None to
self.cache.blockhash, and finally release the mutex.
🪄 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: 9724d224-449a-4a20-ab1c-eb77c492b7c0

📥 Commits

Reviewing files that changed from the base of the PR and between 22db84c and 01e4190.

📒 Files selected for processing (5)
  • magicblock-committor-service/src/intent_executor/intent_execution_client.rs
  • magicblock-committor-service/src/intent_executor/single_stage_executor.rs
  • magicblock-committor-service/src/intent_executor/two_stage_executor.rs
  • magicblock-committor-service/src/transaction_preparator/delivery_preparator.rs
  • magicblock-rpc-client/src/lib.rs

Comment thread magicblock-rpc-client/src/lib.rs Outdated
Copy link
Copy Markdown
Collaborator

@thlorenz thlorenz left a comment

Choose a reason for hiding this comment

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

Overall looks good, but some things seem to be unclean and we should evaluate if they can be improved.

Comment thread magicblock-rpc-client/src/lib.rs Outdated
}

let _guard = self.cache.blockhash_refresh.lock().await;
if let Some(blockhash) = self.cached_blockhash().await {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This seems to repeat the check from above except above we don't guard the access.
Also needing to take a guard of one resource to access another resource is not good practice. Instead if both resources are that dependent then maybe they should be kept inside a single wrapping resource which will be guarded to access the inner pieces.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The second check is intentional double-checked locking: another task may have refreshed the cache while this task was waiting on the refresh mutex. Agree that looks unclean, will refactor

return Ok(blockhash);
}

let resp: Response<RpcBlockhash> = self
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why not keep self.client.get_latest_blockhash() here instead of manually repeating what's in that method?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Solana's Rust RPC client only exposes the parsed blockhash from get_latest_blockhash(), while the RPC response also includes context.slot. This is dumb, but afaik there is not way to get both without manually implementing it

Comment thread magicblock-rpc-client/src/lib.rs Outdated
))
})?;

self.cache_slot(resp.context.slot).await;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Seems like slot and blockhash are very coupled here, maybe keep them together in a wrapper as well?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Resolved in d9be235: cached blockhash now carries the context slot and records it into the shared observed slot.

Comment thread magicblock-rpc-client/src/lib.rs Outdated
Comment on lines +328 to +329
if let Some(slot) = self.cached_slot().await {
return Ok(slot);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

How was this resolved? Which commit?

Comment thread magicblock-rpc-client/src/lib.rs Outdated
return Ok(slot);
}

let _guard = self.cache.slot_refresh.lock().await;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ditto separate guard (cache) for other resource (cached_slot) which seems like a code smell.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Resolved in d9be235: slot cache now uses a single mutex, so the separate refresh guard is gone.

Comment thread magicblock-rpc-client/src/lib.rs Outdated

pub async fn clear_cached_blockhash(&self) {
let _guard = self.cache.blockhash_refresh.lock().await;
let mut cached = self.cache.blockhash.write().await;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ditto separate guard

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Resolved in d9be235: same cleanup, no separate refresh guard remains here.

"bytemuck",
] }
futures-util = { workspace = true }
serde_json = { workspace = true }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Afaik this is only needed since we reimplemented code that's already in the solana rpc client crate.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

See this: #1259 (comment)

Agree on the ugliness, but afaik there is not way to cleanly do it and I want to avoid 2 RPC calls when the slot information is already in the response

Comment thread magicblock-table-mania/src/manager.rs Outdated
debug!("Still waiting for remote tables");
}
last_slot = slot;
sleep(Duration::from_millis(400)).await;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If I understand correctly this is to remove polling the RPC over and over for the next slot?
So here we just assume that after 400ms the next slot was reached?

In that case we should take slot jitter into account and wait for at least 450ms (slots don't update exactly after 400ms)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Another option would be to wire the chain slot subscription that chainlink already has to avoid polling and still be able to continue exactly when the next slot was reached.

To me that seems the more correct solution, but may require a lot more changes.

Copy link
Copy Markdown
Collaborator Author

@GabrielePicco GabrielePicco May 29, 2026

Choose a reason for hiding this comment

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

Resolved in d9be235: TableMania now waits on the shared Chainlink-observed slot (added an AtomicU64 to wire chain slot subscription from chainlink), with a 450ms fallback.

Comment thread magicblock-rpc-client/src/lib.rs Outdated
.map_err(|e| MagicBlockRpcClientError::GetSlot(Box::new(e)))
}

pub async fn clear_cached_blockhash(&self) {
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.

Why does one have to call this manually? I would imagine simple flow like this
get_latest_blockhash()

  1. Check cache
    • Missing -> fetch, store, return
    • Present -> continue
  2. Is cached value still fresh?
    • Yes -> return cached value
    • No -> fetch new value, overwrite cache, return

clear_cached_blockhash seems like an extra piece which is also error prone as one could forget to trigger that.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The normal freshness path is already TTL-based. This explicit invalidation is for immediate retry paths after cleanup/patching, where we do not want to sign the next prepared transaction with the same cached hash even if it is still TTL-fresh. Will clarify the names

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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

ℹ️ 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 thread magicblock-rpc-client/src/lib.rs
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.

Caution

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

⚠️ Outside diff range comments (2)
test-integration/test-chainlink/src/ixtest_context.rs (1)

111-117: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add one integration path that passes a real shared Arc<AtomicU64>.

Every updated helper still passes None, so the new shared-slot wiring is untested here. A regression in the injected-slot path would still compile and these tests would keep passing because the provider falls back to its own default slot.

🤖 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 `@test-integration/test-chainlink/src/ixtest_context.rs` around lines 111 -
117, The call to RemoteAccountProvider::try_from_urls_and_config is always
passing None for the injected slot, leaving the new shared-slot wiring untested;
create a real shared Arc<AtomicU64> (e.g. Arc::new(AtomicU64::new(0))) and pass
Some(shared_slot.clone()) into the function call where currently None is
supplied (keep references to endpoints, commitment, tx, and
config.remote_account_provider unchanged), and add one integration path/test
that uses this variant so the injected-slot code path is exercised.
test-integration/test-committor-service/tests/test_ix_commit_local.rs (1)

248-254: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Exercise CommittorService::try_start(..., Some(chain_slot)) in at least one integration test.

All updated helpers still pass None, so the new shared-slot branch in CommittorProcessor::try_new never runs in CI. That leaves the cross-service slot-sharing path effectively unvalidated.

Also applies to: 331-337, 805-811, 875-881

🤖 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 `@test-integration/test-committor-service/tests/test_ix_commit_local.rs` around
lines 248 - 254, Update at least one integration test to call
CommittorService::try_start with Some(chain_slot) instead of None so the
shared-slot branch in CommittorProcessor::try_new is exercised; create a
ChainSlot/value (or reuse an existing slot-producing helper) before the
try_start call and pass it as the fourth argument, ensuring any other services
in the test that should share the slot get the same ChainSlot so the
cross-service slot-sharing path runs; update one of the occurrences (e.g., the
call with validator_auth.insecure_clone()) and adjust setup/teardown as needed
so the test compiles and asserts behavior when a shared slot is provided.
🤖 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 `@test-integration/test-chainlink/src/ixtest_context.rs`:
- Around line 111-117: The call to
RemoteAccountProvider::try_from_urls_and_config is always passing None for the
injected slot, leaving the new shared-slot wiring untested; create a real shared
Arc<AtomicU64> (e.g. Arc::new(AtomicU64::new(0))) and pass
Some(shared_slot.clone()) into the function call where currently None is
supplied (keep references to endpoints, commitment, tx, and
config.remote_account_provider unchanged), and add one integration path/test
that uses this variant so the injected-slot code path is exercised.

In `@test-integration/test-committor-service/tests/test_ix_commit_local.rs`:
- Around line 248-254: Update at least one integration test to call
CommittorService::try_start with Some(chain_slot) instead of None so the
shared-slot branch in CommittorProcessor::try_new is exercised; create a
ChainSlot/value (or reuse an existing slot-producing helper) before the
try_start call and pass it as the fourth argument, ensuring any other services
in the test that should share the slot get the same ChainSlot so the
cross-service slot-sharing path runs; update one of the occurrences (e.g., the
call with validator_auth.insecure_clone()) and adjust setup/teardown as needed
so the test compiles and asserts behavior when a shared slot is provided.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4dbae04e-0533-453e-80d8-5f66339ed63d

📥 Commits

Reviewing files that changed from the base of the PR and between 01e4190 and d9be235.

📒 Files selected for processing (14)
  • magicblock-api/src/magic_validator.rs
  • magicblock-chainlink/src/chainlink/mod.rs
  • magicblock-chainlink/src/remote_account_provider/mod.rs
  • magicblock-committor-service/src/committor_processor.rs
  • magicblock-committor-service/src/intent_executor/intent_execution_client.rs
  • magicblock-committor-service/src/intent_executor/single_stage_executor.rs
  • magicblock-committor-service/src/intent_executor/two_stage_executor.rs
  • magicblock-committor-service/src/service.rs
  • magicblock-committor-service/src/transaction_preparator/delivery_preparator.rs
  • magicblock-rpc-client/src/lib.rs
  • magicblock-table-mania/src/manager.rs
  • test-integration/test-chainlink/src/ixtest_context.rs
  • test-integration/test-chainlink/tests/ix_remote_account_provider.rs
  • test-integration/test-committor-service/tests/test_ix_commit_local.rs

Copy link
Copy Markdown
Collaborator

@thlorenz thlorenz left a comment

Choose a reason for hiding this comment

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

LGTM now. Thanks for addressing all points I raised.

@GabrielePicco GabrielePicco merged commit ff62ad1 into master May 29, 2026
45 checks passed
@GabrielePicco GabrielePicco deleted the perf/cache-rpc-slot-blockhash branch May 29, 2026 11:34
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.

3 participants