-
Notifications
You must be signed in to change notification settings - Fork 130
[NC-2058] Improve block propagation time #808
[NC-2058] Improve block propagation time #808
Conversation
107ee40
to
a94f0d1
Compare
a94f0d1
to
2c3b7e4
Compare
55a5465
to
aa58cfa
Compare
aa58cfa
to
e3d12b6
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.
Only real issue is the hard coded capability but otherwise looks good. Would be useful to get the PR description updated to indicate what parts of the story this PR doesn't yet cover.
@@ -130,6 +133,15 @@ public ResponseStream send(final MessageData messageData) throws PeerNotConnecte | |||
} | |||
} | |||
|
|||
public void propagateBlock(final Block block, final UInt256 totalDifficulty) { | |||
final NewBlockMessage newBlockMessage = NewBlockMessage.create(block, totalDifficulty); | |||
final Capability capability = Capability.create("eth", 63); |
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 don't want to create a new capability here and need to use the passed in protocolName
. I suspect connection.sendForProtocol(protocolName, newBlockMessage)
is what you want.
@@ -144,12 +144,21 @@ private void onBlockAdded(final BlockAddedEvent blockAddedEvent, final Blockchai | |||
} | |||
} | |||
|
|||
private void handleNewBlockFromNetwork(final EthMessage message) { | |||
void broadcastBlock(final Block block, final UInt256 difficulty) { |
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.
Is there a reason this is package private instead of private?
I'd also suggest as you move forward to extract this out into a separate BlockBroadcaster
class. It just provides a better separation of concerns and will let us write a more focussed unit test for the approach to broadcasting blocks. Happy enough for it to live here for now though.
for (EthPeer ethPeer : availablePeers) { | ||
ethPeer.propagateBlock(block, difficulty); | ||
} | ||
} |
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.
If you're feeling adventurous you could merge the collection into a list and forLoop into a single line:
ethContext.getEthPeers().availablePeers().forEach(ethPeer -> ethPeer.propagateBlock(block, difficulty));
.forEach(ethPeer -> ethPeer.propagateBlock(block, difficulty)); | ||
} | ||
|
||
void effectuateBroadcast() { |
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.
optional Too many syllables. Perhaps maybeBroadcast
or possiblyBroadcast.
. Effectuate communicates to me that the block will unconditionally be broadcast, while what we need to communicate is that if it passes some tests it will be broadcast.
@@ -77,12 +78,14 @@ | |||
final EthContext ethContext, | |||
final SyncState syncState, | |||
final PendingBlocks pendingBlocks, | |||
final LabelledMetric<OperationTimer> ethTasksTimer) { | |||
final LabelledMetric<OperationTimer> ethTasksTimer, | |||
final BlockBroadcaster<C> blockBroadcaster) { |
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 field is always overwritten, no need for it in the constructor.
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.
as I wrote in the description, it's easier to test if it's in the constructor. do you suggest making an explicit getter and setter?
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 say passing it in in the constructor is a good thing - allows you to mock it, but you're also creating a new one on line 115 so it's not using the one passed into the constructor.
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 fact it is overwritten and never used prior to that is the real concern. If it's fixed later then it can stay.
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.
yeah- good point about it getting overwritten- mock passed to constructor never getting used 😬- so then we should have a getter and setter then?
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.
No, passing it into the constructor is good, but BlockBroadcaster
shouldn't take Bock
into its constructor, only pass in the block to broadcast to the method that actual broadcasts it. That way you can reuse the same BlockBroadcaster
instance to broadcast multiple blocks and never need to create a new one in BlockPropagationManager
.
import com.google.common.annotations.VisibleForTesting; | ||
|
||
class BlockBroadcaster<C> { | ||
|
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.
No real state is kept. Could this be rewitten as a static function? Or perhaps make block a parameter in effectuateBroadcast
, but re-use gets tricky when the schedule changes.
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'm not sure how to interpret this comment. it seems that your suggesting to remove these instance variables, and then not
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 like the idea of this being a separate class with an instance method to broadcast just for the testing benefits. However you wouldn't pass the block
into the constructor and I'd be tempted to limit this just to broadcasting at the moment, not the header validation so you probably only need EthContext
.
Not 100% sure about where the validation should live but I think keeping it in BlockPropagationManager
will keep the validation concerns together since it will also choose the validation to apply when importing the block (which could be reduced based on what validation has already been done). Making broadcastBlock
@VisibleForTesting
is also a sign that having something purely responsible for the broadcast, separate from validation, is useful.
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.
And yes I appreciate the original ticket description is based on the validation being done in the separate task which would push it into this class - the fact that we're just applying full validation instead of a new validation class and having seen the number of extra dependencies is pushing me to change that view.
@@ -63,6 +63,7 @@ | |||
private final EthContext ethContext; | |||
private final SyncState syncState; | |||
private final LabelledMetric<OperationTimer> ethTasksTimer; | |||
private BlockBroadcaster<C> blockBroadcaster; |
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 field is always overwritten prior to use. Consider making it a variable instead.
@@ -75,6 +75,7 @@ public static void setupSuite() { | |||
fullBlockchain = BlockchainSetupUtil.forTesting().importAllBlocks(); | |||
} | |||
|
|||
@SuppressWarnings("unchecked") |
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 really don't like disabling the unchecked warning. Consider using <?> when you access no templated methods.
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.
@SuppressWarnings("unchecked")
is already used in this class in shouldNotImportBlocksThatAreAlreadyBeingImported
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.
Yeah you have to @SuppressWarnings
when creating a mock of a generic class. You can use @Mock
and the runner instead but that's a whole other conversation.
@@ -77,12 +78,14 @@ | |||
final EthContext ethContext, | |||
final SyncState syncState, | |||
final PendingBlocks pendingBlocks, | |||
final LabelledMetric<OperationTimer> ethTasksTimer) { | |||
final LabelledMetric<OperationTimer> ethTasksTimer, | |||
final BlockBroadcaster<C> blockBroadcaster) { |
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 say passing it in in the constructor is a good thing - allows you to mock it, but you're also creating a new one on line 115 so it's not using the one passed into the constructor.
import com.google.common.annotations.VisibleForTesting; | ||
|
||
class BlockBroadcaster<C> { | ||
|
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 like the idea of this being a separate class with an instance method to broadcast just for the testing benefits. However you wouldn't pass the block
into the constructor and I'd be tempted to limit this just to broadcasting at the moment, not the header validation so you probably only need EthContext
.
Not 100% sure about where the validation should live but I think keeping it in BlockPropagationManager
will keep the validation concerns together since it will also choose the validation to apply when importing the block (which could be reduced based on what validation has already been done). Making broadcastBlock
@VisibleForTesting
is also a sign that having something purely responsible for the broadcast, separate from validation, is useful.
protocolSchedule.getByBlockNumber(block.getHeader().getNumber()); | ||
final BlockHeaderValidator<C> blockHeaderValidator = protocolSpec.getBlockHeaderValidator(); | ||
final Optional<BlockHeader> maybeParent = | ||
protocolContext.getBlockchain().getBlockHeader(block.getHeader().getParentHash()); |
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 know the parent is available by now so could just use getBlockHeader(...).orElseThrow(() -> new IllegalArgumentException(...))
. Better than silently not broadcasting the block.
@@ -75,6 +75,7 @@ public static void setupSuite() { | |||
fullBlockchain = BlockchainSetupUtil.forTesting().importAllBlocks(); | |||
} | |||
|
|||
@SuppressWarnings("unchecked") |
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.
Yeah you have to @SuppressWarnings
when creating a mock of a generic class. You can use @Mock
and the runner instead but that's a whole other conversation.
import com.google.common.annotations.VisibleForTesting; | ||
|
||
class BlockBroadcaster<C> { | ||
|
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.
And yes I appreciate the original ticket description is based on the validation being done in the separate task which would push it into this class - the fact that we're just applying full validation instead of a new validation class and having seen the number of extra dependencies is pushing me to change that view.
@Test | ||
public void purgesOldBlocks() { | ||
final int oldBlocksToImport = 3; | ||
syncConfig = | ||
SynchronizerConfiguration.builder().blockPropagationRange(-oldBlocksToImport, 5).build(); | ||
BlockBroadcaster<Void> blockBroadcaster = mock(BlockBroadcaster.class); |
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 able to make the blockBroadcaster
from setUp
a field and just re-use it here. Which then avoids the need for the @SuppressWarnings("unchecked")
on this method.
@@ -63,6 +66,7 @@ | |||
private final EthContext ethContext; | |||
private final SyncState syncState; | |||
private final LabelledMetric<OperationTimer> ethTasksTimer; | |||
private BlockBroadcaster blockBroadcaster; |
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: this should be final
@@ -105,9 +110,34 @@ private void setupListeners() { | |||
.subscribe(EthPV62.NEW_BLOCK_HASHES, this::handleNewBlockHashesFromNetwork); | |||
} | |||
|
|||
@VisibleForTesting | |||
void broadcastBlock(final Block block) { | |||
blockBroadcaster = new BlockBroadcaster(ethContext); |
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.
Don't create a new BlockBroadcaster
here, just use the one passed into the constructor.
new IllegalArgumentException( | ||
"Incapable of retrieving header from non-existent parent of " | ||
+ block.getHeader().getNumber() | ||
+ ".")); |
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 ok for this PR, but could you follow up with a PR to change importOrSavePendingBlock
so that instead of just using protocolContext.getBlockchain().contains(block.getHeader().getParentHash())
it actually gets the parent header and checks if it's present. Then you can pass that header into broadcastBlock
and not need this exception. Just be careful that the lookup of the parent header stays inside the synchronized
block.
@@ -105,9 +110,34 @@ private void setupListeners() { | |||
.subscribe(EthPV62.NEW_BLOCK_HASHES, this::handleNewBlockHashesFromNetwork); | |||
} | |||
|
|||
@VisibleForTesting | |||
void broadcastBlock(final Block 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.
Since the validation is being done in here this should probably be called validateAndBroadcastBlock
. And hopefully we don't need it to be @VisibleForTesting
- it's a code smell that tests need access to private internals of the class being tested.
private void onBlockAdded(final BlockAddedEvent blockAddedEvent, final Blockchain blockchain) { | ||
// Check to see if any of our pending blocks are now ready for import | ||
final Block newBlock = blockAddedEvent.getBlock(); | ||
broadcastBlock(newBlock); |
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 the right place to trigger block propagation. It will be called after we've processed transactions and imported to the chain - we want to do it before then, so in importOrSavePendingBlock
just before the PersistBlockTask
.
@Test | ||
public void verifyBroadcastBlockInvocation() { | ||
final BlockPropagationManager<Void> blockPropagationManager = | ||
spy( |
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 suggest using a mock here instead of a spy. Spies are generally overly complicated and we don't need the real block propagation - just want to check that the broadcast method was called.
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.
why do you say they're overly complicated? if I change that to mock everything breaks in a confusing way
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.
something related to that generic component
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.
Because a spy is a combination of a mock and a real implementation. So you're simultaneously saying this thing is within the scope of the unit I'm testing (thus using the real one) but also that it's outside of the unit (thus I want to check how it's interacted with).
And the voodoo required to make a spy work leads to a number of weird implications (e.g. that test you and I hit a few weeks back where moving a method reference to the constructor broke the tests because the spy no longer picked it up).
So for this test you don't want to spy on the BlockPropagationManager
, you already have a mock BlockBroadcaster
that you can assert on:
@Test
public void verifyBroadcastBlockInvocation() {
blockchainUtil.importFirstBlocks(2);
final Block block = blockchainUtil.getBlock(2);
blockPropagationManager.start();
// Setup peer and messages
final RespondingEthPeer peer = EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 0);
final UInt256 totalDifficulty = fullBlockchain.getTotalDifficultyByHash(block.getHash()).get();
final NewBlockMessage newBlockMessage = NewBlockMessage.create(block, totalDifficulty);
// Broadcast message
EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, newBlockMessage);
final Responder responder = RespondingEthPeer.blockchainResponder(fullBlockchain);
peer.respondWhile(responder, peer::hasOutstandingRequests);
verify(blockBroadcaster, times(1)).propagate(block, totalDifficulty);
}
And that test will fail because the total difficulty doesn't match. The test is right, the code is wrong. :) Total difficulty is being calculated as:
final UInt256 totalDifficulty =
parent.getDifficulty().plus(block.getHeader().getDifficulty());
which is just parent difficulty + child difficulty when you need the sum of the difficulty for every block in the chain to get the total difficulty. final UInt256 totalDifficulty = protocolContext.getBlockchain().getTotalDifficultyByHash(parent.getHash()).get().plus(block.getHeader().getDifficulty());
will get you what you want.
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.
interesting. so that's just the difficulty of that one parent block in isolation, the parent block doesn't incorporate the difficulty of the blocks that came before it. && more interesting still -- the genesis block has an actual difficulty. anyway - thanks for pointing this out.
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.
ethContext | ||
.getEthPeers() | ||
.availablePeers() | ||
.forEach(ethPeer -> ethPeer.propagateBlock(block, difficulty)); |
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'll need to filter out peers that have already seen this block:
.filter(ethPeer -> ~ethPeer.hasSeenBlock(block.getHash()))
and then should add a test for that as well.
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.
And in EthPeer.propagateBlock
we should add the block to the knownBlocks
set.
I forgot we needed to filter out peers that have already seen the 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.
LGTM. Thanks Matthew.
* o * add test * clean up * scaffolding * update * update * comments * add test * update * update ii * format * update ii * fix * verifyBroadcastBlockInvocation * test * update * update to difficulty calculation * remove BlockBroadcasterTest from this pr * update * update * update II
PR description
Addresses feedback raised in previous round of review and contains a test that verifies the invocation of the newly added
broadcastBlock()
methodNOTE:
what do you make of the passage of
BlockBroadcaster
in theDefaultSynchronizer
?Next Step:
add some tests that verify that the propagated block is in fact being received by peers