-
Notifications
You must be signed in to change notification settings - Fork 8
Optimize EOA executor by combining pending transactions and nonce fetching #93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optimize EOA executor by combining pending transactions and nonce fetching #93
Conversation
WalkthroughA new public async method Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Tip 📝 Customizable high-level summaries are now available in beta!You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.
Example instruction:
Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later. Comment |
This stack of pull requests is managed by Graphite. Learn more about stacking. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (2)
executors/src/eoa/store/mod.rs (2)
637-640: Consider optimizing the limit==0 case to maintain consistency.When
limit == 0, the method callsget_optimistic_transaction_count(), which makes a separate Redis round-trip. This is inconsistent with the optimization goal of the method (combining fetches into a single operation). Consider using a pipeline even for this case to maintain consistency.Apply this diff:
pub async fn peek_pending_transactions_with_optimistic_nonce( &self, limit: u64, ) -> Result<(Vec<PendingTransaction>, u64), TransactionStoreError> { - if limit == 0 { - let optimistic = self.get_optimistic_transaction_count().await?; - return Ok((Vec::new(), optimistic)); - } - let pending_key = self.pending_transactions_zset_name(); let optimistic_key = self.optimistic_transaction_count_key_name(); let mut conn = self.redis.clone(); - // First pipeline: Get transaction IDs and optimistic nonce together - let start = 0isize; - let stop = (limit - 1) as isize; - - let (transaction_ids, optimistic_nonce): ( - Vec<PendingTransactionStringWithQueuedAt>, - Option<u64>, - ) = twmq::redis::pipe() - .zrange_withscores(&pending_key, start, stop) - .get(&optimistic_key) - .query_async(&mut conn) - .await?; + // First pipeline: Get transaction IDs (if limit > 0) and optimistic nonce together + let (transaction_ids, optimistic_nonce): ( + Vec<PendingTransactionStringWithQueuedAt>, + Option<u64>, + ) = if limit == 0 { + let optimistic: Option<u64> = twmq::redis::pipe() + .get(&optimistic_key) + .query_async(&mut conn) + .await?; + (Vec::new(), optimistic) + } else { + let start = 0isize; + let stop = (limit - 1) as isize; + twmq::redis::pipe() + .zrange_withscores(&pending_key, start, stop) + .get(&optimistic_key) + .query_async(&mut conn) + .await? + }; let optimistic = optimistic_nonce.ok_or_else(|| self.nonce_sync_required_error())?;
666-701: Consider refactoring to reduce code duplication.The logic in lines 666-701 (fetching user_request data, handling missing data, and scheduling deletions) is duplicated from
peek_pending_transactions_paginated(lines 573-609). Consider extracting this common logic into a private helper method to improve maintainability.For example, extract a helper method like:
async fn fetch_pending_transaction_data( &self, transaction_ids: Vec<PendingTransactionStringWithQueuedAt>, ) -> Result<Vec<PendingTransaction>, TransactionStoreError> { if transaction_ids.is_empty() { return Ok(Vec::new()); } let mut conn = self.redis.clone(); let mut pipe = twmq::redis::pipe(); for (transaction_id, _) in &transaction_ids { let tx_data_key = self.transaction_data_key_name(transaction_id); pipe.hget(&tx_data_key, "user_request"); } let user_requests: Vec<Option<String>> = pipe.query_async(&mut conn).await?; let mut pending_transactions: Vec<PendingTransaction> = Vec::new(); let mut deletion_pipe = twmq::redis::pipe(); for ((transaction_id, queued_at), user_request) in transaction_ids.into_iter().zip(user_requests) { match user_request { Some(user_request) => { let user_request_parsed = serde_json::from_str(&user_request)?; pending_transactions.push(PendingTransaction { transaction_id, queued_at, user_request: user_request_parsed, }); } None => { tracing::warn!( "Transaction {} data was missing, deleting transaction from redis", transaction_id ); deletion_pipe.zrem(self.keys.pending_transactions_zset_name(), transaction_id); } } } if !deletion_pipe.is_empty() { deletion_pipe.query_async::<()>(&mut conn).await?; } Ok(pending_transactions) }Then use it in both methods to eliminate duplication.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
executors/src/eoa/store/mod.rs(1 hunks)executors/src/eoa/worker/send.rs(1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-20T06:58:40.230Z
Learnt from: d4mr
Repo: thirdweb-dev/engine-core PR: 48
File: executors/src/eoa/store/submitted.rs:229-230
Timestamp: 2025-09-20T06:58:40.230Z
Learning: The diff in executors/src/eoa/store/submitted.rs shows correct brace structure - the first closing brace closes the remove_transaction_from_redis_submitted_zset method and the second closing brace closes the impl CleanSubmittedTransactions block. The change only adds whitespace formatting.
Applied to files:
executors/src/eoa/store/mod.rsexecutors/src/eoa/worker/send.rs
📚 Learning: 2025-07-06T15:44:13.701Z
Learnt from: d4mr
Repo: thirdweb-dev/engine-core PR: 5
File: executors/src/eoa/worker.rs:173-176
Timestamp: 2025-07-06T15:44:13.701Z
Learning: The EOA executor store uses comprehensive WATCH-based coordination where every Redis state mutation watches the lock key and validates ownership before proceeding. If the lock is lost during any operation, the transaction fails with LockLost error. This makes aggressive lock acquisition safe because only the actual lock holder can successfully perform state mutations, regardless of who claims the lock.
Applied to files:
executors/src/eoa/store/mod.rs
🧬 Code graph analysis (2)
executors/src/eoa/store/mod.rs (1)
executors/src/eoa/store/hydrate.rs (4)
deletion_pipe(113-114)serde_json(92-92)serde_json(132-132)serde_json(159-159)
executors/src/eoa/worker/send.rs (2)
executors/src/metrics.rs (2)
current_timestamp_ms(287-289)calculate_duration_seconds(273-275)executors/src/eoa/store/atomic.rs (3)
eoa(84-86)chain_id(89-91)worker_id(94-96)
🔇 Additional comments (2)
executors/src/eoa/store/mod.rs (1)
631-704: LGTM! Effective optimization to reduce Redis round-trips.The new method successfully combines fetching pending transactions and the optimistic nonce into a single operation, reducing Redis round-trips from 2 to 1. The implementation correctly:
- Uses Redis pipelines for efficient batching
- Validates optimistic nonce presence
- Handles missing transaction data by scheduling deletions
- Returns both results together
This optimization should improve performance in the send flow.
executors/src/eoa/worker/send.rs (1)
402-421: LGTM! Clean integration of the optimized method.The changes properly integrate the new
peek_pending_transactions_with_optimistic_noncemethod to:
- Eliminate a separate Redis call for fetching the optimistic nonce
- Calculate
batch_sizeexplicitly for clearer logic- Update logging to reflect the combined operation
The nonce handling is correct, using
optimistic_nonce + ifor sequential nonce allocation (line 442).

Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.