TimeFold初始评分计算后VariableListener数据损坏问题排查
问题场景
我有一个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); }
问题分析
重复触发影子变量更新:
beforeVariableChanged和afterVariableChanged都调用了updateStartAndEndTimes,导致影子变量被计算两次:before阶段:源变量timeslot仍为旧值,此时计算的影子变量值是基于旧资源的结果;after阶段:源变量已更新为新值,重新计算影子变量得到正确结果。
在TRACKED_FULL_ASSERT模式下,OptaPlanner会检测到影子变量在无真实变量变更的情况下发生值变化,因此抛出“损坏”异常。
Piggyback Shadow Variable 与手动更新冲突:
actualEndTime标记为@PiggybackShadowVariable依赖actualStartTime,但监听器中又手动更新该变量。这会导致OptaPlanner的自动piggyback更新与手动更新产生冲突,破坏影子变量的状态一致性。重新求解时的状态残留:
重新求解时,解决方案可能保留了上一次求解的影子变量值,而before阶段基于旧timeslot重新计算的结果覆盖了残留值,之后after阶段又用新timeslot计算正确值,两次变更触发了断言检查。不必要的
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

