OptaPlanner局部搜索触发InverseRelationShadowVariable状态不一致异常
编辑补充 - 经排查,该问题由不当使用SolutionManager#explain(Solution_)导致,详见已采纳答案
我在使用OptaPlanner时遇到一个问题,无法确定是代码问题还是框架问题。
问题描述
采用“Chained through time”模式,涉及以下类:
Employee(继承TaskOrEmployee)- 问题事实Task(继承TaskOrEmployee),包含@PlanningVariable TaskOrEmployee previousTaskOrEmployee字段,标注@PlanningEntityTaskOrEmployee,包含@InverseRelationShadowVariable Task nextTask字段,标注@PlanningEntity
OptaPlanner版本:8.44.0.Final,Java版本11
构造启发式阶段运行正常,但局部搜索时抛出异常,提示解决方案状态不一致,即task.getPreviousTaskOrEmployee().getNextTask() != task。我未传入已初始化的解决方案,也从未手动设置nextTask或previousTaskOrEmployee字段。
我已在SolverManager#solveAndListen的bestSolutionConsumer中验证,构造启发式生成的解决方案状态是一致的。
错误堆栈信息
2024-01-06 23:48:57 DEBUG DefaultConstructionHeuristicPhase#stepEnded CH step (1503), time spent (1256), score (0hard/-319262medium/494035696soft), selected move count (1), picked move (<263/22.6 [2]> {null -> <263/22.6 [1]>}). 2024-01-06 23:48:57 INFO DefaultConstructionHeuristicPhase#phaseEnded Construction Heuristic phase (0) ended: time spent (1266), best score (0hard/-319262medium/494035696soft), score calculation speed (1525/sec), step total (1504). 2024-01-06 23:48:57 ERROR DefaultSolverManager#lambda$new$0 Solving failed for problemId (1). java.lang.IllegalStateException: The entity (<276/22.1 [7]>) has a variable (previousTaskOrEmployee) with value (<276/22.1 [6]>) which has a sourceVariableName variable (nextTask) with a value (null) which is not that entity. Verify the consistency of your input problem for that sourceVariableName variable. at org.optaplanner.core.impl.domain.variable.inverserelation.SingletonInverseVariableListener.retract(SingletonInverseVariableListener.java:76) ~[optaplanner-core-impl-8.44.0.Final.jar:8.44.0.Final] at org.optaplanner.core.impl.domain.variable.inverserelation.SingletonInverseVariableListener.beforeVariableChanged(SingletonInverseVariableListener.java:32) ~[optaplanner-core-impl-8.44.0.Final.jar:8.44.0.Final] at org.optaplanner.core.impl.domain.variable.listener.support.VariableChangedNotification.triggerBefore(VariableChangedNotification.java:15) ~[optaplanner-core-impl-8.44.0.Final.jar:8.44.0.Final] at org.optaplanner.core.impl.domain.variable.listener.support.VariableChangedNotification.triggerBefore(VariableChangedNotification.java:6) ~[optaplanner-core-impl-8.44.0.Final.jar:8.44.0.Final] at org.optaplanner.core.impl.domain.variable.listener.support.AbstractNotifiable.triggerBefore(AbstractNotifiable.java:70) ~[optaplanner-core-impl-8.44.0.Final.jar:8.44.0.Final] at org.optaplanner.core.impl.domain.variable.listener.support.VariableListenerNotifiable.notifyBefore(VariableListenerNotifiable.java:26) ~[optaplanner-core-impl-8.44.0.Final.jar:8.44.0.Final] at org.optaplanner.core.impl.domain.variable.listener.support.VariableListenerSupport.beforeVariableChanged(VariableListenerSupport.java:161) ~[optaplanner-core-impl-8.44.0.Final.jar:8.44.0.Final] at org.optaplanner.core.impl.score.director.AbstractScoreDirector.beforeVariableChanged(AbstractScoreDirector.java:423) ~[optaplanner-core-impl-8.44.0.Final.jar:8.44.0.Final] at org.optaplanner.core.impl.score.director.AbstractScoreDirector.changeVariableFacade(AbstractScoreDirector.java:435) ~[optaplanner-core-impl-8.44.0.Final.jar:8.44.0.Final] at org.optaplanner.core.impl.heuristic.selector.move.generic.chained.ChainedChangeMove.doMoveOnGenuineVariables(ChainedChangeMove.java:60) ~[optaplanner-core-impl-8.44.0.Final.jar:8.44.0.Final] at org.optaplanner.core.impl.heuristic.move.AbstractMove.doMoveOnly(AbstractMove.java:26) ~[optaplanner-core-impl-8.44.0.Final.jar:8.44.0.Final] at org.optaplanner.core.impl.heuristic.move.AbstractMove.doMove(AbstractMove.java:20) ~[optaplanner-core-impl-8.44.0.Final.jar:8.44.0.Final] at org.optaplanner.core.impl.heuristic.move.AbstractMove.doMove(AbstractMove.java:15) ~[optaplanner-core-impl-8.44.0.Final.jar:8.44.0.Final] at org.optaplanner.core.impl.score.director.AbstractScoreDirector.doAndProcessMove(AbstractScoreDirector.java:212) ~[optaplanner-core-impl-8.44.0.Final.jar:8.44.0.Final] at org.optaplanner.core.impl.localsearch.decider.LocalSearchDecider.doMove(LocalSearchDecider.java:117) ~[optaplanner-core-impl-8.44.0.Final.jar:8.44.0.Final] at org.optaplanner.core.impl.localsearch.decider.LocalSearchDecider.decideNextStep(LocalSearchDecider.java:101) ~[optaplanner-core-impl-8.44.0.Final.jar:8.44.0.Final] at org.optaplanner.core.impl.localsearch.DefaultLocalSearchPhase.solve(DefaultLocalSearchPhase.java:72) ~[optaplanner-core-impl-8.44.0.Final.jar:8.44.0.Final] at org.optaplanner.core.impl.solver.AbstractSolver.runPhases(AbstractSolver.java:83) ~[optaplanner-core-impl-8.44.0.Final.jar:8.44.0.Final] at org.optaplanner.core.impl.solver.DefaultSolver.solve(DefaultSolver.java:193) ~[optaplanner-core-impl-8.44.0.Final.jar:8.44.0.Final] at org.optaplanner.core.impl.solver.DefaultSolverJob.call(DefaultSolverJob.java:109) [optaplanner-core-impl-8.44.0.Final.jar:8.44.0.Final] at java.util.concurrent.FutureTask.run(FutureTask.java:264) [?:?] at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) [?:?] at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) [?:?] at java.lang.Thread.run(Thread.java:829) [?:?]
从堆栈信息可知,涉及的两个实体(<276/22.1 [7]>和<276/22.1 [6]>)均为Task类实例,而非Employee。
领域模型
业务需求为给员工分配任务,按照文档实现了“Chained through time”模式。
// 解决方案类 @PlanningSolution public class TaskAssigningSolution implements Serializable { @ValueRangeProvider(id = "employeeRange") @ProblemFactCollectionProperty List<Employee> employees; // 初始未初始化解决方案创建时,任务按构造启发式应追加到链的顺序排序 @ValueRangeProvider(id = "taskRange") @PlanningEntityCollectionProperty List<Task> tasks; @PlanningScore HardMediumSoftScore score; } // Employee和Task的父类 @PlanningEntity public abstract class TaskOrEmployee implements Serializable { @InverseRelationShadowVariable(sourceVariableName = "previousTaskOrEmployee") Task nextTask; } // 问题事实,链锚点 public class Employee extends TaskOrEmployee { @PlanningId String employeeId; // 移除了与问题无关的字段,这些字段只读,仅被StartAndEndTimeUpdatingPlanningVariableListener使用 } @PlanningEntity public class Task extends TaskOrEmployee { @PlanningVariable(valueRangeProviderRefs = { "employeeRange", "taskRange" }, graphType = CHAINED) TaskOrEmployee previousTaskOrEmployee; @AnchorShadowVariable(sourceVariableName = "previousTaskOrEmployee") Employee employee; @ShadowVariable(sourceVariableName = "previousTaskOrEmployee", variableListenerClass = StartAndEndTimeUpdatingPlanningVariableListener.class) Long startTime; @ShadowVariable(sourceVariableName = "previousTaskOrEmployee", variableListenerClass = StartAndEndTimeUpdatingPlanningVariableListener.class) Long endTime; @PlanningId String taskId; // 移除了与问题无关的字段,这些字段只读,仅被StartAndEndTimeUpdatingPlanningVariableListener使用 }
无需提供完整的StartAndEndTimeUpdatingPlanningVariableListener代码,可保证它不会修改nextTask字段,仅设置以下两个字段:
scoreDirector.beforeVariableChanged(task, "startTime"); task.setStartTime(resultTuple.startTime); scoreDirector.afterVariableChanged(task, "startTime"); scoreDirector.beforeVariableChanged(task, "endTime"); task.setEndTime(resultTuple.endTime); scoreDirector.afterVariableChanged(task, "endTime");
配置信息
<?xml version="1.0" encoding="UTF-8"?> <solver xmlns="https://www.optaplanner.org/xsd/solver" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://www.optaplanner.org/xsd/solver https://www.optaplanner.org/xsd/solver/solver.xsd"> <environmentMode>REPRODUCIBLE</environmentMode> <moveThreadCount>8</moveThreadCount> <solutionClass>package.TaskAssigningSolution</solutionClass> <entityClass>package.TaskOrEmployee</entityClass> <entityClass>package.Task</entityClass> <domainAccessType>REFLECTION</domainAccessType> <scoreDirectorFactory> <constraintProviderClass>package.TaskAssigningConstraintProvider</constraintProviderClass> <!-- 使用drools时也会出现相同错误 --> <constraintStreamImplType>BAVET</constraintStreamImplType> </scoreDirectorFactory> <constructionHeuristic> <changeMoveSelector> <!-- 该过滤器检查移动操作要追加任务的链锚点(Employee)是否具备完成该任务的能力(是否拥有所需技能) --> <filterClass>package.TaskChangeMoveSelectionFilter</filterClass> <valueSelector> <!-- 该过滤器检查值是否为链的末端(其nextTask为null),否则构造启发式在大数据集上会运行数小时 --> <filterClass>package.ConstructionHeuristicValueSelectionFilter</filterClass> </valueSelector> </changeMoveSelector> </constructionHeuristic> <localSearch> <localSearchType>TABU_SEARCH</localSearchType> <!-- 单一changeMoveSelector并非最终目标,我只是尝试配置不触发错误的局部搜索,添加更多moveSelectors也无法解决错误 --> <changeMoveSelector> <filterClass>package.TaskChangeMoveSelectionFilter</filterClass> </changeMoveSelector> </localSearch> </solver>
还尝试了不同的局部搜索类型(TABU_SEARCH、HILL_CLIMBING、LATE_ACCEPTANCE)——HILL_CLIMBING和LATE_ACCEPTANCE通常能执行几步(根据调试日志),但最终都会抛出相同错误。据此认为,构造启发式阶段(Phase 0)结束后,解决方案状态尚未损坏。
内容的提问来源于stack exchange,提问作者Kamil Bęben

