Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ public final class RecordAndReplayPropagator<Tuple_ extends Tuple> implements Pr
private final Supplier<BavetPrecomputeBuildHelper<Tuple_>> precomputeBuildHelperSupplier;
private final UnaryOperator<Tuple_> internalTupleToOutputTupleMapper;
private final Map<Object, List<Tuple_>> objectToOutputTuplesMap;
/**
* Output tuples which depend only on problem facts.
* Unlike entity-derived output, these are not stored per-source
* (facts never update, so they need no replay),
* but they must still be delivered downstream exactly once.
* Retained between recalculations so they can be retracted on cache invalidation.
*/
private final List<Tuple_> factOutputTupleList = new ArrayList<>();
private final Set<Object> alreadyUpdatingSet = Collections.newSetFromMap(new IdentityHashMap<>());
private final Map<Class<?>, Boolean> objectClassToIsEntitySourceClassMap;

Expand Down Expand Up @@ -189,13 +197,19 @@ private void retractIfPresent(Tuple_ tuple) {
}

private void invalidateCache() {
objectToOutputTuplesMap.values().stream().flatMap(List::stream).forEach(this::retractIfPresent);
objectToOutputTuplesMap.values()
.stream()
.flatMap(List::stream)
.forEach(this::retractIfPresent);
objectToOutputTuplesMap.clear();
factOutputTupleList.forEach(this::retractIfPresent);
factOutputTupleList.clear();
}

private void recalculateTuples(NodeNetwork internalNodeNetwork, Map<Class<?>, List<BavetRootNode<?>>> classToRootNodeList,
RecordingTupleLifecycle<Tuple_> recordingTupleLifecycle) {
var internalTupleToOutputTupleMap = new IdentityHashMap<Tuple_, Tuple_>(seenEntitySet.size());
var internalTupleToOutputTupleMap =
new IdentityHashMap<Tuple_, Tuple_>(seenEntitySet.size() + seenFactSet.size());
for (var invalidated : seenEntitySet) {
var mappedTuples = new ArrayList<Tuple_>();
try (var unusedActiveRecordingLifecycle = recordingTupleLifecycle.recordInto(
Expand All @@ -212,7 +226,28 @@ private void recalculateTuples(NodeNetwork internalNodeNetwork, Map<Class<?>, Li
objectToOutputTuplesMap.put(invalidated, mappedTuples);
}
}
objectToOutputTuplesMap.values().stream().flatMap(List::stream).forEach(this::insertIfAbsent);
objectToOutputTuplesMap.values()
.stream()
.flatMap(List::stream)
.forEach(this::insertIfAbsent);
// Output tuples derived purely from facts are not re-emitted by any entity update above,
// so they would never be delivered.
// Facts never update during solving,
// so a single recording pass over all facts is enough to capture every fact-dependent output tuple exactly once.
// Tuples also derived from an entity were already delivered above;
// insertIfAbsent skips them via tuple-state deduplication.
if (!seenFactSet.isEmpty()) {
try (var unusedActiveRecordingLifecycle =
recordingTupleLifecycle.recordInto(new TupleRecorder<>(factOutputTupleList,
internalTupleToOutputTupleMapper, internalTupleToOutputTupleMap))) {
for (var fact : seenFactSet) {
classToRootNodeList.get(fact.getClass())
.forEach(node -> ((BavetRootNode<Object>) node).update(fact));
}
internalNodeNetwork.settle();
}
factOutputTupleList.forEach(this::insertIfAbsent);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,40 @@ protected AbstractUniConstraintStreamPrecomputeTest(ConstraintStreamImplSupport
super(implSupport);
}

/**
* A precompute that simply enumerates a problem-fact class must emit one tuple per fact.
* Removing a problem fact must re-derive the fact-only output
* and retract the removed fact's tuple.
* Exercises the cache-invalidation path for fact-derived output.
*/
@TestTemplate
void forEachUnfiltered_fact() {
var solution = TestdataLavishSolution.generateEmptySolution();
var value1 = new TestdataLavishValue();
var value2 = new TestdataLavishValue();
var value3 = new TestdataLavishValue();
solution.getValueList().addAll(List.of(value1, value2, value3));

var scoreDirector = buildScoreDirector(
factory -> factory.precompute(pf -> pf.forEachUnfiltered(TestdataLavishValue.class))
.penalize(SimpleScore.ONE)
.asConstraint(TEST_CONSTRAINT_ID));

scoreDirector.setWorkingSolution(solution);
assertScore(scoreDirector,
assertMatch(value1),
assertMatch(value2),
assertMatch(value3));

scoreDirector.beforeProblemFactRemoved(value3);
solution.getValueList().remove(value3);
scoreDirector.afterProblemFactRemoved(value3);

assertScore(scoreDirector,
assertMatch(value1),
assertMatch(value2));
}

@Override
@TestTemplate
public void filter_0_changed() {
Expand Down
Loading