diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/AbstractConcatNode.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/AbstractConcatNode.java index 0e1fe99c2b..f6307c5b23 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/AbstractConcatNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/AbstractConcatNode.java @@ -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); } @@ -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); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetRegressionTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetRegressionTest.java index d9d50e9848..2e1909b3a2 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetRegressionTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetRegressionTest.java @@ -396,4 +396,76 @@ public void mapPlanningEntityChanges() { assertMatch(entity2)); } + /** + * @see Timefold Solver Github Issue 828 + */ + @TestTemplate + public void concatSameTupleDeadAndAlive() { + InnerScoreDirector 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)); + } + }