-
Notifications
You must be signed in to change notification settings - Fork 130
[PAN-1062] Evict old transactions (1 of 2) #1299
[PAN-1062] Evict old transactions (1 of 2) #1299
Conversation
|
||
@Test | ||
public void shouldEvictOldTransactions() { | ||
Clock clock = Clock.systemUTC(); |
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 shouldn't use a real clock in tests as it tends to make them indeterminate. TestClock
makes it easy to get a clock with a fixed time for testing with a stepMillis
method to let you progress time in a controlled way. Then you can update the test to add a few transactions at different time points and check that only the ones that have reached the max age are actually evicted.
@@ -119,6 +119,15 @@ boolean addLocalTransaction(final Transaction transaction) { | |||
return addTransaction; | |||
} | |||
|
|||
void evictOldTransactions(Instant maximumRetentionPeriod) { | |||
for (final TransactionInfo transactionInfo : prioritizedTransactions) { |
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 would all need to be inside a synchronized block.
void evictOldTransactions(Instant maximumRetentionPeriod) { | ||
for (final TransactionInfo transactionInfo : prioritizedTransactions) { | ||
int difference = transactionInfo.getAddedToPoolAt().compareTo(maximumRetentionPeriod); | ||
if (difference > 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.
We can use Instant.isBefore
here.
@@ -119,6 +119,15 @@ boolean addLocalTransaction(final Transaction transaction) { | |||
return addTransaction; | |||
} | |||
|
|||
void evictOldTransactions(Instant maximumRetentionPeriod) { |
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.
maximumRetentionPeriod seems like a misleading name - this would have to be the earliestAllowedTransaction or should be a Duration
which is subtracted from the current time in this method.
for (final TransactionInfo transactionInfo : prioritizedTransactions) { | ||
int difference = transactionInfo.getAddedToPoolAt().compareTo(maximumRetentionPeriod); | ||
if (difference > 0) { | ||
pendingTransactions.remove(transactionInfo.getTransaction().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.
This isn't fully removing the transaction from the pool (prioritizedTransactions and transactionsBySender aren't updated and dropped transaction notifications aren't sent). It needs to call removeTransaction
to ensure the removal is done correctly.
Once that's fixed, I suspect this will cause a ConcurrentModificationException
when there are multiple transactions in the pool because you're removing from the set as you iterate it. You'll need to create a list of transactions to remove with something like:
final List<TransactionInfo> transactionsToRemove = prioritizedTransactions.stream().filter(transaction -> transaction.getAddedToPoolAt().isBefore(earliestAllowedAddTime)).collect(toList());
transactionsToRemove.forEach(this::removeTransaction)
9cf26bd
to
fcc06e6
Compare
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 to be on the right track. I think the key thing is if TimerUtil
is moved out of PendingTransactions
it will be much easier to test and require fewer changes overall.
@@ -108,12 +112,17 @@ public void proposerAddressCanBeExtractFromAConstructedBlock() { | |||
final CliqueExtraData extraData = | |||
new CliqueExtraData(BytesValue.wrap(new byte[32]), null, validatorList); | |||
|
|||
final Vertx vertx = Vertx.vertx(); | |||
final TimerUtil timerUtil = new VertxTimerUtil(vertx); | |||
final long TRANSACTION_EVICTION_INTERVAL_MS = TimeUnit.HOURS.toMillis(1); |
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 shouldn't be introducing a real Vertx here. just a mock or stub TimerUtil
would be enough.
Probably should make Transaction_EVICTION_INTERVAL_MS
an actual constant as well instead of duplicating it for each test.
Same thing for all the other test code.
ethereum/blockcreation/build.gradle
Outdated
@@ -15,6 +15,7 @@ jar { | |||
dependencies { | |||
implementation project(':ethereum:core') | |||
implementation project(':ethereum:eth') | |||
implementation project(':ethereum:p2p') |
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 shouldn't need to add this dependency. I'm guessing you need it for TimerUtil but we need to work something out to avoid it.
One possibility is that PendingTransactions
shouldn't have TimerUtil
injected, but just exposes the method that it would periodically call. Then as part of creating PendingTransactions
a periodic timer can be setup to call that method. PeriodicTransactions
then remains completely separate from the mechanism used to periodically perform the call.
private void evictOldTransactions() { | ||
synchronized (pendingTransactions) { | ||
final List<TransactionInfo> transactionsToRemove = | ||
prioritizedTransactions.stream().filter(this::applyEvictionThreshold).collect(toList()); |
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 basically just style but I'd inline applyEvictionThreshold
here and calculate the cut-off time just once. That makes the keep/remove condition really straight forward so it's a bit easier to follow.
Also, System.currentTimeMillis
is a native method so incurs JNI overhead - we don't want to call that for every transaction in the pool if we can help it given we're inside a synchronized block.
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'd still recommend this change.
@@ -43,8 +47,18 @@ | |||
private static final KeyPair KEYS2 = KeyPair.generate(); | |||
|
|||
private final MetricsSystem metricsSystem = new NoOpMetricsSystem(); | |||
|
|||
final Vertx vertx = Vertx.vertx(); | |||
final TimerUtil timerUtil = new VertxTimerUtil(vertx); |
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 should be using a MockTimerUtil
here so that we can test things in a. single thread and avoid creating a Vertx instance. Also note that every created Vertx must be closed or threads leak and cause problems for other tests.
TimeUnit.SECONDS.sleep(2); | ||
} catch (Exception ignored) { | ||
} | ||
assertThat(transactions.size()).isEqualTo(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.
We should have a test that has a combination of old and new transactions so we know it doesn't just evict all transactions.
eb70468
to
42736e9
Compare
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.
Generally good but we need to switch back to using Instant
for the addedToPoolAt
value as the change to a long has leaked out to affect one of our JSON-RPC responses.
private void evictOldTransactions() { | ||
synchronized (pendingTransactions) { | ||
final List<TransactionInfo> transactionsToRemove = | ||
prioritizedTransactions.stream().filter(this::applyEvictionThreshold).collect(toList()); |
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'd still recommend this change.
private final long sequence; // Allows prioritization based on order transactions are added | ||
|
||
TransactionInfo( | ||
final Transaction transaction, | ||
final boolean receivedFromLocalSource, | ||
final Instant addedToPoolAt) { | ||
final long addedToPoolAt) { |
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 should keep this as an Instant
- no need to change it here and having better typing is always good.
|
||
public class TransactionPoolFactory { | ||
|
||
private static final long TRANSACTION_EVICTION_INTERVAL_MS = TimeUnit.MINUTES.toMillis(1); |
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 default needs to be much longer than 1 minute. Probably more like 12 hours since it's not currently configurable.
@Test | ||
public void shouldEvictMultipleOldTransactions() { | ||
final long transactionEvictionIntervalMs = 1L; | ||
final TestClock clock = new TestClock(); |
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.
nit: Make clock a field so it can be reused.
I'd also stick with the default 1 minute timeout for transactions - we can step the clock forward as much as we like so don't need to have a custom value here and can then reuse the PendingTransactions
instance in the field.
@@ -39,7 +37,7 @@ public String getHash() { | |||
|
|||
@JsonGetter(value = "addedToPoolAt") | |||
public String getAddedToPoolAt() { | |||
return addedToPoolAt.toString(); | |||
return Long.toString(addedToPoolAt); |
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 wrong - Instant.toString outputs a ISO8601 formatted date time but this outputs a number.
778303b
to
94e28b3
Compare
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.
LGTM.
@@ -92,6 +99,20 @@ public PendingTransactions( | |||
"operation"); | |||
} | |||
|
|||
public void evictOldTransactions() { | |||
synchronized (pendingTransactions) { | |||
long now = clock.millis(); |
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.
nit: final
Even more nitty: :) Personally I would have made this final Instant removeTransactionsBefore = clock.instant().minusMillis(transactionEvictionIntervalMs)
and then the filter would be transaction -> transaction.getAddedToPoolAt().isBefore(removeTransactionsBefore)
. Both ways work fine and it's not a big deal but I find the clarity of calculating the cut-off timepoint helpful.
a8d9806
to
16d354a
Compare
PR description
The acceptance criteria for this task has two components:
Configuration option to specify maximum length of time transactions are held in the pending transaction list before being dropped
When a transaction has been in the pending transaction list for the configured maximum period, it is removed
This change is intended to address the latter.
Fixed Issue(s)
Evict transactions from the pending transaction list when
addedToPoolAt
predates a specified criteria.