基于Chained Through Time模式的任务调度Score Corruption问题求助
任务调度中Chained Through Time模式的Score Corruption异常解决
问题背景
在任务调度场景中采用Chained Through Time模式处理任务前后依赖(如同一产品的工序任务),要求任务必须在前置任务完成后启动。基于TaskAssigning示例,在Job类中新增规划变量delay,任务startTime由前置任务完成时间加delay计算。当任务早于前置任务完成时间启动时,通过约束触发惩罚,但运行时抛出Score Corruption异常。已排查约束逻辑未发现问题,询问方案是否存在遗漏。选择Chained Through Time而非PlanningListVariable的原因是系统需要实时规划,而后者暂不支持该特性。
异常信息
Exception in thread "main" java.lang.IllegalStateException: Score corruption (420medium): the workingScore (0hard/0medium/0soft) is not the uncorruptedScore (0hard/-420medium/0soft) after completedAction (M1J1[R2] {9 -> 9}): Score corruption analysis: The corrupted scoreDirector has no ConstraintMatch(s) which are in excess. The corrupted scoreDirector has 1 ConstraintMatch(s) which are missing: com.easyplan.domain/jobStartTimeLimitation/[M1J2[R1]]=0hard/-420medium/0soft Maybe there is a bug in the score constraints of those ConstraintMatch(s). Maybe a score constraint doesn't select all the entities it depends on, but finds some through a reference in a selected entity. This corrupts incremental score calculation, because the constraint is not re-evaluated if such a non-selected entity changes. Shadow variable corruption in the corrupted scoreDirector: None at org.optaplanner.core.impl.score.director.AbstractScoreDirector.assertScoreFromScratch(AbstractScoreDirector.java:627) at org.optaplanner.core.impl.score.director.AbstractScoreDirector.assertWorkingScoreFromScratch(AbstractScoreDirector.java:603) at org.optaplanner.core.impl.score.director.AbstractScoreDirector.doAndProcessMove(AbstractScoreDirector.java:210) at org.optaplanner.core.impl.localsearch.decider.LocalSearchDecider.doMove(LocalSearchDecider.java:117) at org.optaplanner.core.impl.localsearch.decider.LocalSearchDecider.decideNextStep(LocalSearchDecider.java:101) at org.optaplanner.core.impl.localsearch.DefaultLocalSearchPhase.solve(DefaultLocalSearchPhase.java:72) at org.optaplanner.core.impl.solver.AbstractSolver.runPhases(AbstractSolver.java:83) at org.optaplanner.core.impl.solver.DefaultSolver.solve(DefaultSolver.java:193) at com.easyplan.Main.main(Main.java:299)
相关代码
ConstraintProvider中的约束方法
// method in ConstraintProvider protected Constraint jobStartBeforePredecessorEnd(ConstraintFactory factory) { return factory.forEach(Job.class) .filter(job -> job.getResource() != null && job.startedTooEarly() > 0) .penalizeLong(HardMediumSoftLongScore.ONE_MEDIUM, Job::startedTooEarly) .asConstraint("jobStartTimeLimitation"); }
Job类中的startedTooEarly方法
// method in class Job public Long startedTooEarly() { if(this.previousJobOrResource == null) { return 0L; } if(this.predecessorJobList != null && !this.predecessorJobList.isEmpty()) { LocalDateTime latestPredecessorEndTime = this.startTime; for (Job predecessorJob : this.predecessorJobList) { if(predecessorJob.getPreviousJobOrResource() == null) continue; if (predecessorJob.getEndTime().isAfter(latestPredecessorEndTime)) { latestPredecessorEndTime = predecessorJob.getEndTime(); } } if (latestPredecessorEndTime.isAfter(this.startTime)) { Duration leadingTime = Duration.between(this.startTime, latestPredecessorEndTime); return leadingTime.toMinutes(); } else { return 0L; } } return 0L; }
问题原因与解决方案
问题根源
异常提示明确指出:约束未选择所有依赖的实体,而是通过选中实体的内部引用获取前置任务。这种隐式依赖会破坏增量分数计算——当前置任务的属性(如endTime)变化时,约束不会被重新评估,导致工作分数与实际分数不一致。
当前约束仅遍历Job实体,但在startedTooEarly方法中依赖predecessorJobList中的前置任务,这些前置任务未被约束的流式API显式选中,OptaPlanner无法追踪它们的变化。
解决方案
修改约束逻辑,显式关联当前任务与所有前置任务,确保所有影响分数的实体都被约束纳入追踪范围。示例代码如下:
protected Constraint jobStartBeforePredecessorEnd(ConstraintFactory factory) { return factory.forEach(Job.class) // 过滤有资源分配且存在前置任务的Job .filter(job -> job.getResource() != null && job.getPredecessorJobList() != null && !job.getPredecessorJobList().isEmpty()) // 展开当前Job的所有有效前置任务(排除未分配资源的前置任务) .flatMap(job -> job.getPredecessorJobList().stream() .filter(predecessor -> predecessor.getPreviousJobOrResource() != null) .map(predecessor -> Tuple.of(job, predecessor))) // 检查当前Job的 startTime 是否早于前置任务的 endTime .filter(tuple -> tuple.getRight().getEndTime().isAfter(tuple.getLeft().getStartTime())) // 计算惩罚值:前置任务结束时间与当前任务开始时间的差值(分钟) .penalizeLong(HardMediumSoftLongScore.ONE_MEDIUM, tuple -> Duration.between(tuple.getLeft().getStartTime(), tuple.getRight().getEndTime()).toMinutes()) .asConstraint("jobStartTimeLimitation"); }
额外检查点
- 确认
startTime作为影子变量的更新逻辑正确:当前置任务的endTime或delay变化时,startTime能自动更新。 - 确保
predecessorJobList的引用关系在规划过程中不会被意外修改,避免出现无效依赖。
内容的提问来源于stack exchange,提问作者Kent Zhang
相关产品推荐
相关产品推荐

