11package ai .timefold .solver .core .impl .solver ;
22
33import static org .assertj .core .api .Assertions .assertThat ;
4- import static org .awaitility .Awaitility .await ;
54import static org .mockito .Mockito .mock ;
65import static org .mockito .Mockito .times ;
76import static org .mockito .Mockito .verify ;
87
9- import java .time .Duration ;
10- import java .util .ArrayList ;
118import java .util .List ;
12- import java .util .Random ;
13- import java .util .UUID ;
149import java .util .concurrent .CompletableFuture ;
15- import java .util .concurrent .CountDownLatch ;
16- import java .util .concurrent .Executors ;
1710
1811import ai .timefold .solver .core .api .solver .Solver ;
19- import ai .timefold .solver .core .api .solver .SolverManager ;
2012import ai .timefold .solver .core .api .solver .change .ProblemChange ;
21- import ai .timefold .solver .core .api .solver .change .ProblemChangeDirector ;
22- import ai .timefold .solver .core .config .solver .SolverConfig ;
23- import ai .timefold .solver .core .config .solver .SolverManagerConfig ;
24- import ai .timefold .solver .core .testdomain .TestdataEasyScoreCalculator ;
25- import ai .timefold .solver .core .testdomain .TestdataEntity ;
2613import ai .timefold .solver .core .testdomain .TestdataSolution ;
2714
28- import org .jspecify .annotations .NullMarked ;
29- import org .junit .jupiter .api .RepeatedTest ;
3015import org .junit .jupiter .api .Test ;
3116import org .mockito .Mockito ;
3217
@@ -92,7 +77,7 @@ void cancelPendingChanges_noChangesRetrieved() {
9277 assertThat (problemChange ).isCancelled ();
9378 }
9479
95- private CompletableFuture <Void > addProblemChange (BestSolutionHolder <TestdataSolution > bestSolutionHolder ) {
80+ private static CompletableFuture <Void > addProblemChange (BestSolutionHolder <TestdataSolution > bestSolutionHolder ) {
9681 Solver <TestdataSolution > solver = mock (Solver .class );
9782 ProblemChange <TestdataSolution > problemChange = mock (ProblemChange .class );
9883 CompletableFuture <Void > futureChange = bestSolutionHolder .addProblemChange (solver , List .of (problemChange ));
@@ -101,129 +86,4 @@ private CompletableFuture<Void> addProblemChange(BestSolutionHolder<TestdataSolu
10186 return futureChange ;
10287 }
10388
104- @ RepeatedTest (value = 10 , failureThreshold = 1 ) // Run it multiple times to increase the chance of catching a concurrency issue.
105- void problemChangeBarrageIntermediateBestSolutionConsumer () throws InterruptedException {
106- var solverConfig = new SolverConfig ()
107- .withSolutionClass (TestdataSolution .class )
108- .withEntityClasses (TestdataEntity .class )
109- .withEasyScoreCalculatorClass (TestdataEasyScoreCalculator .class );
110-
111- var futureList = new ArrayList <RecordedFuture >();
112- var executorService = Executors .newFixedThreadPool (2 );
113- try (var solverManager = SolverManager .<TestdataSolution , UUID > create (solverConfig , new SolverManagerConfig ())) {
114- var solverStartedLatch = new CountDownLatch (1 );
115- var solution = TestdataSolution .generateSolution ();
116- var solverJob = solverManager .solveBuilder ()
117- .withProblemId (UUID .randomUUID ())
118- .withProblem (solution )
119- .withFirstInitializedSolutionConsumer ((testdataSolution , isTerminatedEarly ) -> {
120- solverStartedLatch .countDown ();
121- })
122- .withBestSolutionConsumer (testdataSolution -> {
123- // No need to do anything.
124- })
125- .run ();
126- solverStartedLatch .await (); // Only start adding problem changes after CH finished.
127-
128- var random = new Random (0 );
129- var problemChangeCount = 200 ; // Arbitrary, for a reasonable test duration.
130- var problemChangesAddedLatch = new CountDownLatch (problemChangeCount );
131- for (int i = 0 ; i < problemChangeCount ; i ++) {
132- // Emulate a random delay between problem changes, as it would happen in real world.
133- var randomDelayNanos = random .nextInt (1_000_000 );
134- var start = System .nanoTime ();
135- while ((System .nanoTime () - randomDelayNanos ) < start ) {
136- Thread .onSpinWait ();
137- }
138- // Submit the problem change and store the future.
139- var problemChange = random .nextBoolean ()
140- ? new EntityAddingProblemChange (problemChangesAddedLatch )
141- : new EntityRemovingProblemChange (problemChangesAddedLatch );
142- futureList .add (new RecordedFuture (i , solverJob .addProblemChange (problemChange )));
143- }
144- // All problem changes have been added.
145- // Does not guarantee all have been processed though.
146- problemChangesAddedLatch .await ();
147-
148- // A best solution should have been produced for all the processed changes.
149- // Any incomplete futures here means some problem change was "lost".
150- var lostFutureList = futureList .stream ()
151- .filter (future -> {
152- await ().atMost (Duration .ofSeconds (1 ))
153- .pollInterval (Duration .ofMillis (1 ))
154- .until (future ::isDone );
155- return !future .isDone ();
156- })
157- .toList ();
158- var lostFutureCount = lostFutureList .size ();
159- if (lostFutureCount == 0 ) {
160- return ;
161- }
162- // The only exception to the rule:
163- // the very last problem changes, which might not have been processed yet
164- // by the time the solver was forced to terminate.
165- var minIncompleteFutureId = lostFutureList .stream ()
166- .mapToInt (f -> f .id )
167- .min ()
168- .orElseThrow (() -> new AssertionError ("Impossible state: no incomplete future found." ));
169- assertThat (minIncompleteFutureId ).isEqualTo (problemChangeCount - lostFutureCount );
170- } finally {
171- executorService .shutdownNow ();
172- // The solver is terminated.
173- // All incomplete futures should have been canceled.
174- var incompleteFutureList = futureList .stream ()
175- .filter (future -> {
176- await ().atMost (Duration .ofSeconds (1 ))
177- .pollInterval (Duration .ofMillis (1 ))
178- .until (future ::isDone );
179- return !future .isDone ();
180- })
181- .toList ();
182- assertThat (incompleteFutureList )
183- .as ("All futures should have been completed by now." )
184- .isEmpty ();
185- }
186-
187- }
188-
189- private record RecordedFuture (int id , CompletableFuture <Void > future ) {
190-
191- boolean isDone () {
192- return future .isDone ();
193- }
194-
195- }
196-
197- @ NullMarked
198- private record EntityAddingProblemChange (CountDownLatch latch ) implements ProblemChange <TestdataSolution > {
199-
200- @ Override
201- public void doChange (TestdataSolution workingSolution , ProblemChangeDirector problemChangeDirector ) {
202- var entity = new TestdataEntity (UUID .randomUUID ().toString ());
203- problemChangeDirector .addEntity (entity ,
204- e -> workingSolution .getEntityList ().add (e ));
205- problemChangeDirector .updateShadowVariables ();
206- latch .countDown ();
207- }
208-
209- }
210-
211- @ NullMarked
212- private record EntityRemovingProblemChange (CountDownLatch latch ) implements ProblemChange <TestdataSolution > {
213-
214- @ Override
215- public void doChange (TestdataSolution workingSolution , ProblemChangeDirector problemChangeDirector ) {
216- if (workingSolution .getEntityList ().size () < 2 ) {
217- latch .countDown ();
218- return ;
219- }
220- var entity = workingSolution .getEntityList ().get (0 );
221- problemChangeDirector .removeEntity (entity ,
222- e -> workingSolution .getEntityList ().remove (e ));
223- problemChangeDirector .updateShadowVariables ();
224- latch .countDown ();
225- }
226-
227- }
228-
22989}
0 commit comments