feat: add environment mode per phase - #2595
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces per-phase EnvironmentMode overrides so individual solver phases can run with stricter assertion modes (e.g., FULL_ASSERT) without imposing that cost on the entire solve, and it refactors score director factory creation/lifecycle to support per-phase score directors.
Changes:
- Add
environmentModetoPhaseConfig, wire it into XSDs, and document configuration usage. - Refactor solver/phase construction so each phase can run under its own environment mode and corresponding score director (via
DelegateScoreDirectorFactory). - Fix list move selectors to avoid stale
ListVariableStateSupplyreferences when score directors are rebuilt, by introducingListVariableStateSupplyHolder.
Reviewed changes
Copilot reviewed 61 out of 61 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tools/benchmark/src/main/resources/benchmark.xsd | Adds environmentMode element to phase config schema. |
| docs/src/modules/ROOT/pages/running-timefold-solver/solver-diagnostics.adoc | Documents per-phase environment mode overrides with an example. |
| core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java | Updates latch-await assertions to JUnit assertDoesNotThrow. |
| core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java | Adds tests covering per-phase environment mode behavior and context restoration. |
| core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactoryTest.java | Adds validation tests for environment mode constraints across phases. |
| core/src/test/java/ai/timefold/solver/core/impl/score/director/stream/ConstraintStreamsBavetScoreDirectorSemanticsTest.java | Updates to use DelegateScoreDirectorFactory. |
| core/src/test/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorTest.java | Mocks getEnvironmentMode() and adjusts no-op listener methods. |
| core/src/test/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorSemanticsTest.java | Updates to use DelegateScoreDirectorFactory. |
| core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorSemanticsTest.java | Updates to use DelegateScoreDirectorFactory. |
| core/src/test/java/ai/timefold/solver/core/impl/score/director/DelegateScoreDirectorFactoryTest.java | Renames/extends tests for the new delegate factory behavior. |
| core/src/test/java/ai/timefold/solver/core/impl/neighborhood/NeighborhoodsTest.java | Adapts acceptor/phase builder APIs to pass environment mode. |
| core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactoryTest.java | Updates acceptor factory API usage to include environment mode. |
| core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomSubListSwapMoveSelectorTest.java | Ensures selectors receive phaseStarted lifecycle for fresh supplies. |
| core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomSubListChangeMoveSelectorTest.java | Ensures selectors receive phaseStarted lifecycle for fresh supplies. |
| core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomListChangeIteratorTest.java | Ensures destination selector receives phaseStarted lifecycle. |
| core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListSwapMoveSelectorTest.java | Ensures selector receives phaseStarted lifecycle for fresh supplies. |
| core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveSelectorTest.java | Ensures selector receives phaseStarted lifecycle for fresh supplies. |
| core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/RandomSubListSelectorTest.java | Ensures selector receives phaseStarted lifecycle for fresh supplies. |
| core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelectorTest.java | Ensures selector receives phaseStarted lifecycle for fresh supplies. |
| core/src/test/java/ai/timefold/solver/core/impl/domain/variable/ListVariableStateSupplyHolderTest.java | Adds unit test for ListVariableStateSupplyHolder demand/cancel behavior. |
| core/src/main/resources/solver.xsd | Adds environmentMode element to phase config schema. |
| core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java | Routes assertScoreFromScratch through score director instance. |
| core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecallerFactory.java | Delegates assertion enabling to BestSolutionRecaller. |
| core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecaller.java | Adds enableAssertions(EnvironmentMode) method. |
| core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java | Introduces default environment mode, delegate factory, and env-mode validation across phases. |
| core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java | Refactors construction to carry default context and delegate factory; logs default env mode. |
| core/src/main/java/ai/timefold/solver/core/impl/solver/AbstractSolver.java | Adds per-phase context swapping to run phases under different environment modes. |
| core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactory.java | Adds getEnvironmentMode() and adjusts builder generics; removes factory-level assert method. |
| core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java | Adds assertScoreFromScratch and a counted increment method. |
| core/src/main/java/ai/timefold/solver/core/impl/score/director/DelegateScoreDirectorFactory.java | Replaces ScoreDirectorFactoryFactory and centralizes score director creation per env mode. |
| core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorFactory.java | Implements getEnvironmentMode() and moves score-from-scratch assertion off the factory. |
| core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java | Stores environment mode, adjusts tracking/assert logic, and implements assertScoreFromScratch. |
| core/src/main/java/ai/timefold/solver/core/impl/phase/Phase.java | Adds getEnvironmentMode() to phase API. |
| core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhaseFactory.java | Resolves per-phase environment mode and passes it into phase builder. |
| core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhase.java | Logs environment mode and threads it through the builder hierarchy. |
| core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPossiblyInitializingPhase.java | Threads environment mode into initializing phase builder base. |
| core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhaseFactory.java | Adds shared resolveEnvironmentMode helper for phase factories. |
| core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java | Stores phase environment mode and uses it for assertion enabling. |
| core/src/main/java/ai/timefold/solver/core/impl/partitionedsearch/DefaultPartitionedSearchPhaseFactory.java | Passes resolved environment mode into enterprise partitioned search builder. |
| core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java | Propagates environment mode into decider/acceptor construction. |
| core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java | Logs environment mode and threads it through phase builder. |
| core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/tabu/AbstractTabuAcceptor.java | Enables tabu assertions based on environment mode. |
| core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java | Adds environment mode parameter and enables assertions accordingly. |
| core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseFactory.java | Passes environment mode into decider construction (root mode by default). |
| core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseBuilder.java | Threads environment mode into builder construction/copying. |
| core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ruin/ListRuinRecreateMoveSelector.java | Switches to ListVariableStateSupplyHolder to avoid stale supply across phase swaps. |
| core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListSwapMoveSelector.java | Switches to ListVariableStateSupplyHolder and phase lifecycle hooks. |
| core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveSelector.java | Switches to ListVariableStateSupplyHolder and phase lifecycle hooks. |
| core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMoveSelector.java | Switches to ListVariableStateSupplyHolder and phase lifecycle hooks. |
| core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/RandomSubListSelector.java | Switches to ListVariableStateSupplyHolder and phase lifecycle hooks. |
| core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelector.java | Switches to ListVariableStateSupplyHolder and phase lifecycle hooks. |
| core/src/main/java/ai/timefold/solver/core/impl/heuristic/HeuristicConfigPolicy.java | Renames/adjusts copying methods used by phase/child-thread policy creation. |
| core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhaseFactory.java | Resolves per-phase environment mode and propagates it into decider and phase builder. |
| core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhase.java | Logs environment mode and threads it through phase builder. |
| core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/AbstractExhaustiveSearchDecider.java | Enables decider assertions based on environment mode. |
| core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ListVariableStateSupplyHolder.java | Introduces helper to demand/cancel list state supply per phase start/end. |
| core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseFactory.java | Resolves per-phase environment mode and propagates it into decider and phase builder. |
| core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java | Logs environment mode and threads it through phase builder. |
| core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java | Extends enterprise partitioned search API to accept environment mode. |
| core/src/main/java/ai/timefold/solver/core/config/phase/PhaseConfig.java | Adds per-phase environmentMode config with JAXB/XSD support and inheritance. |
| core/src/build/revapi-differences.json | Ignores the JAXB @XmlType.propOrder change for PhaseConfig. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
d5debf9 to
10a9562
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 61 out of 61 changed files in this pull request and generated no new comments.
Suppressed comments (5)
core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:228
- Using JUnit's assertDoesNotThrow violates the repository test convention requiring AssertJ assertions (see CONSTITUTION.md), so switch this to AssertJ and remove the JUnit import.
core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:291 - Using JUnit's assertDoesNotThrow violates the repository test convention requiring AssertJ assertions (see CONSTITUTION.md), so switch this to AssertJ and remove the JUnit import.
core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:440 - Using JUnit's assertDoesNotThrow violates the repository test convention requiring AssertJ assertions (see CONSTITUTION.md), so switch this to AssertJ and remove the JUnit import.
core/src/main/java/ai/timefold/solver/core/impl/solver/AbstractSolver.java:133 - AbstractSolver.preparePhase() always builds a new ScoreDirectorFactory/ScoreDirector when switching into a non-default environment mode, which contradicts the PR description of lazily reusing factories per distinct mode and may add avoidable overhead when phases switch modes repeatedly.
core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:138 - Using JUnit's assertDoesNotThrow violates the repository test convention requiring AssertJ assertions (see CONSTITUTION.md), so switch this to AssertJ and remove the JUnit import.
This issue also appears in the following locations of the same file:
- line 228
- line 291
- line 440
b786b7b to
1a23c8e
Compare
1a23c8e to
df2abeb
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 61 out of 61 changed files in this pull request and generated no new comments.
Suppressed comments (5)
core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:138
- This uses JUnit's assertDoesNotThrow, but CONSTITUTION.md requires AssertJ assertions instead of JUnit assertions.
core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:228 - This uses JUnit's assertDoesNotThrow, but CONSTITUTION.md requires AssertJ assertions instead of JUnit assertions.
core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:291 - This uses JUnit's assertDoesNotThrow, but CONSTITUTION.md requires AssertJ assertions instead of JUnit assertions.
core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:440 - This uses JUnit's assertDoesNotThrow, but CONSTITUTION.md requires AssertJ assertions instead of JUnit assertions.
core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:6 - This test introduces a static import of JUnit Assertions (assertDoesNotThrow), but CONSTITUTION.md forbids JUnit assertions in favor of AssertJ assertions.
This issue also appears in the following locations of the same file:
- line 138
- line 228
- line 291
- line 440
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 84 out of 84 changed files in this pull request and generated 5 comments.
Suppressed comments (3)
core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:228
- CONSTITUTION.md forbids JUnit assertions in tests, so replace this assertDoesNotThrow call with an AssertJ equivalent.
core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:291 - CONSTITUTION.md forbids JUnit assertions in tests, so replace this assertDoesNotThrow call with an AssertJ equivalent.
core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java:440 - CONSTITUTION.md forbids JUnit assertions in tests, so replace this assertDoesNotThrow call with an AssertJ equivalent.
| } catch (InterruptedException e) { | ||
| Assertions.fail("Failed waiting for the event to happen.", e); | ||
| } | ||
| assertDoesNotThrow(() -> latch.await(10, TimeUnit.SECONDS), "Failed waiting for the event to happen."); |
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static org.assertj.core.api.Assertions.assertThatCode; | ||
| import static org.assertj.core.api.Assertions.fail; | ||
| import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; |
| var originalScoreDirector = solverScope.getScoreDirector(); | ||
| var workingSolution = originalScoreDirector.getWorkingSolution(); | ||
|
|
| // The working solution carries over rather than being re-cloned. | ||
| assertThat(newScoreDirector.getWorkingSolution()).isSameAs(workingSolution); | ||
| // The problem change director follows the score director, so problem changes hit the live one. | ||
| assertThat(solverScope.getProblemChangeDirector()).isNotSameAs(originalScoreDirector); |
| void assertPhaseEnvironmentMode() { | ||
| var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class); | ||
| solverConfig.setEnvironmentMode(EnvironmentMode.FULL_ASSERT); | ||
| // LS with NO_ASSERT |
Allows each solver phase (Construction Heuristic, Local Search, Exhaustive Search, Partitioned Search, Custom) to override the solver's
environmentModewith a stricter mode of its own viaPhaseConfig.withEnvironmentMode(...). This makes it possible to run a suspect phase underFULL_ASSERT(or another stricter mode) for debugging, without paying that performance cost for the whole solving run.NON_REPRODUCIBLE, no phase can override it (every other mode is reproducible, hence stricter).Key changes
PhaseConfig: newenvironmentModefield/getter/setter/withEnvironmentMode(...), added to the@XmlTypepropOrder, and wired intosolver.xsd/benchmark.xsd. Includes arevapi-differences.jsonignore entry for the resulting@XmlType.propOrderchange.ScoreDirectorFactoryFactory→DelegateScoreDirectorFactory: renamed and reworked to lazily build aScoreDirectorFactoryper distinct environment mode encountered across the solver config and its phases, instead of a single factory built once from the solver's mode.DefaultSolverFactorykeeps one default factory (built from the solver's own environment mode) for consumers that are decoupled from the solving lifecycle (e.g.SolverManager, Quarkus DI injectingConstraintMetaModel), while phases needing a stricter mode get their own score director from the delegate.AbstractSolver/DefaultSolver/ phase factories: restructured so each phase can be built with (and run under) its own score director/environment mode rather than always reusing the solver-level one.ListVariableStateSupplyin list move selectors (ElementDestinationSelector,RandomSubListSelector,ListChangeMoveSelector,ListSwapMoveSelector,KOptListMoveSelector,ListRuinRecreateMoveSelector): these previously cached the supply once at selector construction time; now a newListVariableStateSupplyHolderfetches it fresh, so a rebuilt score director (as introduced by the per-phase change) doesn't leave selectors pointing at a stale supply.solver-diagnostics.adocwith a config example.