【问题标题】:OptaPlanner, shadow variable's corrupted value changed to uncorrupted valueOptaPlanner,影子变量的损坏值更改为未损坏值
【发布时间】:2022-01-05 07:09:32
【问题描述】:

optaplanner-bom 版本 7.45.0.Final.

@PlanningEntity
public class Task {
    
@PlanningVariable(valueRangeProviderRefs = "timeGrainRange")
private TimeGrain startingTimeGrain;

@CustomShadowVariable(variableListenerClass = DurationUpdatingVariableListener.class,
        sources = { @PlanningVariableReference(variableName = "startingTimeGrain") })
private Long durationInGrains;
......

在 DurationUpdatingVariableListener 类中:

public void afterVariableChanged(ScoreDirector scoreDirector, Task e) { 
    if (null == e.getStartingTimeGrain()) {
        return;
    }
    Schedule s = (Schedule)scoreDirector.getWorkingSolution();
    List<Task> tasksToBeUpdated = s.getTasksToBeUpdated(e);  // calculate all updates
    for (Task t: tasksToBeUpdated) {
        scoreDirector.beforeVariableChanged(t, NAME_DURATION);
        t.setDurationInGrains(convertToGrain(t.getDurationInSecs()));
        scoreDirector.afterVariableChanged(t, NAME_DURATION);
    }
}

逻辑是,当一个任务的startingTimeGrain改变时,会影响一些任务的时长。问题是,当变量“tasksToBeUpdated”只包含一个任务时,没有错误。当它包含多个任务时,出现以下错误:

ERROR 30844 --- [pool-1-thread-1] o.o.c.impl.solver.DefaultSolverManager   : Solving failed for problemId (1).

java.lang.IllegalStateException: The move thread with moveThreadIndex (3) has thrown an exception. Relayed here in the parent thread.
    at org.optaplanner.core.impl.heuristic.thread.OrderByMoveIndexBlockingQueue.take(OrderByMoveIndexBlockingQueue.java:147) ~[optaplanner-core-7.45.0.Final.jar:7.45.0.Final]
    at org.optaplanner.core.impl.constructionheuristic.decider.MultiThreadedConstructionHeuristicDecider.forageResult(MultiThreadedConstructionHeuristicDecider.java:186) ~[optaplanner-core-7.45.0.Final.jar:7.45.0.Final]
    ......
Caused by: java.lang.IllegalStateException: VariableListener corruption after completedAction (Undo(id=1, ...., startingTimeGrain=null {null -> {"grainIndex":1,"id":1}})):
    The entity (id=2, ..., startingTimeGrain=1)'s shadow variable (Task.durationInGrains)'s corrupted value (4) changed to uncorrupted value (3) after all VariableListeners were triggered without changes to the genuine variables.
      Maybe the VariableListener class (DurationUpdatingVariableListener) for that shadow variable (Task.durationInGrains) forgot to update it when one of its sources changed.

    at org.optaplanner.core.impl.score.director.AbstractScoreDirector.assertShadowVariablesAreNotStale(AbstractScoreDirector.java:545) ~[optaplanner-core-7.45.0.Final.jar:7.45.0.Final]
    ......

可以肯定的是,Task.setDurationInGrains() 只在 afterVariableChanged() 中被调用。为什么会发生错误? HereSchedule.getTasksToBeUpdated(Task task)的代码。

【问题讨论】:

  • 你也可以显示Schedule.getTasksToBeUpdated(Task task) body 吗?
  • 将方法的链接附加到问题中。

标签: optaplanner


【解决方案1】:

确保您的持续时间更新侦听器能够正确“清理”受撤消移动影响的所有任务的影子变量。因此,例如,如果您有:

Task(id=1, startingTimeGrain=null, durationInGrains=null)
Task(id=2, startingTimeGrain={"grainIndex":2,"id":1}, durationInGrains=3)

你会的

Move(id=1, ...., startingTimeGrain=null {null -> {"grainIndex":1,"id":1}})

您的持续时间更新侦听器可能会导致如下结果:

Task(id=1, startingTimeGrain={"grainIndex":1,"id":1}, durationInGrains=1)
Task(id=2, startingTimeGrain={"grainIndex":2,"id":1}, durationInGrains=4)

请注意,任务 2 受到更改任务 1 的移动的影响,并且其持续时间由侦听器更新。

如果这是真的,那么在上面的移动被撤消之后:

Undo(id=1, ...., startingTimeGrain=null {null -> {"grainIndex":1,"id":1}})

为了避免监听器损坏,这是绝对必须发生的:

Task(id=1, startingTimeGrain=null, durationInGrains=null)
Task(id=2, startingTimeGrain={"grainIndex":2,"id":1}, durationInGrains=3)

注意持续时间更新监听器负责:

  1. 在撤消后将任务 1 上的 durationInGrains 设置为 null,将其从 Grain 1 中取消分配。
  2. 重新计算受任务 1 移动影响的任务 2 的持续时间,并将其持续时间设置为与任务 1 移动完成之前相同的值 (3)。

【讨论】:

  • 提示:您可能还需要使用beforeVariableChanged() 方法让侦听器能够按照上述方式进行清理。
  • 非常感谢Jiří,我想你所说的正是发生的事情。我不知道撤消移动并且我的代码中没有任何自定义移动。 (1) optaplanner-example 项目中是否有Undo Move 示例? (2) 如何判断对 before/afterVariableChanged() 的调用是撤消移动还是正常移动?这样我就知道什么时候该清理,什么时候不清理。 (3) 我的持续时间计算有点特殊,它没有序列化。为了撤消,我是否需要在更改之前保存阴影值?工作量太大了。 :(
  • (1) 每个移动类都需要实现createUndoMove(),因此每个移动都知道如何撤消自身。在 optaplanner-core(内置移动类型)中查找 ChangeMove 或在 optaplanner-examples(自定义移动类型)中查找 InvestmentQuantityTransferMove。 (2) 你不应该需要这些信息。挑战在于不仅要计算任务初始化后的持续时间,还要将所有影子变量精确地重置为给定任务初始化之前的状态。当然,不知道最后一步是“执行”还是“撤消”移动。
  • (3) 可以使侦听器有状态并使其记住调用之间的一些信息,但不建议这样做。希望在您的情况下没有必要。如果您最终采用这种方法,请不要忘记覆盖 resetWorkingSolution()close()。有关影子变量的更多信息,请参阅 VariableListenerJavadoc 和 optaplanner.org/docs/optaplanner/latest/shadow-variable/…
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-09-17
  • 2018-12-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多