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 @@ -204,7 +204,7 @@ public void setMonitorTagMap(Map<String, String> monitorTagMap) {
restartSolver = checkProblemFactChanges();
}
outerSolvingEnded(solverScope);
return solverScope.getBestSolution();
return solverScope.cloneBestSolution();
}

public void outerSolvingStarted(SolverScope<Solution_> solverScope) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ public void fireBestSolutionChanged(SolverScope<Solution_> solverScope, Solution
var timeMillisSpent = solverScope.getBestSolutionTimeMillisSpent();
var bestScore = solverScope.getBestScore();
if (it.hasNext()) {
var event = new BestSolutionChangedEvent<>(solver, timeMillisSpent, newBestSolution, bestScore.raw(),
// We need to clone the new best solution, or we may share the same instance with user consumers.
// Reusing the instance can lead to inconsistent states if intermediary consumers change the solution.
var newBestSolutionCloned = solverScope.getScoreDirector().cloneSolution(newBestSolution);
var event = new BestSolutionChangedEvent<>(solver, timeMillisSpent, newBestSolutionCloned, bestScore.raw(),
bestScore.fullyAssigned());
do {
it.next().bestSolutionChanged(event);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,12 @@ public void updateBestSolutionAndFireIfInitialized(SolverScope<Solution_> solver
private void updateBestSolutionAndFire(SolverScope<Solution_> solverScope, InnerScore<?> bestScore,
Solution_ bestSolution) {
updateBestSolutionWithoutFiring(solverScope, bestScore, bestSolution);
solverEventSupport.fireBestSolutionChanged(solverScope, solverScope.getBestSolution());
solverEventSupport.fireBestSolutionChanged(solverScope, bestSolution);
}

@SuppressWarnings({ "unchecked", "rawtypes" })
private void updateBestSolutionWithoutFiring(SolverScope<Solution_> solverScope) {
// We clone the existing working solution to set it as the best current solution
var newBestSolution = solverScope.getScoreDirector().cloneWorkingSolution();
var newBestScore = solverScope.getSolutionDescriptor().<Score> getScore(newBestSolution);
var innerScore = InnerScore.withUnassignedCount(newBestScore, -solverScope.getScoreDirector().getWorkingInitScore());
Expand All @@ -144,11 +145,10 @@ private void updateBestSolutionWithoutFiring(SolverScope<Solution_> solverScope)

private void updateBestSolutionWithoutFiring(SolverScope<Solution_> solverScope, InnerScore<?> bestScore,
Solution_ bestSolution) {
if (bestScore.fullyAssigned()) {
if (!solverScope.isBestSolutionInitialized()) {
solverScope.setStartingInitializedScore(bestScore.raw());
}
if (bestScore.fullyAssigned() && !solverScope.isBestSolutionInitialized()) {
solverScope.setStartingInitializedScore(bestScore.raw());
}

solverScope.setBestSolution(bestSolution);
solverScope.setBestScore(bestScore);
solverScope.setBestSolutionTimeMillis(solverScope.getClock().millis());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@ public Solution_ getBestSolution() {
return bestSolution.get();
}

public Solution_ cloneBestSolution() {
return scoreDirector.cloneSolution(bestSolution.get());
}

/**
* The {@link PlanningSolution best solution} must never be the same instance
* as the {@link PlanningSolution working solution}, it should be a (un)changed clone.
Expand Down Expand Up @@ -332,7 +336,7 @@ public long getMoveEvaluationSpeed() {

public void setWorkingSolutionFromBestSolution() {
// The workingSolution must never be the same instance as the bestSolution.
scoreDirector.setWorkingSolution(scoreDirector.cloneSolution(getBestSolution()));
scoreDirector.setWorkingSolution(cloneBestSolution());
}

public SolverScope<Solution_> createChildThreadSolverScope(ChildThreadType childThreadType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ void solveWithInitializedSolution() {
new TestdataEntity("e3", v3)));

var solution = PlannerTestUtils.solve(solverConfig, inputProblem, false);
assertThat(inputProblem).isSameAs(solution);
assertThat(inputProblem.getEntityList().stream().map(TestdataEntity::getValue).toList())
.isEqualTo(solution.getEntityList().stream().map(TestdataEntity::getValue).toList());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;

import ai.timefold.solver.core.api.solver.Solver;
import ai.timefold.solver.core.api.solver.SolverConfigOverride;
import ai.timefold.solver.core.api.solver.SolverManager;
import ai.timefold.solver.core.api.solver.change.ProblemChange;
import ai.timefold.solver.core.api.solver.change.ProblemChangeDirector;
import ai.timefold.solver.core.config.solver.SolverConfig;
import ai.timefold.solver.core.config.solver.SolverManagerConfig;
import ai.timefold.solver.core.config.solver.termination.TerminationConfig;
import ai.timefold.solver.core.testdomain.TestdataEasyScoreCalculator;
import ai.timefold.solver.core.testdomain.TestdataEntity;
import ai.timefold.solver.core.testdomain.TestdataSolution;
Expand Down Expand Up @@ -186,6 +189,36 @@ void problemChangeBarrageIntermediateBestSolutionConsumer() throws InterruptedEx

}

@Test
void testCloningBetweenPhases() throws ExecutionException, InterruptedException {
var solverConfig = new SolverConfig()
.withSolutionClass(TestdataSolution.class)
.withEntityClasses(TestdataEntity.class)
.withEasyScoreCalculatorClass(TestdataEasyScoreCalculator.class);

try (var solverManager = SolverManager.<TestdataSolution, UUID> create(solverConfig, new SolverManagerConfig())) {
var problem = TestdataSolution.generateSolution(2, 2);
problem.getEntityList().forEach(entity -> entity.setValue(null));
var countDownLatch = new CountDownLatch(1);
var solution = solverManager.solveBuilder()
.withProblemId(UUID.randomUUID())
.withProblem(problem)
.withBestSolutionConsumer(
intermediate -> {
// Add a new entity to the cloned solution
intermediate.getEntityList().add(new TestdataEntity("Bad Entity 3"));
countDownLatch.countDown();
})
.withConfigOverride(new SolverConfigOverride<TestdataSolution>()
.withTerminationConfig(new TerminationConfig().withMoveCountLimit(100L)))
.run()
.getFinalBestSolution();
countDownLatch.await();
// At this point, the events generated by the best solution consumer should not add any new entities
assertThat(solution.getEntityList()).hasSize(2);
Comment thread
zepfred marked this conversation as resolved.
}
}

private record RecordedFuture(int id, CompletableFuture<Void> future) {

boolean isDone() {
Expand Down
Loading