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));
+ }
+
}