Skip to content

Commit 86d58e6

Browse files
feat: Report missing before/after calls on undo corruption (#433)
Co-authored-by: Lukáš Petrovický <[email protected]>
1 parent 8768155 commit 86d58e6

File tree

23 files changed

+1629
-108
lines changed

23 files changed

+1629
-108
lines changed

benchmark/src/main/resources/benchmark.xsd

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2444,6 +2444,9 @@
24442444
<xs:restriction base="xs:string">
24452445

24462446

2447+
<xs:enumeration value="TRACKED_FULL_ASSERT"/>
2448+
2449+
24472450
<xs:enumeration value="FULL_ASSERT"/>
24482451

24492452

core/core-impl/src/main/java/ai/timefold/solver/core/config/solver/EnvironmentMode.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,23 @@
1919
*/
2020
@XmlEnum
2121
public enum EnvironmentMode {
22+
/**
23+
* This mode turns on {@link #FULL_ASSERT} and enables variable tracking to fail-fast on a bug in a {@link Move}
24+
* implementation,
25+
* a constraint, the engine itself or something else at the highest performance cost.
26+
* <p>
27+
* Because it tracks genuine and shadow variables, it is able to report precisely what variables caused the corruption and
28+
* report any missed {@link ai.timefold.solver.core.api.domain.variable.VariableListener} events.
29+
* <p>
30+
* This mode is reproducible (see {@link #REPRODUCIBLE} mode).
31+
* <p>
32+
* This mode is intrusive because it calls the {@link InnerScoreDirector#calculateScore()} more frequently than a non assert
33+
* mode.
34+
* <p>
35+
* This mode is by far the slowest of all the modes.
36+
*/
37+
TRACKED_FULL_ASSERT(true),
38+
2239
/**
2340
* This mode turns on all assertions
2441
* to fail-fast on a bug in a {@link Move} implementation, a constraint, the engine itself or something else
@@ -111,4 +128,7 @@ public boolean isReproducible() {
111128
return this != NON_REPRODUCIBLE;
112129
}
113130

131+
public boolean isTracking() {
132+
return this == TRACKED_FULL_ASSERT;
133+
}
114134
}

core/core-impl/src/main/java/ai/timefold/solver/core/impl/domain/variable/listener/support/VariableListenerSupport.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,15 @@ public void forceTriggerAllVariableListeners(Solution_ workingSolution) {
236236
triggerVariableListenersInNotificationQueues();
237237
}
238238

239+
/**
240+
* Sets all shadow variables to null and then force triggers all variable listeners.
241+
* This effectively recalculates the shadow variables from scratch.
242+
*/
243+
public void recalculateAllShadowVariablesFromScratch(Solution_ workingSolution) {
244+
scoreDirector.getSolutionDescriptor().visitAllEntities(workingSolution, this::resetShadowVariables);
245+
forceTriggerAllVariableListeners(workingSolution);
246+
}
247+
239248
private void simulateGenuineVariableChange(Object entity) {
240249
var entityDescriptor = scoreDirector.getSolutionDescriptor()
241250
.findEntityDescriptorOrFail(entity.getClass());
@@ -255,6 +264,17 @@ private void simulateGenuineVariableChange(Object entity) {
255264
}
256265
}
257266

267+
private void resetShadowVariables(Object entity) {
268+
var entityDescriptor = scoreDirector.getSolutionDescriptor()
269+
.findEntityDescriptorOrFail(entity.getClass());
270+
271+
for (var variableDescriptor : entityDescriptor.getShadowVariableDescriptors()) {
272+
if (!variableDescriptor.getVariablePropertyType().isPrimitive()) {
273+
variableDescriptor.setValue(entity, null);
274+
}
275+
}
276+
}
277+
258278
public void assertNotificationQueuesAreEmpty() {
259279
if (!notificationQueuesAreEmpty) {
260280
throw new IllegalStateException("The notificationQueues might not be empty (" + notificationQueuesAreEmpty
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package ai.timefold.solver.core.impl.domain.variable.listener.support.violation;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
6+
import ai.timefold.solver.core.api.domain.variable.ListVariableListener;
7+
import ai.timefold.solver.core.api.score.director.ScoreDirector;
8+
import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor;
9+
import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor;
10+
import ai.timefold.solver.core.impl.domain.variable.listener.SourcedVariableListener;
11+
import ai.timefold.solver.core.impl.domain.variable.supply.Demand;
12+
import ai.timefold.solver.core.impl.domain.variable.supply.Supply;
13+
import ai.timefold.solver.core.impl.domain.variable.supply.SupplyManager;
14+
import ai.timefold.solver.core.impl.score.director.InnerScoreDirector;
15+
16+
/**
17+
* Tracks variable listener events for a given {@link ai.timefold.solver.core.api.domain.variable.PlanningListVariable}.
18+
*/
19+
public class ListVariableTracker<Solution_>
20+
implements SourcedVariableListener<Solution_>, ListVariableListener<Solution_, Object, Object>, Supply {
21+
private final ListVariableDescriptor<Solution_> variableDescriptor;
22+
private final List<Object> beforeVariableChangedEntityList;
23+
private final List<Object> afterVariableChangedEntityList;
24+
25+
public ListVariableTracker(ListVariableDescriptor<Solution_> variableDescriptor) {
26+
this.variableDescriptor = variableDescriptor;
27+
beforeVariableChangedEntityList = new ArrayList<>();
28+
afterVariableChangedEntityList = new ArrayList<>();
29+
}
30+
31+
@Override
32+
public VariableDescriptor<Solution_> getSourceVariableDescriptor() {
33+
return variableDescriptor;
34+
}
35+
36+
@Override
37+
public void resetWorkingSolution(ScoreDirector<Solution_> scoreDirector) {
38+
beforeVariableChangedEntityList.clear();
39+
afterVariableChangedEntityList.clear();
40+
}
41+
42+
@Override
43+
public void beforeEntityAdded(ScoreDirector<Solution_> scoreDirector, Object entity) {
44+
45+
}
46+
47+
@Override
48+
public void afterEntityAdded(ScoreDirector<Solution_> scoreDirector, Object entity) {
49+
50+
}
51+
52+
@Override
53+
public void beforeEntityRemoved(ScoreDirector<Solution_> scoreDirector, Object entity) {
54+
55+
}
56+
57+
@Override
58+
public void afterEntityRemoved(ScoreDirector<Solution_> scoreDirector, Object entity) {
59+
60+
}
61+
62+
@Override
63+
public void afterListVariableElementUnassigned(ScoreDirector<Solution_> scoreDirector, Object element) {
64+
65+
}
66+
67+
@Override
68+
public void beforeListVariableChanged(ScoreDirector<Solution_> scoreDirector, Object entity, int fromIndex, int toIndex) {
69+
beforeVariableChangedEntityList.add(entity);
70+
}
71+
72+
@Override
73+
public void afterListVariableChanged(ScoreDirector<Solution_> scoreDirector, Object entity, int fromIndex, int toIndex) {
74+
afterVariableChangedEntityList.add(entity);
75+
}
76+
77+
public List<String> getEntitiesMissingBeforeAfterEvents(
78+
List<VariableId<Solution_>> changedVariables) {
79+
List<String> out = new ArrayList<>();
80+
for (var changedVariable : changedVariables) {
81+
if (!variableDescriptor.equals(changedVariable.variableDescriptor())) {
82+
continue;
83+
}
84+
Object entity = changedVariable.entity();
85+
if (!beforeVariableChangedEntityList.contains(entity)) {
86+
out.add("Entity (" + entity
87+
+ ") is missing a beforeListVariableChanged call for list variable ("
88+
+ variableDescriptor.getVariableName() + ").");
89+
}
90+
if (!afterVariableChangedEntityList.contains(entity)) {
91+
out.add("Entity (" + entity
92+
+ ") is missing a afterListVariableChanged call for list variable ("
93+
+ variableDescriptor.getVariableName() + ").");
94+
}
95+
}
96+
beforeVariableChangedEntityList.clear();
97+
afterVariableChangedEntityList.clear();
98+
return out;
99+
}
100+
101+
public TrackerDemand demand() {
102+
return new TrackerDemand();
103+
}
104+
105+
/**
106+
* In order for the {@link ListVariableTracker} to be registered as a variable listener,
107+
* it needs to be passed to the {@link InnerScoreDirector#getSupplyManager()}, which requires a {@link Demand}.
108+
* <p>
109+
* Unlike most other {@link Demand}s, there will only be one instance of
110+
* {@link ListVariableTracker} in the {@link InnerScoreDirector} for each list variable.
111+
*/
112+
public class TrackerDemand implements Demand<ListVariableTracker<Solution_>> {
113+
@Override
114+
public ListVariableTracker<Solution_> createExternalizedSupply(SupplyManager supplyManager) {
115+
return ListVariableTracker.this;
116+
}
117+
}
118+
}

0 commit comments

Comments
 (0)