Skip to content

Commit 881f725

Browse files
committed
feat: allow SolverManager and SolverJob to submit problem changes in batches
1 parent df3c418 commit 881f725

File tree

11 files changed

+70
-44
lines changed

11 files changed

+70
-44
lines changed

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package ai.timefold.solver.core.api.solver;
22

33
import java.time.Duration;
4+
import java.util.Collections;
5+
import java.util.List;
46
import java.util.UUID;
57
import java.util.concurrent.CompletableFuture;
68
import java.util.concurrent.ExecutionException;
@@ -36,16 +38,26 @@ public interface SolverJob<Solution_, ProblemId_> {
3638
SolverStatus getSolverStatus();
3739

3840
/**
39-
* Schedules a {@link ProblemChange} to be processed by the underlying {@link Solver} and returns immediately.
40-
* <p>
41-
* To learn more about problem change semantics, please refer to the {@link ProblemChange} Javadoc.
41+
* As defined by {@link #addProblemChanges(List)}, only for a single problem change.
42+
* Prefer to submit multiple {@link ProblemChange}s at once to reduce the considerable overhead of multiple calls.
43+
*/
44+
@NonNull
45+
default CompletableFuture<Void> addProblemChange(@NonNull ProblemChange<Solution_> problemChange) {
46+
return addProblemChanges(Collections.singletonList(problemChange));
47+
}
48+
49+
/**
50+
* Schedules a batch of {@link ProblemChange problem changes} to be processed
51+
* by the underlying {@link Solver} and returns immediately.
4252
*
53+
* @param problemChangeList at least one problem change to be processed
4354
* @return completes after the best solution containing this change has been consumed.
4455
* @throws IllegalStateException if the underlying {@link Solver} is not in the {@link SolverStatus#SOLVING_ACTIVE}
4556
* state
57+
* @see ProblemChange Learn more about problem change semantics.
4658
*/
4759
@NonNull
48-
CompletableFuture<Void> addProblemChange(@NonNull ProblemChange<Solution_> problemChange);
60+
CompletableFuture<Void> addProblemChanges(@NonNull List<ProblemChange<Solution_>> problemChangeList);
4961

5062
/**
5163
* Terminates the solver or cancels the solver job if it hasn't (re)started yet.

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package ai.timefold.solver.core.api.solver;
22

3+
import java.util.Collections;
4+
import java.util.List;
35
import java.util.UUID;
46
import java.util.concurrent.CompletableFuture;
57
import java.util.function.BiConsumer;
@@ -381,22 +383,32 @@ public interface SolverManager<Solution_, ProblemId_> extends AutoCloseable {
381383
@NonNull
382384
SolverStatus getSolverStatus(@NonNull ProblemId_ problemId);
383385

384-
// TODO Future features
385-
// void reloadProblem(ProblemId_ problemId, Function<? super ProblemId_, Solution_> problemFinder);
386+
/**
387+
* As defined by {@link #addProblemChanges(Object, List)}, only with a single {@link ProblemChange}.
388+
* Prefer to submit multiple {@link ProblemChange}s at once to reduce the considerable overhead of multiple calls.
389+
*/
390+
@NonNull
391+
default CompletableFuture<Void> addProblemChange(@NonNull ProblemId_ problemId,
392+
@NonNull ProblemChange<Solution_> problemChange) {
393+
return addProblemChanges(problemId, Collections.singletonList(problemChange));
394+
}
386395

387396
/**
388-
* Schedules a {@link ProblemChange} to be processed by the underlying {@link Solver} and returns immediately.
397+
* Schedules a batch of {@link ProblemChange problem changes} to be processed
398+
* by the underlying {@link Solver} and returns immediately.
389399
* If the solver already terminated or the problemId was never added, throws an exception.
390400
* The same applies if the underlying {@link Solver} is not in the {@link SolverStatus#SOLVING_ACTIVE} state.
391401
*
392402
* @param problemId a value given to {@link #solve(Object, Object, Consumer)}
393403
* or {@link #solveAndListen(Object, Object, Consumer)}
404+
* @param problemChangeList a list of {@link ProblemChange}s to apply to the problem
394405
* @return completes after the best solution containing this change has been consumed.
395406
* @throws IllegalStateException if there is no solver actively solving the problem associated with the problemId
396407
* @see ProblemChange Learn more about problem change semantics.
397408
*/
398409
@NonNull
399-
CompletableFuture<Void> addProblemChange(@NonNull ProblemId_ problemId, @NonNull ProblemChange<Solution_> problemChange);
410+
CompletableFuture<Void> addProblemChanges(@NonNull ProblemId_ problemId,
411+
@NonNull List<ProblemChange<Solution_>> problemChangeList);
400412

401413
/**
402414
* Terminates the solver or cancels the solver job if it hasn't (re)started yet.

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,22 +129,22 @@ void set(Solution_ bestSolution, BooleanSupplier isEveryProblemChangeProcessed)
129129
}
130130

131131
/**
132-
* Adds a new problem change to a solver and registers the problem change
132+
* Adds a batch of problem changes to a solver and registers them
133133
* to be later retrieved together with a relevant best solution by the {@link #take()} method.
134134
*
135135
* @return CompletableFuture that will be completed after the best solution containing this change is passed to
136136
* a user-defined Consumer.
137137
*/
138138
@NonNull
139-
CompletableFuture<Void> addProblemChange(Solver<Solution_> solver, ProblemChange<Solution_> problemChange) {
139+
CompletableFuture<Void> addProblemChange(Solver<Solution_> solver, List<ProblemChange<Solution_>> problemChangeList) {
140140
var futureProblemChange = new CompletableFuture<Void>();
141141
synchronized (this) {
142142
// This actually needs to be synchronized,
143143
// as we want the new problem change and its version to be linked.
144144
var futureProblemChangeList =
145145
problemChangesPerVersionRef.get().computeIfAbsent(currentVersion.get(), version -> new ArrayList<>());
146146
futureProblemChangeList.add(futureProblemChange);
147-
solver.addProblemChange(problemChange);
147+
solver.addProblemChanges(problemChangeList);
148148
}
149149
return futureProblemChange;
150150
}

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,29 +131,30 @@ public boolean isTerminateEarly() {
131131

132132
@Override
133133
public boolean addProblemFactChange(@NonNull ProblemFactChange<Solution_> problemFactChange) {
134-
return basicPlumbingTermination.addProblemChange(ProblemChangeAdapter.create(problemFactChange));
134+
return addProblemFactChanges(Collections.singletonList(problemFactChange));
135135
}
136136

137137
@Override
138138
public boolean addProblemFactChanges(@NonNull List<ProblemFactChange<Solution_>> problemFactChangeList) {
139139
Objects.requireNonNull(problemFactChangeList,
140140
() -> "The list of problem fact changes (" + problemFactChangeList + ") cannot be null.");
141-
List<ProblemChangeAdapter<Solution_>> problemChangeAdapterList = problemFactChangeList.stream()
141+
return basicPlumbingTermination.addProblemChanges(problemFactChangeList.stream()
142142
.map(ProblemChangeAdapter::create)
143-
.collect(Collectors.toList());
144-
return basicPlumbingTermination.addProblemChanges(problemChangeAdapterList);
143+
.collect(Collectors.toList()));
145144
}
146145

147146
@Override
148147
public void addProblemChange(@NonNull ProblemChange<Solution_> problemChange) {
149-
basicPlumbingTermination.addProblemChange(ProblemChangeAdapter.create(problemChange));
148+
addProblemChanges(Collections.singletonList(problemChange));
150149
}
151150

152151
@Override
153152
public void addProblemChanges(@NonNull List<ProblemChange<Solution_>> problemChangeList) {
154153
Objects.requireNonNull(problemChangeList,
155154
() -> "The list of problem changes (" + problemChangeList + ") cannot be null.");
156-
problemChangeList.forEach(this::addProblemChange);
155+
basicPlumbingTermination.addProblemChanges(problemChangeList.stream()
156+
.map(ProblemChangeAdapter::create)
157+
.toList());
157158
}
158159

159160
@Override

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package ai.timefold.solver.core.impl.solver;
22

33
import java.time.Duration;
4+
import java.util.List;
45
import java.util.Objects;
56
import java.util.UUID;
67
import java.util.concurrent.Callable;
@@ -158,14 +159,18 @@ private void solvingTerminated() {
158159
}
159160

160161
@Override
161-
public @NonNull CompletableFuture<Void> addProblemChange(@NonNull ProblemChange<Solution_> problemChange) {
162-
Objects.requireNonNull(problemChange, () -> "A problem change (%s) must not be null.".formatted(problemId));
163-
if (solverStatus == SolverStatus.NOT_SOLVING) {
164-
throw new IllegalStateException("Cannot add the problem change (%s) because the solver job (%s) is not solving."
165-
.formatted(problemChange, solverStatus));
162+
public @NonNull CompletableFuture<Void> addProblemChanges(@NonNull List<ProblemChange<Solution_>> problemChangeList) {
163+
Objects.requireNonNull(problemChangeList, () -> "A problem change list for problem (%s) must not be null."
164+
.formatted(problemId));
165+
if (problemChangeList.isEmpty()) {
166+
throw new IllegalArgumentException("The problem change list for problem (%s) must not be empty."
167+
.formatted(problemId));
168+
} else if (solverStatus == SolverStatus.NOT_SOLVING) {
169+
throw new IllegalStateException("Cannot add the problem changes (%s) because the solver job (%s) is not solving."
170+
.formatted(problemChangeList, solverStatus));
166171
}
167172

168-
return bestSolutionHolder.addProblemChange(solver, problemChange);
173+
return bestSolutionHolder.addProblemChange(solver, problemChangeList);
169174
}
170175

171176
@Override

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package ai.timefold.solver.core.impl.solver;
22

3+
import java.util.List;
34
import java.util.Map;
45
import java.util.Objects;
56
import java.util.UUID;
@@ -143,16 +144,16 @@ SolverJob<Solution_, ProblemId_> solve(ProblemId_ problemId,
143144
// }
144145

145146
@Override
146-
public @NonNull CompletableFuture<Void> addProblemChange(@NonNull ProblemId_ problemId,
147-
@NonNull ProblemChange<Solution_> problemChange) {
147+
public @NonNull CompletableFuture<Void> addProblemChanges(@NonNull ProblemId_ problemId,
148+
@NonNull List<ProblemChange<Solution_>> problemChangeList) {
148149
DefaultSolverJob<Solution_, ProblemId_> solverJob = getSolverJob(problemId);
149150
if (solverJob == null) {
150151
// We cannot distinguish between "already terminated" and "never solved" without causing a memory leak.
151152
throw new IllegalStateException(
152-
"Cannot add the problem change (" + problemChange + ") because there is no solver solving the problemId ("
153-
+ problemId + ").");
153+
"Cannot add the problem changes (%s) because there is no solver solving the problemId (%s)."
154+
.formatted(problemChangeList, problemId));
154155
}
155-
return solverJob.addProblemChange(problemChange);
156+
return solverJob.addProblemChanges(problemChangeList);
156157
}
157158

158159
@Override

core/src/main/java/ai/timefold/solver/core/impl/solver/termination/BasicPlumbingTermination.java

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -81,18 +81,6 @@ public synchronized boolean waitForRestartSolverDecision() {
8181
}
8282
}
8383

84-
/**
85-
* Concurrency note: unblocks {@link #waitForRestartSolverDecision()}.
86-
*
87-
* @param problemChange never null
88-
* @return as specified by {@link Collection#add}
89-
*/
90-
public synchronized boolean addProblemChange(ProblemChangeAdapter<Solution_> problemChange) {
91-
boolean added = problemChangeQueue.add(problemChange);
92-
notifyAll();
93-
return added;
94-
}
95-
9684
/**
9785
* Concurrency note: unblocks {@link #waitForRestartSolverDecision()}.
9886
*

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
import static org.mockito.Mockito.times;
66
import static org.mockito.Mockito.verify;
77

8+
import java.util.List;
89
import java.util.concurrent.CompletableFuture;
910

1011
import ai.timefold.solver.core.api.solver.Solver;
1112
import ai.timefold.solver.core.api.solver.change.ProblemChange;
1213
import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution;
1314

1415
import org.junit.jupiter.api.Test;
16+
import org.mockito.Mockito;
1517

1618
class BestSolutionHolderTest {
1719

@@ -78,8 +80,9 @@ void cancelPendingChanges_noChangesRetrieved() {
7880
private CompletableFuture<Void> addProblemChange(BestSolutionHolder<TestdataSolution> bestSolutionHolder) {
7981
Solver<TestdataSolution> solver = mock(Solver.class);
8082
ProblemChange<TestdataSolution> problemChange = mock(ProblemChange.class);
81-
CompletableFuture<Void> futureChange = bestSolutionHolder.addProblemChange(solver, problemChange);
82-
verify(solver, times(1)).addProblemChange(problemChange);
83+
CompletableFuture<Void> futureChange = bestSolutionHolder.addProblemChange(solver, List.of(problemChange));
84+
verify(solver, times(1)).addProblemChanges(
85+
Mockito.argThat(problemChanges -> problemChanges.size() == 1 && problemChanges.get(0) == problemChange));
8386
return futureChange;
8487
}
8588
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ void pendingProblemChangesAreCanceled_afterFinalBestSolutionIsConsumed() throws
132132
}
133133

134134
private CompletableFuture<Void> addProblemChange(BestSolutionHolder<TestdataSolution> bestSolutionHolder) {
135-
return bestSolutionHolder.addProblemChange(mock(Solver.class), mock(ProblemChange.class));
135+
return bestSolutionHolder.addProblemChange(mock(Solver.class), List.of(mock(ProblemChange.class)));
136136
}
137137

138138
private void consumeIntermediateBestSolution(TestdataSolution bestSolution) {

core/src/test/java/ai/timefold/solver/core/impl/solver/termination/BasicPlumbingTerminationTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static org.mockito.Mockito.mock;
55

66
import java.util.Arrays;
7+
import java.util.Collections;
78
import java.util.concurrent.atomic.AtomicInteger;
89

910
import ai.timefold.solver.core.impl.score.director.InnerScoreDirector;
@@ -23,7 +24,7 @@ void addProblemChangeWithoutDaemon() {
2324
assertThat(basicPlumbingTermination.waitForRestartSolverDecision()).isFalse();
2425
ProblemChangeAdapter<TestdataSolution> problemChangeAdapter =
2526
ProblemChangeAdapter.create((workingSolution, problemChangeDirector) -> count.getAndIncrement());
26-
basicPlumbingTermination.addProblemChange(problemChangeAdapter);
27+
basicPlumbingTermination.addProblemChanges(Collections.singletonList(problemChangeAdapter));
2728
assertThat(basicPlumbingTermination.waitForRestartSolverDecision()).isTrue();
2829
assertThat(count).hasValue(0);
2930

0 commit comments

Comments
 (0)