Skip to content

Declarative Shadow Variable throws an UndoMove corruption exception #1641

Description

@kentbill

Describe the bug
I use Depreciate Variable Listeners to implement dependencies between tasks. The scene is as follows:
In the workshop scheduling process planning, there are three tasks Job01, Job02, and Job03, among which Job03 depends on Job01 and Job02, meaning that Job03 can only start after Job01 and Job02 are completed.
see the code here: https://github.com/kentbill/dsv-testing.git

Expected behavior
The start and end times of each task should satisfy their dependency - the start time of Job03 should be after the end time of Job01 and Job02.

Actual behavior
When I start solve, the following exception is thrown.

09:10:37.039 [main ] TRACE Model annotations parsed for solution JobScheduling:
09:10:37.040 [main ] TRACE Entity Job:
09:10:37.040 [main ] TRACE Shadow variable endTime (reflection)
09:10:37.040 [main ] TRACE Shadow variable looped (reflection)
09:10:37.040 [main ] TRACE Shadow variable nextJob (reflection)
09:10:37.040 [main ] TRACE Shadow variable previousJob (reflection)
09:10:37.040 [main ] TRACE Shadow variable productionLine (reflection)
09:10:37.040 [main ] TRACE Shadow variable startTime (reflection)
09:10:37.040 [main ] TRACE Entity ProductionLine:
09:10:37.040 [main ] TRACE Genuine variable jobList (reflection)
09:10:37.139 [main ] DEBUG Using SLF4J as the default logging framework
09:10:37.163 [main ] DEBUG Constraint weights for solution (1):
Constraint (com.easyplan.domain/Unavailable production line) weight set to (-1hard/0medium/0soft).
Constraint (com.easyplan.domain/Job has looped shadow variables.) weight set to (0hard/-1medium/0soft).
09:10:37.218 [main ] INFO Solving started: time spent (70), best score (0hard/0medium/0soft), environment mode (TRACKED_FULL_ASSERT), move thread count (NONE), random (JDK with seed 0).
09:10:37.220 [main ] INFO Problem scale: entity count (3), variable count (3), approximate value count (3), approximate problem scale (60).
09:10:37.235 [main ] TRACE Move index (0), score (0hard/0medium/0soft), move (JobO1 {null -> Line1[0]}).
09:10:37.236 [main ] TRACE Move index (1), score (0hard/0medium/0soft), move (JobO1 {null -> Line2[0]}).
09:10:37.237 [main ] TRACE Move index (2), score (0hard/0medium/0soft), move (JobO1 {null -> Line3[0]}).
09:10:37.238 [main ] DEBUG CH step (0), time spent (90), score (0hard/0medium/0soft), selected move count (3), picked move (JobO1 {null -> Line1[0]}).
09:10:37.240 [main ] TRACE Move index (0), score (0hard/0medium/0soft), move (JobO2 {null -> Line1[0]}).
09:10:37.242 [main ] TRACE Move index (1), score (0hard/0medium/0soft), move (JobO2 {null -> Line2[0]}).
09:10:37.243 [main ] TRACE Move index (2), score (0hard/0medium/0soft), move (JobO2 {null -> Line3[0]}).
09:10:37.244 [main ] TRACE Move index (3), score (0hard/0medium/0soft), move (JobO2 {null -> Line1[1]}).
09:10:37.245 [main ] DEBUG CH step (1), time spent (97), score (0hard/0medium/0soft), selected move count (4), picked move (JobO2 {null -> Line1[0]}).
09:10:37.253 [main ] TRACE Corruption detected. Diagnosing...
Exception in thread "main" ai.timefold.solver.core.impl.solver.exception.UndoScoreCorruptionException: UndoMove corruption (-1medium):
the beforeMoveScore (-1init/0hard/0medium/0soft) is not the undoScore (-1init/0hard/-1medium/0soft),
which is the uncorruptedScore (-1init/0hard/-1medium/0soft) of the workingSolution.

Corruption diagnosis:
Variables that are different between before and undo:

  • Actual value (true) of variable looped on Job entity (JobO3) differs from expected (false)
  • Actual value (true) of variable looped on Job entity (JobO1) differs from expected (false)
  1. Enable EnvironmentMode TRACKED_FULL_ASSERT (if you haven't already)
    to fail-faster in case of a score corruption or variable listener corruption.
    Let the solver run until it reaches the same point in its lifecycle (Phase index (0), step index (2), move index (0).),
    even though it may take a very long time.
    If the solver throws an exception before reaching that point,
    there may be yet another problem that needs to be fixed.

  2. If you use custom moves, check the Move.createUndoMove(...) method of the custom move class (LegacyMoveAdapter).
    The move (JobO3 {null -> Line1[0]}) might have a corrupted undoMove (Undo(JobO3 {null -> Line1[0]})).

  3. If you use custom VariableListeners,
    check them for shadow variables that are used by score constraints
    that could cause the scoreDifference (-1medium).
    at ai.timefold.solver.core.impl.score.director.AbstractScoreDirector.assertExpectedUndoMoveScore(AbstractScoreDirector.java:778)
    at ai.timefold.solver.core.impl.constructionheuristic.decider.ConstructionHeuristicDecider.doMove(ConstructionHeuristicDecider.java:161)
    at ai.timefold.solver.core.impl.constructionheuristic.decider.ConstructionHeuristicDecider.decideNextStep(ConstructionHeuristicDecider.java:112)
    at ai.timefold.solver.core.impl.constructionheuristic.DefaultConstructionHeuristicPhase.solve(DefaultConstructionHeuristicPhase.java:74)
    at ai.timefold.solver.core.impl.solver.AbstractSolver.runPhases(AbstractSolver.java:79)
    at ai.timefold.solver.core.impl.solver.DefaultSolver.solve(DefaultSolver.java:194)
    at com.easyplan.Main.main(Main.java:64)

To Reproduce
Clone and run the follow simple code with main method of Main class
https://github.com/kentbill/dsv-testing.git

Environment

Timefold Solver Version or Git ref: Timefold Solver 1.23.0

Output of java -version: Open JDK 22.0.1

Output of uname -a or ver:

Additional information

There is another bug in Timefold version 1.22.0: #1576

Metadata

Metadata

Labels

No labels
No labels

Type

No fields configured for Bug.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions