mirror of https://github.com/hyperledger/besu
[NC-1508] Added iBFT 2.0 BlockTimerExpiry Event and realated timer management class (#58)
parent
2ede24fe95
commit
2bd6ce74ba
@ -0,0 +1,94 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2018 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.consensus.ibft; |
||||||
|
|
||||||
|
import tech.pegasys.pantheon.consensus.ibft.ibftevent.BlockTimerExpiry; |
||||||
|
import tech.pegasys.pantheon.ethereum.core.BlockHeader; |
||||||
|
import tech.pegasys.pantheon.util.time.Clock; |
||||||
|
|
||||||
|
import java.util.Optional; |
||||||
|
import java.util.concurrent.ScheduledExecutorService; |
||||||
|
import java.util.concurrent.ScheduledFuture; |
||||||
|
import java.util.concurrent.TimeUnit; |
||||||
|
|
||||||
|
/** Class for starting and keeping organised block timers */ |
||||||
|
public class BlockTimer { |
||||||
|
private final ScheduledExecutorService timerExecutor; |
||||||
|
private Optional<ScheduledFuture<?>> currentTimerTask; |
||||||
|
private final IbftEventQueue queue; |
||||||
|
private final long minimumTimeBetweenBlocksMillis; |
||||||
|
private final Clock clock; |
||||||
|
|
||||||
|
/** |
||||||
|
* Construct a BlockTimer with primed executor service ready to start timers |
||||||
|
* |
||||||
|
* @param queue The queue in which to put block expiry events |
||||||
|
* @param minimumTimeBetweenBlocksMillis Minimum timestamp difference between blocks |
||||||
|
* @param timerExecutor Executor service that timers can be scheduled with |
||||||
|
* @param clock System clock |
||||||
|
*/ |
||||||
|
public BlockTimer( |
||||||
|
final IbftEventQueue queue, |
||||||
|
final long minimumTimeBetweenBlocksMillis, |
||||||
|
final ScheduledExecutorService timerExecutor, |
||||||
|
final Clock clock) { |
||||||
|
this.queue = queue; |
||||||
|
this.timerExecutor = timerExecutor; |
||||||
|
this.currentTimerTask = Optional.empty(); |
||||||
|
this.minimumTimeBetweenBlocksMillis = minimumTimeBetweenBlocksMillis; |
||||||
|
this.clock = clock; |
||||||
|
} |
||||||
|
|
||||||
|
/** Cancels the current running round timer if there is one */ |
||||||
|
public synchronized void cancelTimer() { |
||||||
|
currentTimerTask.ifPresent(t -> t.cancel(false)); |
||||||
|
currentTimerTask = Optional.empty(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Whether there is a timer currently running or not |
||||||
|
* |
||||||
|
* @return boolean of whether a timer is ticking or not |
||||||
|
*/ |
||||||
|
public synchronized boolean isRunning() { |
||||||
|
return currentTimerTask.map(t -> !t.isDone()).orElse(false); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Starts a timer for the supplied round cancelling any previously active block timer |
||||||
|
* |
||||||
|
* @param round The round identifier which this timer is tracking |
||||||
|
* @param chainHeadHeader The header of the chain head |
||||||
|
*/ |
||||||
|
public synchronized void startTimer( |
||||||
|
final ConsensusRoundIdentifier round, final BlockHeader chainHeadHeader) { |
||||||
|
cancelTimer(); |
||||||
|
|
||||||
|
final long now = clock.millisecondsSinceEpoch(); |
||||||
|
|
||||||
|
// absolute time when the timer is supposed to expire
|
||||||
|
final long expiryTime = chainHeadHeader.getTimestamp() * 1_000 + minimumTimeBetweenBlocksMillis; |
||||||
|
|
||||||
|
if (expiryTime > now) { |
||||||
|
final long delay = expiryTime - now; |
||||||
|
|
||||||
|
final Runnable newTimerRunnable = () -> queue.add(new BlockTimerExpiry(round)); |
||||||
|
|
||||||
|
final ScheduledFuture<?> newTimerTask = |
||||||
|
timerExecutor.schedule(newTimerRunnable, delay, TimeUnit.MILLISECONDS); |
||||||
|
currentTimerTask = Optional.of(newTimerTask); |
||||||
|
} else { |
||||||
|
queue.add(new BlockTimerExpiry(round)); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,66 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2018 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.consensus.ibft.ibftevent; |
||||||
|
|
||||||
|
import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier; |
||||||
|
import tech.pegasys.pantheon.consensus.ibft.IbftEvent; |
||||||
|
import tech.pegasys.pantheon.consensus.ibft.IbftEvents.Type; |
||||||
|
|
||||||
|
import java.util.Objects; |
||||||
|
|
||||||
|
import com.google.common.base.MoreObjects; |
||||||
|
|
||||||
|
/** Event indicating a block timer has expired */ |
||||||
|
public final class BlockTimerExpiry implements IbftEvent { |
||||||
|
private final ConsensusRoundIdentifier roundIdentifier; |
||||||
|
|
||||||
|
/** |
||||||
|
* Constructor for a BlockTimerExpiry event |
||||||
|
* |
||||||
|
* @param roundIdentifier The roundIdentifier that the expired timer belonged to |
||||||
|
*/ |
||||||
|
public BlockTimerExpiry(final ConsensusRoundIdentifier roundIdentifier) { |
||||||
|
this.roundIdentifier = roundIdentifier; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Type getType() { |
||||||
|
return Type.BLOCK_TIMER_EXPIRY; |
||||||
|
} |
||||||
|
|
||||||
|
public ConsensusRoundIdentifier getRoundIndentifier() { |
||||||
|
return roundIdentifier; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() { |
||||||
|
return MoreObjects.toStringHelper(this).add("Round Identifier", roundIdentifier).toString(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean equals(final Object o) { |
||||||
|
if (this == o) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
if (o == null || getClass() != o.getClass()) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
final BlockTimerExpiry that = (BlockTimerExpiry) o; |
||||||
|
return Objects.equals(roundIdentifier, that.roundIdentifier); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int hashCode() { |
||||||
|
return Objects.hash(roundIdentifier); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,239 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2018 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.consensus.ibft; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.awaitility.Awaitility.await; |
||||||
|
import static org.hamcrest.CoreMatchers.equalTo; |
||||||
|
import static org.mockito.ArgumentMatchers.any; |
||||||
|
import static org.mockito.ArgumentMatchers.anyLong; |
||||||
|
import static org.mockito.ArgumentMatchers.eq; |
||||||
|
import static org.mockito.Mockito.mock; |
||||||
|
import static org.mockito.Mockito.never; |
||||||
|
import static org.mockito.Mockito.times; |
||||||
|
import static org.mockito.Mockito.verify; |
||||||
|
import static org.mockito.Mockito.when; |
||||||
|
|
||||||
|
import tech.pegasys.pantheon.consensus.ibft.ibftevent.BlockTimerExpiry; |
||||||
|
import tech.pegasys.pantheon.ethereum.core.BlockHeader; |
||||||
|
import tech.pegasys.pantheon.ethereum.core.BlockHeaderTestFixture; |
||||||
|
import tech.pegasys.pantheon.util.time.Clock; |
||||||
|
|
||||||
|
import java.util.concurrent.Executors; |
||||||
|
import java.util.concurrent.ScheduledExecutorService; |
||||||
|
import java.util.concurrent.ScheduledFuture; |
||||||
|
import java.util.concurrent.TimeUnit; |
||||||
|
|
||||||
|
import org.junit.Before; |
||||||
|
import org.junit.Test; |
||||||
|
import org.junit.runner.RunWith; |
||||||
|
import org.mockito.ArgumentCaptor; |
||||||
|
import org.mockito.Mockito; |
||||||
|
import org.mockito.junit.MockitoJUnitRunner; |
||||||
|
|
||||||
|
@RunWith(MockitoJUnitRunner.StrictStubs.class) |
||||||
|
public class BlockTimerTest { |
||||||
|
private ScheduledExecutorService mockExecutorService; |
||||||
|
private IbftEventQueue mockQueue; |
||||||
|
private Clock mockClock; |
||||||
|
|
||||||
|
@Before |
||||||
|
public void initialise() { |
||||||
|
mockExecutorService = mock(ScheduledExecutorService.class); |
||||||
|
mockQueue = mock(IbftEventQueue.class); |
||||||
|
mockClock = mock(Clock.class); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void cancelTimerCancelsWhenNoTimer() { |
||||||
|
final BlockTimer timer = new BlockTimer(mockQueue, 15_000, mockExecutorService, mockClock); |
||||||
|
// Starts with nothing running
|
||||||
|
assertThat(timer.isRunning()).isFalse(); |
||||||
|
// cancel shouldn't die if there's nothing running
|
||||||
|
timer.cancelTimer(); |
||||||
|
// there is still nothing running
|
||||||
|
assertThat(timer.isRunning()).isFalse(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void startTimerSchedulesCorrectlyWhenExpiryIsInTheFuture() { |
||||||
|
final long MINIMAL_TIME_BETWEEN_BLOCKS_MILLIS = 15_000; |
||||||
|
final long NOW_MILLIS = 505_000L; |
||||||
|
final long BLOCK_TIME_STAMP = 500L; |
||||||
|
final long EXPECTED_DELAY = 10_000L; |
||||||
|
|
||||||
|
final BlockTimer timer = |
||||||
|
new BlockTimer( |
||||||
|
mockQueue, MINIMAL_TIME_BETWEEN_BLOCKS_MILLIS, mockExecutorService, mockClock); |
||||||
|
|
||||||
|
when(mockClock.millisecondsSinceEpoch()).thenReturn(NOW_MILLIS); |
||||||
|
|
||||||
|
BlockHeader header = new BlockHeaderTestFixture().timestamp(BLOCK_TIME_STAMP).buildHeader(); |
||||||
|
final ConsensusRoundIdentifier round = |
||||||
|
new ConsensusRoundIdentifier(0xFEDBCA9876543210L, 0x12345678); |
||||||
|
|
||||||
|
final ScheduledFuture<?> mockedFuture = mock(ScheduledFuture.class); |
||||||
|
Mockito.<ScheduledFuture<?>>when( |
||||||
|
mockExecutorService.schedule(any(Runnable.class), anyLong(), any())) |
||||||
|
.thenReturn(mockedFuture); |
||||||
|
|
||||||
|
timer.startTimer(round, header); |
||||||
|
verify(mockExecutorService) |
||||||
|
.schedule(any(Runnable.class), eq(EXPECTED_DELAY), eq(TimeUnit.MILLISECONDS)); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void aBlockTimerExpiryEventIsAddedToTheQueueOnExpiry() { |
||||||
|
|
||||||
|
final long MINIMAL_TIME_BETWEEN_BLOCKS_MILLIS = 1_000; |
||||||
|
final long NOW_MILLIS = 300_500L; |
||||||
|
final long BLOCK_TIME_STAMP = 300; |
||||||
|
final long EXPECTED_DELAY = 500; |
||||||
|
|
||||||
|
when(mockClock.millisecondsSinceEpoch()).thenReturn(NOW_MILLIS); |
||||||
|
|
||||||
|
BlockHeader header = new BlockHeaderTestFixture().timestamp(BLOCK_TIME_STAMP).buildHeader(); |
||||||
|
final ConsensusRoundIdentifier round = |
||||||
|
new ConsensusRoundIdentifier(0xFEDBCA9876543210L, 0x12345678); |
||||||
|
|
||||||
|
BlockTimer timer = |
||||||
|
new BlockTimer( |
||||||
|
mockQueue, |
||||||
|
MINIMAL_TIME_BETWEEN_BLOCKS_MILLIS, |
||||||
|
Executors.newSingleThreadScheduledExecutor(), |
||||||
|
mockClock); |
||||||
|
timer.startTimer(round, header); |
||||||
|
|
||||||
|
// Verify that the event will not be added to the queue immediately
|
||||||
|
verify(mockQueue, never()).add(any()); |
||||||
|
|
||||||
|
await() |
||||||
|
.atMost(EXPECTED_DELAY + 200, TimeUnit.MILLISECONDS) |
||||||
|
.atLeast(EXPECTED_DELAY - 200, TimeUnit.MILLISECONDS) |
||||||
|
.until(timer::isRunning, equalTo(false)); |
||||||
|
|
||||||
|
ArgumentCaptor<IbftEvent> ibftEventCaptor = ArgumentCaptor.forClass(IbftEvent.class); |
||||||
|
verify(mockQueue).add(ibftEventCaptor.capture()); |
||||||
|
|
||||||
|
assertThat(ibftEventCaptor.getValue() instanceof BlockTimerExpiry).isTrue(); |
||||||
|
assertThat(((BlockTimerExpiry) ibftEventCaptor.getValue()).getRoundIndentifier()) |
||||||
|
.isEqualToComparingFieldByField(round); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void eventIsImmediatelyAddedToTheQueueIfAbsoluteExpiryIsEqualToNow() { |
||||||
|
final long MINIMAL_TIME_BETWEEN_BLOCKS_MILLIS = 15_000; |
||||||
|
final long NOW_MILLIS = 515_000L; |
||||||
|
final long BLOCK_TIME_STAMP = 500; |
||||||
|
|
||||||
|
final BlockTimer timer = |
||||||
|
new BlockTimer( |
||||||
|
mockQueue, MINIMAL_TIME_BETWEEN_BLOCKS_MILLIS, mockExecutorService, mockClock); |
||||||
|
|
||||||
|
when(mockClock.millisecondsSinceEpoch()).thenReturn(NOW_MILLIS); |
||||||
|
|
||||||
|
BlockHeader header = new BlockHeaderTestFixture().timestamp(BLOCK_TIME_STAMP).buildHeader(); |
||||||
|
final ConsensusRoundIdentifier round = |
||||||
|
new ConsensusRoundIdentifier(0xFEDBCA9876543210L, 0x12345678); |
||||||
|
|
||||||
|
timer.startTimer(round, header); |
||||||
|
verify(mockExecutorService, never()).schedule(any(Runnable.class), anyLong(), any()); |
||||||
|
|
||||||
|
ArgumentCaptor<IbftEvent> ibftEventCaptor = ArgumentCaptor.forClass(IbftEvent.class); |
||||||
|
verify(mockQueue).add(ibftEventCaptor.capture()); |
||||||
|
|
||||||
|
assertThat(ibftEventCaptor.getValue() instanceof BlockTimerExpiry).isTrue(); |
||||||
|
assertThat(((BlockTimerExpiry) ibftEventCaptor.getValue()).getRoundIndentifier()) |
||||||
|
.isEqualToComparingFieldByField(round); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void eventIsImmediatelyAddedToTheQueueIfAbsoluteExpiryIsInThePast() { |
||||||
|
final long MINIMAL_TIME_BETWEEN_BLOCKS_MILLIS = 15_000; |
||||||
|
final long NOW_MILLIS = 520_000L; |
||||||
|
final long BLOCK_TIME_STAMP = 500L; |
||||||
|
|
||||||
|
final BlockTimer timer = |
||||||
|
new BlockTimer( |
||||||
|
mockQueue, MINIMAL_TIME_BETWEEN_BLOCKS_MILLIS, mockExecutorService, mockClock); |
||||||
|
|
||||||
|
when(mockClock.millisecondsSinceEpoch()).thenReturn(NOW_MILLIS); |
||||||
|
|
||||||
|
BlockHeader header = new BlockHeaderTestFixture().timestamp(BLOCK_TIME_STAMP).buildHeader(); |
||||||
|
final ConsensusRoundIdentifier round = |
||||||
|
new ConsensusRoundIdentifier(0xFEDBCA9876543210L, 0x12345678); |
||||||
|
|
||||||
|
timer.startTimer(round, header); |
||||||
|
verify(mockExecutorService, never()).schedule(any(Runnable.class), anyLong(), any()); |
||||||
|
|
||||||
|
ArgumentCaptor<IbftEvent> ibftEventCaptor = ArgumentCaptor.forClass(IbftEvent.class); |
||||||
|
verify(mockQueue).add(ibftEventCaptor.capture()); |
||||||
|
|
||||||
|
assertThat(ibftEventCaptor.getValue() instanceof BlockTimerExpiry).isTrue(); |
||||||
|
assertThat(((BlockTimerExpiry) ibftEventCaptor.getValue()).getRoundIndentifier()) |
||||||
|
.isEqualToComparingFieldByField(round); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void startTimerCancelsExistingTimer() { |
||||||
|
final long MINIMAL_TIME_BETWEEN_BLOCKS_MILLIS = 15_0000; |
||||||
|
final long NOW_MILLIS = 500_000L; |
||||||
|
final long BLOCK_TIME_STAMP = 500L; |
||||||
|
|
||||||
|
final BlockTimer timer = |
||||||
|
new BlockTimer( |
||||||
|
mockQueue, MINIMAL_TIME_BETWEEN_BLOCKS_MILLIS, mockExecutorService, mockClock); |
||||||
|
|
||||||
|
when(mockClock.millisecondsSinceEpoch()).thenReturn(NOW_MILLIS); |
||||||
|
|
||||||
|
BlockHeader header = new BlockHeaderTestFixture().timestamp(BLOCK_TIME_STAMP).buildHeader(); |
||||||
|
final ConsensusRoundIdentifier round = |
||||||
|
new ConsensusRoundIdentifier(0xFEDBCA9876543210L, 0x12345678); |
||||||
|
final ScheduledFuture<?> mockedFuture = mock(ScheduledFuture.class); |
||||||
|
|
||||||
|
Mockito.<ScheduledFuture<?>>when( |
||||||
|
mockExecutorService.schedule(any(Runnable.class), anyLong(), eq(TimeUnit.MILLISECONDS))) |
||||||
|
.thenReturn(mockedFuture); |
||||||
|
timer.startTimer(round, header); |
||||||
|
verify(mockedFuture, times(0)).cancel(false); |
||||||
|
timer.startTimer(round, header); |
||||||
|
verify(mockedFuture, times(1)).cancel(false); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void runningFollowsTheStateOfTheTimer() { |
||||||
|
final long MINIMAL_TIME_BETWEEN_BLOCKS_MILLIS = 15_0000; |
||||||
|
final long NOW_MILLIS = 500_000L; |
||||||
|
final long BLOCK_TIME_STAMP = 500L; |
||||||
|
|
||||||
|
final BlockTimer timer = |
||||||
|
new BlockTimer( |
||||||
|
mockQueue, MINIMAL_TIME_BETWEEN_BLOCKS_MILLIS, mockExecutorService, mockClock); |
||||||
|
|
||||||
|
when(mockClock.millisecondsSinceEpoch()).thenReturn(NOW_MILLIS); |
||||||
|
|
||||||
|
BlockHeader header = new BlockHeaderTestFixture().timestamp(BLOCK_TIME_STAMP).buildHeader(); |
||||||
|
final ConsensusRoundIdentifier round = |
||||||
|
new ConsensusRoundIdentifier(0xFEDBCA9876543210L, 0x12345678); |
||||||
|
|
||||||
|
final ScheduledFuture<?> mockedFuture = mock(ScheduledFuture.class); |
||||||
|
Mockito.<ScheduledFuture<?>>when( |
||||||
|
mockExecutorService.schedule(any(Runnable.class), anyLong(), eq(TimeUnit.MILLISECONDS))) |
||||||
|
.thenReturn(mockedFuture); |
||||||
|
timer.startTimer(round, header); |
||||||
|
when(mockedFuture.isDone()).thenReturn(false); |
||||||
|
assertThat(timer.isRunning()).isTrue(); |
||||||
|
when(mockedFuture.isDone()).thenReturn(true); |
||||||
|
assertThat(timer.isRunning()).isFalse(); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue