-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Conversation
transaction-pool/src/error.rs
Outdated
display("[{:?}] transaction too cheap to enter the pool", hash) | ||
} | ||
TooCheapToReplace(old_hash: H256, hash: H256) { | ||
description("transaction is too cheap too replace existing transaction in the queue"), |
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.
s/too replace/to replace/
transaction-pool/src/pool.rs
Outdated
}; | ||
|
||
// update best and worst transactions from this sender (if required) | ||
self.update_senders_worst_and_best(result.1, result.2); |
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.
Should we really update the values even if not AddResult::Ok
?
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.
We don't need to update it if we know that new transaction was rejected (or only in case AddResult::Ok | AddResult::PushedOut | AddResult::Replaced
).
I did this for simplicity though, the values would be the same and it is not expensive to compute & compare them so I don't think it will have any performance implications.
transaction-pool/src/pool.rs
Outdated
} | ||
} | ||
|
||
let result = { |
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.
I think, it's better to re-bind values by name here.
transaction-pool/src/pool.rs
Outdated
} | ||
|
||
/// Updates state of the pool statistics if the transaction was added to a set. | ||
fn added(&mut self, new: &Arc<T>, old: Option<&Arc<T>>) { |
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.
I believe, these functions should be named like finalize_*
or commit_*
. Simple added
is not clear and may be confused with the listener's, at least for me.
transaction-pool/src/pool.rs
Outdated
let to_remove = match self.worst_transactions.iter().next_back() { | ||
// No elements to remove? and the pool is still full? | ||
None => { | ||
warn!("The pool is full but there is no transaction to remove."); |
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.
Maybe are no transactions to remove
?
transaction-pool/src/pool.rs
Outdated
Some(removed) => { | ||
let len = removed.len(); | ||
for tx in removed { | ||
self.removed(tx.hash()); |
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.
Too much removed
per line in this function, IMO.
pub fn pending<R: Ready<T>>(&self, ready: R) -> PendingIterator<T, R, S, L> { | ||
PendingIterator { | ||
ready, | ||
best_transactions: self.best_transactions.clone(), |
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.
Maybe use COW instead?
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.
Needs to be cloned on the first next()
anyway, do you think it's worth it?
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.
Probably not
transaction-pool/src/pool.rs
Outdated
pub fn status<R: Ready<T>>(&self, mut ready: R) -> Status { | ||
let mut stalled = 0; | ||
let mut pending = 0; | ||
let mut future = 0; |
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.
let mut Status
?
transaction-pool/src/ready.rs
Outdated
|
||
impl From<bool> for Readiness { | ||
fn from(b: bool) -> Self { | ||
if b { Readiness::Ready } else { Readiness::Future } |
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.
This is very tricky and opaque, IMO. Easy to miss the thing if several negations will take place in an expression.
transaction-pool/src/scoring.rs
Outdated
fn choose(&self, old: &T, new: &T) -> Choice; | ||
|
||
/// Updates the transaction scores given a list of transactions and a change to previous scoring. | ||
/// NOTE: `txs.len() === scores.len()` |
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.
Better to write explicitly
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.
A little bit more grumble comments.
transaction-pool/src/transactions.rs
Outdated
if index >= self.scores.len() { | ||
None | ||
} else { | ||
Some((self.scores[index].clone(), self.transactions[index].clone())) |
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.
Wouldn't it be more clear like this?
if index > self.scores.len() {
None
} else {
let index = index + 1;
Some((self.scores[index].clone(), self.transactions[index].clone()))
}
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.
That's not exactly the same, but changed condition to:
let index = index + 1;
if index < self.scores.len() {
Some(...)
} else {
None
}
which I think makes it clearer.
transaction-pool/src/transactions.rs
Outdated
let len = self.transactions.len(); | ||
if index == len { | ||
// trying to insert least worth transaction | ||
return if len == max_count { |
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.
This statement is part of a rather long method so all early returns should be easily noticeable. When skimming through the code, one may easily misinterpret this statement as return from the first branch only.
I believe, this return
statement should be refactored in favour of two explicit returns
in each condition branch. Another option is to introduce a temporary value, like result
, and add explicit return result
after condition block.
P.S.: Maybe consider splitting this method into smaller parts?
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.
Good point. Refactored push_to_the_end()
as a separate method.
transaction-pool/src/transactions.rs
Outdated
} | ||
|
||
// Decide if the transaction should be replaced | ||
match scoring.choose(&self.transactions[index], &tx) { |
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.
If I were you, I'd introduce result
value here to prepare the reader and put an emphasis on the fact that we expect a value from this match
.
scoring::Choice::InsertNew | ||
}; | ||
|
||
decision |
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.
Since this method is small and intentions are crystal clear, I may suggest omitting the decision
value. Two nested if
s would be OK, in my opinion.
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.
Left overs from println
debugging :)
transaction-pool/src/status.rs
Outdated
/// Memory usage in bytes. | ||
pub mem_usage: usize, | ||
/// Total number of transactions in the pool. | ||
pub count: usize, |
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.
Better to s/count/transaction_count/
. More status fields may be added in future which may cause confusion.
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.
Looks good to me, but someone else should check.
Personally, I would love to see more comments in the algorithmic part of the code about decision making and overall logic.
impl Scoring<Transaction> for DummyScoring { | ||
type Score = U256; | ||
|
||
fn compare(&self, old: &Transaction, other: &Transaction) -> cmp::Ordering { |
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.
s/other/new/
?
transaction-pool/src/transactions.rs
Outdated
// TODO [ToDr] Consider using something that doesn't require shifting all records. | ||
transactions: SmallVec<[Arc<T>; PER_SENDER]>, | ||
scores: SmallVec<[S::Score; PER_SENDER]>, | ||
_score: PhantomData<S>, |
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.
I think, it would be nice to write a comment, why do we need phantom here.
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.
Actually we don't, thanks for noticing :)
return true; | ||
} | ||
|
||
pub fn cull<R: Ready<T>>(&mut self, ready: &mut R, scoring: &S) -> SmallVec<[Arc<T>; PER_SENDER]> { |
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.
Something makes me think, that this method may be refactored using iterators instead of reverse
s.
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.
Since we use SmallVec
it doesn't have methods that allow easily to consume the content.
Current method doesn't require allocation, but is indeed O(n)
, since number of transactions per sender is limited I don't expect it to be a problem.
With Vec
we could use split_off
, although instead of Vec
it would be more wise to use VecDeque
, but that would require more allocations - might be worth to consider though after some initial tests.
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.
I completely agree with my alter ego (sorry, logged with wrong account :)
Thank you for the review @0x7CFE. Addressed grumbles and added some more detailed docs about the algo. |
Initial version of new transaction pool.
Currently implement as a separate crate (and abstract over Transaction).
In subsequent PRs I plan to:
Ready
,Scoring
andVerifier
, penalties, own transactions, etc)The details are explained in docs, but the big picture is this:
Score
(think: nonce). (HashMap<Sender, Vec<Transaction>>
)O(no-of-tx-per-sender + log(no-of-senders))
(worst case), usuallyO(log(no-of-tx-per-sender) + log(no-of-senders))
)O(1)
as it used to be, but we are withinO(nlog(n))
(all transactions from a single sender), usuallyO(n)
)