Skip to content
This repository has been archived by the owner on Sep 26, 2019. It is now read-only.

[PAN-1062] Evict old transactions (1 of 2) #1299

Merged
merged 41 commits into from
Apr 25, 2019

Conversation

smatthewenglish
Copy link
Contributor

@smatthewenglish smatthewenglish commented Apr 17, 2019

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.

@smatthewenglish smatthewenglish changed the title [PAN-1062] Evict old transactions [MINOR / PAN-1062 (A)] Evict old transactions Apr 17, 2019

@Test
public void shouldEvictOldTransactions() {
Clock clock = Clock.systemUTC();
Copy link
Contributor

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

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

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

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

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)

@smatthewenglish smatthewenglish changed the title [MINOR / PAN-1062 (A)] Evict old transactions [PAN-1062 (A)] Evict old transactions Apr 18, 2019
@smatthewenglish smatthewenglish changed the title [PAN-1062 (A)] Evict old transactions [PAN-1062] Evict old transactions (1 of 2) Apr 18, 2019
@smatthewenglish smatthewenglish force-pushed the robinshultz branch 5 times, most recently from 9cf26bd to fcc06e6 Compare April 20, 2019 01:48
Copy link
Contributor

@ajsutton ajsutton left a 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);
Copy link
Contributor

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.

@@ -15,6 +15,7 @@ jar {
dependencies {
implementation project(':ethereum:core')
implementation project(':ethereum:eth')
implementation project(':ethereum:p2p')
Copy link
Contributor

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());
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 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.

Copy link
Contributor

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

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

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.

@smatthewenglish smatthewenglish force-pushed the robinshultz branch 5 times, most recently from eb70468 to 42736e9 Compare April 22, 2019 22:29
Copy link
Contributor

@ajsutton ajsutton left a 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());
Copy link
Contributor

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

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

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

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);
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 wrong - Instant.toString outputs a ISO8601 formatted date time but this outputs a number.

Copy link
Contributor

@ajsutton ajsutton left a 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();
Copy link
Contributor

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.

@smatthewenglish smatthewenglish merged commit f20687f into PegaSysEng:master Apr 25, 2019
notlesh pushed a commit to notlesh/pantheon that referenced this pull request May 4, 2019
notlesh pushed a commit to notlesh/pantheon that referenced this pull request May 14, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants