From 9fad7275da9173a7e5e1688929fd83f3b8224a5b Mon Sep 17 00:00:00 2001 From: Lucas Saldanha Date: Wed, 25 Oct 2023 13:47:57 +1300 Subject: [PATCH] Added StubLabelledOperationTimer to StubMetricSystem (#7625) --- .../StubLabelledOperationTimerTest.java | 99 +++++++++++++++++++ .../metrics/StubOperationTimerTest.java | 86 ++++++++++++++++ .../metrics/StubLabelledOperationTimer.java | 78 +++++++++++++++ .../metrics/StubMetricsSystem.java | 15 ++- .../metrics/StubOperationTimer.java | 76 ++++++++++++++ .../client/BlockDutySchedulerTest.java | 30 ++++-- .../duties/SlotBasedScheduledDutiesTest.java | 35 +++++-- 7 files changed, 400 insertions(+), 19 deletions(-) create mode 100644 infrastructure/metrics/src/test/java/tech/pegasys/teku/infrastructure/metrics/StubLabelledOperationTimerTest.java create mode 100644 infrastructure/metrics/src/test/java/tech/pegasys/teku/infrastructure/metrics/StubOperationTimerTest.java create mode 100644 infrastructure/metrics/src/testFixtures/java/tech/pegasys/teku/infrastructure/metrics/StubLabelledOperationTimer.java create mode 100644 infrastructure/metrics/src/testFixtures/java/tech/pegasys/teku/infrastructure/metrics/StubOperationTimer.java diff --git a/infrastructure/metrics/src/test/java/tech/pegasys/teku/infrastructure/metrics/StubLabelledOperationTimerTest.java b/infrastructure/metrics/src/test/java/tech/pegasys/teku/infrastructure/metrics/StubLabelledOperationTimerTest.java new file mode 100644 index 00000000000..fc29579cbc2 --- /dev/null +++ b/infrastructure/metrics/src/test/java/tech/pegasys/teku/infrastructure/metrics/StubLabelledOperationTimerTest.java @@ -0,0 +1,99 @@ +/* + * Copyright Consensys Software Inc., 2023 + * + * 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.teku.infrastructure.metrics; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.function.Supplier; +import org.hyperledger.besu.plugin.services.metrics.OperationTimer; +import org.hyperledger.besu.plugin.services.metrics.OperationTimer.TimingContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class StubLabelledOperationTimerTest { + + private StubLabelledOperationTimer labelledOperationTimer; + private Supplier timeProvider; + + @BeforeEach + @SuppressWarnings("unchecked") + public void setUp() { + timeProvider = mock(Supplier.class); + when(timeProvider.get()).thenReturn(System.currentTimeMillis()); + + labelledOperationTimer = + new StubLabelledOperationTimer( + TekuMetricCategory.VALIDATOR, "test_name", "help msg", timeProvider); + } + + @Test + public void labelledStubOperationTimerMustMarkTimeCorrectly() { + when(timeProvider.get()).thenReturn(1L, 2L); + + final TimingContext timingContext = labelledOperationTimer.labels("foo", "bar").startTimer(); + timingContext.stopTimer(); + + assertThat(labelledOperationTimer.getAverageDuration("foo", "bar")).hasValue(1.0); + } + + @Test + public void shouldSupportMultipleDurationsWithinSameLabels() { + when(timeProvider.get()).thenReturn(1L, 1L, 6L, 11L); + + final OperationTimer timer1 = labelledOperationTimer.labels("foo", "bar"); + final OperationTimer timer2 = labelledOperationTimer.labels("foo", "bar"); + + final TimingContext timingContext1 = timer1.startTimer(); + final TimingContext timingContext2 = timer2.startTimer(); + + timingContext1.stopTimer(); + timingContext2.stopTimer(); + + assertThat(labelledOperationTimer.getDurations("foo", "bar")).contains(5L, 10L); + } + + @Test + public void shouldSupportMultipleTimersWithDifferentLabels() { + when(timeProvider.get()).thenReturn(1L, 1L, 6L, 11L); + + final OperationTimer timer1 = labelledOperationTimer.labels("foo", "bar"); + final OperationTimer timer2 = labelledOperationTimer.labels("blip", "blop"); + + final TimingContext timingContext1 = timer1.startTimer(); + final TimingContext timingContext2 = timer2.startTimer(); + + timingContext1.stopTimer(); + timingContext2.stopTimer(); + + assertThat(labelledOperationTimer.getDurations("foo", "bar")).contains(5L); + assertThat(labelledOperationTimer.getDurations("blip", "blop")).contains(10L); + } + + @Test + public void shouldFailGettingTimeOfNonExistingTimer() { + assertThatThrownBy(() -> labelledOperationTimer.getAverageDuration("nope")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void shouldReturnExistingTimerForLabels() { + final OperationTimer timer1 = labelledOperationTimer.labels("foo", "bar"); + final OperationTimer timer2 = labelledOperationTimer.labels("foo", "bar"); + + assertThat(timer1).isSameAs(timer2); + } +} diff --git a/infrastructure/metrics/src/test/java/tech/pegasys/teku/infrastructure/metrics/StubOperationTimerTest.java b/infrastructure/metrics/src/test/java/tech/pegasys/teku/infrastructure/metrics/StubOperationTimerTest.java new file mode 100644 index 00000000000..567291a1e4b --- /dev/null +++ b/infrastructure/metrics/src/test/java/tech/pegasys/teku/infrastructure/metrics/StubOperationTimerTest.java @@ -0,0 +1,86 @@ +/* + * Copyright Consensys Software Inc., 2023 + * + * 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.teku.infrastructure.metrics; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.function.Supplier; +import org.hyperledger.besu.plugin.services.metrics.OperationTimer.TimingContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class StubOperationTimerTest { + + private StubOperationTimer operationTimer; + private Supplier timeProvider; + + @BeforeEach + @SuppressWarnings("unchecked") + public void setUp() { + timeProvider = mock(Supplier.class); + when(timeProvider.get()).thenReturn(System.currentTimeMillis()); + + operationTimer = + new StubOperationTimer( + TekuMetricCategory.VALIDATOR, "test_timer", "help msg", timeProvider); + } + + @Test + public void shouldCalculateCorrectDuration() { + // duration 6 - 1 = 5 + when(timeProvider.get()).thenReturn(1L, 6L); + + TimingContext timingContext = operationTimer.startTimer(); + + assertThat(timingContext.stopTimer()).isEqualTo(5); + } + + @Test + public void shouldCalculateMultipleDurations() { + // 1st duration 6 - 1 = 5 + // 2nd duration 11 - 1 = 10 + when(timeProvider.get()).thenReturn(1L, 1L, 6L, 11L); + + TimingContext timingContext1 = operationTimer.startTimer(); + TimingContext timingContext2 = operationTimer.startTimer(); + + assertThat(timingContext1.stopTimer()).isEqualTo(5); + assertThat(timingContext2.stopTimer()).isEqualTo(10); + } + + @Test + public void shouldCalculateCorrectDurationAverage() { + // 1st duration 6 - 1 = 5 + // 2nd duration 11 - 1 = 10 + when(timeProvider.get()).thenReturn(1L, 1L, 6L, 11L); + + TimingContext timingContext1 = operationTimer.startTimer(); + TimingContext timingContext2 = operationTimer.startTimer(); + + timingContext1.stopTimer(); + timingContext2.stopTimer(); + + assertThat(operationTimer.getAverageDuration()).hasValue(7.5); + } + + @Test + public void shouldNotCalculateDurationIfTimerIsNotStopped() { + // We never stop this timer + operationTimer.startTimer(); + + assertThat(operationTimer.getDurations()).isEmpty(); + } +} diff --git a/infrastructure/metrics/src/testFixtures/java/tech/pegasys/teku/infrastructure/metrics/StubLabelledOperationTimer.java b/infrastructure/metrics/src/testFixtures/java/tech/pegasys/teku/infrastructure/metrics/StubLabelledOperationTimer.java new file mode 100644 index 00000000000..8c009d48be2 --- /dev/null +++ b/infrastructure/metrics/src/testFixtures/java/tech/pegasys/teku/infrastructure/metrics/StubLabelledOperationTimer.java @@ -0,0 +1,78 @@ +/* + * Copyright Consensys Software Inc., 2023 + * + * 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.teku.infrastructure.metrics; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.OptionalDouble; +import java.util.Set; +import java.util.function.Supplier; +import org.assertj.core.util.VisibleForTesting; +import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; +import org.hyperledger.besu.plugin.services.metrics.MetricCategory; +import org.hyperledger.besu.plugin.services.metrics.OperationTimer; + +public class StubLabelledOperationTimer extends StubMetric + implements LabelledMetric { + + private final Map, StubOperationTimer> timers = new HashMap<>(); + private final Supplier timeProvider; + + protected StubLabelledOperationTimer( + final MetricCategory category, final String name, final String help) { + super(category, name, help); + this.timeProvider = System::currentTimeMillis; + } + + @VisibleForTesting + StubLabelledOperationTimer( + final MetricCategory category, + final String name, + final String help, + Supplier timeProvider) { + super(category, name, help); + this.timeProvider = timeProvider; + } + + @Override + public OperationTimer labels(final String... labels) { + return timers.computeIfAbsent( + List.of(labels), + __ -> new StubOperationTimer(getCategory(), getName(), getHelp(), timeProvider)); + } + + /** + * Return the average time of operations marked by the timer. + * + * @param labels the labels matching the timer + * @return the average time of operations marked by the timer. + * @throws IllegalArgumentException if the provided labels do not correspond to an existing timer. + */ + public OptionalDouble getAverageDuration(final String... labels) { + final StubOperationTimer operationTimer = timers.get(List.of(labels)); + if (operationTimer == null) { + throw new IllegalArgumentException("Attempting to get time from a non-existing timer"); + } + return operationTimer.getAverageDuration(); + } + + public Set getDurations(final String... labels) { + final StubOperationTimer operationTimer = timers.get(List.of(labels)); + if (operationTimer == null) { + throw new IllegalArgumentException("Attempting to get time from a non-existing timer"); + } + return operationTimer.getDurations(); + } +} diff --git a/infrastructure/metrics/src/testFixtures/java/tech/pegasys/teku/infrastructure/metrics/StubMetricsSystem.java b/infrastructure/metrics/src/testFixtures/java/tech/pegasys/teku/infrastructure/metrics/StubMetricsSystem.java index 4cc4b6cb36d..d7a01f14348 100644 --- a/infrastructure/metrics/src/testFixtures/java/tech/pegasys/teku/infrastructure/metrics/StubMetricsSystem.java +++ b/infrastructure/metrics/src/testFixtures/java/tech/pegasys/teku/infrastructure/metrics/StubMetricsSystem.java @@ -31,6 +31,8 @@ public class StubMetricsSystem implements MetricsSystem { private final Map> gauges = new ConcurrentHashMap<>(); private final Map> labelledGauges = new ConcurrentHashMap<>(); + private final Map> + labelledOperationTimers = new ConcurrentHashMap<>(); private static final Pattern METRIC_NAME_PATTERN = Pattern.compile("[a-zA-Z_:][a-zA-Z0-9_:]*"); private static final Pattern LABEL_NAME_PATTERN = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*"); @@ -85,7 +87,9 @@ public LabelledMetric createLabelledTimer( final String... labelNames) { validateMetricName(name); validateLabelName(labelNames); - throw new UnsupportedOperationException("Timers not supported"); + return labelledOperationTimers + .computeIfAbsent(category, __ -> new ConcurrentHashMap<>()) + .computeIfAbsent(name, __ -> new StubLabelledOperationTimer(category, name, help)); } public StubGauge getGauge(final MetricCategory category, final String name) { @@ -111,6 +115,15 @@ public StubCounter getCounter(final MetricCategory category, final String name) () -> new IllegalArgumentException("Unknown counter: " + category + " " + name)); } + public StubLabelledOperationTimer getLabelledOperationTimer( + final MetricCategory category, final String name) { + validateMetricName(name); + return Optional.ofNullable(labelledOperationTimers.get(category)) + .map(categoryTimers -> categoryTimers.get(name)) + .orElseThrow( + () -> new IllegalArgumentException("Unknown labelled timer: " + category + " " + name)); + } + private void validateMetricName(String metricName) { if (!METRIC_NAME_PATTERN.matcher(metricName).matches()) { throw new IllegalArgumentException( diff --git a/infrastructure/metrics/src/testFixtures/java/tech/pegasys/teku/infrastructure/metrics/StubOperationTimer.java b/infrastructure/metrics/src/testFixtures/java/tech/pegasys/teku/infrastructure/metrics/StubOperationTimer.java new file mode 100644 index 00000000000..07eca35f732 --- /dev/null +++ b/infrastructure/metrics/src/testFixtures/java/tech/pegasys/teku/infrastructure/metrics/StubOperationTimer.java @@ -0,0 +1,76 @@ +/* + * Copyright Consensys Software Inc., 2023 + * + * 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.teku.infrastructure.metrics; + +import java.util.HashSet; +import java.util.Map; +import java.util.OptionalDouble; +import java.util.OptionalLong; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; +import org.hyperledger.besu.plugin.services.metrics.MetricCategory; +import org.hyperledger.besu.plugin.services.metrics.OperationTimer; + +public class StubOperationTimer extends StubMetric implements OperationTimer { + + private final Random idGenerator = new Random(); + private final Supplier timeProvider; + private final Map durations = new ConcurrentHashMap<>(); + + StubOperationTimer( + final MetricCategory category, + final String name, + final String help, + final Supplier timeProvider) { + super(category, name, help); + this.timeProvider = timeProvider; + } + + @Override + public TimingContext startTimer() { + return new StubTimingContext(timeProvider.get()); + } + + public OptionalDouble getAverageDuration() { + return durations.values().stream().mapToLong(v -> v).average(); + } + + public Set getDurations() { + return new HashSet<>(durations.values()); + } + + public OptionalLong getDurationsByTimingContextId(final Long id) { + final Long maybeDuration = durations.get(id); + return maybeDuration == null ? OptionalLong.empty() : OptionalLong.of(maybeDuration); + } + + private class StubTimingContext implements TimingContext { + + private final long id = idGenerator.nextLong(); + private final long startTime; + + public StubTimingContext(final long startTime) { + this.startTime = startTime; + } + + @Override + public double stopTimer() { + final long duration = timeProvider.get() - startTime; + durations.put(id, duration); + return duration; + } + } +} diff --git a/validator/client/src/test/java/tech/pegasys/teku/validator/client/BlockDutySchedulerTest.java b/validator/client/src/test/java/tech/pegasys/teku/validator/client/BlockDutySchedulerTest.java index ebeb04e4998..b5ee8a89a88 100644 --- a/validator/client/src/test/java/tech/pegasys/teku/validator/client/BlockDutySchedulerTest.java +++ b/validator/client/src/test/java/tech/pegasys/teku/validator/client/BlockDutySchedulerTest.java @@ -44,7 +44,9 @@ import tech.pegasys.teku.validator.client.duties.BlockProductionDuty; import tech.pegasys.teku.validator.client.duties.Duty; import tech.pegasys.teku.validator.client.duties.DutyResult; +import tech.pegasys.teku.validator.client.duties.DutyType; import tech.pegasys.teku.validator.client.duties.SlotBasedScheduledDuties; +import tech.pegasys.teku.validator.client.duties.ValidatorDutyMetrics; import tech.pegasys.teku.validator.client.loader.OwnedValidators; public class BlockDutySchedulerTest extends AbstractDutySchedulerTest { @@ -92,16 +94,15 @@ public void shouldNotPerformDutiesForSameSlotTwice() { dataStructureUtil.randomPublicKey(), 6, UInt64.valueOf(4))), false)))); - final BlockProductionDuty blockCreationDuty = mock(BlockProductionDuty.class); - when(blockCreationDuty.performDuty()).thenReturn(new SafeFuture<>()); - when(blockDutyFactory.createProductionDuty(blockProposerSlot, validator1)) - .thenReturn(blockCreationDuty); + final BlockProductionDuty blockCreationDuty = + createBlockProductionDutyAtSlot(blockProposerSlot); // Load duties dutyScheduler.onSlot(spec.computeStartSlotAtEpoch(ZERO)); // Execute dutyScheduler.onBlockProductionDue(blockProposerSlot); + verify(blockCreationDuty).getType(); verify(blockCreationDuty).performDuty(); // Somehow we triggered the same slot again. @@ -122,10 +123,8 @@ public void shouldScheduleBlockProposalDuty() { new ProposerDuties( dataStructureUtil.randomBytes32(), List.of(validator1Duties), false)))); - final BlockProductionDuty blockCreationDuty = mock(BlockProductionDuty.class); - when(blockCreationDuty.performDuty()).thenReturn(new SafeFuture<>()); - when(blockDutyFactory.createProductionDuty(blockProposerSlot, validator1)) - .thenReturn(blockCreationDuty); + final BlockProductionDuty blockCreationDuty = + createBlockProductionDutyAtSlot(blockProposerSlot); // Load duties dutyScheduler.onSlot(spec.computeStartSlotAtEpoch(ZERO)); @@ -135,6 +134,15 @@ public void shouldScheduleBlockProposalDuty() { verify(blockCreationDuty).performDuty(); } + private BlockProductionDuty createBlockProductionDutyAtSlot(final UInt64 blockProposerSlot) { + final BlockProductionDuty blockCreationDuty = mock(BlockProductionDuty.class); + when(blockCreationDuty.getType()).thenReturn(DutyType.BLOCK_PRODUCTION); + when(blockCreationDuty.performDuty()).thenReturn(new SafeFuture<>()); + when(blockDutyFactory.createProductionDuty(blockProposerSlot, validator1)) + .thenReturn(blockCreationDuty); + return blockCreationDuty; + } + @Test public void shouldDelayExecutingDutiesUntilSchedulingIsComplete() { createDutySchedulerWithRealDuties(); @@ -314,6 +322,8 @@ void shouldUseHeadRootWhenDutyIsFromBeyondNextEpoch() { } private void createDutySchedulerWithRealDuties() { + final ValidatorDutyMetrics validatorDutyMetrics = ValidatorDutyMetrics.create(metricsSystem); + dutyScheduler = new BlockDutyScheduler( metricsSystem, @@ -323,7 +333,9 @@ private void createDutySchedulerWithRealDuties() { validatorApiChannel, dependentRoot -> new SlotBasedScheduledDuties<>( - blockDutyFactory, dependentRoot, Duty::performDuty), + blockDutyFactory, + dependentRoot, + validatorDutyMetrics::performDutyWithMetrics), new OwnedValidators( Map.of(VALIDATOR1_KEY, validator1, VALIDATOR2_KEY, validator2)), validatorIndexProvider)), diff --git a/validator/client/src/test/java/tech/pegasys/teku/validator/client/duties/SlotBasedScheduledDutiesTest.java b/validator/client/src/test/java/tech/pegasys/teku/validator/client/duties/SlotBasedScheduledDutiesTest.java index bc6250f76bc..2e8c66d1c65 100644 --- a/validator/client/src/test/java/tech/pegasys/teku/validator/client/duties/SlotBasedScheduledDutiesTest.java +++ b/validator/client/src/test/java/tech/pegasys/teku/validator/client/duties/SlotBasedScheduledDutiesTest.java @@ -49,15 +49,20 @@ class SlotBasedScheduledDutiesTest { @SuppressWarnings("unchecked") private final DutyFactory dutyFactory = mock(DutyFactory.class); + private final ValidatorDutyMetrics validatorDutyMetrics = + ValidatorDutyMetrics.create(metricsSystem); + private final SlotBasedScheduledDuties duties = new SlotBasedScheduledDuties<>( - dutyFactory, Bytes32.fromHexString("0x838382"), Duty::performDuty); + dutyFactory, + Bytes32.fromHexString("0x838382"), + validatorDutyMetrics::performDutyWithMetrics); @Test public void shouldDiscardMissedProductionDuties() { - final ProductionDuty duty0 = mockDuty(ProductionDuty.class); - final ProductionDuty duty1 = mockDuty(ProductionDuty.class); - final ProductionDuty duty2 = mockDuty(ProductionDuty.class); + final ProductionDuty duty0 = mockProductionDuty(); + final ProductionDuty duty1 = mockProductionDuty(); + final ProductionDuty duty2 = mockProductionDuty(); when(dutyFactory.createProductionDuty(ZERO, validator)).thenReturn(duty0); when(dutyFactory.createProductionDuty(ONE, validator)).thenReturn(duty1); when(dutyFactory.createProductionDuty(TWO, validator)).thenReturn(duty2); @@ -80,9 +85,9 @@ public void shouldDiscardMissedProductionDuties() { @Test public void shouldDiscardMissedAggregationDuties() { - final AggregationDuty duty0 = mockDuty(AggregationDuty.class); - final AggregationDuty duty1 = mockDuty(AggregationDuty.class); - final AggregationDuty duty2 = mockDuty(AggregationDuty.class); + final AggregationDuty duty0 = mockAggregationDuty(); + final AggregationDuty duty1 = mockAggregationDuty(); + final AggregationDuty duty2 = mockAggregationDuty(); when(dutyFactory.createAggregationDuty(ZERO, validator)).thenReturn(duty0); when(dutyFactory.createAggregationDuty(ONE, validator)).thenReturn(duty1); when(dutyFactory.createAggregationDuty(TWO, validator)).thenReturn(duty2); @@ -103,11 +108,23 @@ public void shouldDiscardMissedAggregationDuties() { verify(duty2).performDuty(); } - private T mockDuty(final Class dutyType) { - final T mockDuty = mock(dutyType); + private AggregationDuty mockAggregationDuty() { + final AggregationDuty mockDuty = mock(AggregationDuty.class); + when(mockDuty.getType()).thenReturn(DutyType.ATTESTATION_AGGREGATION); when(mockDuty.performDuty()) .thenReturn( SafeFuture.completedFuture(DutyResult.success(dataStructureUtil.randomBytes32()))); + + return mockDuty; + } + + private ProductionDuty mockProductionDuty() { + final ProductionDuty mockDuty = mock(ProductionDuty.class); + when(mockDuty.getType()).thenReturn(DutyType.ATTESTATION_PRODUCTION); + when(mockDuty.performDuty()) + .thenReturn( + SafeFuture.completedFuture(DutyResult.success(dataStructureUtil.randomBytes32()))); + return mockDuty; }