Skip to content
This repository has been archived by the owner on Nov 6, 2020. It is now read-only.

Transaction Pool re-implementation #6994

Merged
merged 22 commits into from
Dec 19, 2017
Merged

Transaction Pool re-implementation #6994

merged 22 commits into from
Dec 19, 2017

Conversation

tomusdrw
Copy link
Collaborator

@tomusdrw tomusdrw commented Nov 6, 2017

Initial version of new transaction pool.
Currently implement as a separate crate (and abstract over Transaction).

In subsequent PRs I plan to:

  • integrate with Parity (implement Ready, Scoring and Verifier, penalties, own transactions, etc)
  • Run some benchmarks (and compare with old queue)
  • Migrate old tests to make sure the behaviour is consistent.
  • Cache pending set (and then update it in-place)

The details are explained in docs, but the big picture is this:

  • The pool maintains a list of transactions per sender ordered by Score (think: nonce). (HashMap<Sender, Vec<Transaction>>)
  • We also know best and worst transaction from every sender and keep a queue of best-first transactions and worst-last transactions.
  • Inserting to the queue is cheap (O(no-of-tx-per-sender + log(no-of-senders)) (worst case), usually O(log(no-of-tx-per-sender) + log(no-of-senders)))
  • Constructing pending set is relatively cheap (it's not O(1) as it used to be, but we are within O(nlog(n)) (all transactions from a single sender), usually O(n))
  • Pluggable scoring allows us to do some interesting prioritizations (like high priority for a chain of small transactions not just looking at a single one = bump priority by sending a very expensive transaction later)
  • Decoupling from nonce/gasPrice will allow to easily integrate account abstraction transactions
  • Readiness abstraction allows to easily implement scheduled transactions

@tomusdrw tomusdrw added A0-pleasereview 🤓 Pull request needs code review. M4-core ⛓ Core client code / Rust. labels Nov 6, 2017
@5chdn 5chdn added this to the 1.9 milestone Nov 6, 2017
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"),
Copy link
Contributor

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/

};

// update best and worst transactions from this sender (if required)
self.update_senders_worst_and_best(result.1, result.2);
Copy link
Contributor

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?

Copy link
Collaborator Author

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.

}
}

let result = {
Copy link
Contributor

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.

}

/// 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>>) {
Copy link
Contributor

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.

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.");
Copy link
Contributor

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?

Some(removed) => {
let len = removed.len();
for tx in removed {
self.removed(tx.hash());
Copy link
Contributor

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(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe use COW instead?

Copy link
Collaborator Author

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?

Copy link
Contributor

Choose a reason for hiding this comment

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

Probably not

pub fn status<R: Ready<T>>(&self, mut ready: R) -> Status {
let mut stalled = 0;
let mut pending = 0;
let mut future = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

let mut Status?


impl From<bool> for Readiness {
fn from(b: bool) -> Self {
if b { Readiness::Ready } else { Readiness::Future }
Copy link
Contributor

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.

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()`
Copy link
Contributor

Choose a reason for hiding this comment

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

Better to write explicitly

@5chdn 5chdn added A5-grumble 🔥 Pull request has minor issues that must be addressed before merging. and removed A0-pleasereview 🤓 Pull request needs code review. labels Nov 10, 2017
Copy link
Contributor

@0x7CFE 0x7CFE left a 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.

if index >= self.scores.len() {
None
} else {
Some((self.scores[index].clone(), self.transactions[index].clone()))
Copy link
Contributor

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()))
}

Copy link
Collaborator Author

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.

let len = self.transactions.len();
if index == len {
// trying to insert least worth transaction
return if len == max_count {
Copy link
Contributor

@0x7CFE 0x7CFE Nov 16, 2017

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?

Copy link
Collaborator Author

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.

}

// Decide if the transaction should be replaced
match scoring.choose(&self.transactions[index], &tx) {
Copy link
Contributor

@0x7CFE 0x7CFE Nov 16, 2017

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
Copy link
Contributor

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 ifs would be OK, in my opinion.

Copy link
Collaborator Author

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

/// Memory usage in bytes.
pub mem_usage: usize,
/// Total number of transactions in the pool.
pub count: usize,
Copy link
Contributor

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.

@tomusdrw tomusdrw added A0-pleasereview 🤓 Pull request needs code review. and removed A5-grumble 🔥 Pull request has minor issues that must be addressed before merging. labels Nov 20, 2017
Copy link

@dkashitsyn dkashitsyn left a 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 {

Choose a reason for hiding this comment

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

s/other/new/ ?

// 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>,

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.

Copy link
Collaborator Author

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]> {

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 reverses.

Copy link
Collaborator Author

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.

Copy link
Contributor

@0x7CFE 0x7CFE left a 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 :)

@tomusdrw
Copy link
Collaborator Author

tomusdrw commented Dec 1, 2017

Thank you for the review @0x7CFE. Addressed grumbles and added some more detailed docs about the algo.

@svyatonik svyatonik added A8-looksgood 🦄 Pull request is reviewed well. and removed A0-pleasereview 🤓 Pull request needs code review. labels Dec 15, 2017
@tomusdrw tomusdrw merged commit 1d92067 into master Dec 19, 2017
@tomusdrw tomusdrw deleted the td-txqueue branch December 19, 2017 09:20
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
A8-looksgood 🦄 Pull request is reviewed well. M4-core ⛓ Core client code / Rust.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants