Kent Zhang
Kent Zhang

Reputation: 89

A Listener corruption with PlanningListVariable

In my project, Assigning some tasks to the production line and calculated their start and end times. Here are the main logics.

  1. Multiple tasks belong to the same manufacturing order, and there is a sequential dependency relationship between these tasks. For instance, there are 2 tasks in a manufacturing order: Job1 and Job2. Job1 is the predecessor job of Job2, so the Job2 is the successor job of Job1. That is, Job2 must wait for Job1 to complete before starting, which means that the start time of Job2 must be later than the end time of Job1.
  2. The jobs in a production line must be sequenced. For instance, there are 2 jobs in a production line - Job3(its index is 0, and it does not belong to the same manufacturing order as job2) and Job2(its index is 1). So Job2's start time must be later than the end time of Job3.

So, The start time of job 2 must be later than the end time of job 1 and job 3.

I use the PlanningListVariable to implement this model. I created a planning list variable - jobList on class ProductionLine and specified the method that needs to update the start time in the Job class with the @CascadingupdateShadowVariable annotation.

The code for the Job class is as follows:

@PlanningEntity
public class Job {

    @PlanningId
    private Long id;

    private String code;

    // which production lines can this task be assigned to
    private List<ProductionLine> availableProductionLineList;

    //  a duration map that contains job duration on each resource
    private Map<ProductionLine, Duration> durationMap;

    // the predecessor job list
    private List<Job> predecessorJobList;

    // the successor job list
    private List<Job> successorJobList;

    @InverseRelationShadowVariable(sourceVariableName = "jobList")
    private ProductionLine productionLine;

    @PreviousElementShadowVariable(sourceVariableName = "jobList")
    private Job previousJob;

    @NextElementShadowVariable(sourceVariableName = "jobList")
    private Job nextJob;

    // a method that updates the job start time
    @CascadingUpdateShadowVariable(targetMethodName = "updateStartTimes")
    private LocalDateTime startTime;

    public Job() {
    }


    public void updateStartTimes() {
        if (this.productionLine == null) {
            if (this.getStartTime() != null) {
                this.setStartTime(null);
            }
        } else {
            this.setStartTime(this.calculateStartTime());
        }
    }

    private LocalDateTime calculateStartTime() {
        // the previous job end time
        LocalDateTime previousEndTime = this.previousJob == null ? this.productionLine.getStartTime() : this.previousJob.getEndTime();

        // the predecessor job end time
        LocalDateTime predecessorEndTime = null;
        if(!this.predecessorJobList.isEmpty()) {
            predecessorEndTime = Collections.max(this.predecessorJobList, Comparator.comparing(Job::getEndTime)).getEndTime();
        }

        // the later between predecessor end time and previous end time
        return this.getMaxDateTime(predecessorEndTime, previousEndTime);
    }


    private LocalDateTime getMaxDateTime(LocalDateTime dateTime1, LocalDateTime dateTime2) {
        if (dateTime1 == null && dateTime2 == null) {
            return null;
        } else if (dateTime1 == null) {
            return dateTime2;
        } else if (dateTime2 == null) {
            return dateTime1;
        } else {
            return dateTime1.isAfter(dateTime2) ? dateTime1 : dateTime2;
        }
    }

    public Duration getDuration() {
        return this.durationMap.getOrDefault(this.productionLine, null);
    }

    public LocalDateTime getStartTime() {
        return startTime;
    }

    public void setStartTime(LocalDateTime startTime) {
        this.startTime = startTime;
    }

    public LocalDateTime getEndTime() {
        if(this.startTime == null) {
            return null;
        }

        return this.startTime.plus(this.getDuration());
    }

    // others getter, setter .....

}

But I got a Variable Listener disruption exception.

16:18:12.147 [main        ] TRACE     Model annotations parsed for solution JobScheduling:
16:18:12.148 [main        ] TRACE         Entity Job:
16:18:12.149 [main        ] TRACE             Shadow variable nextJob (reflection)
16:18:12.149 [main        ] TRACE             Shadow variable previousJob (reflection)
16:18:12.149 [main        ] TRACE             Shadow variable productionLine (reflection)
16:18:12.149 [main        ] TRACE             Shadow variable startTime (reflection)
16:18:12.149 [main        ] TRACE         Entity ProductionLine:
16:18:12.149 [main        ] TRACE             Genuine variable jobList (reflection)
16:18:12.259 [main        ] DEBUG Constraint weights for solution (1):
16:18:12.276 [main        ] DEBUG Constraint weights for solution (1):
16:18:12.296 [main        ] INFO  Solving started: time spent (43), best score (-4init/0hard/0medium/0soft), environment mode (TRACKED_FULL_ASSERT), move thread count (NONE), random (JDK with seed 0).
16:18:12.301 [main        ] INFO  Problem scale: entity count (3), variable count (3), approximate value count (4), approximate problem scale (360).
16:18:12.313 [main        ] DEBUG Constraint weights for solution (1):
16:18:12.315 [main        ] TRACE         Move index (0), score (-3init/0hard/0medium/0soft), move (Order1,Job1[] {null -> Line1[0]}).
16:18:12.318 [main        ] DEBUG Constraint weights for solution (1):
16:18:12.318 [main        ] TRACE         Move index (1), score (-3init/-1hard/0medium/0soft), move (Order1,Job1[] {null -> Line2[0]}).
16:18:12.319 [main        ] DEBUG Constraint weights for solution (1):
16:18:12.319 [main        ] TRACE         Move index (2), score (-3init/-1hard/0medium/0soft), move (Order1,Job1[] {null -> Line3[0]}).
16:18:12.320 [main        ] DEBUG Constraint weights for solution (1):
16:18:12.320 [main        ] DEBUG     CH step (0), time spent (67), score (-3init/0hard/0medium/0soft), selected move count (3), picked move (Order1,Job1[] {null -> Line1[0]}).
16:18:12.324 [main        ] DEBUG Constraint weights for solution (1):
16:18:12.324 [main        ] TRACE         Move index (0), score (-2init/-1hard/0medium/0soft), move (Order1,Job2[] {null -> Line1[0]}).
16:18:12.326 [main        ] DEBUG Constraint weights for solution (1):
16:18:12.326 [main        ] TRACE         Move index (1), score (-2init/0hard/0medium/0soft), move (Order1,Job2[] {null -> Line2[0]}).
16:18:12.326 [main        ] DEBUG Constraint weights for solution (1):
16:18:12.327 [main        ] TRACE         Move index (2), score (-2init/-1hard/0medium/0soft), move (Order1,Job2[] {null -> Line3[0]}).
16:18:12.327 [main        ] DEBUG Constraint weights for solution (1):
16:18:12.327 [main        ] TRACE         Move index (3), score (-2init/-1hard/0medium/0soft), move (Order1,Job2[] {null -> Line1[1]}).
16:18:12.328 [main        ] DEBUG Constraint weights for solution (1):
16:18:12.328 [main        ] DEBUG     CH step (1), time spent (75), score (-2init/0hard/0medium/0soft), selected move count (4), picked move (Order1,Job2[] {null -> Line2[0]}).
16:18:12.328 [main        ] DEBUG Constraint weights for solution (1):
16:18:12.329 [main        ] TRACE         Move index (0), score (-1init/0hard/0medium/0soft), move (Order2,Job1[] {null -> Line1[0]}).
16:18:12.329 [main        ] DEBUG Constraint weights for solution (1):
16:18:12.329 [main        ] TRACE         Move index (1), score (-1init/-1hard/0medium/0soft), move (Order2,Job1[] {null -> Line2[0]}).
16:18:12.330 [main        ] DEBUG Constraint weights for solution (1):
16:18:12.330 [main        ] TRACE         Move index (2), score (-1init/-1hard/0medium/0soft), move (Order2,Job1[] {null -> Line3[0]}).
16:18:12.330 [main        ] DEBUG Constraint weights for solution (1):
16:18:12.330 [main        ] TRACE         Move index (3), score (-1init/0hard/0medium/0soft), move (Order2,Job1[] {null -> Line1[1]}).
16:18:12.331 [main        ] DEBUG Constraint weights for solution (1):
16:18:12.331 [main        ] TRACE         Move index (4), score (-1init/-1hard/0medium/0soft), move (Order2,Job1[] {null -> Line2[1]}).
16:18:12.331 [main        ] DEBUG Constraint weights for solution (1):
Exception in thread "main" java.lang.IllegalStateException: VariableListener corruption after completedAction (Order2,Job1[Line1] {null -> Line1[0]}):
The entity (Order1,Job2[Line2])'s shadow variable (Job.startTime)'s corrupted value (2024-10-01T05:00) changed to uncorrupted value (2024-10-01T10:00) after all variable listeners were triggered without changes to the genuine variables.
Maybe one of the listeners ([]) for that shadow variable (Job.startTime) forgot to update it when one of its sourceVariables ([]) changed.
Or vice versa, maybe one of the listeners computes this shadow variable using a planning variable that is not declared as its source. Use the repeatable @ShadowVariable annotation for each source variable that is used to compute this shadow variable.

    at ai.timefold.solver.core.impl.score.director.AbstractScoreDirector.assertShadowVariablesAreNotStale(AbstractScoreDirector.java:568)
    at ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope.assertShadowVariablesAreNotStale(AbstractPhaseScope.java:186)
    at ai.timefold.solver.core.impl.phase.AbstractPhase.predictWorkingStepScore(AbstractPhase.java:152)
    at ai.timefold.solver.core.impl.constructionheuristic.DefaultConstructionHeuristicPhase.doStep(DefaultConstructionHeuristicPhase.java:97)
    at ai.timefold.solver.core.impl.constructionheuristic.DefaultConstructionHeuristicPhase.solve(DefaultConstructionHeuristicPhase.java:83)
    at ai.timefold.solver.core.impl.solver.AbstractSolver.runPhases(AbstractSolver.java:82)
    at ai.timefold.solver.core.impl.solver.DefaultSolver.solve(DefaultSolver.java:200)
    at com.easyplan.Main.main(Main.java:284)

And there are no complex constraints implemented in ConstraintStream, only matching the production line allocation of tasks, similar to Skill Match in TaskAssignment:

protected Constraint resourceMatch(ConstraintFactory factory) {
Constraint constraint = factory.forEach(Job.class)
.filter(job -> job.getProductionLine() != null && !job.getAvailableResourceList().contains(job.getProductionLine()))
.penalizeLong(HardMediumSoftLongScore.ONE_HARD, Job::getDefaultScore)
.asConstraint("SkillMissing");
return constraint;
}

This should be a simple problem, but I don't have any idea about this Listener corruption. Has anyone encountered this? Or any suggestions?

I have tried the Chained Through Time pattern to implement this scenario, but PlanningListVariable is simpler and more user-friendly.

Upvotes: 0

Views: 10

Answers (0)

Related Questions