-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Recently rejected cache for transaction queue #9005
Changes from all commits
f07f6ba
4e3a0d9
43eb599
1d31e4e
080bd76
e6d480a
842bf95
d3fe51e
5bf7033
ee199f4
44d2476
222e5d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,7 +19,7 @@ | |
| use std::{cmp, fmt}; | ||
| use std::sync::Arc; | ||
| use std::sync::atomic::{self, AtomicUsize}; | ||
| use std::collections::BTreeMap; | ||
| use std::collections::{BTreeMap, HashMap}; | ||
|
|
||
| use ethereum_types::{H256, U256, Address}; | ||
| use parking_lot::RwLock; | ||
|
|
@@ -138,6 +138,50 @@ impl CachedPending { | |
| } | ||
| } | ||
|
|
||
| #[derive(Debug)] | ||
| struct RecentlyRejected { | ||
| inner: RwLock<HashMap<H256, transaction::Error>>, | ||
| limit: usize, | ||
| } | ||
|
|
||
| impl RecentlyRejected { | ||
| fn new(limit: usize) -> Self { | ||
| RecentlyRejected { | ||
| limit, | ||
| inner: RwLock::new(HashMap::with_capacity(MIN_REJECTED_CACHE_SIZE)), | ||
| } | ||
| } | ||
|
|
||
| fn clear(&self) { | ||
| self.inner.write().clear(); | ||
| } | ||
|
|
||
| fn get(&self, hash: &H256) -> Option<transaction::Error> { | ||
| self.inner.read().get(hash).cloned() | ||
| } | ||
|
|
||
| fn insert(&self, hash: H256, err: &transaction::Error) { | ||
| if self.inner.read().contains_key(&hash) { | ||
| return; | ||
| } | ||
|
|
||
| let mut inner = self.inner.write(); | ||
| inner.insert(hash, err.clone()); | ||
|
|
||
| // clean up | ||
| if inner.len() > self.limit { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we never go above the limit the cache entries are never expired, is that ok?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The cache is completely cleared on |
||
| // randomly remove half of the entries | ||
| let to_remove: Vec<_> = inner.keys().take(self.limit / 2).cloned().collect(); | ||
| for key in to_remove { | ||
| inner.remove(&key); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Minimal size of rejection cache, by default it's equal to queue size. | ||
| const MIN_REJECTED_CACHE_SIZE: usize = 2048; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe use this to instantiate the hashmap
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea, the cache is used quite extensively, especially when you are running with a small transaction pool. |
||
|
|
||
| /// Ethereum Transaction Queue | ||
| /// | ||
| /// Responsible for: | ||
|
|
@@ -150,6 +194,7 @@ pub struct TransactionQueue { | |
| pool: RwLock<Pool>, | ||
| options: RwLock<verifier::Options>, | ||
| cached_pending: RwLock<CachedPending>, | ||
| recently_rejected: RecentlyRejected, | ||
| } | ||
|
|
||
| impl TransactionQueue { | ||
|
|
@@ -159,11 +204,13 @@ impl TransactionQueue { | |
| verification_options: verifier::Options, | ||
| strategy: PrioritizationStrategy, | ||
| ) -> Self { | ||
| let max_count = limits.max_count; | ||
| TransactionQueue { | ||
| insertion_id: Default::default(), | ||
| pool: RwLock::new(txpool::Pool::new(Default::default(), scoring::NonceAndGasPrice(strategy), limits)), | ||
| options: RwLock::new(verification_options), | ||
| cached_pending: RwLock::new(CachedPending::none()), | ||
| recently_rejected: RecentlyRejected::new(cmp::max(MIN_REJECTED_CACHE_SIZE, max_count / 4)), | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -195,26 +242,42 @@ impl TransactionQueue { | |
| None | ||
| } | ||
| }; | ||
|
|
||
| let verifier = verifier::Verifier::new( | ||
| client, | ||
| options, | ||
| self.insertion_id.clone(), | ||
| transaction_to_replace, | ||
| ); | ||
|
|
||
| let results = transactions | ||
| .into_iter() | ||
| .map(|transaction| { | ||
| if self.pool.read().find(&transaction.hash()).is_some() { | ||
| bail!(transaction::Error::AlreadyImported) | ||
| let hash = transaction.hash(); | ||
|
|
||
| if self.pool.read().find(&hash).is_some() { | ||
| return Err(transaction::Error::AlreadyImported); | ||
| } | ||
|
|
||
| verifier.verify_transaction(transaction) | ||
| if let Some(err) = self.recently_rejected.get(&hash) { | ||
| trace!(target: "txqueue", "[{:?}] Rejecting recently rejected: {:?}", hash, err); | ||
| return Err(err); | ||
| } | ||
|
|
||
| let imported = verifier | ||
| .verify_transaction(transaction) | ||
| .and_then(|verified| { | ||
| self.pool.write().import(verified).map_err(convert_error) | ||
| }); | ||
|
|
||
| match imported { | ||
| Ok(_) => Ok(()), | ||
| Err(err) => { | ||
| self.recently_rejected.insert(hash, &err); | ||
| Err(err) | ||
| }, | ||
| } | ||
| }) | ||
| .map(|result| result.and_then(|verified| { | ||
| self.pool.write().import(verified) | ||
| .map(|_imported| ()) | ||
| .map_err(convert_error) | ||
| })) | ||
| .collect::<Vec<_>>(); | ||
|
|
||
| // Notify about imported transactions. | ||
|
|
@@ -342,6 +405,7 @@ impl TransactionQueue { | |
|
|
||
| let state_readiness = ready::State::new(client, stale_id, nonce_cap); | ||
|
|
||
| self.recently_rejected.clear(); | ||
| let removed = self.pool.write().cull(None, state_readiness); | ||
| debug!(target: "txqueue", "Removed {} stalled transactions. {}", removed, self.status()); | ||
| } | ||
|
|
||
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.
Curious about why you're storing the error here as opposed to using a
HashSet. Do you plan to use the error count stats to refine the logic further down?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.
The error is stored here, so that we can return it later (we could return generic
RecentlyRejected, but It's more descriptive to have the actual values), remember thatErrorcan look like this:InsufficientGasPrice { got, minimal }, so for every transaction the values might be completely different.I'm planning to refine the logic a bit as well, especially with regard to cache invalidation (like: "Clear all
InsufficientBlanaceerrors efficiently"), and was considering approaches likeVec<(Error, HashSet)>orVec<ErrorCode, HashMap<Hash, Error>>, but haven't decided yet, what path to follow. Will run some performance tests, cause maybeHashMap<Hash, Error>and iterating over all entries (or usingretain) will be efficient enough.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.
Thank you for the explanation! :)