Skip to content

Commit fe93472

Browse files
authored
revert: "fix: clone the solution before sending it to the consumer"
1 parent 12154de commit fe93472

File tree

7 files changed

+137
-45
lines changed

7 files changed

+137
-45
lines changed

core/src/main/java/ai/timefold/solver/core/api/solver/SolverJobBuilder.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ public interface SolverJobBuilder<Solution_, ProblemId_> {
5959

6060
/**
6161
* Sets the best solution consumer, which may be called multiple times during the solving process.
62+
* <p>
63+
* Don't apply any changes to the solution instance while the solver runs.
64+
* The solver's best solution instance is the same as the one in the event,
65+
* and any modifications may lead to solver corruption due to its internal reuse.
6266
*
6367
* @param bestSolutionConsumer called multiple times for each new best solution on a consumer thread
6468
* @return this

core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ public void setMonitorTagMap(Map<String, String> monitorTagMap) {
204204
restartSolver = checkProblemFactChanges();
205205
}
206206
outerSolvingEnded(solverScope);
207-
return solverScope.cloneBestSolution();
207+
return solverScope.getBestSolution();
208208
}
209209

210210
public void outerSolvingStarted(SolverScope<Solution_> solverScope) {

core/src/main/java/ai/timefold/solver/core/impl/solver/event/SolverEventSupport.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,7 @@ public void fireBestSolutionChanged(SolverScope<Solution_> solverScope, Solution
2323
var timeMillisSpent = solverScope.getBestSolutionTimeMillisSpent();
2424
var bestScore = solverScope.getBestScore();
2525
if (it.hasNext()) {
26-
// We need to clone the new best solution, or we may share the same instance with user consumers.
27-
// Reusing the instance can lead to inconsistent states if intermediary consumers change the solution.
28-
var newBestSolutionCloned = solverScope.getScoreDirector().cloneSolution(newBestSolution);
29-
var event = new DefaultBestSolutionChangedEvent<>(solver, timeMillisSpent, newBestSolutionCloned, bestScore);
26+
var event = new DefaultBestSolutionChangedEvent<>(solver, timeMillisSpent, newBestSolution, bestScore);
3027
do {
3128
it.next().bestSolutionChanged(event);
3229
} while (it.hasNext());

core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -224,10 +224,6 @@ public Solution_ getBestSolution() {
224224
return bestSolution.get();
225225
}
226226

227-
public Solution_ cloneBestSolution() {
228-
return scoreDirector.cloneSolution(bestSolution.get());
229-
}
230-
231227
/**
232228
* The {@link PlanningSolution best solution} must never be the same instance
233229
* as the {@link PlanningSolution working solution}, it should be a (un)changed clone.
@@ -336,7 +332,7 @@ public long getMoveEvaluationSpeed() {
336332

337333
public void setWorkingSolutionFromBestSolution() {
338334
// The workingSolution must never be the same instance as the bestSolution.
339-
scoreDirector.setWorkingSolution(cloneBestSolution());
335+
scoreDirector.setWorkingSolution(scoreDirector.cloneSolution(getBestSolution()));
340336
}
341337

342338
public SolverScope<Solution_> createChildThreadSolverScope(ChildThreadType childThreadType) {

core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,7 @@ void solveWithInitializedSolution() {
9898
new TestdataEntity("e3", v3)));
9999

100100
var solution = PlannerTestUtils.solve(solverConfig, inputProblem, false);
101-
assertThat(inputProblem.getEntityList().stream().map(TestdataEntity::getValue).toList())
102-
.isEqualTo(solution.getEntityList().stream().map(TestdataEntity::getValue).toList());
101+
assertThat(inputProblem).isSameAs(solution);
103102
}
104103

105104
@Test

core/src/test/java/ai/timefold/solver/core/impl/solver/BestSolutionHolderTest.java

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,14 @@
1313
import java.util.UUID;
1414
import java.util.concurrent.CompletableFuture;
1515
import java.util.concurrent.CountDownLatch;
16-
import java.util.concurrent.ExecutionException;
1716
import java.util.concurrent.Executors;
1817

1918
import ai.timefold.solver.core.api.solver.Solver;
20-
import ai.timefold.solver.core.api.solver.SolverConfigOverride;
2119
import ai.timefold.solver.core.api.solver.SolverManager;
2220
import ai.timefold.solver.core.api.solver.change.ProblemChange;
2321
import ai.timefold.solver.core.api.solver.change.ProblemChangeDirector;
2422
import ai.timefold.solver.core.config.solver.SolverConfig;
2523
import ai.timefold.solver.core.config.solver.SolverManagerConfig;
26-
import ai.timefold.solver.core.config.solver.termination.TerminationConfig;
2724
import ai.timefold.solver.core.testdomain.TestdataEasyScoreCalculator;
2825
import ai.timefold.solver.core.testdomain.TestdataEntity;
2926
import ai.timefold.solver.core.testdomain.TestdataSolution;
@@ -189,36 +186,6 @@ void problemChangeBarrageIntermediateBestSolutionConsumer() throws InterruptedEx
189186

190187
}
191188

192-
@Test
193-
void testCloningBetweenPhases() throws ExecutionException, InterruptedException {
194-
var solverConfig = new SolverConfig()
195-
.withSolutionClass(TestdataSolution.class)
196-
.withEntityClasses(TestdataEntity.class)
197-
.withEasyScoreCalculatorClass(TestdataEasyScoreCalculator.class);
198-
199-
try (var solverManager = SolverManager.<TestdataSolution, UUID> create(solverConfig, new SolverManagerConfig())) {
200-
var problem = TestdataSolution.generateSolution(2, 2);
201-
problem.getEntityList().forEach(entity -> entity.setValue(null));
202-
var countDownLatch = new CountDownLatch(1);
203-
var solution = solverManager.solveBuilder()
204-
.withProblemId(UUID.randomUUID())
205-
.withProblem(problem)
206-
.withBestSolutionConsumer(
207-
intermediate -> {
208-
// Add a new entity to the cloned solution
209-
intermediate.getEntityList().add(new TestdataEntity("Bad Entity 3"));
210-
countDownLatch.countDown();
211-
})
212-
.withConfigOverride(new SolverConfigOverride<TestdataSolution>()
213-
.withTerminationConfig(new TerminationConfig().withMoveCountLimit(100L)))
214-
.run()
215-
.getFinalBestSolution();
216-
countDownLatch.await();
217-
// At this point, the events generated by the best solution consumer should not add any new entities
218-
assertThat(solution.getEntityList()).hasSize(2);
219-
}
220-
}
221-
222189
private record RecordedFuture(int id, CompletableFuture<Void> future) {
223190

224191
boolean isDone() {

docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,135 @@ The current `SolverManager` implementation runs on a single computer node,
764764
but future work aims to distribute solver loads across a cloud.
765765
766766
767+
[#solverManagerBuilder]
768+
== The `SolverManager` Builder
769+
770+
The `SolverManager` also enables the creation of a builder to customize and submit a planning problem for solving.
771+
772+
[tabs]
773+
====
774+
Java::
775+
+
776+
[source,java,options="nowrap"]
777+
----
778+
public interface SolverManager<Solution_> {
779+
780+
SolverJobBuilder<Solution_, ProblemId_> solveBuilder();
781+
782+
...
783+
}
784+
----
785+
786+
Python::
787+
+
788+
[source,python,options="nowrap"]
789+
----
790+
class SolverManager(Generic[Solution_, ProblemId_]):
791+
...
792+
def solve_builder(self) -> SolverJobBuilder[Solution_, ProblemId_]:
793+
...
794+
----
795+
====
796+
797+
=== Required settings
798+
799+
The `SolverJobBuilder` contract includes many optional methods, but only two are required: `withProblemId(...)` and `withProblem(...)`.
800+
801+
[tabs]
802+
====
803+
Java::
804+
+
805+
[source,java,options="nowrap"]
806+
----
807+
solverManager.solveBuilder()
808+
.withProblemId(problemId)
809+
.withProblem(problem)
810+
...
811+
----
812+
813+
Python::
814+
+
815+
[source,python,options="nowrap"]
816+
----
817+
solver_manager.solve_builder()
818+
.with_problem_id(problemId)
819+
.with_problem(problem)
820+
...
821+
----
822+
====
823+
824+
The job's unique ID is specified using `withProblemId(problemId)`.
825+
The provided ID allows for the identification of a specific problem,
826+
enabling actions such as checking the solving status or terminating its execution.
827+
In addition to the unique ID, we must specify the problem to solve using `withProblem(problem)`.
828+
829+
=== Optional settings
830+
831+
Additional optional methods are also included in the `SolverJobBuilder` contract:
832+
833+
[tabs]
834+
====
835+
Java::
836+
+
837+
[source,java,options="nowrap"]
838+
----
839+
solverManager.solveBuilder()
840+
.withProblemId(problemId)
841+
.withProblem(problem)
842+
.withFirstInitializedSolutionConsumer(firstInitializedSolutionConsumer)
843+
.withBestSolutionConsumer(bestSolutionConsumer)
844+
.withFinalBestSolutionConsumer(finalBestSolutionConsumer)
845+
.withExceptionHandler(exceptionHandler)
846+
.withConfigOverride(configOverride)
847+
...
848+
----
849+
850+
Python::
851+
+
852+
[source,python,options="nowrap"]
853+
----
854+
solver_manager.solve_builder()
855+
.with_problem_id(problemId)
856+
.with_problem(problem)
857+
.with_first_initialized_solution_consumer(on_firs_solution_changed)
858+
.with_best_solution_consumer(on_best_solution_changed)
859+
.with_final_best_solution_consumer(on_final_solution_changed)
860+
.with_exception_handler(on_exception_handler)
861+
.with_config_override(config_override)
862+
...
863+
----
864+
====
865+
866+
A consumer for the first initialized solution can be configured with `withFirstInitializedSolutionConsumer(...)`.
867+
The solution is returned by the last phase that immediately precedes the first local search phase.
868+
869+
Whenever a new best solution is generated by the solver,
870+
it can be consumed by configuring it with `withBestSolutionConsumer(...)`.
871+
The final best solution consumer,
872+
which is called at the end of the solving process,
873+
can be set using `withFinalBestSolutionConsumer(...)`.
874+
Additionally,
875+
an improved solution consumer capable of throttling events is available in the xref:enterprise-edition/enterprise-edition.adoc#throttlingBestSolutionEvents[Enterprise Edition].
876+
877+
[WARNING]
878+
====
879+
Do not modify the solutions returned by the events in `withFirstInitializedSolutionConsumer(...)` and `withBestSolutionConsumer(...)`. These instances are still utilized during the solving process, and any modifications may lead to solver corruption.
880+
====
881+
882+
To handle errors that may arise during the solving process,
883+
set up the handling logic by defining `withExceptionHandler(...)`.
884+
885+
Finally, to build an instance of the solver,
886+
xref:using-timefold-solver/configuration.adoc[a configuration step] is necessary.
887+
These settings are static and applied to any related solving execution.
888+
If you want to override certain settings for a particular job,
889+
such as the termination configuration, you can use the `withConfigOverride(...)` method.
890+
891+
[NOTE]
892+
====
893+
The solver also permits the configuration of multiple solver managers with distinct settings in xref:integration/integration.adoc#integrationWithQuarkusMultipleResources[Quarkus] or xref:integration/integration.adoc#integrationWithSpringBootMultipleResources[Spring Boot].
894+
====
895+
767896
[#solverManagerSolveBatch]
768897
=== Solve batch problems
769898

0 commit comments

Comments
 (0)