Skip to content

Commit 2eb59ef

Browse files
zepfredtriceo
authored andcommitted
fix: avoid using stale references in entity value ranges filtering nodes (#1822)
(cherry picked from commit fa704e9)
1 parent dc97d27 commit 2eb59ef

File tree

9 files changed

+51
-68
lines changed

9 files changed

+51
-68
lines changed

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/iterator/AbstractRandomSwapIterator.java

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,14 @@
55
import ai.timefold.solver.core.impl.heuristic.move.Move;
66

77
public abstract class AbstractRandomSwapIterator<Solution_, Move_ extends Move<Solution_>, SubSelection_>
8-
implements Iterator<Move_> {
8+
extends UpcomingSelectionIterator<Move_> {
99

1010
protected final Iterable<SubSelection_> leftSubSelector;
1111
protected final Iterable<SubSelection_> rightSubSelector;
1212

1313
protected Iterator<SubSelection_> leftSubSelectionIterator;
1414
protected Iterator<SubSelection_> rightSubSelectionIterator;
1515

16-
private SubSelection_ leftSubSelection;
17-
1816
public AbstractRandomSwapIterator(Iterable<SubSelection_> leftSubSelector,
1917
Iterable<SubSelection_> rightSubSelector) {
2018
this.leftSubSelector = leftSubSelector;
@@ -25,34 +23,24 @@ public AbstractRandomSwapIterator(Iterable<SubSelection_> leftSubSelector,
2523
}
2624

2725
@Override
28-
public boolean hasNext() {
29-
leftSubSelection = null;
26+
protected Move_ createUpcomingSelection() {
27+
// Ideally, this code should have read:
28+
// SubS leftSubSelection = leftSubSelectionIterator.next();
29+
// SubS rightSubSelection = rightSubSelectionIterator.next();
30+
// But empty selectors and ending selectors (such as non-random or shuffled) make it more complex
3031
if (!leftSubSelectionIterator.hasNext()) {
3132
leftSubSelectionIterator = leftSubSelector.iterator();
3233
if (!leftSubSelectionIterator.hasNext()) {
33-
return false;
34+
return noUpcomingSelection();
3435
}
3536
}
36-
// The right selection may depend on selecting a left element first. E.g., FilteringEntityByEntitySelector
37-
leftSubSelection = leftSubSelectionIterator.next();
37+
SubSelection_ leftSubSelection = leftSubSelectionIterator.next();
3838
if (!rightSubSelectionIterator.hasNext()) {
3939
rightSubSelectionIterator = rightSubSelector.iterator();
4040
if (!rightSubSelectionIterator.hasNext()) {
41-
return false;
41+
return noUpcomingSelection();
4242
}
4343
}
44-
return true;
45-
}
46-
47-
@Override
48-
public Move_ next() {
49-
if (leftSubSelection == null) {
50-
throw new IllegalStateException("Impossible state: no left element has been selected.");
51-
}
52-
// Ideally, this code should have read:
53-
// SubS leftSubSelection = leftSubSelectionIterator.next();
54-
// SubS rightSubSelection = rightSubSelectionIterator.next();
55-
// But empty selectors and ending selectors (such as non-random or shuffled) make it more complex
5644
SubSelection_ rightSubSelection = rightSubSelectionIterator.next();
5745
return newSwapSelection(leftSubSelection, rightSubSelection);
5846
}

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityByEntitySelector.java

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,6 @@ public final class FilteringEntityByEntitySelector<Solution_> extends AbstractDe
7171
private ValueRangeManager<Solution_> valueRangeManager;
7272
private List<Object> allEntities;
7373

74-
private static long countIterations = 0;
75-
private static long countAttempts = 0;
76-
private static long countSuccess = 0;
77-
private static long countFails = 0;
78-
7974
public FilteringEntityByEntitySelector(EntitySelector<Solution_> childEntitySelector,
8075
EntitySelector<Solution_> replayingEntitySelector, boolean randomSelection) {
8176
this.childEntitySelector = childEntitySelector;
@@ -123,11 +118,6 @@ public void phaseEnded(AbstractPhaseScope<Solution_> phaseScope) {
123118
this.replayedEntity = null;
124119
this.valueRangeManager = null;
125120
this.basicVariableDescriptors = null;
126-
logger.warn(
127-
"Iterations ({}), total attempts ({}), average attempts ({}), succeed count ({}), succeed rate ({}), fails count({}), fails rate ({})",
128-
countIterations, countAttempts, countAttempts / (double) countIterations, countSuccess,
129-
countSuccess / (double) countIterations, countFails,
130-
countFails / (double) countIterations);
131121
}
132122

133123
// ************************************************************************
@@ -168,7 +158,7 @@ private Object selectReplayedEntity() {
168158

169159
@Override
170160
public Iterator<Object> endingIterator() {
171-
return new OriginalFilteringValueRangeIterator<>(this::selectReplayedEntity, childEntitySelector.iterator(),
161+
return new OriginalFilteringValueRangeIterator<>(this::selectReplayedEntity, childEntitySelector.endingIterator(),
172162
basicVariableDescriptors, valueRangeManager);
173163
}
174164

@@ -179,12 +169,12 @@ public Iterator<Object> iterator() {
179169
throw new IllegalArgumentException(
180170
"Impossible state: childEntitySelector must provide a never-ending approach.");
181171
}
182-
// Experiments have shown that a large number of attempts do not scale well,
183-
// and 10 seems like an appropriate limit.
172+
// Experiments have shown that a large number of attempts do not scale well.
184173
// So we won't spend excessive time trying to generate a single move for the current selection.
185174
// If we are unable to generate a move, the move iterator can still be used in later iterations.
175+
var maxBailoutSize = Math.min(10, allEntities.size());
186176
return new RandomFilteringValueRangeIterator<>(this::selectReplayedEntity, childEntitySelector.iterator(),
187-
basicVariableDescriptors, valueRangeManager, 10);
177+
basicVariableDescriptors, valueRangeManager, maxBailoutSize);
188178
} else {
189179
return new OriginalFilteringValueRangeIterator<>(this::selectReplayedEntity, childEntitySelector.iterator(),
190180
basicVariableDescriptors, valueRangeManager);
@@ -350,13 +340,13 @@ void load(Object replayedEntity) {
350340

351341
@Override
352342
public boolean hasNext() {
353-
initialize();
343+
checkReplayedEntity();
354344
return entityIterator.hasNext();
355345
}
356346

357347
@Override
358348
public Object next() {
359-
if (!hasNext()) {
349+
if (!entityIterator.hasNext()) {
360350
throw new NoSuchElementException();
361351
}
362352
var bailoutSize = maxBailoutSize;

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityByValueSelector.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -230,10 +230,7 @@ private RandomFilteringValueRangeIterator(Supplier<Object> upcomingValueSupplier
230230
this.workingRandom = workingRandom;
231231
}
232232

233-
private void initialize() {
234-
if (entityList != null) {
235-
return;
236-
}
233+
private void checkReplayedValue() {
237234
var oldUpcomingValue = currentUpcomingValue;
238235
currentUpcomingValue = upcomingValueSupplier.get();
239236
if (currentUpcomingValue == null) {
@@ -249,7 +246,7 @@ private void loadValues() {
249246

250247
@Override
251248
public boolean hasNext() {
252-
initialize();
249+
checkReplayedValue();
253250
return entityList != null && !entityList.isEmpty();
254251
}
255252

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelector.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ public Object next() {
357357
private class RandomFilteringValueRangeIterator extends AbstractFilteringValueRangeIterator {
358358

359359
private final Random workingRandom;
360-
private int maxBailoutSize = 1;
360+
private int maxBailoutSize;
361361
private Object replayedValue;
362362
private List<Object> reachableValueList = null;
363363

@@ -371,8 +371,8 @@ private RandomFilteringValueRangeIterator(Supplier<Object> upcomingValueSupplier
371371
@Override
372372
void processUpcomingValue(Object upcomingValue, List<Object> upcomingList) {
373373
this.replayedValue = upcomingValue;
374-
this.reachableValueList = upcomingList;
375-
this.maxBailoutSize = upcomingList.size();
374+
this.reachableValueList = Objects.requireNonNull(upcomingList);
375+
this.maxBailoutSize = reachableValueList.size();
376376
}
377377

378378
@Override
@@ -386,12 +386,11 @@ public Object next() {
386386
if (hasNoData()) {
387387
throw new NoSuchElementException();
388388
}
389-
Object next;
390389
var bailoutSize = maxBailoutSize;
391390
do {
392391
bailoutSize--;
393-
var index = workingRandom.nextInt(Objects.requireNonNull(reachableValueList).size());
394-
next = reachableValueList.get(index);
392+
var index = workingRandom.nextInt(reachableValueList.size());
393+
var next = reachableValueList.get(index);
395394
if (isReachable(next)) {
396395
return next;
397396
}

core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -142,16 +142,28 @@ public void stepEnded(LocalSearchStepScope<Solution_> stepScope) {
142142
collectMetrics(stepScope);
143143
var phaseScope = stepScope.getPhaseScope();
144144
if (logger.isDebugEnabled()) {
145-
logger.debug("{} LS step ({}), time spent ({}), score ({}), {} best score ({})," +
146-
" accepted/selected move count ({}/{}), picked move ({}).",
147-
logIndentation,
148-
stepScope.getStepIndex(),
149-
phaseScope.calculateSolverTimeMillisSpentUpToNow(),
150-
stepScope.getScore().raw(),
151-
(stepScope.getBestScoreImproved() ? "new" : " "), phaseScope.getBestScore().raw(),
152-
stepScope.getAcceptedMoveCount(),
153-
stepScope.getSelectedMoveCount(),
154-
stepScope.getStepString());
145+
if (stepScope.getAcceptedMoveCount() == 0 && phaseTermination.isPhaseTerminated(phaseScope)) {
146+
// Terminated early
147+
logger.debug("{} LS step ({}), time spent ({}), score ({}), {} best score ({})," +
148+
" terminated prematurely after selecting {} moves.",
149+
logIndentation,
150+
stepScope.getStepIndex(),
151+
phaseScope.calculateSolverTimeMillisSpentUpToNow(),
152+
stepScope.getScore().raw(),
153+
(stepScope.getBestScoreImproved() ? "new" : " "), phaseScope.getBestScore().raw(),
154+
stepScope.getSelectedMoveCount());
155+
} else {
156+
logger.debug("{} LS step ({}), time spent ({}), score ({}), {} best score ({})," +
157+
" accepted/selected move count ({}/{}), picked move ({}).",
158+
logIndentation,
159+
stepScope.getStepIndex(),
160+
phaseScope.calculateSolverTimeMillisSpentUpToNow(),
161+
stepScope.getScore().raw(),
162+
(stepScope.getBestScoreImproved() ? "new" : " "), phaseScope.getBestScore().raw(),
163+
stepScope.getAcceptedMoveCount(),
164+
stepScope.getSelectedMoveCount(),
165+
stepScope.getStepString());
166+
}
155167
}
156168
}
157169

core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelectorTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,6 @@ void randomWithEntityValueRange() {
213213

214214
// select C for V5 and first unpinned pos C[1]
215215
// 3 - pick random value in ElementPositionRandomIterator and force generating a random position
216-
// 0 - pick entity C in RandomFilteringValueRangeIterator
217216
// 1 - pick random position, v3 and v4 are reachable
218217
// 0 - remaining call
219218
valueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v5, v5, v5, v5, v5); // simulate five positions
@@ -223,7 +222,7 @@ void randomWithEntityValueRange() {
223222
doReturn(List.of(v5).iterator(), Collections.emptyIterator()).when(valueSelector).iterator();
224223
checkEntityValueRange(new FilteringEntityByValueSelector<>(mockEntitySelector(a, b, c), valueSelector, true),
225224
new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, true, false),
226-
scoreDirector, new TestRandom(3, 0, 1, 0), "C[1]");
225+
scoreDirector, new TestRandom(3, 1, 0), "C[1]");
227226
}
228227

229228
private void checkEntityValueRange(FilteringEntityByValueSelector<TestdataListEntityProvidingSolution> entitySelector,

core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/SwapMoveSelectorTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,9 +337,8 @@ void singleVarRandomSelectionWithEntityValueRange() {
337337
e1.setValue(v1);
338338
e2.setValue(v3);
339339
e3.setValue(v4);
340-
// select left A, select right B
341340
// select left A, select right C
342-
random.reset(0, 1, 2, 1);
341+
random.reset(0, 2, 1, 1, 1, 1);
343342
scoreDirector.setWorkingSolution(solution);
344343
assertCodesOfNeverEndingIterableSelector(moveSelector, expectedSize, "A<->C");
345344
}

core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveSelectorTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -411,8 +411,8 @@ void randomWithEntityValueRange() {
411411

412412
assertCodesOfNeverEndingMoveSelector(moveSelector,
413413
"1 {A[1]->A[1]}",
414-
"3 {B[0]->A[0]}",
415-
"3 {B[0]->A[0]}",
414+
"3 {B[0]->B[0]}",
415+
"3 {B[0]->B[0]}",
416416
"3 {B[0]->B[1]}",
417417
"3 {B[0]->B[1]}");
418418
}
@@ -422,7 +422,7 @@ void randomWithEntityValueRangeAndFiltering() {
422422
var v1 = new TestdataListEntityProvidingValue("1");
423423
var v2 = new TestdataListEntityProvidingValue("2");
424424
var v3 = new TestdataListEntityProvidingValue("3");
425-
var a = new TestdataListEntityProvidingEntity("A", List.of(v1, v2), List.of(v2, v1));
425+
var a = new TestdataListEntityProvidingEntity("A", List.of(v1, v2, v3), List.of(v2, v1));
426426
var b = new TestdataListEntityProvidingEntity("B", List.of(v2, v3), List.of(v3));
427427
var solution = new TestdataListEntityProvidingSolution();
428428
solution.setEntityList(List.of(a, b));
@@ -446,7 +446,7 @@ void randomWithEntityValueRangeAndFiltering() {
446446
assertCodesOfNeverEndingMoveSelector(moveSelector,
447447
"1 {A[1]->A[1]}",
448448
"3 {B[0]->A[0]}",
449-
"3 {B[0]->A[0]}",
449+
"3 {B[0]->B[0]}",
450450
"3 {B[0]->A[2]}",
451451
"3 {B[0]->A[1]}");
452452
}

core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListSwapMoveSelectorTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,8 +337,7 @@ void randomWithEntityValueRange() {
337337
"2 {A[0]} <-> 3 {B[0]}",
338338
"1 {A[1]} <-> 2 {A[0]}",
339339
"3 {B[0]} <-> 2 {A[0]}",
340-
"1 {A[1]} <-> 2 {A[0]}",
341-
"3 {B[0]} <-> 2 {A[0]}");
340+
"1 {A[1]} <-> 2 {A[0]}");
342341
}
343342

344343
@Test

0 commit comments

Comments
 (0)