You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

OptaPlanner局部搜索触发InverseRelationShadowVariable状态不一致异常

编辑补充 - 经排查,该问题由不当使用SolutionManager#explain(Solution_)导致,详见已采纳答案

问题背景

我在使用OptaPlanner时遇到一个问题,无法确定是代码问题还是框架问题。

问题描述

采用“Chained through time”模式,涉及以下类:

  • Employee(继承TaskOrEmployee)- 问题事实
  • Task(继承TaskOrEmployee),包含@PlanningVariable TaskOrEmployee previousTaskOrEmployee字段,标注@PlanningEntity
  • TaskOrEmployee,包含@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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 14:24:55