mirror of https://github.com/hyperledger/besu
7311: add peertask foundation code (#7628)
* 7311: Add PeerTask system for use in future PRs Signed-off-by: Matilda Clerke <matilda.clerke@consensys.net>pull/7782/head
parent
dfbfb96f28
commit
2169985ee2
@ -0,0 +1,26 @@ |
||||
/* |
||||
* Copyright contributors to Hyperledger Besu. |
||||
* |
||||
* 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. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.eth.manager.peertask; |
||||
|
||||
public class InvalidPeerTaskResponseException extends Exception { |
||||
|
||||
public InvalidPeerTaskResponseException() { |
||||
super(); |
||||
} |
||||
|
||||
public InvalidPeerTaskResponseException(final Throwable cause) { |
||||
super(cause); |
||||
} |
||||
} |
@ -0,0 +1,17 @@ |
||||
/* |
||||
* Copyright contributors to Hyperledger Besu. |
||||
* |
||||
* 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. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.eth.manager.peertask; |
||||
|
||||
public class NoAvailablePeerException extends Exception {} |
@ -0,0 +1,42 @@ |
||||
/* |
||||
* Copyright contributors to Hyperledger Besu. |
||||
* |
||||
* 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. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.eth.manager.peertask; |
||||
|
||||
import org.hyperledger.besu.ethereum.eth.manager.EthPeer; |
||||
import org.hyperledger.besu.ethereum.p2p.peers.PeerId; |
||||
|
||||
import java.util.Optional; |
||||
import java.util.function.Predicate; |
||||
|
||||
/** Selects the EthPeers for the PeerTaskExecutor */ |
||||
public interface PeerSelector { |
||||
|
||||
/** |
||||
* Gets a peer matching the supplied filter |
||||
* |
||||
* @param filter a Predicate\<EthPeer\> matching desirable peers |
||||
* @return a peer matching the supplied conditions |
||||
*/ |
||||
Optional<EthPeer> getPeer(final Predicate<EthPeer> filter); |
||||
|
||||
/** |
||||
* Attempts to get the EthPeer identified by peerId |
||||
* |
||||
* @param peerId the peerId of the desired EthPeer |
||||
* @return An Optional\<EthPeer\> containing the EthPeer identified by peerId if present in the |
||||
* PeerSelector, or empty otherwise |
||||
*/ |
||||
Optional<EthPeer> getPeerByPeerId(PeerId peerId); |
||||
} |
@ -0,0 +1,83 @@ |
||||
/* |
||||
* Copyright contributors to Hyperledger Besu. |
||||
* |
||||
* 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. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.eth.manager.peertask; |
||||
|
||||
import org.hyperledger.besu.ethereum.eth.manager.EthPeer; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.SubProtocol; |
||||
|
||||
import java.util.function.Predicate; |
||||
|
||||
/** |
||||
* Represents a task to be executed on an EthPeer by the PeerTaskExecutor |
||||
* |
||||
* @param <T> The type of the result of this PeerTask |
||||
*/ |
||||
public interface PeerTask<T> { |
||||
/** |
||||
* Returns the SubProtocol used for this PeerTask |
||||
* |
||||
* @return the SubProtocol used for this PeerTask |
||||
*/ |
||||
SubProtocol getSubProtocol(); |
||||
|
||||
/** |
||||
* Gets the request data to send to the EthPeer |
||||
* |
||||
* @return the request data to send to the EthPeer |
||||
*/ |
||||
MessageData getRequestMessage(); |
||||
|
||||
/** |
||||
* Parses the MessageData response from the EthPeer |
||||
* |
||||
* @param messageData the response MessageData to be parsed |
||||
* @return a T built from the response MessageData |
||||
* @throws InvalidPeerTaskResponseException if the response messageData is invalid |
||||
*/ |
||||
T parseResponse(MessageData messageData) throws InvalidPeerTaskResponseException; |
||||
|
||||
/** |
||||
* Gets the number of times this task may be attempted against other peers |
||||
* |
||||
* @return the number of times this task may be attempted against other peers |
||||
*/ |
||||
default int getRetriesWithOtherPeer() { |
||||
return 5; |
||||
} |
||||
|
||||
/** |
||||
* Gets the number of times this task may be attempted against the same peer |
||||
* |
||||
* @return the number of times this task may be attempted against the same peer |
||||
*/ |
||||
default int getRetriesWithSamePeer() { |
||||
return 5; |
||||
} |
||||
|
||||
/** |
||||
* Gets a Predicate that checks if an EthPeer is suitable for this PeerTask |
||||
* |
||||
* @return a Predicate that checks if an EthPeer is suitable for this PeerTask |
||||
*/ |
||||
Predicate<EthPeer> getPeerRequirementFilter(); |
||||
|
||||
/** |
||||
* Checks if the supplied result is considered a success |
||||
* |
||||
* @return true if the supplied result is considered a success |
||||
*/ |
||||
boolean isSuccess(T result); |
||||
} |
@ -0,0 +1,196 @@ |
||||
/* |
||||
* Copyright contributors to Hyperledger Besu. |
||||
* |
||||
* 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. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.eth.manager.peertask; |
||||
|
||||
import org.hyperledger.besu.ethereum.eth.manager.EthPeer; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection.PeerNotConnected; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; |
||||
import org.hyperledger.besu.metrics.BesuMetricCategory; |
||||
import org.hyperledger.besu.plugin.services.MetricsSystem; |
||||
import org.hyperledger.besu.plugin.services.metrics.Counter; |
||||
import org.hyperledger.besu.plugin.services.metrics.LabelledGauge; |
||||
import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; |
||||
import org.hyperledger.besu.plugin.services.metrics.OperationTimer; |
||||
|
||||
import java.util.Collection; |
||||
import java.util.HashSet; |
||||
import java.util.Map; |
||||
import java.util.Optional; |
||||
import java.util.concurrent.ConcurrentHashMap; |
||||
import java.util.concurrent.ExecutionException; |
||||
import java.util.concurrent.TimeoutException; |
||||
import java.util.concurrent.atomic.AtomicInteger; |
||||
|
||||
/** Manages the execution of PeerTasks, respecting their PeerTaskRetryBehavior */ |
||||
public class PeerTaskExecutor { |
||||
|
||||
private final PeerSelector peerSelector; |
||||
private final PeerTaskRequestSender requestSender; |
||||
|
||||
private final LabelledMetric<OperationTimer> requestTimer; |
||||
private final LabelledMetric<Counter> timeoutCounter; |
||||
private final LabelledMetric<Counter> invalidResponseCounter; |
||||
private final LabelledMetric<Counter> internalExceptionCounter; |
||||
private final LabelledGauge inflightRequestGauge; |
||||
private final Map<String, AtomicInteger> inflightRequestCountByClassName; |
||||
|
||||
public PeerTaskExecutor( |
||||
final PeerSelector peerSelector, |
||||
final PeerTaskRequestSender requestSender, |
||||
final MetricsSystem metricsSystem) { |
||||
this.peerSelector = peerSelector; |
||||
this.requestSender = requestSender; |
||||
requestTimer = |
||||
metricsSystem.createLabelledTimer( |
||||
BesuMetricCategory.PEERS, |
||||
"request_time", |
||||
"Time taken to send a request and receive a response", |
||||
"taskName"); |
||||
timeoutCounter = |
||||
metricsSystem.createLabelledCounter( |
||||
BesuMetricCategory.PEERS, |
||||
"timeout_total", |
||||
"Counter of the number of timeouts occurred", |
||||
"taskName"); |
||||
invalidResponseCounter = |
||||
metricsSystem.createLabelledCounter( |
||||
BesuMetricCategory.PEERS, |
||||
"invalid_response_total", |
||||
"Counter of the number of invalid responses received", |
||||
"taskName"); |
||||
internalExceptionCounter = |
||||
metricsSystem.createLabelledCounter( |
||||
BesuMetricCategory.PEERS, |
||||
"internal_exception_total", |
||||
"Counter of the number of internal exceptions occurred", |
||||
"taskName"); |
||||
inflightRequestGauge = |
||||
metricsSystem.createLabelledGauge( |
||||
BesuMetricCategory.PEERS, |
||||
"inflight_request_gauge", |
||||
"Gauge of the number of inflight requests", |
||||
"taskName"); |
||||
inflightRequestCountByClassName = new ConcurrentHashMap<>(); |
||||
} |
||||
|
||||
public <T> PeerTaskExecutorResult<T> execute(final PeerTask<T> peerTask) { |
||||
PeerTaskExecutorResult<T> executorResult; |
||||
int retriesRemaining = peerTask.getRetriesWithOtherPeer(); |
||||
final Collection<EthPeer> usedEthPeers = new HashSet<>(); |
||||
do { |
||||
Optional<EthPeer> peer = |
||||
peerSelector.getPeer( |
||||
(candidatePeer) -> |
||||
peerTask.getPeerRequirementFilter().test(candidatePeer) |
||||
&& !usedEthPeers.contains(candidatePeer)); |
||||
if (peer.isEmpty()) { |
||||
executorResult = |
||||
new PeerTaskExecutorResult<>( |
||||
Optional.empty(), PeerTaskExecutorResponseCode.NO_PEER_AVAILABLE); |
||||
continue; |
||||
} |
||||
usedEthPeers.add(peer.get()); |
||||
executorResult = executeAgainstPeer(peerTask, peer.get()); |
||||
} while (retriesRemaining-- > 0 |
||||
&& executorResult.responseCode() != PeerTaskExecutorResponseCode.SUCCESS); |
||||
|
||||
return executorResult; |
||||
} |
||||
|
||||
public <T> PeerTaskExecutorResult<T> executeAgainstPeer( |
||||
final PeerTask<T> peerTask, final EthPeer peer) { |
||||
String taskClassName = peerTask.getClass().getSimpleName(); |
||||
AtomicInteger inflightRequestCountForThisTaskClass = |
||||
inflightRequestCountByClassName.computeIfAbsent( |
||||
taskClassName, |
||||
(k) -> { |
||||
AtomicInteger inflightRequests = new AtomicInteger(0); |
||||
inflightRequestGauge.labels(inflightRequests::get, taskClassName); |
||||
return inflightRequests; |
||||
}); |
||||
MessageData requestMessageData = peerTask.getRequestMessage(); |
||||
PeerTaskExecutorResult<T> executorResult; |
||||
int retriesRemaining = peerTask.getRetriesWithSamePeer(); |
||||
do { |
||||
try { |
||||
T result; |
||||
try (final OperationTimer.TimingContext ignored = |
||||
requestTimer.labels(taskClassName).startTimer()) { |
||||
inflightRequestCountForThisTaskClass.incrementAndGet(); |
||||
|
||||
MessageData responseMessageData = |
||||
requestSender.sendRequest(peerTask.getSubProtocol(), requestMessageData, peer); |
||||
|
||||
result = peerTask.parseResponse(responseMessageData); |
||||
} finally { |
||||
inflightRequestCountForThisTaskClass.decrementAndGet(); |
||||
} |
||||
|
||||
if (peerTask.isSuccess(result)) { |
||||
peer.recordUsefulResponse(); |
||||
executorResult = |
||||
new PeerTaskExecutorResult<>( |
||||
Optional.ofNullable(result), PeerTaskExecutorResponseCode.SUCCESS); |
||||
} else { |
||||
// At this point, the result is most likely empty. Technically, this is a valid result, so
|
||||
// we don't penalise the peer, but it's also a useless result, so we return
|
||||
// INVALID_RESPONSE code
|
||||
executorResult = |
||||
new PeerTaskExecutorResult<>( |
||||
Optional.ofNullable(result), PeerTaskExecutorResponseCode.INVALID_RESPONSE); |
||||
} |
||||
|
||||
} catch (PeerNotConnected e) { |
||||
executorResult = |
||||
new PeerTaskExecutorResult<>( |
||||
Optional.empty(), PeerTaskExecutorResponseCode.PEER_DISCONNECTED); |
||||
|
||||
} catch (InterruptedException | TimeoutException e) { |
||||
peer.recordRequestTimeout(requestMessageData.getCode()); |
||||
timeoutCounter.labels(taskClassName).inc(); |
||||
executorResult = |
||||
new PeerTaskExecutorResult<>(Optional.empty(), PeerTaskExecutorResponseCode.TIMEOUT); |
||||
|
||||
} catch (InvalidPeerTaskResponseException e) { |
||||
peer.recordUselessResponse(e.getMessage()); |
||||
invalidResponseCounter.labels(taskClassName).inc(); |
||||
executorResult = |
||||
new PeerTaskExecutorResult<>( |
||||
Optional.empty(), PeerTaskExecutorResponseCode.INVALID_RESPONSE); |
||||
|
||||
} catch (ExecutionException e) { |
||||
internalExceptionCounter.labels(taskClassName).inc(); |
||||
executorResult = |
||||
new PeerTaskExecutorResult<>( |
||||
Optional.empty(), PeerTaskExecutorResponseCode.INTERNAL_SERVER_ERROR); |
||||
} |
||||
} while (retriesRemaining-- > 0 |
||||
&& executorResult.responseCode() != PeerTaskExecutorResponseCode.SUCCESS |
||||
&& executorResult.responseCode() != PeerTaskExecutorResponseCode.PEER_DISCONNECTED |
||||
&& sleepBetweenRetries()); |
||||
|
||||
return executorResult; |
||||
} |
||||
|
||||
private boolean sleepBetweenRetries() { |
||||
try { |
||||
// sleep for 1 second to match implemented wait between retries in AbstractRetryingPeerTask
|
||||
Thread.sleep(1000); |
||||
return true; |
||||
} catch (InterruptedException e) { |
||||
return false; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,24 @@ |
||||
/* |
||||
* Copyright contributors to Hyperledger Besu. |
||||
* |
||||
* 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. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.eth.manager.peertask; |
||||
|
||||
public enum PeerTaskExecutorResponseCode { |
||||
SUCCESS, |
||||
NO_PEER_AVAILABLE, |
||||
PEER_DISCONNECTED, |
||||
INTERNAL_SERVER_ERROR, |
||||
TIMEOUT, |
||||
INVALID_RESPONSE |
||||
} |
@ -0,0 +1,20 @@ |
||||
/* |
||||
* Copyright contributors to Hyperledger Besu. |
||||
* |
||||
* 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. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.eth.manager.peertask; |
||||
|
||||
import java.util.Optional; |
||||
|
||||
public record PeerTaskExecutorResult<T>( |
||||
Optional<T> result, PeerTaskExecutorResponseCode responseCode) {} |
@ -0,0 +1,56 @@ |
||||
/* |
||||
* Copyright contributors to Hyperledger Besu. |
||||
* |
||||
* 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. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.eth.manager.peertask; |
||||
|
||||
import org.hyperledger.besu.ethereum.eth.manager.EthPeer; |
||||
import org.hyperledger.besu.ethereum.eth.manager.RequestManager.ResponseStream; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.SubProtocol; |
||||
|
||||
import java.util.concurrent.CompletableFuture; |
||||
import java.util.concurrent.ExecutionException; |
||||
import java.util.concurrent.TimeUnit; |
||||
import java.util.concurrent.TimeoutException; |
||||
|
||||
public class PeerTaskRequestSender { |
||||
private static final long DEFAULT_TIMEOUT_MS = 5_000; |
||||
|
||||
private final long timeoutMs; |
||||
|
||||
public PeerTaskRequestSender() { |
||||
this.timeoutMs = DEFAULT_TIMEOUT_MS; |
||||
} |
||||
|
||||
public PeerTaskRequestSender(final long timeoutMs) { |
||||
this.timeoutMs = timeoutMs; |
||||
} |
||||
|
||||
public MessageData sendRequest( |
||||
final SubProtocol subProtocol, final MessageData requestMessageData, final EthPeer ethPeer) |
||||
throws PeerConnection.PeerNotConnected, |
||||
ExecutionException, |
||||
InterruptedException, |
||||
TimeoutException { |
||||
ResponseStream responseStream = |
||||
ethPeer.send(requestMessageData, subProtocol.getName(), ethPeer.getConnection()); |
||||
final CompletableFuture<MessageData> responseMessageDataFuture = new CompletableFuture<>(); |
||||
responseStream.then( |
||||
(boolean streamClosed, MessageData message, EthPeer peer) -> { |
||||
responseMessageDataFuture.complete(message); |
||||
}); |
||||
return responseMessageDataFuture.get(timeoutMs, TimeUnit.MILLISECONDS); |
||||
} |
||||
} |
@ -0,0 +1,290 @@ |
||||
/* |
||||
* Copyright contributors to Hyperledger Besu. |
||||
* |
||||
* 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. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.eth.manager.peertask; |
||||
|
||||
import org.hyperledger.besu.ethereum.eth.manager.EthPeer; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.SubProtocol; |
||||
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; |
||||
|
||||
import java.util.Optional; |
||||
import java.util.concurrent.ExecutionException; |
||||
import java.util.concurrent.TimeoutException; |
||||
import java.util.function.Predicate; |
||||
|
||||
import org.junit.jupiter.api.AfterEach; |
||||
import org.junit.jupiter.api.Assertions; |
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.mockito.Mock; |
||||
import org.mockito.Mockito; |
||||
import org.mockito.MockitoAnnotations; |
||||
|
||||
public class PeerTaskExecutorTest { |
||||
private @Mock PeerSelector peerSelector; |
||||
private @Mock PeerTaskRequestSender requestSender; |
||||
private @Mock PeerTask<Object> peerTask; |
||||
private @Mock SubProtocol subprotocol; |
||||
private @Mock MessageData requestMessageData; |
||||
private @Mock MessageData responseMessageData; |
||||
private @Mock EthPeer ethPeer; |
||||
private AutoCloseable mockCloser; |
||||
|
||||
private PeerTaskExecutor peerTaskExecutor; |
||||
|
||||
@BeforeEach |
||||
public void beforeTest() { |
||||
mockCloser = MockitoAnnotations.openMocks(this); |
||||
peerTaskExecutor = new PeerTaskExecutor(peerSelector, requestSender, new NoOpMetricsSystem()); |
||||
} |
||||
|
||||
@AfterEach |
||||
public void afterTest() throws Exception { |
||||
mockCloser.close(); |
||||
} |
||||
|
||||
@Test |
||||
public void testExecuteAgainstPeerWithNoRetriesAndSuccessfulFlow() |
||||
throws PeerConnection.PeerNotConnected, |
||||
ExecutionException, |
||||
InterruptedException, |
||||
TimeoutException, |
||||
InvalidPeerTaskResponseException { |
||||
|
||||
Object responseObject = new Object(); |
||||
|
||||
Mockito.when(peerTask.getRequestMessage()).thenReturn(requestMessageData); |
||||
Mockito.when(peerTask.getRetriesWithSamePeer()).thenReturn(0); |
||||
Mockito.when(peerTask.getSubProtocol()).thenReturn(subprotocol); |
||||
Mockito.when(subprotocol.getName()).thenReturn("subprotocol"); |
||||
Mockito.when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) |
||||
.thenReturn(responseMessageData); |
||||
Mockito.when(peerTask.parseResponse(responseMessageData)).thenReturn(responseObject); |
||||
Mockito.when(peerTask.isSuccess(responseObject)).thenReturn(true); |
||||
|
||||
PeerTaskExecutorResult<Object> result = peerTaskExecutor.executeAgainstPeer(peerTask, ethPeer); |
||||
|
||||
Mockito.verify(ethPeer).recordUsefulResponse(); |
||||
|
||||
Assertions.assertNotNull(result); |
||||
Assertions.assertTrue(result.result().isPresent()); |
||||
Assertions.assertSame(responseObject, result.result().get()); |
||||
Assertions.assertEquals(PeerTaskExecutorResponseCode.SUCCESS, result.responseCode()); |
||||
} |
||||
|
||||
@Test |
||||
public void testExecuteAgainstPeerWithNoRetriesAndPartialSuccessfulFlow() |
||||
throws PeerConnection.PeerNotConnected, |
||||
ExecutionException, |
||||
InterruptedException, |
||||
TimeoutException, |
||||
InvalidPeerTaskResponseException { |
||||
|
||||
Object responseObject = new Object(); |
||||
|
||||
Mockito.when(peerTask.getRequestMessage()).thenReturn(requestMessageData); |
||||
Mockito.when(peerTask.getRetriesWithSamePeer()).thenReturn(0); |
||||
Mockito.when(peerTask.getSubProtocol()).thenReturn(subprotocol); |
||||
Mockito.when(subprotocol.getName()).thenReturn("subprotocol"); |
||||
Mockito.when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) |
||||
.thenReturn(responseMessageData); |
||||
Mockito.when(peerTask.parseResponse(responseMessageData)).thenReturn(responseObject); |
||||
Mockito.when(peerTask.isSuccess(responseObject)).thenReturn(false); |
||||
|
||||
PeerTaskExecutorResult<Object> result = peerTaskExecutor.executeAgainstPeer(peerTask, ethPeer); |
||||
|
||||
Assertions.assertNotNull(result); |
||||
Assertions.assertTrue(result.result().isPresent()); |
||||
Assertions.assertEquals(PeerTaskExecutorResponseCode.INVALID_RESPONSE, result.responseCode()); |
||||
} |
||||
|
||||
@Test |
||||
public void testExecuteAgainstPeerWithRetriesAndSuccessfulFlowAfterFirstFailure() |
||||
throws PeerConnection.PeerNotConnected, |
||||
ExecutionException, |
||||
InterruptedException, |
||||
TimeoutException, |
||||
InvalidPeerTaskResponseException { |
||||
Object responseObject = new Object(); |
||||
int requestMessageDataCode = 123; |
||||
|
||||
Mockito.when(peerTask.getRequestMessage()).thenReturn(requestMessageData); |
||||
Mockito.when(peerTask.getRetriesWithSamePeer()).thenReturn(2); |
||||
|
||||
Mockito.when(peerTask.getSubProtocol()).thenReturn(subprotocol); |
||||
Mockito.when(subprotocol.getName()).thenReturn("subprotocol"); |
||||
Mockito.when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) |
||||
.thenThrow(new TimeoutException()) |
||||
.thenReturn(responseMessageData); |
||||
Mockito.when(requestMessageData.getCode()).thenReturn(requestMessageDataCode); |
||||
Mockito.when(peerTask.parseResponse(responseMessageData)).thenReturn(responseObject); |
||||
Mockito.when(peerTask.isSuccess(responseObject)).thenReturn(true); |
||||
|
||||
PeerTaskExecutorResult<Object> result = peerTaskExecutor.executeAgainstPeer(peerTask, ethPeer); |
||||
|
||||
Mockito.verify(ethPeer).recordRequestTimeout(requestMessageDataCode); |
||||
Mockito.verify(ethPeer).recordUsefulResponse(); |
||||
|
||||
Assertions.assertNotNull(result); |
||||
Assertions.assertTrue(result.result().isPresent()); |
||||
Assertions.assertSame(responseObject, result.result().get()); |
||||
Assertions.assertEquals(PeerTaskExecutorResponseCode.SUCCESS, result.responseCode()); |
||||
} |
||||
|
||||
@Test |
||||
public void testExecuteAgainstPeerWithNoRetriesAndPeerNotConnected() |
||||
throws PeerConnection.PeerNotConnected, |
||||
ExecutionException, |
||||
InterruptedException, |
||||
TimeoutException { |
||||
|
||||
Mockito.when(peerTask.getRequestMessage()).thenReturn(requestMessageData); |
||||
Mockito.when(peerTask.getRetriesWithSamePeer()).thenReturn(0); |
||||
Mockito.when(peerTask.getSubProtocol()).thenReturn(subprotocol); |
||||
Mockito.when(subprotocol.getName()).thenReturn("subprotocol"); |
||||
Mockito.when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) |
||||
.thenThrow(new PeerConnection.PeerNotConnected("")); |
||||
|
||||
PeerTaskExecutorResult<Object> result = peerTaskExecutor.executeAgainstPeer(peerTask, ethPeer); |
||||
|
||||
Assertions.assertNotNull(result); |
||||
Assertions.assertTrue(result.result().isEmpty()); |
||||
Assertions.assertEquals(PeerTaskExecutorResponseCode.PEER_DISCONNECTED, result.responseCode()); |
||||
} |
||||
|
||||
@Test |
||||
public void testExecuteAgainstPeerWithNoRetriesAndTimeoutException() |
||||
throws PeerConnection.PeerNotConnected, |
||||
ExecutionException, |
||||
InterruptedException, |
||||
TimeoutException { |
||||
int requestMessageDataCode = 123; |
||||
|
||||
Mockito.when(peerTask.getRequestMessage()).thenReturn(requestMessageData); |
||||
Mockito.when(peerTask.getRetriesWithSamePeer()).thenReturn(0); |
||||
Mockito.when(peerTask.getSubProtocol()).thenReturn(subprotocol); |
||||
Mockito.when(subprotocol.getName()).thenReturn("subprotocol"); |
||||
Mockito.when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) |
||||
.thenThrow(new TimeoutException()); |
||||
Mockito.when(requestMessageData.getCode()).thenReturn(requestMessageDataCode); |
||||
|
||||
PeerTaskExecutorResult<Object> result = peerTaskExecutor.executeAgainstPeer(peerTask, ethPeer); |
||||
|
||||
Mockito.verify(ethPeer).recordRequestTimeout(requestMessageDataCode); |
||||
|
||||
Assertions.assertNotNull(result); |
||||
Assertions.assertTrue(result.result().isEmpty()); |
||||
Assertions.assertEquals(PeerTaskExecutorResponseCode.TIMEOUT, result.responseCode()); |
||||
} |
||||
|
||||
@Test |
||||
public void testExecuteAgainstPeerWithNoRetriesAndInvalidResponseMessage() |
||||
throws PeerConnection.PeerNotConnected, |
||||
ExecutionException, |
||||
InterruptedException, |
||||
TimeoutException, |
||||
InvalidPeerTaskResponseException { |
||||
|
||||
Mockito.when(peerTask.getRequestMessage()).thenReturn(requestMessageData); |
||||
Mockito.when(peerTask.getRetriesWithSamePeer()).thenReturn(0); |
||||
Mockito.when(peerTask.getSubProtocol()).thenReturn(subprotocol); |
||||
Mockito.when(subprotocol.getName()).thenReturn("subprotocol"); |
||||
Mockito.when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) |
||||
.thenReturn(responseMessageData); |
||||
Mockito.when(peerTask.parseResponse(responseMessageData)) |
||||
.thenThrow(new InvalidPeerTaskResponseException()); |
||||
|
||||
PeerTaskExecutorResult<Object> result = peerTaskExecutor.executeAgainstPeer(peerTask, ethPeer); |
||||
|
||||
Mockito.verify(ethPeer).recordUselessResponse(null); |
||||
|
||||
Assertions.assertNotNull(result); |
||||
Assertions.assertTrue(result.result().isEmpty()); |
||||
Assertions.assertEquals(PeerTaskExecutorResponseCode.INVALID_RESPONSE, result.responseCode()); |
||||
} |
||||
|
||||
@Test |
||||
@SuppressWarnings("unchecked") |
||||
public void testExecuteWithNoRetriesAndSuccessFlow() |
||||
throws PeerConnection.PeerNotConnected, |
||||
ExecutionException, |
||||
InterruptedException, |
||||
TimeoutException, |
||||
InvalidPeerTaskResponseException { |
||||
Object responseObject = new Object(); |
||||
|
||||
Mockito.when(peerSelector.getPeer(Mockito.any(Predicate.class))) |
||||
.thenReturn(Optional.of(ethPeer)); |
||||
|
||||
Mockito.when(peerTask.getRequestMessage()).thenReturn(requestMessageData); |
||||
Mockito.when(peerTask.getRetriesWithOtherPeer()).thenReturn(0); |
||||
Mockito.when(peerTask.getRetriesWithSamePeer()).thenReturn(0); |
||||
Mockito.when(peerTask.getSubProtocol()).thenReturn(subprotocol); |
||||
Mockito.when(subprotocol.getName()).thenReturn("subprotocol"); |
||||
Mockito.when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) |
||||
.thenReturn(responseMessageData); |
||||
Mockito.when(peerTask.parseResponse(responseMessageData)).thenReturn(responseObject); |
||||
Mockito.when(peerTask.isSuccess(responseObject)).thenReturn(true); |
||||
|
||||
PeerTaskExecutorResult<Object> result = peerTaskExecutor.executeAgainstPeer(peerTask, ethPeer); |
||||
|
||||
Mockito.verify(ethPeer).recordUsefulResponse(); |
||||
|
||||
Assertions.assertNotNull(result); |
||||
Assertions.assertTrue(result.result().isPresent()); |
||||
Assertions.assertSame(responseObject, result.result().get()); |
||||
Assertions.assertEquals(PeerTaskExecutorResponseCode.SUCCESS, result.responseCode()); |
||||
} |
||||
|
||||
@Test |
||||
@SuppressWarnings("unchecked") |
||||
public void testExecuteWithPeerSwitchingAndSuccessFlow() |
||||
throws PeerConnection.PeerNotConnected, |
||||
ExecutionException, |
||||
InterruptedException, |
||||
TimeoutException, |
||||
InvalidPeerTaskResponseException { |
||||
Object responseObject = new Object(); |
||||
int requestMessageDataCode = 123; |
||||
EthPeer peer2 = Mockito.mock(EthPeer.class); |
||||
|
||||
Mockito.when(peerSelector.getPeer(Mockito.any(Predicate.class))) |
||||
.thenReturn(Optional.of(ethPeer)) |
||||
.thenReturn(Optional.of(peer2)); |
||||
|
||||
Mockito.when(peerTask.getRequestMessage()).thenReturn(requestMessageData); |
||||
Mockito.when(peerTask.getRetriesWithOtherPeer()).thenReturn(2); |
||||
Mockito.when(peerTask.getRetriesWithSamePeer()).thenReturn(0); |
||||
Mockito.when(peerTask.getSubProtocol()).thenReturn(subprotocol); |
||||
Mockito.when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) |
||||
.thenThrow(new TimeoutException()); |
||||
Mockito.when(requestMessageData.getCode()).thenReturn(requestMessageDataCode); |
||||
Mockito.when(requestSender.sendRequest(subprotocol, requestMessageData, peer2)) |
||||
.thenReturn(responseMessageData); |
||||
Mockito.when(peerTask.parseResponse(responseMessageData)).thenReturn(responseObject); |
||||
Mockito.when(peerTask.isSuccess(responseObject)).thenReturn(true); |
||||
|
||||
PeerTaskExecutorResult<Object> result = peerTaskExecutor.execute(peerTask); |
||||
|
||||
Mockito.verify(ethPeer).recordRequestTimeout(requestMessageDataCode); |
||||
Mockito.verify(peer2).recordUsefulResponse(); |
||||
|
||||
Assertions.assertNotNull(result); |
||||
Assertions.assertTrue(result.result().isPresent()); |
||||
Assertions.assertSame(responseObject, result.result().get()); |
||||
Assertions.assertEquals(PeerTaskExecutorResponseCode.SUCCESS, result.responseCode()); |
||||
} |
||||
} |
@ -0,0 +1,75 @@ |
||||
/* |
||||
* Copyright contributors to Hyperledger Besu. |
||||
* |
||||
* 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. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.eth.manager.peertask; |
||||
|
||||
import org.hyperledger.besu.ethereum.eth.manager.EthPeer; |
||||
import org.hyperledger.besu.ethereum.eth.manager.RequestManager; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.SubProtocol; |
||||
|
||||
import java.util.concurrent.CompletableFuture; |
||||
import java.util.concurrent.ExecutionException; |
||||
|
||||
import org.junit.jupiter.api.Assertions; |
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.mockito.ArgumentCaptor; |
||||
import org.mockito.Mockito; |
||||
|
||||
public class PeerTaskRequestSenderTest { |
||||
|
||||
private PeerTaskRequestSender peerTaskRequestSender; |
||||
|
||||
@BeforeEach |
||||
public void beforeTest() { |
||||
peerTaskRequestSender = new PeerTaskRequestSender(); |
||||
} |
||||
|
||||
@Test |
||||
public void testSendRequest() |
||||
throws PeerConnection.PeerNotConnected, ExecutionException, InterruptedException { |
||||
SubProtocol subprotocol = Mockito.mock(SubProtocol.class); |
||||
MessageData requestMessageData = Mockito.mock(MessageData.class); |
||||
MessageData responseMessageData = Mockito.mock(MessageData.class); |
||||
EthPeer peer = Mockito.mock(EthPeer.class); |
||||
PeerConnection peerConnection = Mockito.mock(PeerConnection.class); |
||||
RequestManager.ResponseStream responseStream = |
||||
Mockito.mock(RequestManager.ResponseStream.class); |
||||
|
||||
Mockito.when(peer.getConnection()).thenReturn(peerConnection); |
||||
Mockito.when(subprotocol.getName()).thenReturn("subprotocol"); |
||||
Mockito.when(peer.send(requestMessageData, "subprotocol", peerConnection)) |
||||
.thenReturn(responseStream); |
||||
|
||||
CompletableFuture<MessageData> actualResponseMessageDataFuture = |
||||
CompletableFuture.supplyAsync( |
||||
() -> { |
||||
try { |
||||
return peerTaskRequestSender.sendRequest(subprotocol, requestMessageData, peer); |
||||
} catch (Exception e) { |
||||
throw new RuntimeException(e); |
||||
} |
||||
}); |
||||
Thread.sleep(500); |
||||
ArgumentCaptor<RequestManager.ResponseCallback> responseCallbackArgumentCaptor = |
||||
ArgumentCaptor.forClass(RequestManager.ResponseCallback.class); |
||||
Mockito.verify(responseStream).then(responseCallbackArgumentCaptor.capture()); |
||||
RequestManager.ResponseCallback responseCallback = responseCallbackArgumentCaptor.getValue(); |
||||
responseCallback.exec(false, responseMessageData, peer); |
||||
|
||||
Assertions.assertSame(responseMessageData, actualResponseMessageDataFuture.get()); |
||||
} |
||||
} |
Loading…
Reference in new issue