From 7875e07cd087d97d24e8b0fa3cdf251c4578cfe3 Mon Sep 17 00:00:00 2001 From: fred Date: Fri, 23 May 2025 14:33:40 -0300 Subject: [PATCH 1/4] chore: filtering move selector enforcing the phase termination --- .../move/decorator/FilteringMoveSelector.java | 22 +++++++++++++--- .../localsearch/DefaultLocalSearchPhase.java | 1 + .../impl/phase/scope/AbstractPhaseScope.java | 14 +++++++++++ .../decorator/FilteringMoveSelectorTest.java | 25 +++++++++++++++++++ 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelector.java index c0a40b18768..c27a981f7ee 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelector.java @@ -9,6 +9,7 @@ import ai.timefold.solver.core.impl.heuristic.selector.move.AbstractMoveSelector; import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; +import ai.timefold.solver.core.impl.solver.termination.PhaseTermination; public final class FilteringMoveSelector extends AbstractMoveSelector { @@ -24,6 +25,7 @@ public static FilteringMoveSelector of(MoveSelector childMoveSelector; private final SelectionFilter> filter; private final boolean bailOutEnabled; + private AbstractPhaseScope phaseScope; private ScoreDirector scoreDirector = null; @@ -42,13 +44,15 @@ private FilteringMoveSelector(MoveSelector childMoveSelector, @Override public void phaseStarted(AbstractPhaseScope phaseScope) { super.phaseStarted(phaseScope); - scoreDirector = phaseScope.getScoreDirector(); + this.scoreDirector = phaseScope.getScoreDirector(); + this.phaseScope = phaseScope; } @Override public void phaseEnded(AbstractPhaseScope phaseScope) { super.phaseEnded(phaseScope); - scoreDirector = null; + this.scoreDirector = null; + this.phaseScope = null; } @Override @@ -68,17 +72,22 @@ public long getSize() { @Override public Iterator> iterator() { - return new JustInTimeFilteringMoveIterator(childMoveSelector.iterator(), determineBailOutSize()); + return new JustInTimeFilteringMoveIterator(childMoveSelector.iterator(), determineBailOutSize(), phaseScope); } private class JustInTimeFilteringMoveIterator extends UpcomingSelectionIterator> { private final Iterator> childMoveIterator; private final long bailOutSize; + private final AbstractPhaseScope phaseScope; + private final PhaseTermination termination; - public JustInTimeFilteringMoveIterator(Iterator> childMoveIterator, long bailOutSize) { + public JustInTimeFilteringMoveIterator(Iterator> childMoveIterator, long bailOutSize, + AbstractPhaseScope phaseScope) { this.childMoveIterator = childMoveIterator; this.bailOutSize = bailOutSize; + this.phaseScope = phaseScope; + this.termination = phaseScope.getTermination(); } @Override @@ -95,6 +104,11 @@ protected Move createUpcomingSelection() { logger.trace("Bailing out of neverEnding selector ({}) after ({}) attempts to avoid infinite loop.", FilteringMoveSelector.this, bailOutSize); return noUpcomingSelection(); + } else if (termination != null && termination.isPhaseTerminated(phaseScope)) { + logger.trace( + "Bailing out of neverEnding selector ({}) because the termination setting has been triggered.", + FilteringMoveSelector.this); + return noUpcomingSelection(); } attemptsBeforeBailOut--; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java index b5c97a015d2..2e2eecdcb2e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java @@ -125,6 +125,7 @@ public void phaseStarted(LocalSearchPhaseScope phaseScope) { super.phaseStarted(phaseScope); decider.phaseStarted(phaseScope); assertWorkingSolutionInitialized(phaseScope); + phaseScope.setTermination(phaseTermination); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java index c0c3bce1c69..c62671d8b7d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java @@ -9,6 +9,7 @@ import ai.timefold.solver.core.impl.score.director.InnerScore; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.solver.scope.SolverScope; +import ai.timefold.solver.core.impl.solver.termination.PhaseTermination; import ai.timefold.solver.core.preview.api.move.Move; import org.slf4j.Logger; @@ -36,6 +37,11 @@ public abstract class AbstractPhaseScope { protected int bestSolutionStepIndex; + /** + * The solver termination configuration + */ + private PhaseTermination termination; + /** * As defined by #AbstractPhaseScope(SolverScope, int, boolean) * with the phaseSendingBestSolutionEvents parameter set to true. @@ -188,6 +194,14 @@ public > InnerScoreDirector getS return solverScope.getScoreDirector(); } + public void setTermination(PhaseTermination termination) { + this.termination = termination; + } + + public PhaseTermination getTermination() { + return termination; + } + public Solution_ getWorkingSolution() { return solverScope.getWorkingSolution(); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelectorTest.java index 023329da107..e2646de6ea9 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelectorTest.java @@ -2,11 +2,15 @@ import static ai.timefold.solver.core.testutil.PlannerAssert.assertAllCodesOfMoveSelector; import static ai.timefold.solver.core.testutil.PlannerAssert.verifyPhaseLifecycle; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.Iterator; + import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; import ai.timefold.solver.core.impl.heuristic.move.DummyMove; import ai.timefold.solver.core.impl.heuristic.selector.SelectorTestUtils; @@ -15,6 +19,7 @@ import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; import ai.timefold.solver.core.impl.solver.scope.SolverScope; +import ai.timefold.solver.core.impl.solver.termination.BasicPlumbingTermination; import ai.timefold.solver.core.testdomain.TestdataSolution; import org.junit.jupiter.api.Test; @@ -41,6 +46,26 @@ void filterCacheTypeJustInTime() { filter(SelectionCacheType.JUST_IN_TIME, 5); } + @Test + void bailOutByTermination() { + var phaseScope = mock(AbstractPhaseScope.class); + var moveSelector = mock(MoveSelector.class); + var termination = mock(BasicPlumbingTermination.class); + var iterator = mock(Iterator.class); + // We set the maximum value to force it to run many evaluations + when(moveSelector.getSize()).thenReturn(Long.MAX_VALUE / 11); + when(moveSelector.isNeverEnding()).thenReturn(true); + when(moveSelector.iterator()).thenReturn(iterator); + when(iterator.hasNext()).thenReturn(true); + when(phaseScope.getTermination()).thenReturn(termination); + when(termination.isPhaseTerminated(any(AbstractPhaseScope.class))).thenReturn(false, false, true); + var filteredMoveSelector = FilteringMoveSelector.of(moveSelector, (scoreDirector, selection) -> false); + filteredMoveSelector.phaseStarted(phaseScope); + assertThat(filteredMoveSelector.iterator().hasNext()).isFalse(); + // The termination returns true at the third call + verify(iterator, times(2)).next(); + } + public void filter(SelectionCacheType cacheType, int timesCalled) { MoveSelector childMoveSelector = SelectorTestUtils.mockMoveSelector( new DummyMove("a1"), new DummyMove("a2"), new DummyMove("a3"), new DummyMove("a4")); From 03e5942500bc8b3c786cd7d6a87b68dd478d9588 Mon Sep 17 00:00:00 2001 From: fred Date: Fri, 23 May 2025 14:49:00 -0300 Subject: [PATCH 2/4] fix: NPE fix --- .../move/decorator/FilteringMoveSelector.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelector.java index c27a981f7ee..73a5cb2f1b8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelector.java @@ -87,7 +87,7 @@ public JustInTimeFilteringMoveIterator(Iterator> childMoveIterat this.childMoveIterator = childMoveIterator; this.bailOutSize = bailOutSize; this.phaseScope = phaseScope; - this.termination = phaseScope.getTermination(); + this.termination = phaseScope != null ? phaseScope.getTermination() : null; } @Override @@ -141,12 +141,11 @@ private long determineBailOutSize() { } private boolean accept(ScoreDirector scoreDirector, Move move) { - if (filter != null) { - if (!filter.accept(scoreDirector, move)) { - logger.trace(" Move ({}) filtered out by a selection filter ({}).", move, filter); - return false; - } + if (filter != null && !filter.accept(scoreDirector, move)) { + logger.trace(" Move ({}) filtered out by a selection filter ({}).", move, filter); + return false; } + return true; } From 2255f551ff9c033253ba21cbe4416738d5e5a88a Mon Sep 17 00:00:00 2001 From: fred Date: Wed, 28 May 2025 12:04:42 -0300 Subject: [PATCH 3/4] fix: improve strategy --- .../move/decorator/FilteringMoveSelector.java | 63 +++++++++++-------- .../decorator/FilteringMoveSelectorTest.java | 9 ++- 2 files changed, 41 insertions(+), 31 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelector.java index 73a5cb2f1b8..523f71e29c4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelector.java @@ -13,6 +13,8 @@ public final class FilteringMoveSelector extends AbstractMoveSelector { + private static final long BAIL_OUT_MULTIPLIER = 10L; + public static FilteringMoveSelector of(MoveSelector moveSelector, SelectionFilter> filter) { if (moveSelector instanceof FilteringMoveSelector filteringMoveSelector) { @@ -75,6 +77,27 @@ public Iterator> iterator() { return new JustInTimeFilteringMoveIterator(childMoveSelector.iterator(), determineBailOutSize(), phaseScope); } + private long determineBailOutSize() { + if (!bailOutEnabled) { + return -1L; + } + try { + return childMoveSelector.getSize() * BAIL_OUT_MULTIPLIER; + } catch (Exception ex) { + // Some move selectors throw an exception when getSize() is called. + // In this case, we choose to disregard it and pick a large-enough bail-out size anyway. + // The ${bailOutSize+1}th move could in theory show up where previous ${bailOutSize} moves did not, + // but we consider this to be an acceptable risk, + // outweighed by the benefit of the solver never running into an endless loop. + // The exception itself is swallowed, as it doesn't bring any useful information. + long bailOutSize = Short.MAX_VALUE * BAIL_OUT_MULTIPLIER; + logger.trace( + " Never-ending move selector ({}) failed to provide size, choosing a bail-out size of ({}) attempts.", + childMoveSelector, bailOutSize); + return bailOutSize; + } + } + private class JustInTimeFilteringMoveIterator extends UpcomingSelectionIterator> { private final Iterator> childMoveIterator; @@ -94,6 +117,10 @@ public JustInTimeFilteringMoveIterator(Iterator> childMoveIterat protected Move createUpcomingSelection() { Move next; long attemptsBeforeBailOut = bailOutSize; + // To reduce the impact of checking for termination on each move, + // we only check for termination + // after filtering out a total number of moves equal to size(childMoveSelector). + long attemptsBeforeCheckTermination = bailOutSize / BAIL_OUT_MULTIPLIER; do { if (!childMoveIterator.hasNext()) { return noUpcomingSelection(); @@ -104,13 +131,18 @@ protected Move createUpcomingSelection() { logger.trace("Bailing out of neverEnding selector ({}) after ({}) attempts to avoid infinite loop.", FilteringMoveSelector.this, bailOutSize); return noUpcomingSelection(); - } else if (termination != null && termination.isPhaseTerminated(phaseScope)) { - logger.trace( - "Bailing out of neverEnding selector ({}) because the termination setting has been triggered.", - FilteringMoveSelector.this); - return noUpcomingSelection(); + } else if (termination != null && attemptsBeforeCheckTermination <= 0L) { + // Reset the counter + attemptsBeforeCheckTermination = bailOutSize / BAIL_OUT_MULTIPLIER; + if (termination.isPhaseTerminated(phaseScope)) { + logger.trace( + "Bailing out of neverEnding selector ({}) because the termination setting has been triggered.", + FilteringMoveSelector.this); + return noUpcomingSelection(); + } } attemptsBeforeBailOut--; + attemptsBeforeCheckTermination--; } next = childMoveIterator.next(); } while (!accept(scoreDirector, next)); @@ -119,27 +151,6 @@ protected Move createUpcomingSelection() { } - private long determineBailOutSize() { - if (!bailOutEnabled) { - return -1L; - } - try { - return childMoveSelector.getSize() * 10L; - } catch (Exception ex) { - // Some move selectors throw an exception when getSize() is called. - // In this case, we choose to disregard it and pick a large-enough bail-out size anyway. - // The ${bailOutSize+1}th move could in theory show up where previous ${bailOutSize} moves did not, - // but we consider this to be an acceptable risk, - // outweighed by the benefit of the solver never running into an endless loop. - // The exception itself is swallowed, as it doesn't bring any useful information. - long bailOutSize = Short.MAX_VALUE * 10L; - logger.trace( - " Never-ending move selector ({}) failed to provide size, choosing a bail-out size of ({}) attempts.", - childMoveSelector, bailOutSize); - return bailOutSize; - } - } - private boolean accept(ScoreDirector scoreDirector, Move move) { if (filter != null && !filter.accept(scoreDirector, move)) { logger.trace(" Move ({}) filtered out by a selection filter ({}).", move, filter); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelectorTest.java index e2646de6ea9..edfcfe82ebe 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelectorTest.java @@ -52,18 +52,17 @@ void bailOutByTermination() { var moveSelector = mock(MoveSelector.class); var termination = mock(BasicPlumbingTermination.class); var iterator = mock(Iterator.class); - // We set the maximum value to force it to run many evaluations - when(moveSelector.getSize()).thenReturn(Long.MAX_VALUE / 11); + when(moveSelector.getSize()).thenReturn(100L); when(moveSelector.isNeverEnding()).thenReturn(true); when(moveSelector.iterator()).thenReturn(iterator); when(iterator.hasNext()).thenReturn(true); when(phaseScope.getTermination()).thenReturn(termination); - when(termination.isPhaseTerminated(any(AbstractPhaseScope.class))).thenReturn(false, false, true); + when(termination.isPhaseTerminated(any(AbstractPhaseScope.class))).thenReturn(false, true); var filteredMoveSelector = FilteringMoveSelector.of(moveSelector, (scoreDirector, selection) -> false); filteredMoveSelector.phaseStarted(phaseScope); assertThat(filteredMoveSelector.iterator().hasNext()).isFalse(); - // The termination returns true at the third call - verify(iterator, times(2)).next(); + // The termination returns true at the second call, and only (100 * 10) * 30% calls are executed per check + verify(iterator, times(200)).next(); } public void filter(SelectionCacheType cacheType, int timesCalled) { From a3619ca6cb262af993bf86e0db330e613aa420c5 Mon Sep 17 00:00:00 2001 From: fred Date: Fri, 30 May 2025 11:30:57 -0300 Subject: [PATCH 4/4] chore: address comments --- .../selector/move/decorator/FilteringMoveSelector.java | 8 ++++---- .../core/impl/localsearch/DefaultLocalSearchPhase.java | 1 - .../ai/timefold/solver/core/impl/phase/AbstractPhase.java | 1 + .../solver/core/impl/phase/scope/AbstractPhaseScope.java | 2 +- .../move/decorator/FilteringMoveSelectorTest.java | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelector.java index 523f71e29c4..08b18e07a39 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelector.java @@ -100,6 +100,7 @@ private long determineBailOutSize() { private class JustInTimeFilteringMoveIterator extends UpcomingSelectionIterator> { + private final long TERMINATION_BAIL_OUT_SIZE = 1000L; private final Iterator> childMoveIterator; private final long bailOutSize; private final AbstractPhaseScope phaseScope; @@ -118,9 +119,8 @@ protected Move createUpcomingSelection() { Move next; long attemptsBeforeBailOut = bailOutSize; // To reduce the impact of checking for termination on each move, - // we only check for termination - // after filtering out a total number of moves equal to size(childMoveSelector). - long attemptsBeforeCheckTermination = bailOutSize / BAIL_OUT_MULTIPLIER; + // we only check for termination after filtering out 1000 moves. + long attemptsBeforeCheckTermination = TERMINATION_BAIL_OUT_SIZE; do { if (!childMoveIterator.hasNext()) { return noUpcomingSelection(); @@ -133,7 +133,7 @@ protected Move createUpcomingSelection() { return noUpcomingSelection(); } else if (termination != null && attemptsBeforeCheckTermination <= 0L) { // Reset the counter - attemptsBeforeCheckTermination = bailOutSize / BAIL_OUT_MULTIPLIER; + attemptsBeforeCheckTermination = TERMINATION_BAIL_OUT_SIZE; if (termination.isPhaseTerminated(phaseScope)) { logger.trace( "Bailing out of neverEnding selector ({}) because the termination setting has been triggered.", diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java index 2e2eecdcb2e..b5c97a015d2 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java @@ -125,7 +125,6 @@ public void phaseStarted(LocalSearchPhaseScope phaseScope) { super.phaseStarted(phaseScope); decider.phaseStarted(phaseScope); assertWorkingSolutionInitialized(phaseScope); - phaseScope.setTermination(phaseTermination); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java index 86b5e897aa3..a2f4915717c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java @@ -95,6 +95,7 @@ public void phaseStarted(AbstractPhaseScope phaseScope) { solver.phaseStarted(phaseScope); } phaseTermination.phaseStarted(phaseScope); + phaseScope.setTermination(phaseTermination); phaseLifecycleSupport.firePhaseStarted(phaseScope); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java index c62671d8b7d..8c85c8359ac 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java @@ -38,7 +38,7 @@ public abstract class AbstractPhaseScope { protected int bestSolutionStepIndex; /** - * The solver termination configuration + * The phase termination configuration */ private PhaseTermination termination; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelectorTest.java index edfcfe82ebe..5d1cda3b1de 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelectorTest.java @@ -52,7 +52,7 @@ void bailOutByTermination() { var moveSelector = mock(MoveSelector.class); var termination = mock(BasicPlumbingTermination.class); var iterator = mock(Iterator.class); - when(moveSelector.getSize()).thenReturn(100L); + when(moveSelector.getSize()).thenReturn(1000L); when(moveSelector.isNeverEnding()).thenReturn(true); when(moveSelector.iterator()).thenReturn(iterator); when(iterator.hasNext()).thenReturn(true); @@ -61,8 +61,8 @@ void bailOutByTermination() { var filteredMoveSelector = FilteringMoveSelector.of(moveSelector, (scoreDirector, selection) -> false); filteredMoveSelector.phaseStarted(phaseScope); assertThat(filteredMoveSelector.iterator().hasNext()).isFalse(); - // The termination returns true at the second call, and only (100 * 10) * 30% calls are executed per check - verify(iterator, times(200)).next(); + // The termination returns true at the second call, and 2000 calls are executed in total + verify(iterator, times(2000)).next(); } public void filter(SelectionCacheType cacheType, int timesCalled) {