First initialized solution event shares the working solution with the consumer thread (ConcurrentModificationException)
Maintainers usually reply within 1 day
Assessment
This issue has not been assessed yet.
Description
Describe the bug
The first initialized solution event gives the consumer the live working solution, not a planning clone. The consumer reads it on the consumer thread, while the solver thread continues to change the same object.
Mechanism (line numbers on main at 9de70fcf and on v2.7.0):
DefaultSolverJob.FirstInitializedSolutionPhaseLifecycleListener.phaseEnded(line 424) callsconsumerSupport.consumeFirstInitializedSolution(phaseScope.getWorkingSolution(), ...)without a clone. The best solution and final best solution events receive clones.ConsumerSupportruns the consumer asynchronously on its consumer thread.- On the solver thread,
AbstractPhase.phaseEndedfires the phase listeners (line 136). Then, ifassertPhaseScoreFromScratchis true (every asserted environment mode, including the defaultPHASE_ASSERT), it asserts the score from scratch on the same working solution (lines 137-144). - The assertion calls
uncorruptedScoreDirector.setWorkingSolution(workingSolution). This goes toBasicVariableState.resetWorkingSolution, which clears every@InverseRelationShadowVariablecollection and fills it again. - A consumer that iterates such a collection at that moment gets a
ConcurrentModificationException. A consumer that does not fail can read a half-filled collection.
The event fires only when another phase follows the construction heuristic. In our tests, local search did not change the consumed solution (more than 200,000 steps with NO_ASSERT). Only the assertion from scratch did.
Expected behavior
The solution in FirstInitializedSolutionEvent does not change while the consumer reads it, as with NewBestSolutionEvent and FinalBestSolutionEvent.
Actual behavior
With PHASE_ASSERT (the default) or FULL_ASSERT, the inverse relation shadow collections of the consumed solution are cleared and filled again while the consumer reads them. The consumer gets a ConcurrentModificationException.
In the Timefold service SDK, SolverWorker.notifyOnInit converts this solution to the model output on the consumer thread. A model whose ModelConvertor.toModelOutput reads an inverse relation shadow collection fails at random with Unable to create model output for dataset with id ... (caused by ConcurrentModificationException), and the dataset ends in SOLVING_FAILED. We saw this in about one of three runs of a packaged Quarkus integration test (test profile, FULL_ASSERT).
To Reproduce
Reproducer: a test in SolverManagerTest on branch larsbeck/timefold-solver:reproducer/first-initialized-solution-race (commit e449eb41). It uses the existing TestdataInverseRelation* test domain.
mvn -pl core -am test -Dtest='SolverManagerTest#firstInitializedSolutionDoesNotChangeWhileTheConsumerReadsIt' -Dsurefire.failIfNoSpecifiedTests=false
| Environment mode | Result |
|---|---|
PHASE_ASSERT (the default) |
Fails with ConcurrentModificationException (4 of 4 runs) |
NO_ASSERT |
Passes |
The consumer gets an iterator on every inverse relation collection, waits 500 ms, and then iterates them. The problem has 50 values and 2,000 entities, so that the assertion from scratch takes longer than the start of the consumer thread. With a small problem, the failure occurs only at random.
Environment
Timefold Solver Version or Git ref: 2.7.0 and main (9de70fcf)
Output of java -version: openjdk version "25.0.2" 2026-01-20 LTS (Temurin-25.0.2+10)
Output of uname -a or ver: Darwin 25.6.0 arm64
Additional information
- Possible fix (not tested): send a planning clone in the event, for example
phaseScope.getScoreDirector().cloneSolution(phaseScope.getWorkingSolution()), as the best solution events already do. - No merged pull request after the 2.7.0 release addresses this. #1583 ("clone the solution before sending it to the consumer") changed only the best solution events and was reverted in #1648.
- Dominant language
- Java
- Stars
- 1.8k
- Forks
- 238
- Avg merge
- 3d 2h
- Merged PRs (30d)
- 59
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from TimefoldAI/timefold-solver
-
component/docs
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
TimefoldAI/timefold-solver#2671 ·
Maintainers usually reply within 1 day
-
Swap move selector applies entity value range filtering to entity classes without entity-level rangesPossibly taken @zepfred claimed this today. Openprocess/needs triage
TimefoldAI/timefold-solver#2761 · 1 assignee ·
Maintainers usually reply within 1 day
-
SDK: Waypoints class has not OpenAPI Spec annotations.Possibly taken @TomCools claimed this today. Opencomponent/service
TimefoldAI/timefold-solver#2759 · 1 assignee ·
Maintainers usually reply within 1 day
-
process/needs triage
Difficulty 5/5 Over a week Newbie friendliness 30/100
TimefoldAI/timefold-solver#2755 ·
Maintainers usually reply within 1 day
-
fix: Partitioned Search error with list variablesPossibly taken @zepfred claimed this 2 days ago. Openprocess/needs triage
TimefoldAI/timefold-solver#2749 · 1 assignee ·
Maintainers usually reply within 1 day
All issues in TimefoldAI/timefold-solver
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
apache/rocketmq-dashboard#6872 ·
Maintainers usually reply within 3 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
ADORSYS-GIS/token-status-link#168 ·
Maintainers usually reply within 2 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
ADORSYS-GIS/keycloak-oid4vp-plugin#232 ·
Maintainers usually reply within 2 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 2 days
-
documentation
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
devops-thiago/ThrillhouseBot#1132 ·
Maintainers usually reply within 1 day