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

TimeFold初始评分计算后VariableListener数据损坏问题排查

OptaPlanner Shadow Variable 值损坏问题排查

问题场景

我有一个Task规划实体和一个真实规划变量timeslot,该变量是分配给任务的粗粒度时间范围资源。分配timeslot后,需要通过timeslot和任务的其他字段推导“实际”时间范围(actualStartTime和actualEndTime)。

使用VariableListener实现影子变量更新时,开启TRACKED_FULL_ASSERT模式后,求解开始阶段抛出如下异常:

java.lang.IllegalStateException: VariableListener corruption after completedAction (Initial score calculated):
The entity (Task() assigned to TimeSlot(2024-07-07T08:59:49.956Z to 2024-07-07T09:10:36.337Z: duration PT10M46.381S))'s shadow variable (Task.actualStartTime)'s corrupted value (2024-07-07T18:09:49.927Z) changed to uncorrupted value (2024-07-07T09:00:22.500Z) after all variable listeners were triggered without changes to the genuine variables.

“损坏”值与预期时间范围差异极大,且该问题仅在重新求解时触发,未初始化的新解决方案不会出现此问题。

相关代码

Task 规划实体

@PlanningVariable(valueRangeProviderRefs = "timeslotRange", allowsUnassigned = true)
private TimeSlot timeslot = null;

private Instant actualStartTime = null;

@ShadowVariable(
        sourceVariableName = "timeslot",
        variableListenerClass = TimeSlotStartTimeListener.class)
public Instant getActualStartTime() {
    return this.actualStartTime;
}

private Instant actualEndTime = null;

@PiggybackShadowVariable(shadowVariableName = "actualStartTime")
public Instant getActualEndTime() {
    return this.actualEndTime;
}

TimeSlotStartTimeListener 变量监听器

@Override
public boolean requiresUniqueEntityEvents() { // 不确定是否需要
    return true;
}

private void updateStartAndEndTimes(ScoreDirector<Solution> scoreDirector, Task task) {
    scoreDirector.beforeVariableChanged(task, "actualStartTime");
    task.setActualStartTime(someStartTimeMath(task)); // 纯函数,基于task字段计算
    scoreDirector.afterVariableChanged(task, "actualStartTime");
    scoreDirector.beforeVariableChanged(task, "actualEndTime");
    task.setActualEndTime(someEndTimeMath(task));
    scoreDirector.afterVariableChanged(task, "actualEndTime");
}

@Override
public void beforeVariableChanged(ScoreDirector<Solution> scoreDirector, Task task) {
    this.updateStartAndEndTimes(scoreDirector, task);
}

@Override
public void afterVariableChanged(ScoreDirector<Solution> scoreDirector, Task task) {
    this.updateStartAndEndTimes(scoreDirector, task);
}

问题分析

  1. 重复触发影子变量更新:
    beforeVariableChanged和afterVariableChanged都调用了updateStartAndEndTimes,导致影子变量被计算两次:

    • before阶段:源变量timeslot仍为旧值,此时计算的影子变量值是基于旧资源的结果;
    • after阶段:源变量已更新为新值,重新计算影子变量得到正确结果。
      在TRACKED_FULL_ASSERT模式下,OptaPlanner会检测到影子变量在无真实变量变更的情况下发生值变化,因此抛出“损坏”异常。
  2. Piggyback Shadow Variable 与手动更新冲突:
    actualEndTime标记为@PiggybackShadowVariable依赖actualStartTime,但监听器中又手动更新该变量。这会导致OptaPlanner的自动piggyback更新与手动更新产生冲突,破坏影子变量的状态一致性。

  3. 重新求解时的状态残留:
    重新求解时,解决方案可能保留了上一次求解的影子变量值,而before阶段基于旧timeslot重新计算的结果覆盖了残留值,之后after阶段又用新timeslot计算正确值,两次变更触发了断言检查。

  4. 不必要的requiresUniqueEntityEvents配置:
    该方法返回true仅适用于监听器需要处理同一实体的多个并发变更事件的场景,你的场景中不需要此配置,默认返回false即可,错误配置可能导致事件处理逻辑异常。

修正方案

1. 调整变量监听器的阶段职责

将before阶段用于清理旧影子变量值,after阶段用于基于新timeslot计算新值:

@Override
public boolean requiresUniqueEntityEvents() {
    return false; // 恢复默认值
}

private void updateStartTime(ScoreDirector<Solution> scoreDirector, Task task) {
    Instant newStartTime = someStartTimeMath(task);
    if (!Objects.equals(task.getActualStartTime(), newStartTime)) {
        scoreDirector.beforeVariableChanged(task, "actualStartTime");
        task.setActualStartTime(newStartTime);
        scoreDirector.afterVariableChanged(task, "actualStartTime");
    }
}

private void updateEndTime(ScoreDirector<Solution> scoreDirector, Task task) {
    Instant newEndTime = someEndTimeMath(task);
    if (!Objects.equals(task.getActualEndTime(), newEndTime)) {
        scoreDirector.beforeVariableChanged(task, "actualEndTime");
        task.setActualEndTime(newEndTime);
        scoreDirector.afterVariableChanged(task, "actualEndTime");
    }
}

@Override
public void beforeVariableChanged(ScoreDirector<Solution> scoreDirector, Task task) {
    // 清理旧的影子变量值
    scoreDirector.beforeVariableChanged(task, "actualStartTime");
    task.setActualStartTime(null);
    scoreDirector.afterVariableChanged(task, "actualStartTime");

    scoreDirector.beforeVariableChanged(task, "actualEndTime");
    task.setActualEndTime(null);
    scoreDirector.afterVariableChanged(task, "actualEndTime");
}

@Override
public void afterVariableChanged(ScoreDirector<Solution> scoreDirector, Task task) {
    // 基于新timeslot计算影子变量
    updateStartTime(scoreDirector, task);
    // 如果保留@PiggybackShadowVariable,则无需手动调用updateEndTime,由OptaPlanner自动触发
    // updateEndTime(scoreDirector, task);
}

2. 统一影子变量更新方式

如果使用@PiggybackShadowVariable,则为actualEndTime单独实现一个依赖actualStartTime的监听器,而非手动更新:

// Task实体中保留@PiggybackShadowVariable
@PiggybackShadowVariable(shadowVariableName = "actualStartTime", variableListenerClass = ActualEndTimeListener.class)
public Instant getActualEndTime() {
    return this.actualEndTime;
}

// 新增ActualEndTimeListener
public class ActualEndTimeListener implements VariableListener<Solution> {
    @Override
    public void beforeVariableChanged(ScoreDirector<Solution> scoreDirector, Task task) {
        // 清理旧值
        scoreDirector.beforeVariableChanged(task, "actualEndTime");
        task.setActualEndTime(null);
        scoreDirector.afterVariableChanged(task, "actualEndTime");
    }

    @Override
    public void afterVariableChanged(ScoreDirector<Solution> scoreDirector, Task task) {
        Instant newEndTime = someEndTimeMath(task);
        scoreDirector.beforeVariableChanged(task, "actualEndTime");
        task.setActualEndTime(newEndTime);
        scoreDirector.afterVariableChanged(task, "actualEndTime");
    }
}

3. 重新求解时的状态初始化

确保重新求解前,解决方案的所有影子变量被重置为初始状态(如null),避免上一次求解的残留值干扰。

关键建议

  • 严格区分beforeVariableChanged和afterVariableChanged的职责:before用于清理旧状态,after用于计算新状态,禁止重复执行更新逻辑。
  • 影子变量的计算必须是纯函数,仅依赖源变量和实体的固定字段,不依赖外部可变状态。
  • 避免手动更新标记为@PiggybackShadowVariable的字段,由OptaPlanner通过依赖关系自动触发更新。
  • 仅在明确需要的场景下设置requiresUniqueEntityEvents = true,默认保持false即可。

内容的提问来源于stack exchange,提问作者Pezetter

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 14:22:09