Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

First initialized solution event shares the working solution with the consumer thread (ConcurrentModificationException)

Closed
#2,728 0 comments 0 reactions 1 assignee View on GitHub

Maintainers usually reply within 1 day

@zepfred is already working on this.

Since Oct 6, 2026.

  • #2732 by @zepfred — open

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):

  1. DefaultSolverJob.FirstInitializedSolutionPhaseLifecycleListener.phaseEnded (line 424) calls consumerSupport.consumeFirstInitializedSolution(phaseScope.getWorkingSolution(), ...) without a clone. The best solution and final best solution events receive clones.
  2. ConsumerSupport runs the consumer asynchronously on its consumer thread.
  3. On the solver thread, AbstractPhase.phaseEnded fires the phase listeners (line 136). Then, if assertPhaseScoreFromScratch is true (every asserted environment mode, including the default PHASE_ASSERT), it asserts the score from scratch on the same working solution (lines 137-144).
  4. The assertion calls uncorruptedScoreDirector.setWorkingSolution(workingSolution). This goes to BasicVariableState.resetWorkingSolution, which clears every @InverseRelationShadowVariable collection and fills it again.
  5. 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

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from TimefoldAI/timefold-solver

All issues in TimefoldAI/timefold-solver

Similar issues

More Java issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.