mirror of https://github.com/hyperledger/besu
Separate download state tracking from WorldStateDownloader (#967)
Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>pull/2/head
parent
b81ad01b0f
commit
9f8d14522e
@ -0,0 +1,212 @@ |
||||
/* |
||||
* Copyright 2019 ConsenSys AG. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on |
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the |
||||
* specific language governing permissions and limitations under the License. |
||||
*/ |
||||
package tech.pegasys.pantheon.ethereum.eth.sync.worldstate; |
||||
|
||||
import tech.pegasys.pantheon.ethereum.core.BlockHeader; |
||||
import tech.pegasys.pantheon.ethereum.eth.manager.task.EthTask; |
||||
import tech.pegasys.pantheon.ethereum.worldstate.WorldStateStorage; |
||||
import tech.pegasys.pantheon.ethereum.worldstate.WorldStateStorage.Updater; |
||||
import tech.pegasys.pantheon.services.queue.TaskQueue; |
||||
import tech.pegasys.pantheon.services.queue.TaskQueue.Task; |
||||
import tech.pegasys.pantheon.util.ExceptionUtils; |
||||
import tech.pegasys.pantheon.util.bytes.BytesValue; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.Set; |
||||
import java.util.concurrent.ArrayBlockingQueue; |
||||
import java.util.concurrent.CancellationException; |
||||
import java.util.concurrent.CompletableFuture; |
||||
import java.util.concurrent.ConcurrentHashMap; |
||||
import java.util.concurrent.TimeUnit; |
||||
import java.util.concurrent.atomic.AtomicBoolean; |
||||
import java.util.stream.Stream; |
||||
|
||||
import org.apache.logging.log4j.LogManager; |
||||
import org.apache.logging.log4j.Logger; |
||||
|
||||
class WorldDownloadState { |
||||
private static final Logger LOG = LogManager.getLogger(); |
||||
|
||||
private final TaskQueue<NodeDataRequest> pendingRequests; |
||||
private final ArrayBlockingQueue<Task<NodeDataRequest>> requestsToPersist; |
||||
private final int maxOutstandingRequests; |
||||
private final Set<EthTask<?>> outstandingRequests = |
||||
Collections.newSetFromMap(new ConcurrentHashMap<>()); |
||||
private final AtomicBoolean sendingRequests = new AtomicBoolean(false); |
||||
private final CompletableFuture<Void> internalFuture; |
||||
private final CompletableFuture<Void> downloadFuture; |
||||
private boolean waitingForNewPeer = false; |
||||
private BytesValue rootNodeData; |
||||
private EthTask<?> persistenceTask; |
||||
|
||||
public WorldDownloadState( |
||||
final TaskQueue<NodeDataRequest> pendingRequests, |
||||
final ArrayBlockingQueue<Task<NodeDataRequest>> requestsToPersist, |
||||
final int maxOutstandingRequests) { |
||||
this.pendingRequests = pendingRequests; |
||||
this.requestsToPersist = requestsToPersist; |
||||
this.maxOutstandingRequests = maxOutstandingRequests; |
||||
this.internalFuture = new CompletableFuture<>(); |
||||
this.downloadFuture = new CompletableFuture<>(); |
||||
this.internalFuture.whenComplete(this::cleanup); |
||||
this.downloadFuture.exceptionally( |
||||
error -> { |
||||
// Propagate cancellation back to our internal future.
|
||||
if (error instanceof CancellationException) { |
||||
this.internalFuture.cancel(true); |
||||
} |
||||
return null; |
||||
}); |
||||
} |
||||
|
||||
private synchronized void cleanup(final Void result, final Throwable error) { |
||||
// Handle cancellations
|
||||
if (internalFuture.isCancelled()) { |
||||
LOG.info("World state download cancelled"); |
||||
} else if (error != null) { |
||||
if (!(ExceptionUtils.rootCause(error) instanceof StalledDownloadException)) { |
||||
LOG.info("World state download failed. ", error); |
||||
} |
||||
} |
||||
if (persistenceTask != null) { |
||||
persistenceTask.cancel(); |
||||
} |
||||
for (final EthTask<?> outstandingRequest : outstandingRequests) { |
||||
outstandingRequest.cancel(); |
||||
} |
||||
pendingRequests.clear(); |
||||
requestsToPersist.clear(); |
||||
if (error != null) { |
||||
downloadFuture.completeExceptionally(error); |
||||
} else { |
||||
downloadFuture.complete(result); |
||||
} |
||||
} |
||||
|
||||
public void whileAdditionalRequestsCanBeSent(final Runnable action) { |
||||
while (shouldRequestNodeData()) { |
||||
if (sendingRequests.compareAndSet(false, true)) { |
||||
try { |
||||
action.run(); |
||||
} finally { |
||||
sendingRequests.set(false); |
||||
} |
||||
} else { |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
public synchronized void setWaitingForNewPeer(final boolean waitingForNewPeer) { |
||||
this.waitingForNewPeer = waitingForNewPeer; |
||||
} |
||||
|
||||
public synchronized void addOutstandingTask(final EthTask<?> task) { |
||||
outstandingRequests.add(task); |
||||
} |
||||
|
||||
public synchronized void removeOutstandingTask(final EthTask<?> task) { |
||||
outstandingRequests.remove(task); |
||||
} |
||||
|
||||
public int getOutstandingRequestCount() { |
||||
return outstandingRequests.size(); |
||||
} |
||||
|
||||
private synchronized boolean shouldRequestNodeData() { |
||||
return !internalFuture.isDone() |
||||
&& outstandingRequests.size() < maxOutstandingRequests |
||||
&& !pendingRequests.isEmpty() |
||||
&& !waitingForNewPeer; |
||||
} |
||||
|
||||
public CompletableFuture<Void> getDownloadFuture() { |
||||
return downloadFuture; |
||||
} |
||||
|
||||
public synchronized void setPersistenceTask(final EthTask<?> persistenceTask) { |
||||
this.persistenceTask = persistenceTask; |
||||
} |
||||
|
||||
public synchronized void enqueueRequest(final NodeDataRequest request) { |
||||
if (!internalFuture.isDone()) { |
||||
pendingRequests.enqueue(request); |
||||
} |
||||
} |
||||
|
||||
public synchronized void enqueueRequests(final Stream<NodeDataRequest> requests) { |
||||
if (!internalFuture.isDone()) { |
||||
requests.forEach(pendingRequests::enqueue); |
||||
} |
||||
} |
||||
|
||||
public synchronized Task<NodeDataRequest> dequeueRequest() { |
||||
if (internalFuture.isDone()) { |
||||
return null; |
||||
} |
||||
return pendingRequests.dequeue(); |
||||
} |
||||
|
||||
public synchronized void setRootNodeData(final BytesValue rootNodeData) { |
||||
this.rootNodeData = rootNodeData; |
||||
} |
||||
|
||||
public ArrayBlockingQueue<Task<NodeDataRequest>> getRequestsToPersist() { |
||||
return requestsToPersist; |
||||
} |
||||
|
||||
public void addToPersistenceQueue(final Task<NodeDataRequest> task) { |
||||
while (!internalFuture.isDone()) { |
||||
try { |
||||
if (requestsToPersist.offer(task, 1, TimeUnit.SECONDS)) { |
||||
break; |
||||
} |
||||
} catch (final InterruptedException e) { |
||||
task.markFailed(); |
||||
Thread.currentThread().interrupt(); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
public int getPersistenceQueueSize() { |
||||
return requestsToPersist.size(); |
||||
} |
||||
|
||||
public synchronized void markAsStalled(final int maxNodeRequestRetries) { |
||||
final String message = |
||||
"Download stalled due to too many failures to retrieve node data (>" |
||||
+ maxNodeRequestRetries |
||||
+ " failures)"; |
||||
final WorldStateDownloaderException e = new StalledDownloadException(message); |
||||
internalFuture.completeExceptionally(e); |
||||
} |
||||
|
||||
public synchronized boolean checkCompletion( |
||||
final WorldStateStorage worldStateStorage, final BlockHeader header) { |
||||
if (!internalFuture.isDone() && pendingRequests.allTasksCompleted()) { |
||||
final Updater updater = worldStateStorage.updater(); |
||||
updater.putAccountStateTrieNode(header.getStateRoot(), rootNodeData); |
||||
updater.commit(); |
||||
internalFuture.complete(null); |
||||
LOG.info("Finished downloading world state from peers"); |
||||
return true; |
||||
} else { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
public synchronized boolean isDownloading() { |
||||
return !internalFuture.isDone(); |
||||
} |
||||
} |
@ -0,0 +1,237 @@ |
||||
/* |
||||
* Copyright 2019 ConsenSys AG. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on |
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the |
||||
* specific language governing permissions and limitations under the License. |
||||
*/ |
||||
package tech.pegasys.pantheon.ethereum.eth.sync.worldstate; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
||||
import static org.assertj.core.api.Assertions.fail; |
||||
import static org.mockito.Mockito.doAnswer; |
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.times; |
||||
import static org.mockito.Mockito.verify; |
||||
import static tech.pegasys.pantheon.ethereum.eth.sync.worldstate.NodeDataRequest.createAccountDataRequest; |
||||
|
||||
import tech.pegasys.pantheon.ethereum.core.BlockHeader; |
||||
import tech.pegasys.pantheon.ethereum.core.BlockHeaderTestFixture; |
||||
import tech.pegasys.pantheon.ethereum.core.Hash; |
||||
import tech.pegasys.pantheon.ethereum.eth.manager.task.EthTask; |
||||
import tech.pegasys.pantheon.ethereum.storage.keyvalue.KeyValueStorageWorldStateStorage; |
||||
import tech.pegasys.pantheon.ethereum.worldstate.WorldStateStorage; |
||||
import tech.pegasys.pantheon.services.kvstore.InMemoryKeyValueStorage; |
||||
import tech.pegasys.pantheon.services.queue.InMemoryTaskQueue; |
||||
import tech.pegasys.pantheon.services.queue.TaskQueue.Task; |
||||
import tech.pegasys.pantheon.util.bytes.BytesValue; |
||||
|
||||
import java.util.concurrent.ArrayBlockingQueue; |
||||
import java.util.concurrent.CompletableFuture; |
||||
import java.util.stream.Stream; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
|
||||
public class WorldDownloadStateTest { |
||||
|
||||
private static final BytesValue ROOT_NODE_DATA = BytesValue.of(1, 2, 3, 4); |
||||
private static final Hash ROOT_NODE_HASH = Hash.hash(ROOT_NODE_DATA); |
||||
private static final int MAX_OUTSTANDING_REQUESTS = 3; |
||||
|
||||
private final WorldStateStorage worldStateStorage = |
||||
new KeyValueStorageWorldStateStorage(new InMemoryKeyValueStorage()); |
||||
|
||||
private final BlockHeader header = |
||||
new BlockHeaderTestFixture().stateRoot(ROOT_NODE_HASH).buildHeader(); |
||||
private final InMemoryTaskQueue<NodeDataRequest> pendingRequests = new InMemoryTaskQueue<>(); |
||||
private final ArrayBlockingQueue<Task<NodeDataRequest>> requestsToPersist = |
||||
new ArrayBlockingQueue<>(100); |
||||
|
||||
private final WorldDownloadState downloadState = |
||||
new WorldDownloadState(pendingRequests, requestsToPersist, MAX_OUTSTANDING_REQUESTS); |
||||
|
||||
private final CompletableFuture<Void> future = downloadState.getDownloadFuture(); |
||||
|
||||
@Before |
||||
public void setUp() { |
||||
downloadState.setRootNodeData(ROOT_NODE_DATA); |
||||
assertThat(downloadState.isDownloading()).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void shouldCompleteReturnedFutureWhenNoPendingTasksRemain() { |
||||
downloadState.checkCompletion(worldStateStorage, header); |
||||
|
||||
assertThat(future).isCompleted(); |
||||
assertThat(downloadState.isDownloading()).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void shouldStoreRootNodeBeforeReturnedFutureCompletes() { |
||||
final CompletableFuture<Void> postFutureChecks = |
||||
future.thenAccept( |
||||
result -> |
||||
assertThat(worldStateStorage.getAccountStateTrieNode(ROOT_NODE_HASH)) |
||||
.contains(ROOT_NODE_DATA)); |
||||
|
||||
downloadState.checkCompletion(worldStateStorage, header); |
||||
|
||||
assertThat(future).isCompleted(); |
||||
assertThat(postFutureChecks).isCompleted(); |
||||
} |
||||
|
||||
@Test |
||||
public void shouldNotCompleteWhenThereArePendingTasks() { |
||||
pendingRequests.enqueue(createAccountDataRequest(Hash.EMPTY_TRIE_HASH)); |
||||
|
||||
downloadState.checkCompletion(worldStateStorage, header); |
||||
|
||||
assertThat(future).isNotDone(); |
||||
assertThat(worldStateStorage.getAccountStateTrieNode(ROOT_NODE_HASH)).isEmpty(); |
||||
assertThat(downloadState.isDownloading()).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
@SuppressWarnings("unchecked") |
||||
public void shouldCancelOutstandingTasksWhenFutureIsCancelled() { |
||||
final EthTask<?> persistenceTask = mock(EthTask.class); |
||||
final EthTask<?> outstandingTask1 = mock(EthTask.class); |
||||
final EthTask<?> outstandingTask2 = mock(EthTask.class); |
||||
final Task<NodeDataRequest> toPersist1 = mock(Task.class); |
||||
final Task<NodeDataRequest> toPersist2 = mock(Task.class); |
||||
downloadState.setPersistenceTask(persistenceTask); |
||||
downloadState.addOutstandingTask(outstandingTask1); |
||||
downloadState.addOutstandingTask(outstandingTask2); |
||||
|
||||
pendingRequests.enqueue(createAccountDataRequest(Hash.EMPTY_TRIE_HASH)); |
||||
pendingRequests.enqueue(createAccountDataRequest(Hash.EMPTY)); |
||||
requestsToPersist.add(toPersist1); |
||||
requestsToPersist.add(toPersist2); |
||||
|
||||
future.cancel(true); |
||||
|
||||
verify(persistenceTask).cancel(); |
||||
verify(outstandingTask1).cancel(); |
||||
verify(outstandingTask2).cancel(); |
||||
|
||||
assertThat(pendingRequests.isEmpty()).isTrue(); |
||||
assertThat(requestsToPersist).isEmpty(); |
||||
assertThat(downloadState.isDownloading()).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void shouldNotSendAdditionalRequestsWhenWaitingForANewPeer() { |
||||
pendingRequests.enqueue(createAccountDataRequest(Hash.EMPTY_TRIE_HASH)); |
||||
|
||||
downloadState.setWaitingForNewPeer(true); |
||||
downloadState.whileAdditionalRequestsCanBeSent(mustNotBeCalled()); |
||||
} |
||||
|
||||
@Test |
||||
public void shouldResumeSendingAdditionalRequestsWhenNoLongerWaitingForPeer() { |
||||
pendingRequests.enqueue(createAccountDataRequest(Hash.EMPTY_TRIE_HASH)); |
||||
final Runnable sendRequest = |
||||
mockWithAction(() -> downloadState.addOutstandingTask(mock(EthTask.class))); |
||||
|
||||
downloadState.setWaitingForNewPeer(true); |
||||
downloadState.whileAdditionalRequestsCanBeSent(mustNotBeCalled()); |
||||
|
||||
downloadState.setWaitingForNewPeer(false); |
||||
downloadState.whileAdditionalRequestsCanBeSent(sendRequest); |
||||
verify(sendRequest, times(MAX_OUTSTANDING_REQUESTS)).run(); |
||||
} |
||||
|
||||
@Test |
||||
public void shouldStopSendingAdditionalRequestsWhenPendingRequestsIsEmpty() { |
||||
pendingRequests.enqueue(createAccountDataRequest(Hash.EMPTY_TRIE_HASH)); |
||||
pendingRequests.enqueue(createAccountDataRequest(Hash.EMPTY_TRIE_HASH)); |
||||
|
||||
final Runnable sendRequest = mockWithAction(pendingRequests::dequeue); |
||||
downloadState.whileAdditionalRequestsCanBeSent(sendRequest); |
||||
|
||||
verify(sendRequest, times(2)).run(); |
||||
} |
||||
|
||||
@Test |
||||
public void shouldStopSendingAdditionalRequestsWhenMaximumOutstandingRequestCountReached() { |
||||
pendingRequests.enqueue(createAccountDataRequest(Hash.EMPTY_TRIE_HASH)); |
||||
final Runnable sendRequest = |
||||
mockWithAction(() -> downloadState.addOutstandingTask(mock(EthTask.class))); |
||||
|
||||
downloadState.whileAdditionalRequestsCanBeSent(sendRequest); |
||||
verify(sendRequest, times(MAX_OUTSTANDING_REQUESTS)).run(); |
||||
} |
||||
|
||||
@Test |
||||
public void shouldStopSendingAdditionalRequestsWhenFutureIsCancelled() { |
||||
pendingRequests.enqueue(createAccountDataRequest(Hash.EMPTY_TRIE_HASH)); |
||||
final Runnable sendRequest = mockWithAction(() -> future.cancel(true)); |
||||
|
||||
downloadState.whileAdditionalRequestsCanBeSent(sendRequest); |
||||
verify(sendRequest, times(1)).run(); |
||||
} |
||||
|
||||
@Test |
||||
public void shouldStopSendingAdditionalRequestsWhenDownloadIsMarkedAsStalled() { |
||||
pendingRequests.enqueue(createAccountDataRequest(Hash.EMPTY_TRIE_HASH)); |
||||
final Runnable sendRequest = mockWithAction(() -> downloadState.markAsStalled(1)); |
||||
|
||||
downloadState.whileAdditionalRequestsCanBeSent(sendRequest); |
||||
verify(sendRequest, times(1)).run(); |
||||
} |
||||
|
||||
@Test |
||||
public void shouldNotAllowMultipleCallsToSendAdditionalRequestsAtOnce() { |
||||
pendingRequests.enqueue(createAccountDataRequest(Hash.EMPTY_TRIE_HASH)); |
||||
final Runnable sendRequest = |
||||
mockWithAction( |
||||
() -> { |
||||
downloadState.whileAdditionalRequestsCanBeSent(mustNotBeCalled()); |
||||
downloadState.addOutstandingTask(mock(EthTask.class)); |
||||
}); |
||||
|
||||
downloadState.whileAdditionalRequestsCanBeSent(sendRequest); |
||||
verify(sendRequest, times(MAX_OUTSTANDING_REQUESTS)).run(); |
||||
} |
||||
|
||||
@Test |
||||
public void shouldNotEnqueueRequestsAfterDownloadIsStalled() { |
||||
downloadState.checkCompletion(worldStateStorage, header); |
||||
|
||||
downloadState.enqueueRequests(Stream.of(createAccountDataRequest(Hash.EMPTY_TRIE_HASH))); |
||||
downloadState.enqueueRequest(createAccountDataRequest(Hash.EMPTY_TRIE_HASH)); |
||||
|
||||
assertThat(pendingRequests.isEmpty()).isTrue(); |
||||
} |
||||
|
||||
@Test // Sanity check for the test structure
|
||||
public void shouldFailWhenMustNotBeCalledIsCalled() { |
||||
|
||||
pendingRequests.enqueue(createAccountDataRequest(Hash.EMPTY_TRIE_HASH)); |
||||
assertThatThrownBy(() -> downloadState.whileAdditionalRequestsCanBeSent(mustNotBeCalled())) |
||||
.hasMessage("Unexpected invocation"); |
||||
} |
||||
|
||||
private Runnable mustNotBeCalled() { |
||||
return () -> fail("Unexpected invocation"); |
||||
} |
||||
|
||||
private Runnable mockWithAction(final Runnable action) { |
||||
final Runnable runnable = mock(Runnable.class); |
||||
doAnswer( |
||||
invocation -> { |
||||
action.run(); |
||||
return null; |
||||
}) |
||||
.when(runnable) |
||||
.run(); |
||||
return runnable; |
||||
} |
||||
} |
Loading…
Reference in new issue