From 0c8ca1ec849957d16947f15b903807fe690e626e Mon Sep 17 00:00:00 2001 From: Fred Date: Thu, 12 Sep 2024 10:35:30 -0300 Subject: [PATCH 1/8] feat: new start solver job handler --- .../core/api/solver/SolverJobBuilder.java | 8 ++++++ .../core/impl/solver/ConsumerSupport.java | 10 ++++++- .../core/impl/solver/DefaultSolverJob.java | 28 +++++++++++++++++-- .../impl/solver/DefaultSolverJobBuilder.java | 13 +++++++-- .../impl/solver/DefaultSolverManager.java | 7 +++-- .../core/api/solver/SolverManagerTest.java | 22 +++++++++++++++ .../core/impl/solver/ConsumerSupportTest.java | 8 +++--- 7 files changed, 85 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/api/solver/SolverJobBuilder.java b/core/src/main/java/ai/timefold/solver/core/api/solver/SolverJobBuilder.java index 25b1fb9c1c..3c52a2b4a3 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/solver/SolverJobBuilder.java +++ b/core/src/main/java/ai/timefold/solver/core/api/solver/SolverJobBuilder.java @@ -81,6 +81,14 @@ default SolverJobBuilder withProblem(Solution_ problem) { SolverJobBuilder withFirstInitializedSolutionConsumer(Consumer firstInitializedSolutionConsumer); + /** + * Sets the runnable action for when the solver starts its solving process. + * + * @param startSolverJobHandler never null, called only once when the solver is starting the solving process + * @return this, never null + */ + SolverJobBuilder withStartSolverJobHandler(Runnable startSolverJobHandler); + /** * Sets the custom exception handler. * diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/ConsumerSupport.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/ConsumerSupport.java index 69a8d3d041..91fe6c7adb 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/ConsumerSupport.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/ConsumerSupport.java @@ -14,6 +14,7 @@ final class ConsumerSupport implements AutoCloseable { private final Consumer bestSolutionConsumer; private final Consumer finalBestSolutionConsumer; private final Consumer firstInitializedSolutionConsumer; + private final Runnable startSolverJobHandler; private final BiConsumer exceptionHandler; private final Semaphore activeConsumption = new Semaphore(1); private final Semaphore firstSolutionConsumption = new Semaphore(1); @@ -23,7 +24,7 @@ final class ConsumerSupport implements AutoCloseable { public ConsumerSupport(ProblemId_ problemId, Consumer bestSolutionConsumer, Consumer finalBestSolutionConsumer, Consumer firstInitializedSolutionConsumer, - BiConsumer exceptionHandler, + Runnable startSolverJobHandler, BiConsumer exceptionHandler, BestSolutionHolder bestSolutionHolder) { this.problemId = problemId; this.bestSolutionConsumer = bestSolutionConsumer; @@ -33,6 +34,8 @@ public ConsumerSupport(ProblemId_ problemId, Consumer bestSol this.exceptionHandler = exceptionHandler; this.bestSolutionHolder = bestSolutionHolder; this.firstInitializedSolution = null; + this.startSolverJobHandler = startSolverJobHandler != null ? startSolverJobHandler : () -> { + }; } // Called on the Solver thread. @@ -62,6 +65,11 @@ void consumeFirstInitializedSolution(Solution_ firstInitializedSolution) { scheduleFirstInitializedSolutionConsumption(); } + // Called on the solver thread when it starts + void triggerStartSolverJob() { + startSolverJobHandler.run(); + } + // Called on the Solver thread after Solver#solve() returns. void consumeFinalBestSolution(Solution_ finalBestSolution) { try { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java index 051718d0f4..9c4b51d77f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java @@ -46,6 +46,7 @@ public final class DefaultSolverJob implements SolverJob< private final Consumer bestSolutionConsumer; private final Consumer finalBestSolutionConsumer; private final Consumer firstInitializedSolutionConsumer; + private final Runnable startSolverJobHandler; private final BiConsumer exceptionHandler; private volatile SolverStatus solverStatus; @@ -63,7 +64,7 @@ public DefaultSolverJob( Consumer bestSolutionConsumer, Consumer finalBestSolutionConsumer, Consumer firstInitializedSolutionConsumer, - BiConsumer exceptionHandler) { + Runnable startSolverJobHandler, BiConsumer exceptionHandler) { this.solverManager = solverManager; this.problemId = problemId; if (!(solver instanceof DefaultSolver)) { @@ -75,6 +76,7 @@ public DefaultSolverJob( this.bestSolutionConsumer = bestSolutionConsumer; this.finalBestSolutionConsumer = finalBestSolutionConsumer; this.firstInitializedSolutionConsumer = firstInitializedSolutionConsumer; + this.startSolverJobHandler = startSolverJobHandler; this.exceptionHandler = exceptionHandler; solverStatus = SolverStatus.SOLVING_SCHEDULED; terminatedLatch = new CountDownLatch(1); @@ -108,13 +110,15 @@ public Solution_ call() { solverStatus = SolverStatus.SOLVING_ACTIVE; // Create the consumer thread pool only when this solver job is active. consumerSupport = new ConsumerSupport<>(getProblemId(), bestSolutionConsumer, finalBestSolutionConsumer, - firstInitializedSolutionConsumer, exceptionHandler, bestSolutionHolder); + firstInitializedSolutionConsumer, startSolverJobHandler, exceptionHandler, bestSolutionHolder); Solution_ problem = problemFinder.apply(problemId); // add a phase lifecycle listener that unlock the solver status lock when solving started solver.addPhaseLifecycleListener(new UnlockLockPhaseLifecycleListener()); // add a phase lifecycle listener that consumes the first initialized solution solver.addPhaseLifecycleListener(new FirstInitializedSolutionPhaseLifecycleListener(consumerSupport)); + // add a phase lifecycle listener once when the solver starts its execution + solver.addPhaseLifecycleListener(new StartSolverJobPhaseLifecycleListener(consumerSupport)); solver.addEventListener(this::onBestSolutionChangedEvent); final Solution_ finalBestSolution = solver.solve(problem); consumerSupport.consumeFinalBestSolution(finalBestSolution); @@ -306,4 +310,24 @@ public void phaseEnded(AbstractPhaseScope phaseScope) { } } } + + /** + * A listener that is triggered once when the solver starts the solving process. + */ + private final class StartSolverJobPhaseLifecycleListener extends PhaseLifecycleListenerAdapter { + + private final ConsumerSupport consumerSupport; + + public StartSolverJobPhaseLifecycleListener(ConsumerSupport consumerSupport) { + this.consumerSupport = consumerSupport; + } + + @Override + public void phaseStarted(AbstractPhaseScope phaseScope) { + // The event is triggered once in the first phase of the solving process + if (phaseScope.getPhaseIndex() == 0) { + consumerSupport.triggerStartSolverJob(); + } + } + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJobBuilder.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJobBuilder.java index 395ab814cb..661e24869c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJobBuilder.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJobBuilder.java @@ -23,6 +23,7 @@ public final class DefaultSolverJobBuilder implements Sol private Consumer bestSolutionConsumer; private Consumer finalBestSolutionConsumer; private Consumer initializedSolutionConsumer; + private Runnable startSolverJobHandler; private BiConsumer exceptionHandler; private SolverConfigOverride solverConfigOverride; @@ -66,6 +67,14 @@ public SolverJobBuilder withBestSolutionConsumer(Consumer return this; } + @Override + public SolverJobBuilder + withStartSolverJobHandler(Runnable startSolverJobHandler) { + this.startSolverJobHandler = Objects.requireNonNull(startSolverJobHandler, + "Invalid startSolverJobHandler (null) given to SolverJobBuilder."); + return this; + } + @Override public SolverJobBuilder withExceptionHandler(BiConsumer exceptionHandler) { @@ -90,10 +99,10 @@ public SolverJob run() { if (this.bestSolutionConsumer == null) { return solverManager.solve(problemId, problemFinder, null, finalBestSolutionConsumer, - initializedSolutionConsumer, exceptionHandler, solverConfigOverride); + initializedSolutionConsumer, startSolverJobHandler, exceptionHandler, solverConfigOverride); } else { return solverManager.solveAndListen(problemId, problemFinder, bestSolutionConsumer, finalBestSolutionConsumer, - initializedSolutionConsumer, exceptionHandler, solverConfigOverride); + initializedSolutionConsumer, startSolverJobHandler, exceptionHandler, solverConfigOverride); } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverManager.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverManager.java index bc3842d82f..963d2a2958 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverManager.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverManager.java @@ -83,13 +83,14 @@ protected SolverJob solveAndListen(ProblemId_ problemId, Consumer bestSolutionConsumer, Consumer finalBestSolutionConsumer, Consumer initializedSolutionConsumer, + Runnable startSolverJobHandler, BiConsumer exceptionHandler, SolverConfigOverride solverConfigOverride) { if (bestSolutionConsumer == null) { throw new IllegalStateException("The consumer bestSolutionConsumer is required."); } return solve(getProblemIdOrThrow(problemId), problemFinder, bestSolutionConsumer, finalBestSolutionConsumer, - initializedSolutionConsumer, exceptionHandler, solverConfigOverride); + initializedSolutionConsumer, startSolverJobHandler, exceptionHandler, solverConfigOverride); } protected SolverJob solve(ProblemId_ problemId, @@ -97,6 +98,7 @@ protected SolverJob solve(ProblemId_ problemId, Consumer bestSolutionConsumer, Consumer finalBestSolutionConsumer, Consumer initializedSolutionConsumer, + Runnable startSolverJobHandler, BiConsumer exceptionHandler, SolverConfigOverride configOverride) { Solver solver = solverFactory.buildSolver(configOverride); @@ -111,7 +113,8 @@ protected SolverJob solve(ProblemId_ problemId, throw new IllegalStateException("The problemId (" + problemId + ") is already solving."); } else { return new DefaultSolverJob<>(this, solver, problemId, problemFinder, bestSolutionConsumer, - finalBestSolutionConsumer, initializedSolutionConsumer, finalExceptionHandler); + finalBestSolutionConsumer, initializedSolutionConsumer, startSolverJobHandler, + finalExceptionHandler); } }); Future future = solverThreadPool.submit(solverJob); diff --git a/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java b/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java index c82fe6483e..2e7e6ead8a 100644 --- a/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java @@ -461,6 +461,28 @@ void firstInitializedSolutionConsumerWith2Custom() throws ExecutionException, In assertThat(hasInitializedSolution.booleanValue()).isFalse(); } + @Test + @Timeout(60) + void testStartJobRunnable() throws ExecutionException, InterruptedException { + SolverConfig solverConfig = PlannerTestUtils + .buildSolverConfig(TestdataSolution.class, TestdataEntity.class); + solverManager = SolverManager + .create(solverConfig, new SolverManagerConfig()); + + Function problemFinder = o -> new TestdataUnannotatedExtendedSolution( + PlannerTestUtils.generateTestdataSolution("s1")); + + MutableObject started = new MutableObject<>(Boolean.FALSE); + + SolverJob solverJob = solverManager.solveBuilder() + .withProblemId(1L) + .withProblemFinder(problemFinder) + .withStartSolverJobHandler(() -> started.setValue(Boolean.TRUE)) + .run(); + solverJob.getFinalBestSolution(); + assertThat(started.getValue()).isTrue(); + } + @Test void solveWithOverride() { // Default spent limit is 1L diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/ConsumerSupportTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/ConsumerSupportTest.java index e67573db67..6317228bd5 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/ConsumerSupportTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/ConsumerSupportTest.java @@ -52,7 +52,7 @@ void skipAhead() throws InterruptedException { } catch (InterruptedException e) { error.set(new IllegalStateException("Interrupted waiting.", e)); } - }, null, null, null, bestSolutionHolder); + }, null, null, null, null, bestSolutionHolder); consumeIntermediateBestSolution(TestdataSolution.generateSolution(1, 1)); consumptionStarted.await(); @@ -78,7 +78,7 @@ void problemChangesComplete_afterFinalBestSolutionIsConsumed() throws ExecutionE BestSolutionHolder bestSolutionHolder = new BestSolutionHolder<>(); AtomicReference finalBestSolutionRef = new AtomicReference<>(); consumerSupport = new ConsumerSupport<>(1L, null, - finalBestSolution -> finalBestSolutionRef.set(finalBestSolution), null, null, bestSolutionHolder); + finalBestSolution -> finalBestSolutionRef.set(finalBestSolution), null, null, null, bestSolutionHolder); CompletableFuture futureProblemChange = addProblemChange(bestSolutionHolder); @@ -99,7 +99,7 @@ void problemChangesCompleteExceptionally_afterExceptionInConsumer() { Consumer errorneousConsumer = bestSolution -> { throw new RuntimeException(errorMessage); }; - consumerSupport = new ConsumerSupport<>(1L, errorneousConsumer, null, null, null, bestSolutionHolder); + consumerSupport = new ConsumerSupport<>(1L, errorneousConsumer, null, null, null, null, bestSolutionHolder); CompletableFuture futureProblemChange = addProblemChange(bestSolutionHolder); consumeIntermediateBestSolution(TestdataSolution.generateSolution()); @@ -116,7 +116,7 @@ void problemChangesCompleteExceptionally_afterExceptionInConsumer() { void pendingProblemChangesAreCanceled_afterFinalBestSolutionIsConsumed() throws ExecutionException, InterruptedException { BestSolutionHolder bestSolutionHolder = new BestSolutionHolder<>(); consumerSupport = new ConsumerSupport<>(1L, null, null, - null, null, bestSolutionHolder); + null, null, null, bestSolutionHolder); CompletableFuture futureProblemChange = addProblemChange(bestSolutionHolder); From d671a234e19bf70a88a49c39d75dd72314813c38 Mon Sep 17 00:00:00 2001 From: Fred Date: Thu, 12 Sep 2024 14:20:06 -0300 Subject: [PATCH 2/8] feat: new start solver job handler for python --- .../src/main/python/_solver_manager.py | 43 +++++++++++++++++++ .../python-core/tests/test_solver_manager.py | 15 ++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/python/python-core/src/main/python/_solver_manager.py b/python/python-core/src/main/python/_solver_manager.py index 38eb0fcf0c..ca25308c94 100644 --- a/python/python-core/src/main/python/_solver_manager.py +++ b/python/python-core/src/main/python/_solver_manager.py @@ -285,6 +285,49 @@ def with_final_best_solution_consumer(self, final_best_solution_consumer: Callab return SolverJobBuilder( self._delegate.withFinalBestSolutionConsumer(java_consumer)) + def with_first_initialized_solution_consumer(self, first_initialized_solution_consumer: Callable[[Solution_], None]) -> 'SolverJobBuilder': + """ + Sets the consumer of the first initialized solution. First initialized solution is the solution at the end of + the last phase that immediately precedes the first local search phase. This solution marks the beginning of + actual optimization process. + + Parameters + ---------- + first_initialized_solution_consumer : Callable[[Solution_], None] + called only once before starting the first Local Search phase + + Returns + ------- + SolverJobBuilder + This `SolverJobBuilder`. + """ + from java.util.function import Consumer + from _jpyinterpreter import unwrap_python_like_object + + java_consumer = Consumer @ (lambda solution: first_initialized_solution_consumer(unwrap_python_like_object(solution))) + return SolverJobBuilder( + self._delegate.withFirstInitializedSolutionConsumer(java_consumer)) + + def with_start_solver_job_handler(self, start_solver_job_handler: Callable[[], None]) -> 'SolverJobBuilder': + """ + Sets the runnable action for when the solver starts its solving process. + + Parameters + ---------- + start_solver_job_handler : Callable[[], None] + called only once when the solver is starting the solving process + + Returns + ------- + SolverJobBuilder + This `SolverJobBuilder`. + """ + from java.lang import Runnable + + java_runnable = Runnable @ (lambda: start_solver_job_handler()) + return SolverJobBuilder( + self._delegate.withStartSolverJobHandler(java_runnable)) + def with_exception_handler(self, exception_handler: Callable[[ProblemId_, Exception], None]) -> 'SolverJobBuilder': """ Sets the custom exception handler. diff --git a/python/python-core/tests/test_solver_manager.py b/python/python-core/tests/test_solver_manager.py index 6c6c90201d..e57cf707c8 100644 --- a/python/python-core/tests/test_solver_manager.py +++ b/python/python-core/tests/test_solver_manager.py @@ -244,7 +244,7 @@ def my_exception_handler(problem_id, exception): assert the_exception is not None -def test_solver_config_override(): +def test_solver_config(): @dataclass class Value: value: Annotated[int, PlanningId] @@ -285,6 +285,15 @@ class Solution: ) problem: Solution = Solution([Entity('A')], [Value(1), Value(2), Value(3)], SimpleScore.ONE) + first_initialized_solution_consumer_called = [] + start_solver_job_handler_called = [] + + def on_first_initialized_solution_consumer(solution): + first_initialized_solution_consumer_called.append(1) + + def on_start_solver_job_handler(): + start_solver_job_handler_called.append(1) + with SolverManager.create(SolverFactory.create(solver_config)) as solver_manager: solver_job = (solver_manager.solve_builder() .with_problem_id(1) @@ -294,7 +303,11 @@ class Solution: best_score_limit='3' ) )) + .with_first_initialized_solution_consumer(on_first_initialized_solution_consumer) + .with_start_solver_job_handler(on_start_solver_job_handler) .run()) solution = solver_job.get_final_best_solution() assert solution.score.score == 3 + assert len(first_initialized_solution_consumer_called) == 1 + assert len(start_solver_job_handler_called) == 1 \ No newline at end of file From 1f6d5a25b54ad11229ac4209c336298c24997f0d Mon Sep 17 00:00:00 2001 From: Fred Date: Mon, 16 Sep 2024 13:31:47 -0300 Subject: [PATCH 3/8] feat: start solver job consumer improvement --- .../core/api/solver/SolverJobBuilder.java | 6 +-- .../core/impl/solver/ConsumerSupport.java | 51 ++++++++++++++----- .../core/impl/solver/DefaultSolverJob.java | 11 ++-- .../impl/solver/DefaultSolverJobBuilder.java | 10 ++-- .../impl/solver/DefaultSolverManager.java | 8 +-- .../core/api/solver/SolverManagerTest.java | 4 +- .../src/main/python/_solver_manager.py | 13 ++--- .../python-core/tests/test_solver_manager.py | 10 ++-- 8 files changed, 71 insertions(+), 42 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/api/solver/SolverJobBuilder.java b/core/src/main/java/ai/timefold/solver/core/api/solver/SolverJobBuilder.java index 3c52a2b4a3..da13a18977 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/solver/SolverJobBuilder.java +++ b/core/src/main/java/ai/timefold/solver/core/api/solver/SolverJobBuilder.java @@ -82,12 +82,12 @@ default SolverJobBuilder withProblem(Solution_ problem) { withFirstInitializedSolutionConsumer(Consumer firstInitializedSolutionConsumer); /** - * Sets the runnable action for when the solver starts its solving process. + * Sets the consumer for when the solver starts its solving process. * - * @param startSolverJobHandler never null, called only once when the solver is starting the solving process + * @param startSolverJobConsumer never null, called only once when the solver is starting the solving process * @return this, never null */ - SolverJobBuilder withStartSolverJobHandler(Runnable startSolverJobHandler); + SolverJobBuilder withStartSolverJobConsumer(Consumer startSolverJobConsumer); /** * Sets the custom exception handler. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/ConsumerSupport.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/ConsumerSupport.java index 91fe6c7adb..e50bb03beb 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/ConsumerSupport.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/ConsumerSupport.java @@ -14,17 +14,20 @@ final class ConsumerSupport implements AutoCloseable { private final Consumer bestSolutionConsumer; private final Consumer finalBestSolutionConsumer; private final Consumer firstInitializedSolutionConsumer; - private final Runnable startSolverJobHandler; + private final Consumer startSolverJobConsumer; private final BiConsumer exceptionHandler; private final Semaphore activeConsumption = new Semaphore(1); private final Semaphore firstSolutionConsumption = new Semaphore(1); + private final Semaphore startSolverJobConsumption = new Semaphore(1); private final BestSolutionHolder bestSolutionHolder; private final ExecutorService consumerExecutor = Executors.newSingleThreadExecutor(); private Solution_ firstInitializedSolution; + private Solution_ initialSolution; public ConsumerSupport(ProblemId_ problemId, Consumer bestSolutionConsumer, Consumer finalBestSolutionConsumer, Consumer firstInitializedSolutionConsumer, - Runnable startSolverJobHandler, BiConsumer exceptionHandler, + Consumer startSolverJobConsumer, + BiConsumer exceptionHandler, BestSolutionHolder bestSolutionHolder) { this.problemId = problemId; this.bestSolutionConsumer = bestSolutionConsumer; @@ -34,8 +37,7 @@ public ConsumerSupport(ProblemId_ problemId, Consumer bestSol this.exceptionHandler = exceptionHandler; this.bestSolutionHolder = bestSolutionHolder; this.firstInitializedSolution = null; - this.startSolverJobHandler = startSolverJobHandler != null ? startSolverJobHandler : () -> { - }; + this.startSolverJobConsumer = startSolverJobConsumer; } // Called on the Solver thread. @@ -65,9 +67,19 @@ void consumeFirstInitializedSolution(Solution_ firstInitializedSolution) { scheduleFirstInitializedSolutionConsumption(); } - // Called on the solver thread when it starts - void triggerStartSolverJob() { - startSolverJobHandler.run(); + // Called on the consumer thread + void consumeStartSolverJob(Solution_ initialSolution) { + try { + // Called on the solver thread + // During the solving process, this lock is called once, and it won't block the Solver thread + startSolverJobConsumption.acquire(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted when waiting for the start solver job consumption."); + } + // called on the Consumer thread + this.initialSolution = initialSolution; + scheduleStartJobConsumption(); } // Called on the Solver thread after Solver#solve() returns. @@ -76,6 +88,8 @@ void consumeFinalBestSolution(Solution_ finalBestSolution) { // Wait for the previous consumption to complete. // As the solver has already finished, holding the solver thread is not an issue. activeConsumption.acquire(); + // Wait for the start job event to complete + startSolverJobConsumption.acquire(); // Wait for the first solution consumption to complete firstSolutionConsumption.acquire(); } catch (InterruptedException e) { @@ -143,21 +157,34 @@ private CompletableFuture scheduleIntermediateBestSolutionConsumption() { /** * Called on the Consumer thread. - * Don't call without locking firstSolutionConsumption, because the consumption may not be executed before the final best - * solution is executed. + * Don't call without locking firstSolutionConsumption, + * because the consumption may not be executed before the final best solution is executed. */ private void scheduleFirstInitializedSolutionConsumption() { + scheduleConsumption(firstSolutionConsumption, firstInitializedSolutionConsumer, firstInitializedSolution); + } + + /** + * Called on the Consumer thread. + * Don't call without locking startSolverJobConsumption, + * because the consumption may not be executed before the final best solution is executed. + */ + private void scheduleStartJobConsumption() { + scheduleConsumption(startSolverJobConsumption, startSolverJobConsumer, initialSolution); + } + + private void scheduleConsumption(Semaphore semaphore, Consumer consumer, Solution_ solution) { CompletableFuture.runAsync(() -> { try { - if (firstInitializedSolutionConsumer != null && firstInitializedSolution != null) { - firstInitializedSolutionConsumer.accept(firstInitializedSolution); + if (consumer != null && solution != null) { + consumer.accept(solution); } } catch (Throwable throwable) { if (exceptionHandler != null) { exceptionHandler.accept(problemId, throwable); } } finally { - firstSolutionConsumption.release(); + semaphore.release(); } }, consumerExecutor); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java index 9c4b51d77f..aa5439a5a0 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java @@ -46,7 +46,7 @@ public final class DefaultSolverJob implements SolverJob< private final Consumer bestSolutionConsumer; private final Consumer finalBestSolutionConsumer; private final Consumer firstInitializedSolutionConsumer; - private final Runnable startSolverJobHandler; + private final Consumer startSolverJobConsumer; private final BiConsumer exceptionHandler; private volatile SolverStatus solverStatus; @@ -64,7 +64,8 @@ public DefaultSolverJob( Consumer bestSolutionConsumer, Consumer finalBestSolutionConsumer, Consumer firstInitializedSolutionConsumer, - Runnable startSolverJobHandler, BiConsumer exceptionHandler) { + Consumer startSolverJobConsumer, + BiConsumer exceptionHandler) { this.solverManager = solverManager; this.problemId = problemId; if (!(solver instanceof DefaultSolver)) { @@ -76,7 +77,7 @@ public DefaultSolverJob( this.bestSolutionConsumer = bestSolutionConsumer; this.finalBestSolutionConsumer = finalBestSolutionConsumer; this.firstInitializedSolutionConsumer = firstInitializedSolutionConsumer; - this.startSolverJobHandler = startSolverJobHandler; + this.startSolverJobConsumer = startSolverJobConsumer; this.exceptionHandler = exceptionHandler; solverStatus = SolverStatus.SOLVING_SCHEDULED; terminatedLatch = new CountDownLatch(1); @@ -110,7 +111,7 @@ public Solution_ call() { solverStatus = SolverStatus.SOLVING_ACTIVE; // Create the consumer thread pool only when this solver job is active. consumerSupport = new ConsumerSupport<>(getProblemId(), bestSolutionConsumer, finalBestSolutionConsumer, - firstInitializedSolutionConsumer, startSolverJobHandler, exceptionHandler, bestSolutionHolder); + firstInitializedSolutionConsumer, startSolverJobConsumer, exceptionHandler, bestSolutionHolder); Solution_ problem = problemFinder.apply(problemId); // add a phase lifecycle listener that unlock the solver status lock when solving started @@ -326,7 +327,7 @@ public StartSolverJobPhaseLifecycleListener(ConsumerSupport phaseScope) { // The event is triggered once in the first phase of the solving process if (phaseScope.getPhaseIndex() == 0) { - consumerSupport.triggerStartSolverJob(); + consumerSupport.consumeStartSolverJob(phaseScope.getWorkingSolution()); } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJobBuilder.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJobBuilder.java index 661e24869c..3a2e594cac 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJobBuilder.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJobBuilder.java @@ -23,7 +23,7 @@ public final class DefaultSolverJobBuilder implements Sol private Consumer bestSolutionConsumer; private Consumer finalBestSolutionConsumer; private Consumer initializedSolutionConsumer; - private Runnable startSolverJobHandler; + private Consumer startSolverJobConsumer; private BiConsumer exceptionHandler; private SolverConfigOverride solverConfigOverride; @@ -69,8 +69,8 @@ public SolverJobBuilder withBestSolutionConsumer(Consumer @Override public SolverJobBuilder - withStartSolverJobHandler(Runnable startSolverJobHandler) { - this.startSolverJobHandler = Objects.requireNonNull(startSolverJobHandler, + withStartSolverJobConsumer(Consumer startSolverJobConsumer) { + this.startSolverJobConsumer = Objects.requireNonNull(startSolverJobConsumer, "Invalid startSolverJobHandler (null) given to SolverJobBuilder."); return this; } @@ -99,10 +99,10 @@ public SolverJob run() { if (this.bestSolutionConsumer == null) { return solverManager.solve(problemId, problemFinder, null, finalBestSolutionConsumer, - initializedSolutionConsumer, startSolverJobHandler, exceptionHandler, solverConfigOverride); + initializedSolutionConsumer, startSolverJobConsumer, exceptionHandler, solverConfigOverride); } else { return solverManager.solveAndListen(problemId, problemFinder, bestSolutionConsumer, finalBestSolutionConsumer, - initializedSolutionConsumer, startSolverJobHandler, exceptionHandler, solverConfigOverride); + initializedSolutionConsumer, startSolverJobConsumer, exceptionHandler, solverConfigOverride); } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverManager.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverManager.java index 963d2a2958..7c78a250ae 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverManager.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverManager.java @@ -83,14 +83,14 @@ protected SolverJob solveAndListen(ProblemId_ problemId, Consumer bestSolutionConsumer, Consumer finalBestSolutionConsumer, Consumer initializedSolutionConsumer, - Runnable startSolverJobHandler, + Consumer startSolverJobConsumer, BiConsumer exceptionHandler, SolverConfigOverride solverConfigOverride) { if (bestSolutionConsumer == null) { throw new IllegalStateException("The consumer bestSolutionConsumer is required."); } return solve(getProblemIdOrThrow(problemId), problemFinder, bestSolutionConsumer, finalBestSolutionConsumer, - initializedSolutionConsumer, startSolverJobHandler, exceptionHandler, solverConfigOverride); + initializedSolutionConsumer, startSolverJobConsumer, exceptionHandler, solverConfigOverride); } protected SolverJob solve(ProblemId_ problemId, @@ -98,7 +98,7 @@ protected SolverJob solve(ProblemId_ problemId, Consumer bestSolutionConsumer, Consumer finalBestSolutionConsumer, Consumer initializedSolutionConsumer, - Runnable startSolverJobHandler, + Consumer startSolverJobConsumer, BiConsumer exceptionHandler, SolverConfigOverride configOverride) { Solver solver = solverFactory.buildSolver(configOverride); @@ -113,7 +113,7 @@ protected SolverJob solve(ProblemId_ problemId, throw new IllegalStateException("The problemId (" + problemId + ") is already solving."); } else { return new DefaultSolverJob<>(this, solver, problemId, problemFinder, bestSolutionConsumer, - finalBestSolutionConsumer, initializedSolutionConsumer, startSolverJobHandler, + finalBestSolutionConsumer, initializedSolutionConsumer, startSolverJobConsumer, finalExceptionHandler); } }); diff --git a/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java b/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java index 2e7e6ead8a..061987284e 100644 --- a/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java @@ -463,7 +463,7 @@ void firstInitializedSolutionConsumerWith2Custom() throws ExecutionException, In @Test @Timeout(60) - void testStartJobRunnable() throws ExecutionException, InterruptedException { + void testStartJobConsumer() throws ExecutionException, InterruptedException { SolverConfig solverConfig = PlannerTestUtils .buildSolverConfig(TestdataSolution.class, TestdataEntity.class); solverManager = SolverManager @@ -477,7 +477,7 @@ void testStartJobRunnable() throws ExecutionException, InterruptedException { SolverJob solverJob = solverManager.solveBuilder() .withProblemId(1L) .withProblemFinder(problemFinder) - .withStartSolverJobHandler(() -> started.setValue(Boolean.TRUE)) + .withStartSolverJobConsumer((solution) -> started.setValue(Boolean.TRUE)) .run(); solverJob.getFinalBestSolution(); assertThat(started.getValue()).isTrue(); diff --git a/python/python-core/src/main/python/_solver_manager.py b/python/python-core/src/main/python/_solver_manager.py index ca25308c94..64ea7e16f9 100644 --- a/python/python-core/src/main/python/_solver_manager.py +++ b/python/python-core/src/main/python/_solver_manager.py @@ -308,13 +308,13 @@ def with_first_initialized_solution_consumer(self, first_initialized_solution_co return SolverJobBuilder( self._delegate.withFirstInitializedSolutionConsumer(java_consumer)) - def with_start_solver_job_handler(self, start_solver_job_handler: Callable[[], None]) -> 'SolverJobBuilder': + def with_start_solver_job_consumer(self, start_solver_job_consumer: Callable[[Solution_], None]) -> 'SolverJobBuilder': """ - Sets the runnable action for when the solver starts its solving process. + Sets the consumer for when the solver starts its solving process. Parameters ---------- - start_solver_job_handler : Callable[[], None] + start_solver_job_consumer : Callable[[Solution_], None] called only once when the solver is starting the solving process Returns @@ -322,11 +322,12 @@ def with_start_solver_job_handler(self, start_solver_job_handler: Callable[[], N SolverJobBuilder This `SolverJobBuilder`. """ - from java.lang import Runnable + from java.util.function import Consumer + from _jpyinterpreter import unwrap_python_like_object - java_runnable = Runnable @ (lambda: start_solver_job_handler()) + java_consumer = Consumer @ (lambda solution: start_solver_job_consumer(unwrap_python_like_object(solution))) return SolverJobBuilder( - self._delegate.withStartSolverJobHandler(java_runnable)) + self._delegate.withStartSolverJobConsumer(java_consumer)) def with_exception_handler(self, exception_handler: Callable[[ProblemId_, Exception], None]) -> 'SolverJobBuilder': """ diff --git a/python/python-core/tests/test_solver_manager.py b/python/python-core/tests/test_solver_manager.py index e57cf707c8..335ef38f87 100644 --- a/python/python-core/tests/test_solver_manager.py +++ b/python/python-core/tests/test_solver_manager.py @@ -286,13 +286,13 @@ class Solution: problem: Solution = Solution([Entity('A')], [Value(1), Value(2), Value(3)], SimpleScore.ONE) first_initialized_solution_consumer_called = [] - start_solver_job_handler_called = [] + start_solver_job_consumer_called = [] def on_first_initialized_solution_consumer(solution): first_initialized_solution_consumer_called.append(1) - def on_start_solver_job_handler(): - start_solver_job_handler_called.append(1) + def on_start_solver_job_consumer(solution): + start_solver_job_consumer_called.append(1) with SolverManager.create(SolverFactory.create(solver_config)) as solver_manager: solver_job = (solver_manager.solve_builder() @@ -304,10 +304,10 @@ def on_start_solver_job_handler(): ) )) .with_first_initialized_solution_consumer(on_first_initialized_solution_consumer) - .with_start_solver_job_handler(on_start_solver_job_handler) + .with_start_solver_job_consumer(on_start_solver_job_consumer) .run()) solution = solver_job.get_final_best_solution() assert solution.score.score == 3 assert len(first_initialized_solution_consumer_called) == 1 - assert len(start_solver_job_handler_called) == 1 \ No newline at end of file + assert len(start_solver_job_consumer_called) == 1 \ No newline at end of file From 5413c1e35586afc0e584f0d6e3ed33e822323835 Mon Sep 17 00:00:00 2001 From: Fred Date: Tue, 17 Sep 2024 07:50:59 -0300 Subject: [PATCH 4/8] chore: address PR comments --- .../solver/core/impl/solver/DefaultSolverJob.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java index aa5439a5a0..bb7b951f92 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java @@ -25,6 +25,7 @@ import ai.timefold.solver.core.impl.phase.AbstractPhase; import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListenerAdapter; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; +import ai.timefold.solver.core.impl.solver.event.SolverLifecycleListenerAdapter; import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.impl.solver.termination.Termination; @@ -324,11 +325,8 @@ public StartSolverJobPhaseLifecycleListener(ConsumerSupport phaseScope) { - // The event is triggered once in the first phase of the solving process - if (phaseScope.getPhaseIndex() == 0) { - consumerSupport.consumeStartSolverJob(phaseScope.getWorkingSolution()); - } + public void solvingStarted(SolverScope solverScope) { + consumerSupport.consumeStartSolverJob(solverScope.getWorkingSolution()); } } } From 8bf70e217a37c759fc45a73b582cf478f94d4679 Mon Sep 17 00:00:00 2001 From: Fred Date: Tue, 17 Sep 2024 08:05:31 -0300 Subject: [PATCH 5/8] chore: address PR comments --- .../solver/core/api/solver/SolverJobBuilder.java | 4 ++-- .../solver/core/impl/solver/ConsumerSupport.java | 8 ++++---- .../solver/core/impl/solver/DefaultSolverJob.java | 9 ++++----- .../core/impl/solver/DefaultSolverJobBuilder.java | 10 +++++----- .../core/impl/solver/DefaultSolverManager.java | 8 ++++---- .../solver/core/api/solver/SolverManagerTest.java | 7 ++++--- .../python-core/src/main/python/_solver_manager.py | 8 ++++---- python/python-core/tests/test_solver_manager.py | 12 ++++++------ 8 files changed, 33 insertions(+), 33 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/api/solver/SolverJobBuilder.java b/core/src/main/java/ai/timefold/solver/core/api/solver/SolverJobBuilder.java index da13a18977..6a3f9dcba8 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/solver/SolverJobBuilder.java +++ b/core/src/main/java/ai/timefold/solver/core/api/solver/SolverJobBuilder.java @@ -84,10 +84,10 @@ default SolverJobBuilder withProblem(Solution_ problem) { /** * Sets the consumer for when the solver starts its solving process. * - * @param startSolverJobConsumer never null, called only once when the solver is starting the solving process + * @param solverJobStartedConsumer never null, called only once when the solver is starting the solving process * @return this, never null */ - SolverJobBuilder withStartSolverJobConsumer(Consumer startSolverJobConsumer); + SolverJobBuilder withSolverJobStartedConsumer(Consumer solverJobStartedConsumer); /** * Sets the custom exception handler. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/ConsumerSupport.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/ConsumerSupport.java index e50bb03beb..a4894f7d65 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/ConsumerSupport.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/ConsumerSupport.java @@ -14,7 +14,7 @@ final class ConsumerSupport implements AutoCloseable { private final Consumer bestSolutionConsumer; private final Consumer finalBestSolutionConsumer; private final Consumer firstInitializedSolutionConsumer; - private final Consumer startSolverJobConsumer; + private final Consumer solverJobStartedConsumer; private final BiConsumer exceptionHandler; private final Semaphore activeConsumption = new Semaphore(1); private final Semaphore firstSolutionConsumption = new Semaphore(1); @@ -26,7 +26,7 @@ final class ConsumerSupport implements AutoCloseable { public ConsumerSupport(ProblemId_ problemId, Consumer bestSolutionConsumer, Consumer finalBestSolutionConsumer, Consumer firstInitializedSolutionConsumer, - Consumer startSolverJobConsumer, + Consumer solverJobStartedConsumer, BiConsumer exceptionHandler, BestSolutionHolder bestSolutionHolder) { this.problemId = problemId; @@ -37,7 +37,7 @@ public ConsumerSupport(ProblemId_ problemId, Consumer bestSol this.exceptionHandler = exceptionHandler; this.bestSolutionHolder = bestSolutionHolder; this.firstInitializedSolution = null; - this.startSolverJobConsumer = startSolverJobConsumer; + this.solverJobStartedConsumer = solverJobStartedConsumer; } // Called on the Solver thread. @@ -170,7 +170,7 @@ private void scheduleFirstInitializedSolutionConsumption() { * because the consumption may not be executed before the final best solution is executed. */ private void scheduleStartJobConsumption() { - scheduleConsumption(startSolverJobConsumption, startSolverJobConsumer, initialSolution); + scheduleConsumption(startSolverJobConsumption, solverJobStartedConsumer, initialSolution); } private void scheduleConsumption(Semaphore semaphore, Consumer consumer, Solution_ solution) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java index bb7b951f92..1cbb12a37e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java @@ -25,7 +25,6 @@ import ai.timefold.solver.core.impl.phase.AbstractPhase; import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListenerAdapter; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; -import ai.timefold.solver.core.impl.solver.event.SolverLifecycleListenerAdapter; import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.impl.solver.termination.Termination; @@ -47,7 +46,7 @@ public final class DefaultSolverJob implements SolverJob< private final Consumer bestSolutionConsumer; private final Consumer finalBestSolutionConsumer; private final Consumer firstInitializedSolutionConsumer; - private final Consumer startSolverJobConsumer; + private final Consumer solverJobStartedConsumer; private final BiConsumer exceptionHandler; private volatile SolverStatus solverStatus; @@ -65,7 +64,7 @@ public DefaultSolverJob( Consumer bestSolutionConsumer, Consumer finalBestSolutionConsumer, Consumer firstInitializedSolutionConsumer, - Consumer startSolverJobConsumer, + Consumer solverJobStartedConsumer, BiConsumer exceptionHandler) { this.solverManager = solverManager; this.problemId = problemId; @@ -78,7 +77,7 @@ public DefaultSolverJob( this.bestSolutionConsumer = bestSolutionConsumer; this.finalBestSolutionConsumer = finalBestSolutionConsumer; this.firstInitializedSolutionConsumer = firstInitializedSolutionConsumer; - this.startSolverJobConsumer = startSolverJobConsumer; + this.solverJobStartedConsumer = solverJobStartedConsumer; this.exceptionHandler = exceptionHandler; solverStatus = SolverStatus.SOLVING_SCHEDULED; terminatedLatch = new CountDownLatch(1); @@ -112,7 +111,7 @@ public Solution_ call() { solverStatus = SolverStatus.SOLVING_ACTIVE; // Create the consumer thread pool only when this solver job is active. consumerSupport = new ConsumerSupport<>(getProblemId(), bestSolutionConsumer, finalBestSolutionConsumer, - firstInitializedSolutionConsumer, startSolverJobConsumer, exceptionHandler, bestSolutionHolder); + firstInitializedSolutionConsumer, solverJobStartedConsumer, exceptionHandler, bestSolutionHolder); Solution_ problem = problemFinder.apply(problemId); // add a phase lifecycle listener that unlock the solver status lock when solving started diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJobBuilder.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJobBuilder.java index 3a2e594cac..1a86b31ba6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJobBuilder.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJobBuilder.java @@ -23,7 +23,7 @@ public final class DefaultSolverJobBuilder implements Sol private Consumer bestSolutionConsumer; private Consumer finalBestSolutionConsumer; private Consumer initializedSolutionConsumer; - private Consumer startSolverJobConsumer; + private Consumer solverJobStartedConsumer; private BiConsumer exceptionHandler; private SolverConfigOverride solverConfigOverride; @@ -69,8 +69,8 @@ public SolverJobBuilder withBestSolutionConsumer(Consumer @Override public SolverJobBuilder - withStartSolverJobConsumer(Consumer startSolverJobConsumer) { - this.startSolverJobConsumer = Objects.requireNonNull(startSolverJobConsumer, + withSolverJobStartedConsumer(Consumer solverJobStartedConsumer) { + this.solverJobStartedConsumer = Objects.requireNonNull(solverJobStartedConsumer, "Invalid startSolverJobHandler (null) given to SolverJobBuilder."); return this; } @@ -99,10 +99,10 @@ public SolverJob run() { if (this.bestSolutionConsumer == null) { return solverManager.solve(problemId, problemFinder, null, finalBestSolutionConsumer, - initializedSolutionConsumer, startSolverJobConsumer, exceptionHandler, solverConfigOverride); + initializedSolutionConsumer, solverJobStartedConsumer, exceptionHandler, solverConfigOverride); } else { return solverManager.solveAndListen(problemId, problemFinder, bestSolutionConsumer, finalBestSolutionConsumer, - initializedSolutionConsumer, startSolverJobConsumer, exceptionHandler, solverConfigOverride); + initializedSolutionConsumer, solverJobStartedConsumer, exceptionHandler, solverConfigOverride); } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverManager.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverManager.java index 7c78a250ae..0e68c7efe9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverManager.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverManager.java @@ -83,14 +83,14 @@ protected SolverJob solveAndListen(ProblemId_ problemId, Consumer bestSolutionConsumer, Consumer finalBestSolutionConsumer, Consumer initializedSolutionConsumer, - Consumer startSolverJobConsumer, + Consumer solverJobStartedConsumer, BiConsumer exceptionHandler, SolverConfigOverride solverConfigOverride) { if (bestSolutionConsumer == null) { throw new IllegalStateException("The consumer bestSolutionConsumer is required."); } return solve(getProblemIdOrThrow(problemId), problemFinder, bestSolutionConsumer, finalBestSolutionConsumer, - initializedSolutionConsumer, startSolverJobConsumer, exceptionHandler, solverConfigOverride); + initializedSolutionConsumer, solverJobStartedConsumer, exceptionHandler, solverConfigOverride); } protected SolverJob solve(ProblemId_ problemId, @@ -98,7 +98,7 @@ protected SolverJob solve(ProblemId_ problemId, Consumer bestSolutionConsumer, Consumer finalBestSolutionConsumer, Consumer initializedSolutionConsumer, - Consumer startSolverJobConsumer, + Consumer solverJobStartedConsumer, BiConsumer exceptionHandler, SolverConfigOverride configOverride) { Solver solver = solverFactory.buildSolver(configOverride); @@ -113,7 +113,7 @@ protected SolverJob solve(ProblemId_ problemId, throw new IllegalStateException("The problemId (" + problemId + ") is already solving."); } else { return new DefaultSolverJob<>(this, solver, problemId, problemFinder, bestSolutionConsumer, - finalBestSolutionConsumer, initializedSolutionConsumer, startSolverJobConsumer, + finalBestSolutionConsumer, initializedSolutionConsumer, solverJobStartedConsumer, finalExceptionHandler); } }); diff --git a/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java b/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java index 061987284e..71327518cb 100644 --- a/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java @@ -53,6 +53,7 @@ import ai.timefold.solver.core.impl.testdata.util.PlannerTestUtils; import org.apache.commons.lang3.mutable.MutableBoolean; +import org.apache.commons.lang3.mutable.MutableInt; import org.apache.commons.lang3.mutable.MutableObject; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; @@ -472,15 +473,15 @@ void testStartJobConsumer() throws ExecutionException, InterruptedException { Function problemFinder = o -> new TestdataUnannotatedExtendedSolution( PlannerTestUtils.generateTestdataSolution("s1")); - MutableObject started = new MutableObject<>(Boolean.FALSE); + MutableInt started = new MutableInt(0); SolverJob solverJob = solverManager.solveBuilder() .withProblemId(1L) .withProblemFinder(problemFinder) - .withStartSolverJobConsumer((solution) -> started.setValue(Boolean.TRUE)) + .withSolverJobStartedConsumer((solution) -> started.increment()) .run(); solverJob.getFinalBestSolution(); - assertThat(started.getValue()).isTrue(); + assertThat(started.getValue()).isOne(); } @Test diff --git a/python/python-core/src/main/python/_solver_manager.py b/python/python-core/src/main/python/_solver_manager.py index 64ea7e16f9..e7428345b7 100644 --- a/python/python-core/src/main/python/_solver_manager.py +++ b/python/python-core/src/main/python/_solver_manager.py @@ -308,13 +308,13 @@ def with_first_initialized_solution_consumer(self, first_initialized_solution_co return SolverJobBuilder( self._delegate.withFirstInitializedSolutionConsumer(java_consumer)) - def with_start_solver_job_consumer(self, start_solver_job_consumer: Callable[[Solution_], None]) -> 'SolverJobBuilder': + def with_solver_job_started_consumer(self, solver_job_started_consumer: Callable[[Solution_], None]) -> 'SolverJobBuilder': """ Sets the consumer for when the solver starts its solving process. Parameters ---------- - start_solver_job_consumer : Callable[[Solution_], None] + solver_job_started_consumer : Callable[[Solution_], None] called only once when the solver is starting the solving process Returns @@ -325,9 +325,9 @@ def with_start_solver_job_consumer(self, start_solver_job_consumer: Callable[[So from java.util.function import Consumer from _jpyinterpreter import unwrap_python_like_object - java_consumer = Consumer @ (lambda solution: start_solver_job_consumer(unwrap_python_like_object(solution))) + java_consumer = Consumer @ (lambda solution: solver_job_started_consumer(unwrap_python_like_object(solution))) return SolverJobBuilder( - self._delegate.withStartSolverJobConsumer(java_consumer)) + self._delegate.withSolverJobStartedConsumer(java_consumer)) def with_exception_handler(self, exception_handler: Callable[[ProblemId_, Exception], None]) -> 'SolverJobBuilder': """ diff --git a/python/python-core/tests/test_solver_manager.py b/python/python-core/tests/test_solver_manager.py index 335ef38f87..25d5046fc4 100644 --- a/python/python-core/tests/test_solver_manager.py +++ b/python/python-core/tests/test_solver_manager.py @@ -286,13 +286,13 @@ class Solution: problem: Solution = Solution([Entity('A')], [Value(1), Value(2), Value(3)], SimpleScore.ONE) first_initialized_solution_consumer_called = [] - start_solver_job_consumer_called = [] + solver_job_started_consumer_called = [] def on_first_initialized_solution_consumer(solution): - first_initialized_solution_consumer_called.append(1) + first_initialized_solution_consumer_called.append(solution) - def on_start_solver_job_consumer(solution): - start_solver_job_consumer_called.append(1) + def on_solver_job_started_consumer(solution): + solver_job_started_consumer_called.append(solution) with SolverManager.create(SolverFactory.create(solver_config)) as solver_manager: solver_job = (solver_manager.solve_builder() @@ -304,10 +304,10 @@ def on_start_solver_job_consumer(solution): ) )) .with_first_initialized_solution_consumer(on_first_initialized_solution_consumer) - .with_start_solver_job_consumer(on_start_solver_job_consumer) + .with_solver_job_started_consumer(on_solver_job_started_consumer) .run()) solution = solver_job.get_final_best_solution() assert solution.score.score == 3 assert len(first_initialized_solution_consumer_called) == 1 - assert len(start_solver_job_consumer_called) == 1 \ No newline at end of file + assert len(solver_job_started_consumer_called) == 1 \ No newline at end of file From c017ec22819091593c4aaaf35313ff0dadd7435c Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 18 Sep 2024 18:16:16 -0300 Subject: [PATCH 6/8] fix: release consumer lock --- .../ai/timefold/solver/core/impl/solver/ConsumerSupport.java | 4 +++- python/python-core/tests/test_solver_manager.py | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/ConsumerSupport.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/ConsumerSupport.java index a4894f7d65..b8d6ca5c7d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/ConsumerSupport.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/ConsumerSupport.java @@ -34,10 +34,11 @@ public ConsumerSupport(ProblemId_ problemId, Consumer bestSol this.finalBestSolutionConsumer = finalBestSolutionConsumer == null ? finalBestSolution -> { } : finalBestSolutionConsumer; this.firstInitializedSolutionConsumer = firstInitializedSolutionConsumer; + this.solverJobStartedConsumer = solverJobStartedConsumer; this.exceptionHandler = exceptionHandler; this.bestSolutionHolder = bestSolutionHolder; this.firstInitializedSolution = null; - this.solverJobStartedConsumer = solverJobStartedConsumer; + this.initialSolution = null; } // Called on the Solver thread. @@ -115,6 +116,7 @@ void consumeFinalBestSolution(Solution_ finalBestSolution) { // Cancel problem changes that arrived after the solver terminated. bestSolutionHolder.cancelPendingChanges(); activeConsumption.release(); + startSolverJobConsumption.release(); firstSolutionConsumption.release(); disposeConsumerThread(); } diff --git a/python/python-core/tests/test_solver_manager.py b/python/python-core/tests/test_solver_manager.py index 25d5046fc4..71d7ef5618 100644 --- a/python/python-core/tests/test_solver_manager.py +++ b/python/python-core/tests/test_solver_manager.py @@ -243,7 +243,6 @@ def my_exception_handler(problem_id, exception): assert the_problem_id == 1 assert the_exception is not None - def test_solver_config(): @dataclass class Value: From f14444c620758541cccd4c47955ae9c1747354ac Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 18 Sep 2024 20:41:14 -0300 Subject: [PATCH 7/8] fix: ensure all jobs are closed --- .../solver/core/impl/solver/DefaultSolverManager.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverManager.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverManager.java index 0e68c7efe9..80ce4c52de 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverManager.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverManager.java @@ -1,5 +1,8 @@ package ai.timefold.solver.core.impl.solver; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.UUID; @@ -40,6 +43,7 @@ public final class DefaultSolverManager implements Solver private final SolverFactory solverFactory; private final ExecutorService solverThreadPool; private final ConcurrentMap> problemIdToSolverJobMap; + private final List> resourcesToRelease; public DefaultSolverManager(SolverFactory solverFactory, SolverManagerConfig solverManagerConfig) { @@ -55,6 +59,7 @@ public DefaultSolverManager(SolverFactory solverFactory, } solverThreadPool = Executors.newFixedThreadPool(parallelSolverCount, threadFactory); problemIdToSolverJobMap = new ConcurrentHashMap<>(parallelSolverCount * 10); + resourcesToRelease = Collections.synchronizedList(new ArrayList<>(parallelSolverCount * 10)); } public SolverFactory getSolverFactory() { @@ -170,10 +175,12 @@ public void terminateEarly(ProblemId_ problemId) { public void close() { solverThreadPool.shutdownNow(); problemIdToSolverJobMap.values().forEach(DefaultSolverJob::close); + resourcesToRelease.forEach(DefaultSolverJob::close); } void unregisterSolverJob(ProblemId_ problemId) { - problemIdToSolverJobMap.remove(getProblemIdOrThrow(problemId)); + var job = problemIdToSolverJobMap.remove(getProblemIdOrThrow(problemId)); + resourcesToRelease.add(job); } } From f82e5aaf172b683f1eb201b2d29e53f716a5fb65 Mon Sep 17 00:00:00 2001 From: Fred Date: Thu, 19 Sep 2024 10:07:24 -0300 Subject: [PATCH 8/8] fix: close a job after it is done and ensure consumers finished before closing a job --- .../core/impl/solver/ConsumerSupport.java | 45 +++++++++++++------ .../core/impl/solver/DefaultSolverJob.java | 1 + .../impl/solver/DefaultSolverManager.java | 9 +--- .../core/api/solver/SolverManagerTest.java | 2 +- 4 files changed, 35 insertions(+), 22 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/ConsumerSupport.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/ConsumerSupport.java index b8d6ca5c7d..2393a80ce4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/ConsumerSupport.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/ConsumerSupport.java @@ -86,13 +86,7 @@ void consumeStartSolverJob(Solution_ initialSolution) { // Called on the Solver thread after Solver#solve() returns. void consumeFinalBestSolution(Solution_ finalBestSolution) { try { - // Wait for the previous consumption to complete. - // As the solver has already finished, holding the solver thread is not an issue. - activeConsumption.acquire(); - // Wait for the start job event to complete - startSolverJobConsumption.acquire(); - // Wait for the first solution consumption to complete - firstSolutionConsumption.acquire(); + acquireAll(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IllegalStateException("Interrupted when waiting for the final best solution consumption."); @@ -111,13 +105,14 @@ void consumeFinalBestSolution(Solution_ finalBestSolution) { } finally { // If there is no intermediate best solution consumer, complete the problem changes now. if (bestSolutionConsumer == null) { - bestSolutionHolder.take().completeProblemChanges(); + var solutionHolder = bestSolutionHolder.take(); + if (solutionHolder != null) { + solutionHolder.completeProblemChanges(); + } } // Cancel problem changes that arrived after the solver terminated. bestSolutionHolder.cancelPendingChanges(); - activeConsumption.release(); - startSolverJobConsumption.release(); - firstSolutionConsumption.release(); + releaseAll(); disposeConsumerThread(); } }); @@ -191,10 +186,34 @@ private void scheduleConsumption(Semaphore semaphore, Consumer implements Solver private final SolverFactory solverFactory; private final ExecutorService solverThreadPool; private final ConcurrentMap> problemIdToSolverJobMap; - private final List> resourcesToRelease; public DefaultSolverManager(SolverFactory solverFactory, SolverManagerConfig solverManagerConfig) { @@ -59,7 +55,6 @@ public DefaultSolverManager(SolverFactory solverFactory, } solverThreadPool = Executors.newFixedThreadPool(parallelSolverCount, threadFactory); problemIdToSolverJobMap = new ConcurrentHashMap<>(parallelSolverCount * 10); - resourcesToRelease = Collections.synchronizedList(new ArrayList<>(parallelSolverCount * 10)); } public SolverFactory getSolverFactory() { @@ -175,12 +170,10 @@ public void terminateEarly(ProblemId_ problemId) { public void close() { solverThreadPool.shutdownNow(); problemIdToSolverJobMap.values().forEach(DefaultSolverJob::close); - resourcesToRelease.forEach(DefaultSolverJob::close); } void unregisterSolverJob(ProblemId_ problemId) { - var job = problemIdToSolverJobMap.remove(getProblemIdOrThrow(problemId)); - resourcesToRelease.add(job); + problemIdToSolverJobMap.remove(getProblemIdOrThrow(problemId)); } } diff --git a/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java b/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java index 71327518cb..862a6316b3 100644 --- a/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java @@ -478,7 +478,7 @@ void testStartJobConsumer() throws ExecutionException, InterruptedException { SolverJob solverJob = solverManager.solveBuilder() .withProblemId(1L) .withProblemFinder(problemFinder) - .withSolverJobStartedConsumer((solution) -> started.increment()) + .withSolverJobStartedConsumer(solution -> started.increment()) .run(); solverJob.getFinalBestSolution(); assertThat(started.getValue()).isOne();