-
Notifications
You must be signed in to change notification settings - Fork 59
Trp debug #901
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
Merged
Trp debug #901
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
89ad831
tidy up interfaces before refactoring
scarmuega 35952a9
redb mempool attempt 1
scarmuega e49498a
tidy up redb mempool
scarmuega 059b4d7
tidy up mempool trait
scarmuega f4418d4
tidy up mempool impl
scarmuega 8a5a14c
wrap it up
scarmuega e771dbc
remove config until better approach
scarmuega bbc1922
fix lints
scarmuega 51cd0ad
apply feedback
scarmuega 8cd13a1
imprlement non-confirmations
scarmuega b6400b0
refactor mempool internals
scarmuega 5d8f4b7
more nomenclature fixes
scarmuega 9d23cd8
docs: add skill for patterns on redb
scarmuega 6d217cd
refactor: avoid collect-and-mutate pattern when possible
scarmuega 9ee9c96
fix retry on built-in mempool
scarmuega 416fa24
fix deadlock on test harness
scarmuega 3b7c5ab
make mark ops fallible
scarmuega fcbf759
check inflight for duplicates too
scarmuega d17e7d6
introduce dropped txs
scarmuega File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,315 @@ | ||
| //! Built-in in-memory mempool implementation. | ||
| //! | ||
| //! A basic FIFO mempool backed by in-memory data structures and a | ||
| //! broadcast channel for event notifications. Suitable for single-node | ||
| //! deployments and development/testing. | ||
|
|
||
| use std::{ | ||
| collections::{HashMap, HashSet, VecDeque}, | ||
| sync::{Arc, RwLock}, | ||
| }; | ||
| use tokio::sync::broadcast; | ||
| use tokio_stream::wrappers::BroadcastStream; | ||
| use tracing::{debug, info}; | ||
|
|
||
| use crate::{ | ||
| ChainPoint, MempoolError, MempoolEvent, MempoolPage, MempoolStore, MempoolTx, MempoolTxStage, | ||
| TxHash, TxStatus, | ||
| }; | ||
|
|
||
| #[derive(Default)] | ||
| struct MempoolState { | ||
| pending: Vec<MempoolTx>, | ||
| inflight: Vec<MempoolTx>, | ||
| acknowledged: HashMap<TxHash, MempoolTx>, | ||
| finalized_log: VecDeque<MempoolTx>, | ||
| } | ||
|
scarmuega marked this conversation as resolved.
|
||
|
|
||
| const MAX_FINALIZED_LOG: usize = 1000; | ||
|
|
||
| /// A basic, FIFO, in-memory mempool. | ||
| #[derive(Clone)] | ||
| pub struct EphemeralMempool { | ||
| state: Arc<RwLock<MempoolState>>, | ||
| updates: broadcast::Sender<MempoolEvent>, | ||
| } | ||
|
|
||
| impl Default for EphemeralMempool { | ||
| fn default() -> Self { | ||
| Self::new() | ||
| } | ||
| } | ||
|
|
||
| impl EphemeralMempool { | ||
| pub fn new() -> Self { | ||
| let state = Arc::new(RwLock::new(MempoolState::default())); | ||
| let (updates, _) = broadcast::channel(16); | ||
|
|
||
| Self { state, updates } | ||
| } | ||
|
|
||
| fn notify(&self, tx: MempoolTx) { | ||
| if self.updates.send(MempoolEvent { tx }).is_err() { | ||
| debug!("no mempool update receivers"); | ||
| } | ||
| } | ||
|
|
||
| fn log_state(&self, state: &MempoolState) { | ||
| debug!( | ||
| pending = state.pending.len(), | ||
| inflight = state.inflight.len(), | ||
| acknowledged = state.acknowledged.len(), | ||
| "mempool state changed" | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| pub struct EphemeralMempoolStream { | ||
| inner: BroadcastStream<MempoolEvent>, | ||
| } | ||
|
|
||
| impl futures_core::Stream for EphemeralMempoolStream { | ||
| type Item = Result<MempoolEvent, MempoolError>; | ||
|
|
||
| fn poll_next( | ||
| mut self: std::pin::Pin<&mut Self>, | ||
| cx: &mut std::task::Context<'_>, | ||
| ) -> std::task::Poll<Option<Self::Item>> { | ||
| use futures_util::StreamExt; | ||
|
|
||
| match self.inner.poll_next_unpin(cx) { | ||
| std::task::Poll::Ready(Some(x)) => match x { | ||
| Ok(x) => std::task::Poll::Ready(Some(Ok(x))), | ||
| Err(err) => { | ||
| std::task::Poll::Ready(Some(Err(MempoolError::Internal(Box::new(err))))) | ||
| } | ||
| }, | ||
| std::task::Poll::Ready(None) => std::task::Poll::Ready(None), | ||
| std::task::Poll::Pending => std::task::Poll::Pending, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl MempoolStore for EphemeralMempool { | ||
| type Stream = EphemeralMempoolStream; | ||
|
|
||
| fn receive(&self, tx: MempoolTx) -> Result<(), MempoolError> { | ||
| let mut state = self.state.write().unwrap(); | ||
|
|
||
| if state.pending.iter().any(|p| p.hash == tx.hash) { | ||
| return Err(MempoolError::DuplicateTx); | ||
| } | ||
|
|
||
| info!(tx.hash = %tx.hash, "tx received"); | ||
| state.pending.push(tx.clone()); | ||
| self.notify(tx); | ||
| self.log_state(&state); | ||
|
|
||
| Ok(()) | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| fn has_pending(&self) -> bool { | ||
| let state = self.state.read().unwrap(); | ||
| !state.pending.is_empty() | ||
| } | ||
|
|
||
| fn peek_pending(&self, limit: usize) -> Vec<MempoolTx> { | ||
| let state = self.state.read().unwrap(); | ||
| state.pending.iter().take(limit).cloned().collect() | ||
| } | ||
|
|
||
| fn mark_inflight(&self, hashes: &[TxHash]) -> Result<(), MempoolError> { | ||
| let hash_set: HashSet<_> = hashes.iter().collect(); | ||
| let mut state = self.state.write().unwrap(); | ||
|
|
||
| let mut moved = Vec::new(); | ||
| state.pending.retain(|tx| { | ||
| if hash_set.contains(&tx.hash) { | ||
| moved.push(tx.clone()); | ||
| false | ||
| } else { | ||
| true | ||
| } | ||
| }); | ||
|
|
||
| for mut tx in moved { | ||
| info!(tx.hash = %tx.hash, "tx inflight"); | ||
| tx.stage = MempoolTxStage::Propagated; | ||
| state.inflight.push(tx.clone()); | ||
| self.notify(tx); | ||
| } | ||
|
|
||
| self.log_state(&state); | ||
| Ok(()) | ||
| } | ||
|
|
||
| fn mark_acknowledged(&self, hashes: &[TxHash]) -> Result<(), MempoolError> { | ||
| let hash_set: HashSet<_> = hashes.iter().collect(); | ||
| let mut state = self.state.write().unwrap(); | ||
|
|
||
| let mut moved = Vec::new(); | ||
| state.inflight.retain(|tx| { | ||
| if hash_set.contains(&tx.hash) { | ||
| moved.push(tx.clone()); | ||
| false | ||
| } else { | ||
| true | ||
| } | ||
| }); | ||
|
|
||
| for mut tx in moved { | ||
| info!(tx.hash = %tx.hash, "tx acknowledged"); | ||
| tx.stage = MempoolTxStage::Acknowledged; | ||
| state.acknowledged.insert(tx.hash, tx.clone()); | ||
| self.notify(tx); | ||
| } | ||
|
|
||
| self.log_state(&state); | ||
| Ok(()) | ||
| } | ||
|
|
||
| fn find_inflight(&self, tx_hash: &TxHash) -> Option<MempoolTx> { | ||
| let state = self.state.read().unwrap(); | ||
| // Check propagated (inflight vec) | ||
| if let Some(tx) = state.inflight.iter().find(|x| x.hash.eq(tx_hash)) { | ||
| return Some(tx.clone()); | ||
| } | ||
| // Check acknowledged/confirmed | ||
| state.acknowledged.get(tx_hash).cloned() | ||
| } | ||
|
|
||
| fn peek_inflight(&self, limit: usize) -> Vec<MempoolTx> { | ||
| let state = self.state.read().unwrap(); | ||
|
|
||
| state | ||
| .inflight | ||
| .iter() | ||
| .chain(state.acknowledged.values()) | ||
| .take(limit) | ||
| .cloned() | ||
| .collect() | ||
| } | ||
|
|
||
| fn confirm(&self, point: &ChainPoint, seen_txs: &[TxHash], unseen_txs: &[TxHash], finalize_threshold: u32, drop_threshold: u32) -> Result<(), MempoolError> { | ||
| let mut state = self.state.write().unwrap(); | ||
|
|
||
| if state.acknowledged.is_empty() { | ||
| return Ok(()); | ||
| } | ||
|
|
||
| let seen_set: HashSet<&TxHash> = seen_txs.iter().collect(); | ||
| let unseen_set: HashSet<&TxHash> = unseen_txs.iter().collect(); | ||
|
|
||
| let hashes: Vec<TxHash> = state.acknowledged.keys().copied().collect(); | ||
|
|
||
| for tx_hash in hashes { | ||
| if seen_set.contains(&tx_hash) { | ||
| let tx = state.acknowledged.get_mut(&tx_hash).unwrap(); | ||
| tx.confirm(point); | ||
| // Check if finalizable | ||
| if tx.confirmations >= finalize_threshold { | ||
| let mut finalized = tx.clone(); | ||
| finalized.stage = MempoolTxStage::Finalized; | ||
| state.finalized_log.push_back(finalized.clone()); | ||
| state.acknowledged.remove(&tx_hash); | ||
| info!(tx.hash = %tx_hash, "tx finalized"); | ||
| self.notify(finalized); | ||
| } else { | ||
| self.notify(tx.clone()); | ||
| info!(tx.hash = %tx_hash, "tx confirmed"); | ||
| } | ||
| } else if unseen_set.contains(&tx_hash) { | ||
| let mut tx = state.acknowledged.remove(&tx_hash).unwrap(); | ||
|
|
||
| let mut event_tx = tx.clone(); | ||
| event_tx.stage = MempoolTxStage::RolledBack; | ||
| self.notify(event_tx); | ||
|
|
||
| tx.retry(); | ||
| state.pending.push(tx); | ||
| info!(tx.hash = %tx_hash, "retry tx"); | ||
| } else { | ||
| let tx = state.acknowledged.get_mut(&tx_hash).unwrap(); | ||
| tx.mark_stale(); | ||
| // Check if droppable | ||
| if tx.non_confirmations >= drop_threshold { | ||
| let mut dropped = tx.clone(); | ||
| dropped.stage = MempoolTxStage::Dropped; | ||
| state.finalized_log.push_back(dropped.clone()); | ||
| state.acknowledged.remove(&tx_hash); | ||
| info!(tx.hash = %tx_hash, "tx dropped"); | ||
| self.notify(dropped); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if state.finalized_log.len() > MAX_FINALIZED_LOG { | ||
| let excess = state.finalized_log.len() - MAX_FINALIZED_LOG; | ||
| state.finalized_log.drain(..excess); | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| fn check_status(&self, tx_hash: &TxHash) -> TxStatus { | ||
| let state = self.state.read().unwrap(); | ||
|
|
||
| if let Some(tx) = state.acknowledged.get(tx_hash) { | ||
| TxStatus { | ||
| stage: tx.stage.clone(), | ||
| confirmations: tx.confirmations, | ||
| non_confirmations: tx.non_confirmations, | ||
| confirmed_at: tx.confirmed_at.clone(), | ||
| } | ||
| } else if let Some(tx) = state.inflight.iter().find(|x| x.hash.eq(tx_hash)) { | ||
| TxStatus { | ||
| stage: tx.stage.clone(), | ||
| confirmations: 0, | ||
| non_confirmations: 0, | ||
| confirmed_at: None, | ||
| } | ||
| } else if state.pending.iter().any(|x| x.hash.eq(tx_hash)) { | ||
| TxStatus { | ||
| stage: MempoolTxStage::Pending, | ||
| confirmations: 0, | ||
| non_confirmations: 0, | ||
| confirmed_at: None, | ||
| } | ||
| } else { | ||
| TxStatus { | ||
| stage: MempoolTxStage::Unknown, | ||
| confirmations: 0, | ||
| non_confirmations: 0, | ||
| confirmed_at: None, | ||
| } | ||
| } | ||
| } | ||
|
scarmuega marked this conversation as resolved.
|
||
|
|
||
| fn dump_finalized(&self, cursor: u64, limit: usize) -> MempoolPage { | ||
| let state = self.state.read().unwrap(); | ||
| let start = cursor as usize; | ||
|
|
||
| let items: Vec<MempoolTx> = state | ||
| .finalized_log | ||
| .iter() | ||
| .skip(start) | ||
| .take(limit) | ||
| .cloned() | ||
| .collect(); | ||
|
|
||
| let end = start + items.len(); | ||
| let next_cursor = if end < state.finalized_log.len() { | ||
| Some(end as u64) | ||
| } else { | ||
| None | ||
| }; | ||
|
|
||
| MempoolPage { items, next_cursor } | ||
| } | ||
|
scarmuega marked this conversation as resolved.
|
||
|
|
||
| fn subscribe(&self) -> Self::Stream { | ||
| EphemeralMempoolStream { | ||
| inner: BroadcastStream::new(self.updates.subscribe()), | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Run required workspace checks (clippy/build/test).
Please run the required Rust workspace checks before merging.
As per coding guidelines: “Run
cargo clippy --workspace --all-targets --all-featuresand resolve all clippy warnings before committing changes. Ensure the project builds without warnings by runningcargo build --workspace --all-targets --all-features. Runcargo test --workspace --all-featuresto verify functionality of all changes”.🤖 Prompt for AI Agents