Skip to content

fix: clone solution for first solution and solver started events - #2732

Merged
triceo merged 10 commits into
TimefoldAI:mainfrom
zepfred:fix/clone
Oct 14, 2026
Merged

triceo merged 10 commits into
TimefoldAI:mainfrom
zepfred:fix/clone

Conversation

@zepfred

@zepfred zepfred commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Problem

SolverManager passed the solver's live working solution to two consumers:

  • withFirstInitializedSolutionConsumer(...), called when a phase first initializes the solution
  • withSolverJobStartedConsumer(...), called when solving starts

These consumers run on the consumer thread while the solver thread keeps changing the same instance. A consumer that reads the solution may see an inconsistent state. A consumer that modifies it corrupts the solver's working state, which can make score assertions fail (for example under PHASE_ASSERT).

Fix

  • Both events now get a planning clone of the working solution, made via the new SolverScope#cloneWorkingSolution() / AbstractPhaseScope#cloneWorkingSolution().
  • The clone is made only if the matching consumer is registered. ConsumerSupport takes a Supplier<Solution_> and returns early when there is no consumer, so a plain solve(id, problem) does no extra cloning.
  • The supplier is called on the solver thread, before the event is handed to the consumer thread. That way the consumer thread never reads the working solution, not even to clone it.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Supplier failures can deadlock cleanup, and one new test does not compile.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity · 1 Low severity

Open (3)
What changed in this PR

Ensures solver-started and first-initialized events receive cloned solutions instead of the mutable working solution.

Changes:

  • Lazily clone working solutions when event consumers exist.
  • Add scope-level cloning helpers.
  • Add regression tests for event isolation and lazy suppliers.
File Description
AbstractPhaseScope.java Exposes working-solution cloning.
ConsumerSupport.java Accepts lazy solution suppliers.
DefaultSolverJob.java Supplies cloned solutions to events.
SolverScope.java Delegates working-solution cloning.
ConsumerSupportTest.java Tests lazy supplier behavior.
DefaultSolverTest.java Tests event-solution isolation.
TestdataInverseRelationSolution.java Adds test-data generation.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread core/src/main/java/ai/timefold/solver/core/impl/solver/ConsumerSupport.java Outdated
Comment thread core/src/test/java/ai/timefold/solver/core/impl/solver/ConsumerSupportTest.java Outdated
Copilot AI balanced review requested due to automatic review settings October 6, 2026 18:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Supplier failures can leak semaphore permits and deadlock solver-job cleanup.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (2)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Preserve interruption during clone synchronization

core/​src/​test/​java/​ai/​timefold/​solver/​core/​impl/​solver/​DefaultSolverTest.java:2972

This empty catch violates CONSTITUTION.md:66-70 and can let an interrupted test bypass the synchronization that makes the clone assertion meaningful, so rethrow the interruption with context.

Copilot AI balanced review requested due to automatic review settings October 6, 2026 18:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Clone failures can leak semaphore permits and deadlock job shutdown, and one test swallows interruption.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Rethrow interruption to ensure consumer mutation is observed

core/​src/​test/​java/​ai/​timefold/​solver/​core/​impl/​solver/​DefaultSolverTest.java:2972

Swallowing this interruption violates CONSTITUTION.md:68 and can let the test proceed without observing the consumer mutation, so rethrow it with context as the preceding test does.

@triceo triceo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but before we merge, let's make sure the new tests are not flaky. Let's make sure we've seen each run 100_000 times in a row. @RepeatedTest is your friend.

(I am asking for this, because historically listener-based tests have been the most unreliable of all our tests.)

Copilot AI balanced review requested due to automatic review settings October 7, 2026 18:26
@zepfred

zepfred commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

LGTM, but before we merge, let's make sure the new tests are not flaky. Let's make sure we've seen each run 100_000 times in a row. @RepeatedTest is your friend.

(I am asking for this, because historically listener-based tests have been the most unreliable of all our tests.)

Running the related tests 100,000 times produced no errors.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Solver-started events expose stale caller-owned input instead of cloning the current working solution, and one test swallows interruption.

1 open finding
1 resolved since last review

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 7, 2026 18:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

A new test swallows InterruptedException, potentially allowing a false-positive result.

0 open findings

1 resolved since last review

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Copilot AI balanced review requested due to automatic review settings October 13, 2026 11:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A new test swallows interruption and can pass without verifying event consumption.

1 open finding

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 13, 2026 11:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The solver-started event can expose stale shadow variables from the untouched input solution.

0 open findings

1 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Return a planning clone with updated shadow variables

core/​src/​main/​java/​ai/​timefold/​solver/​core/​impl/​solver/​DefaultSolverJob.java:456

getInitialSolution() returns the caller's untouched input, whose shadow variables may be stale. The score director explicitly updates shadows only on its cloned working solution (AbstractScoreDirector.java:343-354), so this changes SolverJobStartedEvent from exposing that consistent working state to exposing stale or null shadows. Supply a planning clone of the current working solution here.

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 13, 2026 16:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The change affects concurrent event delivery and solver restart lifecycle behavior.

0 open findings

🧠 Review effort: Balanced

@triceo
triceo merged commit 97acb8b into TimefoldAI:main Oct 14, 2026
39 checks passed

This branch was successfully deployed

1 active deployment
internal — ccfaf9ea Deployed Oct 13, 2026 by zepfred via approval_required #5059
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants