Skip to content

Commit

Permalink
fix: Remove too eager fail-fast in concat node's retract (#831)
Browse files Browse the repository at this point in the history
A dead tuple might be retracted in concat if a "complex No-op"
`calculateScore` occurs. A "complex No-op" refers to the following
situlation:

  - Move A
  - Calculate Score
  - Undo Move A
  - Move A again (but potentially expressed a different way)
  - Calculate Score (here is where a dead tuple will be retracted)

The fail-fast seems to be too eager.
  • Loading branch information
Christopher-Chianelli authored May 7, 2024
1 parent 5bfa796 commit c373c2b
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,10 @@ public final void retractLeft(LeftTuple_ tuple) {
}
TupleState state = outTuple.state;
if (!state.isActive()) {
throw new IllegalStateException("Impossible state: The tuple (" + outTuple.state + ") in node (" + this
+ ") is in an unexpected state (" + outTuple.state + ").");
// No fail fast for inactive tuples, since the same tuple can be
// passed twice if they are from the same source;
// @see BavetRegressionTest#concatSameTupleDeadAndAlive for an example.
return;
}
propagationQueue.retract(outTuple, state == TupleState.CREATING ? TupleState.ABORTING : TupleState.DYING);
}
Expand Down Expand Up @@ -128,8 +130,10 @@ public final void retractRight(RightTuple_ tuple) {
}
TupleState state = outTuple.state;
if (!state.isActive()) {
throw new IllegalStateException("Impossible state: The tuple (" + outTuple.state + ") in node (" + this
+ ") is in an unexpected state (" + outTuple.state + ").");
// No fail fast for inactive tuples, since the same tuple can be
// passed twice if they are from the same source;
// @see BavetRegressionTest#concatSameTupleDeadAndAlive for an example.
return;
}
propagationQueue.retract(outTuple, state == TupleState.CREATING ? TupleState.ABORTING : TupleState.DYING);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,4 +396,76 @@ public void mapPlanningEntityChanges() {
assertMatch(entity2));
}

/**
* @see <a href="https://github.com/TimefoldAI/timefold-solver/issues/828">Timefold Solver Github Issue 828</a>
*/
@TestTemplate
public void concatSameTupleDeadAndAlive() {
InnerScoreDirector<TestdataSolution, SimpleScore> scoreDirector =
buildScoreDirector(TestdataSolution.buildSolutionDescriptor(),
factory -> new Constraint[] {
factory.forEach(TestdataEntity.class)
.filter(e -> e.getValue().getCode().equals("A"))
.concat(factory.forEach(TestdataEntity.class))
.penalize(SimpleScore.ONE)
.asConstraint(TEST_CONSTRAINT_NAME)
});

TestdataSolution solution = TestdataSolution.generateSolution(2, 2);
TestdataEntity entity1 = solution.getEntityList().get(0);
TestdataEntity entity2 = solution.getEntityList().get(1);
TestdataValue valueA = solution.getValueList().get(0);
valueA.setCode("A");
TestdataValue valueB = solution.getValueList().get(1);
valueB.setCode("B");

scoreDirector.setWorkingSolution(solution);
assertScore(scoreDirector,
assertMatch(entity1),
assertMatch(entity1),
assertMatch(entity2));

scoreDirector.beforeVariableChanged(entity1, "value");
entity1.setValue(valueB);
scoreDirector.afterVariableChanged(entity1, "value");
scoreDirector.beforeVariableChanged(entity2, "value");
entity2.setValue(valueA);
scoreDirector.afterVariableChanged(entity2, "value");
assertScore(scoreDirector,
assertMatch(entity1),
assertMatch(entity2),
assertMatch(entity2));

scoreDirector.beforeVariableChanged(entity1, "value");
entity1.setValue(valueA);
scoreDirector.afterVariableChanged(entity1, "value");
scoreDirector.beforeVariableChanged(entity2, "value");
entity2.setValue(valueB);
scoreDirector.afterVariableChanged(entity2, "value");
// Do not recalculate score, since this is undo

scoreDirector.beforeVariableChanged(entity2, "value");
entity2.setValue(valueA);
scoreDirector.afterVariableChanged(entity2, "value");
scoreDirector.beforeVariableChanged(entity1, "value");
entity1.setValue(valueB);
scoreDirector.afterVariableChanged(entity1, "value");
assertScore(scoreDirector,
assertMatch(entity1),
assertMatch(entity2),
assertMatch(entity2));

scoreDirector.beforeVariableChanged(entity2, "value");
entity2.setValue(valueB);
scoreDirector.afterVariableChanged(entity2, "value");
scoreDirector.beforeVariableChanged(entity1, "value");
entity1.setValue(valueA);
scoreDirector.afterVariableChanged(entity1, "value");

assertScore(scoreDirector,
assertMatch(entity1),
assertMatch(entity1),
assertMatch(entity2));
}

}

0 comments on commit c373c2b

Please sign in to comment.