From d3ae68f5c1e76cc4444756a50cd40e58ae819f9a Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Fri, 10 Jan 2025 01:11:27 -0500 Subject: [PATCH] fix: Use Named Solver fields and method parameters to determine list of solver names in Quarkus (#1308) If a user is using named solvers, they should have `@Named` annotated fields or method parameters corresponding to the name solver. If they don't have such fields, said named solvers are not accessible anyways. Thus, Set(Named injected Solver types) = Set(accessible Solver names). So instead of using the keys of the SolverBuildTimeConfig as the set of solver names, we can use Set(Named injected Solver types) instead. This removes the need to include runtime properties in SolverBuildTimeConfig, since the map keys are no longer used to determine names. As a consequence, there is a behaviour change in the very contrived case of "exactly one named solver in properties (no default), no Named annotations, injected default solver". Since there was no Named annotations, the (empty) default solver properties will be checked instead of the single named solver properties. I consider the old behaviour to be a bug (named properties should not affect default properties), but the tests expect that behaviour. --- ...arkProcessorMultipleSolversConfigTest.java | 12 +++ .../solver/quarkus/deployment/DotNames.java | 21 +++++ .../deployment/SolverConfigBuildItem.java | 5 ++ .../quarkus/deployment/TimefoldProcessor.java | 53 ++++++++---- .../config/SolverBuildTimeConfig.java | 40 ++------- .../config/TimefoldBuildTimeConfig.java | 6 -- ...ipleSolversInvalidConstraintClassTest.java | 12 +++ ...MultipleSolversInvalidEntityClassTest.java | 12 +++ ...ltipleSolversInvalidSolutionClassTest.java | 12 +++ ...rocessorMultipleSolversPropertiesTest.java | 12 +++ ...efoldProcessorMultipleSolversYamlTest.java | 12 +++ .../TimefoldProcessorSolverResourcesTest.java | 5 ++ ...ldProcessorSolverUnusedPropertiesTest.java | 86 +++++++++++++++++++ ...orWarningBuildTimePropertyChangedTest.java | 37 ++++++++ ...ssorWarningRuntimePropertyChangedTest.java | 39 +++++++++ .../TimefoldDevUIMultipleSolversTest.java | 18 ++-- .../solver/quarkus/TimefoldRecorder.java | 78 +++++++++++++---- .../quarkus/config/SolverRuntimeConfig.java | 18 ++++ .../quarkus/devui/TimefoldDevUIRecorder.java | 29 +++---- 19 files changed, 407 insertions(+), 100 deletions(-) create mode 100644 quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverUnusedPropertiesTest.java create mode 100644 quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorWarningBuildTimePropertyChangedTest.java create mode 100644 quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorWarningRuntimePropertyChangedTest.java diff --git a/quarkus-integration/quarkus-benchmark/deployment/src/test/java/ai/timefold/solver/benchmark/quarkus/TimefoldBenchmarkProcessorMultipleSolversConfigTest.java b/quarkus-integration/quarkus-benchmark/deployment/src/test/java/ai/timefold/solver/benchmark/quarkus/TimefoldBenchmarkProcessorMultipleSolversConfigTest.java index 821207d63e..39e25a94c7 100644 --- a/quarkus-integration/quarkus-benchmark/deployment/src/test/java/ai/timefold/solver/benchmark/quarkus/TimefoldBenchmarkProcessorMultipleSolversConfigTest.java +++ b/quarkus-integration/quarkus-benchmark/deployment/src/test/java/ai/timefold/solver/benchmark/quarkus/TimefoldBenchmarkProcessorMultipleSolversConfigTest.java @@ -5,9 +5,13 @@ import java.util.concurrent.ExecutionException; +import jakarta.inject.Inject; +import jakarta.inject.Named; + import ai.timefold.solver.benchmark.quarkus.testdata.normal.constraints.TestdataQuarkusConstraintProvider; import ai.timefold.solver.benchmark.quarkus.testdata.normal.domain.TestdataQuarkusEntity; import ai.timefold.solver.benchmark.quarkus.testdata.normal.domain.TestdataQuarkusSolution; +import ai.timefold.solver.core.api.solver.SolverManager; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.JavaArchive; @@ -35,6 +39,14 @@ class TimefoldBenchmarkProcessorMultipleSolversConfigTest { When defining multiple solvers, the benchmark feature is not enabled. Consider using separate instances for evaluating different solver configurations.""")); + @Inject + @Named("solver1") + SolverManager solverManager1; + + @Inject + @Named("solver2") + SolverManager solverManager2; + @Test void benchmark() throws ExecutionException, InterruptedException { fail("It won't be executed"); diff --git a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/DotNames.java b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/DotNames.java index d2ec645879..a05591befe 100644 --- a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/DotNames.java +++ b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/DotNames.java @@ -2,6 +2,9 @@ import java.util.Arrays; import java.util.List; +import java.util.Set; + +import jakarta.inject.Named; import ai.timefold.solver.core.api.domain.constraintweight.ConstraintConfigurationProvider; import ai.timefold.solver.core.api.domain.constraintweight.ConstraintWeight; @@ -32,11 +35,18 @@ import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.api.solver.SolverFactory; +import ai.timefold.solver.core.api.solver.SolverManager; +import ai.timefold.solver.core.config.solver.SolverConfig; +import ai.timefold.solver.core.config.solver.SolverManagerConfig; import org.jboss.jandex.DotName; public final class DotNames { + // Jakarta classes + static final DotName NAMED = DotName.createSimple(Named.class); + // Timefold classes static final DotName PLANNING_SOLUTION = DotName.createSimple(PlanningSolution.class.getName()); static final DotName PLANNING_ENTITY_COLLECTION_PROPERTY = DotName.createSimple(PlanningEntityCollectionProperty.class.getName()); @@ -75,6 +85,11 @@ public final class DotNames { static final DotName CASCADING_UPDATE_SHADOW_VARIABLE = DotName.createSimple(CascadingUpdateShadowVariable.class.getName()); + static final DotName SOLVER_CONFIG = DotName.createSimple(SolverConfig.class.getName()); + static final DotName SOLVER_MANAGER_CONFIG = DotName.createSimple(SolverManagerConfig.class.getName()); + static final DotName SOLVER_FACTORY = DotName.createSimple(SolverFactory.class.getName()); + static final DotName SOLVER_MANAGER = DotName.createSimple(SolverManager.class.getName()); + // Need to use String since timefold-solver-test is not on the compile classpath static final DotName CONSTRAINT_VERIFIER = DotName.createSimple("ai.timefold.solver.test.api.score.stream.ConstraintVerifier"); @@ -121,6 +136,12 @@ public final class DotNames { CASCADING_UPDATE_SHADOW_VARIABLE }; + static final Set SOLVER_INJECTABLE_TYPES = Set.of( + SOLVER_CONFIG, + SOLVER_MANAGER_CONFIG, + SOLVER_FACTORY, + SOLVER_MANAGER); + public enum BeanDefiningAnnotations { PLANNING_SCORE(DotNames.PLANNING_SCORE, "scoreDefinitionClass"), PLANNING_SOLUTION(DotNames.PLANNING_SOLUTION, "solutionCloner"), diff --git a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/SolverConfigBuildItem.java b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/SolverConfigBuildItem.java index 281646b80d..f913479591 100644 --- a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/SolverConfigBuildItem.java +++ b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/SolverConfigBuildItem.java @@ -3,6 +3,7 @@ import java.util.Map; import ai.timefold.solver.core.config.solver.SolverConfig; +import ai.timefold.solver.quarkus.deployment.config.TimefoldBuildTimeConfig; import io.quarkus.builder.item.SimpleBuildItem; @@ -18,6 +19,10 @@ public SolverConfigBuildItem(Map solverConfig, GeneratedGi this.generatedGizmoClasses = generatedGizmoClasses; } + public boolean isDefaultSolverConfig(String solverName) { + return solverConfigurations.size() <= 1 || TimefoldBuildTimeConfig.DEFAULT_SOLVER_NAME.equals(solverName); + } + public Map getSolverConfigMap() { return solverConfigurations; } diff --git a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java index e861731f04..ad04eb4db8 100644 --- a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java +++ b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java @@ -184,6 +184,25 @@ SolverConfigBuildItem recordAndRegisterBuildTimeBeans(CombinedIndexBuildItem com BuildProducer transformers) { IndexView indexView = combinedIndex.getIndex(); + // Step 0 - determine list of names used for injected solver components + var solverNames = new HashSet(); + var solverConfigMap = new HashMap(); + for (var namedItem : indexView.getAnnotations(DotNames.NAMED)) { + var target = namedItem.target(); + DotName type = switch (target.kind()) { + case CLASS -> target.asClass().name(); + case FIELD -> target.asField().type().name(); + case METHOD_PARAMETER -> target.asMethodParameter().type().name(); + case RECORD_COMPONENT -> target.asRecordComponent().type().name(); + case TYPE, METHOD -> null; + }; + if (type != null && DotNames.SOLVER_INJECTABLE_TYPES.contains(type)) { + var annotationValue = namedItem.value(); + var value = (annotationValue != null) ? annotationValue.asString() : ""; + solverNames.add(value); + } + } + // Only skip this extension if everything is missing. Otherwise, if some parts are missing, fail fast later. if (indexView.getAnnotations(DotNames.PLANNING_SOLUTION).isEmpty() && indexView.getAnnotations(DotNames.PLANNING_ENTITY).isEmpty()) { @@ -194,24 +213,24 @@ SolverConfigBuildItem recordAndRegisterBuildTimeBeans(CombinedIndexBuildItem com + "application.properties entries (quarkus.index-dependency..group-id" + " and quarkus.index-dependency..artifact-id)."); additionalBeans.produce(new AdditionalBeanBuildItem(UnavailableTimefoldBeanProvider.class)); - Map solverConfigMap = new HashMap<>(); - this.timefoldBuildTimeConfig.solver().keySet().forEach(solverName -> solverConfigMap.put(solverName, null)); + solverNames.forEach(solverName -> solverConfigMap.put(solverName, null)); return new SolverConfigBuildItem(solverConfigMap, null); } // Quarkus extensions must always use getContextClassLoader() // Internally, Timefold defaults the ClassLoader to getContextClassLoader() too - ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + var classLoader = Thread.currentThread().getContextClassLoader(); + TimefoldRecorder.assertNoUnmatchedProperties(solverNames, + timefoldBuildTimeConfig.solver().keySet()); - Map solverConfigMap = new HashMap<>(); // Step 1 - create all SolverConfig // If the config map is empty, we build the config using the default solver name - if (timefoldBuildTimeConfig.solver().isEmpty()) { + if (solverNames.isEmpty()) { solverConfigMap.put(TimefoldBuildTimeConfig.DEFAULT_SOLVER_NAME, createSolverConfig(classLoader, TimefoldBuildTimeConfig.DEFAULT_SOLVER_NAME)); } else { // One config per solver mapped name - this.timefoldBuildTimeConfig.solver().keySet().forEach(solverName -> solverConfigMap.put(solverName, + solverNames.forEach(solverName -> solverConfigMap.put(solverName, createSolverConfig(classLoader, solverName))); } @@ -558,23 +577,23 @@ private SolverConfig loadSolverConfig(IndexView indexView, @Record(RUNTIME_INIT) void recordAndRegisterRuntimeBeans(TimefoldRecorder recorder, RecorderContext recorderContext, BuildProducer syntheticBeanBuildItemBuildProducer, - SolverConfigBuildItem solverConfigBuildItem, - TimefoldRuntimeConfig runtimeConfig) { + SolverConfigBuildItem solverConfigBuildItem) { // Skip this extension if everything is missing. if (solverConfigBuildItem.getGeneratedGizmoClasses() == null) { return; } + recorder.assertNoUnmatchedRuntimeProperties(solverConfigBuildItem.getSolverConfigMap().keySet()); // Using the same name for synthetic beans is impossible, even if they are different types. // Therefore, we allow only the injection of SolverManager, except for the default solver, // which can inject all resources to be retro-compatible. solverConfigBuildItem.getSolverConfigMap().forEach((key, value) -> { - if (timefoldBuildTimeConfig.isDefaultSolverConfig(key)) { + if (solverConfigBuildItem.isDefaultSolverConfig(key)) { // The two configuration resources are required for DefaultTimefoldBeanProvider // to produce all available managed beans for the default solver. syntheticBeanBuildItemBuildProducer.produce(SyntheticBeanBuildItem.configure(SolverConfig.class) .scope(Singleton.class) - .supplier(recorder.solverConfigSupplier(key, value, runtimeConfig, + .supplier(recorder.solverConfigSupplier(key, value, GizmoMemberAccessorEntityEnhancer.getGeneratedGizmoMemberAccessorMap(recorderContext, solverConfigBuildItem .getGeneratedGizmoClasses().generatedGizmoMemberAccessorClassSet), @@ -588,11 +607,12 @@ void recordAndRegisterRuntimeBeans(TimefoldRecorder recorder, RecorderContext re SolverManagerConfig solverManagerConfig = new SolverManagerConfig(); syntheticBeanBuildItemBuildProducer.produce(SyntheticBeanBuildItem.configure(SolverManagerConfig.class) .scope(Singleton.class) - .supplier(recorder.solverManagerConfig(solverManagerConfig, runtimeConfig)) + .supplier(recorder.solverManagerConfig(solverManagerConfig)) .setRuntimeInit() .defaultBean() .done()); - } else { + } + if (!TimefoldBuildTimeConfig.DEFAULT_SOLVER_NAME.equals(key)) { // The default SolverManager instance is generated by DefaultTimefoldBeanProvider syntheticBeanBuildItemBuildProducer.produce( // We generate all required resources only to create a SolverManager and set it as managed bean @@ -602,7 +622,7 @@ void recordAndRegisterRuntimeBeans(TimefoldRecorder recorder, RecorderContext re Type.create(DotName.createSimple(value.getSolutionClass().getName()), Type.Kind.CLASS), TypeVariable.create(Object.class.getName()))) - .supplier(recorder.solverManager(key, value, runtimeConfig, + .supplier(recorder.solverManager(key, value, GizmoMemberAccessorEntityEnhancer.getGeneratedGizmoMemberAccessorMap(recorderContext, solverConfigBuildItem .getGeneratedGizmoClasses().generatedGizmoMemberAccessorClassSet), @@ -622,11 +642,10 @@ public void recordAndRegisterDevUIBean( TimefoldDevUIRecorder devUIRecorder, RecorderContext recorderContext, SolverConfigBuildItem solverConfigBuildItem, - TimefoldRuntimeConfig runtimeConfig, BuildProducer syntheticBeans) { syntheticBeans.produce(SyntheticBeanBuildItem.configure(DevUISolverConfig.class) .scope(ApplicationScoped.class) - .supplier(devUIRecorder.solverConfigSupplier(solverConfigBuildItem.getSolverConfigMap(), runtimeConfig, + .supplier(devUIRecorder.solverConfigSupplier(solverConfigBuildItem.getSolverConfigMap(), GizmoMemberAccessorEntityEnhancer.getGeneratedGizmoMemberAccessorMap(recorderContext, solverConfigBuildItem .getGeneratedGizmoClasses().generatedGizmoMemberAccessorClassSet), @@ -701,10 +720,6 @@ private void applySolverProperties(IndexView indexView, String solverName, Solve applyScoreDirectorFactoryProperties(indexView, solverConfig); // Override the current configuration with values from the solver properties - timefoldBuildTimeConfig.getSolverConfig(solverName).flatMap(SolverBuildTimeConfig::environmentMode) - .ifPresent(solverConfig::setEnvironmentMode); - timefoldBuildTimeConfig.getSolverConfig(solverName).flatMap(SolverBuildTimeConfig::daemon) - .ifPresent(solverConfig::setDaemon); timefoldBuildTimeConfig.getSolverConfig(solverName).flatMap(SolverBuildTimeConfig::domainAccessType) .ifPresent(solverConfig::setDomainAccessType); diff --git a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/SolverBuildTimeConfig.java b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/SolverBuildTimeConfig.java index 3c24e020c6..2673e808eb 100644 --- a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/SolverBuildTimeConfig.java +++ b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/SolverBuildTimeConfig.java @@ -4,11 +4,8 @@ import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.score.stream.ConstraintStreamImplType; -import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.config.solver.SolverConfig; -import ai.timefold.solver.core.config.solver.termination.TerminationConfig; import ai.timefold.solver.quarkus.config.SolverRuntimeConfig; -import ai.timefold.solver.quarkus.config.TerminationRuntimeConfig; import io.quarkus.runtime.annotations.ConfigGroup; @@ -25,47 +22,23 @@ public interface SolverBuildTimeConfig { * A classpath resource to read the specific solver configuration XML. * If this property isn't specified, that solverConfig.xml is optional. */ + // Build time - classes in the SolverConfig are visited by SolverConfig.visitReferencedClasses + // which generates the constructor of classes used by Quarkus Optional solverConfigXml(); - /** - * Enable runtime assertions to detect common bugs in your implementation during development. - * Defaults to {@link EnvironmentMode#REPRODUCIBLE}. - */ - Optional environmentMode(); - - /** - * Enable daemon mode. In daemon mode, non-early termination pauses the solver instead of stopping it, - * until the next problem fact change arrives. - * This is often useful for real-time planning. - * Defaults to "false". - */ - Optional daemon(); - /** * Determines how to access the fields and methods of domain classes. * Defaults to {@link DomainAccessType#GIZMO}. */ + // Build time - GIZMO classes are only generated if at least one solver + // has domain access type GIZMO Optional domainAccessType(); - /** - * Note: this setting is only available - * for Timefold Solver - * Enterprise Edition. - * Enable multithreaded solving for a single problem, which increases CPU consumption. - * Defaults to {@value SolverConfig#MOVE_THREAD_COUNT_NONE}. - * Other options include {@value SolverConfig#MOVE_THREAD_COUNT_AUTO}, a number - * or formula based on the available processor count. - */ - Optional moveThreadCount(); - - /** - * Configuration properties regarding {@link TerminationConfig}. - */ - TerminationRuntimeConfig termination(); - /** * Enable the Nearby Selection quick configuration. */ + // Build time - visited by SolverConfig.visitReferencedClasses + // which generates the constructor used by Quarkus Optional> nearbyDistanceMeterClass(); /** @@ -86,5 +59,6 @@ public interface SolverBuildTimeConfig { * will no longer be triggered. * Defaults to "false". */ + // Build time - modifies the ConstraintProvider class if set Optional constraintStreamAutomaticNodeSharing(); } diff --git a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/TimefoldBuildTimeConfig.java b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/TimefoldBuildTimeConfig.java index ce4f612683..c0b91ff6b8 100644 --- a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/TimefoldBuildTimeConfig.java +++ b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/TimefoldBuildTimeConfig.java @@ -34,12 +34,6 @@ public interface TimefoldBuildTimeConfig { @WithUnnamedKey(DEFAULT_SOLVER_NAME) Map solver(); - default boolean isDefaultSolverConfig(String solverName) { - // 1 - No solver configuration, which means we will use a default empty SolverConfig and default Solver name - // 2 - Only one solve config. It will be the default one. - return solver().isEmpty() || solver().size() == 1 && getSolverConfig(solverName).isPresent(); - } - default Optional getSolverConfig(String solverName) { return Optional.ofNullable(solver().get(solverName)); } diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorMultipleSolversInvalidConstraintClassTest.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorMultipleSolversInvalidConstraintClassTest.java index cdf7cb6bf0..7e904025e1 100644 --- a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorMultipleSolversInvalidConstraintClassTest.java +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorMultipleSolversInvalidConstraintClassTest.java @@ -4,6 +4,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.fail; +import jakarta.inject.Inject; +import jakarta.inject.Named; + +import ai.timefold.solver.core.api.solver.SolverManager; import ai.timefold.solver.quarkus.testdata.dummy.DummyTestdataQuarkusEasyScoreCalculator; import ai.timefold.solver.quarkus.testdata.dummy.DummyTestdataQuarkusIncrementalScoreCalculator; import ai.timefold.solver.quarkus.testdata.dummy.DummyTestdataQuarkusShadowVariableEasyScoreCalculator; @@ -226,6 +230,14 @@ class TimefoldProcessorMultipleSolversInvalidConstraintClassTest { .hasMessageContaining( "Unused classes ([ai.timefold.solver.quarkus.testdata.dummy.DummyTestdataQuarkusShadowVariableIncrementalScoreCalculator]) that implements IncrementalScoreCalculator were found.")); + @Inject + @Named("solver1") + SolverManager solverManager1; + + @Inject + @Named("solver2") + SolverManager solverManager2; + @Test void test() { fail("Should not call this method."); diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorMultipleSolversInvalidEntityClassTest.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorMultipleSolversInvalidEntityClassTest.java index 2569a47edc..f1f69b06ad 100644 --- a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorMultipleSolversInvalidEntityClassTest.java +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorMultipleSolversInvalidEntityClassTest.java @@ -4,6 +4,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.fail; +import jakarta.inject.Inject; +import jakarta.inject.Named; + +import ai.timefold.solver.core.api.solver.SolverManager; import ai.timefold.solver.quarkus.testdata.normal.domain.TestdataQuarkusSolution; import org.jboss.shrinkwrap.api.ShrinkWrap; @@ -26,6 +30,14 @@ class TimefoldProcessorMultipleSolversInvalidEntityClassTest { .hasMessageContaining( "No classes were found with a @PlanningEntity annotation.")); + @Inject + @Named("solver1") + SolverManager solverManager1; + + @Inject + @Named("solver2") + SolverManager solverManager2; + @Test void test() { fail("Should not call this method."); diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorMultipleSolversInvalidSolutionClassTest.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorMultipleSolversInvalidSolutionClassTest.java index f50dc0767d..171a550299 100644 --- a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorMultipleSolversInvalidSolutionClassTest.java +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorMultipleSolversInvalidSolutionClassTest.java @@ -4,6 +4,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.fail; +import jakarta.inject.Inject; +import jakarta.inject.Named; + +import ai.timefold.solver.core.api.solver.SolverManager; import ai.timefold.solver.quarkus.testdata.chained.domain.TestdataChainedQuarkusSolution; import ai.timefold.solver.quarkus.testdata.normal.constraints.TestdataQuarkusConstraintProvider; import ai.timefold.solver.quarkus.testdata.normal.domain.TestdataQuarkusEntity; @@ -78,6 +82,14 @@ class TimefoldProcessorMultipleSolversInvalidSolutionClassTest { .hasMessageContaining( "Unused classes ([ai.timefold.solver.quarkus.testdata.chained.domain.TestdataChainedQuarkusSolution]) found with a @PlanningSolution annotation.")); + @Inject + @Named("solver1") + SolverManager solverManager1; + + @Inject + @Named("solver2") + SolverManager solverManager2; + @Test void test() { fail("Should not call this method."); diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorMultipleSolversPropertiesTest.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorMultipleSolversPropertiesTest.java index 2bdf068f3a..7aec585735 100644 --- a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorMultipleSolversPropertiesTest.java +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorMultipleSolversPropertiesTest.java @@ -2,6 +2,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import jakarta.inject.Inject; +import jakarta.inject.Named; + +import ai.timefold.solver.core.api.solver.SolverManager; import ai.timefold.solver.quarkus.rest.TestdataQuarkusSolutionConfigResource; import ai.timefold.solver.quarkus.testdata.normal.constraints.TestdataQuarkusConstraintProvider; import ai.timefold.solver.quarkus.testdata.normal.domain.TestdataQuarkusEntity; @@ -25,6 +29,14 @@ class TimefoldProcessorMultipleSolversPropertiesTest { .addClasses(TestdataQuarkusEntity.class, TestdataQuarkusSolution.class, TestdataQuarkusConstraintProvider.class, TestdataQuarkusSolutionConfigResource.class)); + @Inject + @Named("solver1") + SolverManager solverManager1; + + @Inject + @Named("solver2") + SolverManager solverManager2; + @Test void solverProperties() { String resp = RestAssured.get("/solver-config/seconds-spent-limit").asString(); diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorMultipleSolversYamlTest.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorMultipleSolversYamlTest.java index 33792d75c5..19a6548349 100644 --- a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorMultipleSolversYamlTest.java +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorMultipleSolversYamlTest.java @@ -2,6 +2,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import jakarta.inject.Inject; +import jakarta.inject.Named; + +import ai.timefold.solver.core.api.solver.SolverManager; import ai.timefold.solver.quarkus.rest.TestdataQuarkusSolutionConfigResource; import ai.timefold.solver.quarkus.testdata.normal.constraints.TestdataQuarkusConstraintProvider; import ai.timefold.solver.quarkus.testdata.normal.domain.TestdataQuarkusEntity; @@ -24,6 +28,14 @@ class TimefoldProcessorMultipleSolversYamlTest { TestdataQuarkusConstraintProvider.class, TestdataQuarkusSolutionConfigResource.class) .addAsResource("ai/timefold/solver/quarkus/multiple-solvers/application.yaml", "application.yaml")); + @Inject + @Named("solver1") + SolverManager solverManager1; + + @Inject + @Named("solver2") + SolverManager solverManager2; + @Test void solverProperties() { String resp = RestAssured.get("/solver-config/seconds-spent-limit").asString(); diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverResourcesTest.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverResourcesTest.java index 4ea15c8dab..c49e9472bb 100644 --- a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverResourcesTest.java +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverResourcesTest.java @@ -5,6 +5,7 @@ import java.time.Duration; import jakarta.inject.Inject; +import jakarta.inject.Named; import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.score.ScoreManager; @@ -51,6 +52,10 @@ class TimefoldProcessorSolverResourcesTest { .addClasses(TestdataQuarkusEntity.class, TestdataQuarkusSolution.class, TestdataQuarkusConstraintProvider.class)); + @Inject + @Named("solver1") + SolverManager solverManager1; + @Inject ConstraintMetaModel constraintMetaModel; diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverUnusedPropertiesTest.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverUnusedPropertiesTest.java new file mode 100644 index 0000000000..c3e989a778 --- /dev/null +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverUnusedPropertiesTest.java @@ -0,0 +1,86 @@ +package ai.timefold.solver.quarkus; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +import jakarta.inject.Inject; +import jakarta.inject.Named; + +import ai.timefold.solver.core.api.solver.SolverManager; +import ai.timefold.solver.core.config.solver.SolverConfig; +import ai.timefold.solver.quarkus.testdata.dummy.DummyDistanceMeter; +import ai.timefold.solver.quarkus.testdata.normal.constraints.TestdataQuarkusConstraintProvider; +import ai.timefold.solver.quarkus.testdata.normal.domain.TestdataQuarkusEntity; +import ai.timefold.solver.quarkus.testdata.normal.domain.TestdataQuarkusSolution; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +class TimefoldProcessorSolverUnusedPropertiesTest { + + @RegisterExtension + static final QuarkusUnitTest config1 = new QuarkusUnitTest() + .overrideConfigKey("quarkus.timefold.solver.environment-mode", "FULL_ASSERT") + .overrideConfigKey("quarkus.timefold.solver.\"solver1\".daemon", "true") + .overrideConfigKey("quarkus.timefold.solver.\"solver1\".nearby-distance-meter-class", + "ai.timefold.solver.quarkus.testdata.dummy.DummyDistanceMeter") + .overrideConfigKey("quarkus.timefold.solver.\"solver2\".move-thread-count", "2") + .overrideConfigKey("quarkus.timefold.solver.\"solver2\".domain-access-type", "REFLECTION") + .overrideConfigKey("quarkus.timefold.solver.\"solver2\".termination.spent-limit", "4h") + .overrideConfigKey("quarkus.timefold.solver.\"solver3\".termination.unimproved-spent-limit", "5h") + .overrideConfigKey("quarkus.timefold.solver.\"solver3\".termination.best-score-limit", "0") + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(TestdataQuarkusEntity.class, TestdataQuarkusSolution.class, + TestdataQuarkusConstraintProvider.class, DummyDistanceMeter.class)) + .assertException(throwable -> { + // This failure happens at build time and does not have access to solver3, which + // is only defined at runtime + assertThat(throwable) + .hasMessageContaining("Some names defined in properties") + .hasMessageContaining("solver2") + .hasMessageContaining("do not have a corresponding @" + Named.class.getSimpleName() + + " injection point") + .hasMessageContaining("solver1"); + }); + + @RegisterExtension + static final QuarkusUnitTest config2 = new QuarkusUnitTest() + .overrideConfigKey("quarkus.timefold.solver.environment-mode", "FULL_ASSERT") + .overrideConfigKey("quarkus.timefold.solver.\"solver1\".daemon", "true") + .overrideConfigKey("quarkus.timefold.solver.\"solver1\".nearby-distance-meter-class", + "ai.timefold.solver.quarkus.testdata.dummy.DummyDistanceMeter") + .overrideConfigKey("quarkus.timefold.solver.\"solver2\".termination.unimproved-spent-limit", "5h") + .overrideConfigKey("quarkus.timefold.solver.\"solver2\".termination.best-score-limit", "0") + .overrideConfigKey("quarkus.timefold.solver.\"solver3\".termination.unimproved-spent-limit", "5h") + .overrideConfigKey("quarkus.timefold.solver.\"solver3\".termination.best-score-limit", "0") + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(TestdataQuarkusEntity.class, TestdataQuarkusSolution.class, + TestdataQuarkusConstraintProvider.class, DummyDistanceMeter.class)) + .assertException(throwable -> { + // The build succeeds, but runtime fails at startup due to runtime properties referencing + // missing Named annotations + assertThat(throwable) + .hasMessageContaining("Some names defined in properties") + .hasMessageContaining("solver2") + .hasMessageContaining("solver3") + .hasMessageContaining("do not have a corresponding @" + Named.class.getSimpleName() + + " injection point") + .hasMessageContaining("solver1"); + }); + + @Inject + SolverConfig solverConfig; + + @Inject + @Named("solver1") + SolverManager solverManager; + + @Test + void solve() { + fail("Build should fail"); + } +} diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorWarningBuildTimePropertyChangedTest.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorWarningBuildTimePropertyChangedTest.java new file mode 100644 index 0000000000..3e1b6f1fca --- /dev/null +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorWarningBuildTimePropertyChangedTest.java @@ -0,0 +1,37 @@ +package ai.timefold.solver.quarkus; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.logging.Level; + +import ai.timefold.solver.quarkus.testdata.normal.constraints.TestdataQuarkusConstraintProvider; +import ai.timefold.solver.quarkus.testdata.normal.domain.TestdataQuarkusEntity; +import ai.timefold.solver.quarkus.testdata.normal.domain.TestdataQuarkusSolution; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +class TimefoldProcessorWarningBuildTimePropertyChangedTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.timefold.solver.daemon", "true") + // We overwrite the value at runtime + .overrideRuntimeConfigKey("quarkus.timefold.solver.daemon", "false") + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(TestdataQuarkusEntity.class, TestdataQuarkusSolution.class, + TestdataQuarkusConstraintProvider.class)) + // Make sure Quarkus does not produce a warning for overwriting a build time value at runtime + .setLogRecordPredicate(logRecord -> logRecord.getLoggerName().startsWith("io.quarkus") + && logRecord.getLevel().intValue() >= Level.WARNING.intValue()); + + @Test + void solverProperties() { + config.assertLogRecords(logRecords -> { + assertEquals(1, logRecords.size(), "expected warning to be generated"); + }); + } +} diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorWarningRuntimePropertyChangedTest.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorWarningRuntimePropertyChangedTest.java new file mode 100644 index 0000000000..8c15e0296d --- /dev/null +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorWarningRuntimePropertyChangedTest.java @@ -0,0 +1,39 @@ +package ai.timefold.solver.quarkus; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.logging.Level; + +import ai.timefold.solver.quarkus.testdata.normal.constraints.TestdataQuarkusConstraintProvider; +import ai.timefold.solver.quarkus.testdata.normal.domain.TestdataQuarkusEntity; +import ai.timefold.solver.quarkus.testdata.normal.domain.TestdataQuarkusSolution; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +class TimefoldProcessorWarningRuntimePropertyChangedTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.timefold.solver.move-thread-count", "1") + .overrideConfigKey("quarkus.timefold.solver.termination.spent-limit", "1s") + // We overwrite the value at runtime + .overrideRuntimeConfigKey("quarkus.timefold.solver.move-thread-count", "2") + .overrideRuntimeConfigKey("quarkus.timefold.solver.termination.spent-limit", "2s") + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(TestdataQuarkusEntity.class, TestdataQuarkusSolution.class, + TestdataQuarkusConstraintProvider.class)) + // Make sure Quarkus does not produce a warning for overwriting a build time value at runtime + .setLogRecordPredicate(logRecord -> logRecord.getLoggerName().startsWith("io.quarkus") + && logRecord.getLevel().intValue() >= Level.WARNING.intValue()); + + @Test + void solverProperties() { + config.assertLogRecords(logRecords -> { + assertEquals(0, logRecords.size(), "expected no warnings to be generated"); + }); + } +} diff --git a/quarkus-integration/quarkus/devui-integration-test/src/test/java/ai/timefold/solver/quarkus/it/devui/TimefoldDevUIMultipleSolversTest.java b/quarkus-integration/quarkus/devui-integration-test/src/test/java/ai/timefold/solver/quarkus/it/devui/TimefoldDevUIMultipleSolversTest.java index b8a578a6ff..83e4f3c1c1 100644 --- a/quarkus-integration/quarkus/devui-integration-test/src/test/java/ai/timefold/solver/quarkus/it/devui/TimefoldDevUIMultipleSolversTest.java +++ b/quarkus-integration/quarkus/devui-integration-test/src/test/java/ai/timefold/solver/quarkus/it/devui/TimefoldDevUIMultipleSolversTest.java @@ -28,8 +28,6 @@ public class TimefoldDevUIMultipleSolversTest extends DevUIJsonRPCTest { @RegisterExtension static final QuarkusDevModeTest config = new QuarkusDevModeTest() - .setBuildSystemProperty("quarkus.timefold.solver.\"solver1\".environment-mode", "FULL_ASSERT") - .setBuildSystemProperty("quarkus.timefold.solver.\"solver2\".environment-mode", "REPRODUCIBLE") .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) .addClasses(StringLengthVariableListener.class, TestdataStringLengthShadowEntity.class, TestdataStringLengthShadowSolution.class, @@ -54,16 +52,24 @@ public TimefoldDevUIMultipleSolversTest() { @Test void testSolverConfigPage() throws Exception { JsonNode configResponse = super.executeJsonRPCMethod("getConfig"); - assertSolverConfigPage(configResponse.get("config").get("solver1").asText(), "FULL_ASSERT"); - assertSolverConfigPage(configResponse.get("config").get("solver2").asText(), "REPRODUCIBLE"); + + // All properties in SolverBuildTimeConfig either are + // - Enterprise properties + // - Have fail fasts if they are not consistent across solvers + // - Store the entire solver XML + // + // Since runtime properties are not included in the generated XML file, + // this leaves a surprising few ways to make the SolverConfig different. + // So we only check that they both exist. + assertSolverConfigPage(configResponse.get("config").get("solver1").asText()); + assertSolverConfigPage(configResponse.get("config").get("solver2").asText()); } - private void assertSolverConfigPage(String solverConfig, String environment) { + private void assertSolverConfigPage(String solverConfig) { assertThat(solverConfig).isEqualToIgnoringWhitespace( "\n" + "\n" + "\n" - + " " + environment + "\n" + " " + TestdataStringLengthShadowSolution.class.getCanonicalName() + "\n" + " " + TestdataStringLengthShadowEntity.class.getCanonicalName() + "\n" diff --git a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/TimefoldRecorder.java b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/TimefoldRecorder.java index 1f3333de47..628976143e 100644 --- a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/TimefoldRecorder.java +++ b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/TimefoldRecorder.java @@ -1,9 +1,15 @@ package ai.timefold.solver.quarkus; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; import java.util.function.Supplier; +import jakarta.inject.Named; + import ai.timefold.solver.core.api.domain.solution.cloner.SolutionCloner; import ai.timefold.solver.core.api.solver.SolverFactory; import ai.timefold.solver.core.api.solver.SolverManager; @@ -14,19 +20,50 @@ import ai.timefold.solver.quarkus.config.SolverRuntimeConfig; import ai.timefold.solver.quarkus.config.TimefoldRuntimeConfig; +import org.jspecify.annotations.Nullable; + import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; @Recorder public class TimefoldRecorder { + final TimefoldRuntimeConfig timefoldRuntimeConfig; + + public TimefoldRecorder(final TimefoldRuntimeConfig timefoldRuntimeConfig) { + this.timefoldRuntimeConfig = timefoldRuntimeConfig; + } + + public static void assertNoUnmatchedProperties(Set expectedNames, Set actualNames) { + var allExpectedNames = new HashSet<>(expectedNames); + allExpectedNames.add(TimefoldRuntimeConfig.DEFAULT_SOLVER_NAME); + + if (!allExpectedNames.containsAll(actualNames)) { + var expectedNamesSorted = expectedNames.stream() + .sorted() + .toList(); + var unmatchedNamesSorted = actualNames.stream() + .filter(Predicate.not(allExpectedNames::contains)) + .sorted() + .toList(); + throw new IllegalStateException(""" + Some names defined in properties (%s) do not have \ + a corresponding @%s injection point (%s). Maybe you \ + misspelled them? + """.formatted(unmatchedNamesSorted, Named.class.getSimpleName(), + expectedNamesSorted)); + } + } + + public void assertNoUnmatchedRuntimeProperties(Set names) { + assertNoUnmatchedProperties(names, timefoldRuntimeConfig.solver().keySet()); + } public Supplier solverConfigSupplier(final String solverName, final SolverConfig solverConfig, - final TimefoldRuntimeConfig timefoldRuntimeConfig, Map> generatedGizmoMemberAccessorMap, Map> generatedGizmoSolutionClonerMap) { return () -> { - updateSolverConfigWithRuntimeProperties(solverName, solverConfig, timefoldRuntimeConfig); + updateSolverConfigWithRuntimeProperties(solverName, solverConfig); Map memberAccessorMap = new HashMap<>(); Map solutionClonerMap = new HashMap<>(); generatedGizmoMemberAccessorMap @@ -40,21 +77,19 @@ public Supplier solverConfigSupplier(final String solverName, }; } - public Supplier solverManagerConfig(final SolverManagerConfig solverManagerConfig, - final TimefoldRuntimeConfig timefoldRuntimeConfig) { + public Supplier solverManagerConfig(final SolverManagerConfig solverManagerConfig) { return () -> { - updateSolverManagerConfigWithRuntimeProperties(solverManagerConfig, timefoldRuntimeConfig); + updateSolverManagerConfigWithRuntimeProperties(solverManagerConfig); return solverManagerConfig; }; } public Supplier> solverManager(final String solverName, final SolverConfig solverConfig, - final TimefoldRuntimeConfig timefoldRuntimeConfig, Map> generatedGizmoMemberAccessorMap, Map> generatedGizmoSolutionClonerMap) { return () -> { - updateSolverConfigWithRuntimeProperties(solverName, solverConfig, timefoldRuntimeConfig); + updateSolverConfigWithRuntimeProperties(solverName, solverConfig); Map memberAccessorMap = new HashMap<>(); Map solutionClonerMap = new HashMap<>(); generatedGizmoMemberAccessorMap @@ -66,7 +101,7 @@ public Supplier> so solverConfig.setGizmoSolutionClonerMap(solutionClonerMap); SolverManagerConfig solverManagerConfig = new SolverManagerConfig(); - updateSolverManagerConfigWithRuntimeProperties(solverManagerConfig, timefoldRuntimeConfig); + updateSolverManagerConfigWithRuntimeProperties(solverManagerConfig); SolverFactory solverFactory = SolverFactory.create(solverConfig); @@ -74,26 +109,35 @@ public Supplier> so }; } - private void updateSolverConfigWithRuntimeProperties(String solverName, SolverConfig solverConfig, - TimefoldRuntimeConfig timefoldRunTimeConfig) { + private void updateSolverConfigWithRuntimeProperties(String solverName, SolverConfig solverConfig) { + updateSolverConfigWithRuntimeProperties(solverConfig, timefoldRuntimeConfig + .getSolverRuntimeConfig(solverName).orElse(null)); + } + + public static void updateSolverConfigWithRuntimeProperties(SolverConfig solverConfig, + @Nullable SolverRuntimeConfig solverRuntimeConfig) { TerminationConfig terminationConfig = solverConfig.getTerminationConfig(); if (terminationConfig == null) { terminationConfig = new TerminationConfig(); solverConfig.setTerminationConfig(terminationConfig); } - timefoldRunTimeConfig.getSolverRuntimeConfig(solverName).flatMap(config -> config.termination().spentLimit()) + var maybeSolverRuntimeConfig = Optional.ofNullable(solverRuntimeConfig); + maybeSolverRuntimeConfig.flatMap(config -> config.termination().spentLimit()) .ifPresent(terminationConfig::setSpentLimit); - timefoldRunTimeConfig.getSolverRuntimeConfig(solverName).flatMap(config -> config.termination().unimprovedSpentLimit()) + maybeSolverRuntimeConfig.flatMap(config -> config.termination().unimprovedSpentLimit()) .ifPresent(terminationConfig::setUnimprovedSpentLimit); - timefoldRunTimeConfig.getSolverRuntimeConfig(solverName).flatMap(config -> config.termination().bestScoreLimit()) + maybeSolverRuntimeConfig.flatMap(config -> config.termination().bestScoreLimit()) .ifPresent(terminationConfig::setBestScoreLimit); - timefoldRunTimeConfig.getSolverRuntimeConfig(solverName).flatMap(SolverRuntimeConfig::moveThreadCount) + maybeSolverRuntimeConfig.flatMap(SolverRuntimeConfig::environmentMode) + .ifPresent(solverConfig::setEnvironmentMode); + maybeSolverRuntimeConfig.flatMap(SolverRuntimeConfig::daemon) + .ifPresent(solverConfig::setDaemon); + maybeSolverRuntimeConfig.flatMap(SolverRuntimeConfig::moveThreadCount) .ifPresent(solverConfig::setMoveThreadCount); } - private void updateSolverManagerConfigWithRuntimeProperties(SolverManagerConfig solverManagerConfig, - TimefoldRuntimeConfig timefoldRunTimeConfig) { - timefoldRunTimeConfig.solverManager().parallelSolverCount().ifPresent(solverManagerConfig::setParallelSolverCount); + private void updateSolverManagerConfigWithRuntimeProperties(SolverManagerConfig solverManagerConfig) { + timefoldRuntimeConfig.solverManager().parallelSolverCount().ifPresent(solverManagerConfig::setParallelSolverCount); } } diff --git a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/config/SolverRuntimeConfig.java b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/config/SolverRuntimeConfig.java index 237a37975a..82b9889c98 100644 --- a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/config/SolverRuntimeConfig.java +++ b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/config/SolverRuntimeConfig.java @@ -2,6 +2,7 @@ import java.util.Optional; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.config.solver.SolverConfig; import ai.timefold.solver.core.config.solver.termination.TerminationConfig; @@ -14,6 +15,23 @@ @ConfigGroup public interface SolverRuntimeConfig { /** + * Enable runtime assertions to detect common bugs in your implementation during development. + * Defaults to {@link EnvironmentMode#REPRODUCIBLE}. + */ + Optional environmentMode(); + + /** + * Enable daemon mode. In daemon mode, non-early termination pauses the solver instead of stopping it, + * until the next problem fact change arrives. + * This is often useful for real-time planning. + * Defaults to "false". + */ + Optional daemon(); + + /** + * Note: this setting is only available + * for Timefold Solver + * Enterprise Edition. * Enable multithreaded solving for a single problem, which increases CPU consumption. * Defaults to {@value SolverConfig#MOVE_THREAD_COUNT_NONE}. * Other options include {@value SolverConfig#MOVE_THREAD_COUNT_AUTO}, a number diff --git a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/TimefoldDevUIRecorder.java b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/TimefoldDevUIRecorder.java index 43ea98b3fc..faf2e18cf7 100644 --- a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/TimefoldDevUIRecorder.java +++ b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/TimefoldDevUIRecorder.java @@ -8,10 +8,9 @@ import ai.timefold.solver.core.api.domain.solution.cloner.SolutionCloner; import ai.timefold.solver.core.api.solver.SolverFactory; import ai.timefold.solver.core.config.solver.SolverConfig; -import ai.timefold.solver.core.config.solver.termination.TerminationConfig; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; import ai.timefold.solver.core.impl.io.jaxb.SolverConfigIO; -import ai.timefold.solver.quarkus.config.SolverRuntimeConfig; +import ai.timefold.solver.quarkus.TimefoldRecorder; import ai.timefold.solver.quarkus.config.TimefoldRuntimeConfig; import io.quarkus.runtime.RuntimeValue; @@ -19,15 +18,19 @@ @Recorder public class TimefoldDevUIRecorder { + final TimefoldRuntimeConfig timefoldRuntimeConfig; + + public TimefoldDevUIRecorder(final TimefoldRuntimeConfig timefoldRuntimeConfig) { + this.timefoldRuntimeConfig = timefoldRuntimeConfig; + } public Supplier solverConfigSupplier(Map allSolverConfig, - TimefoldRuntimeConfig timefoldRuntimeConfig, Map> generatedGizmoMemberAccessorMap, Map> generatedGizmoSolutionClonerMap) { return () -> { DevUISolverConfig uiSolverConfig = new DevUISolverConfig(); allSolverConfig.forEach((solverName, solverConfig) -> { - updateSolverConfigWithRuntimeProperties(solverName, solverConfig, timefoldRuntimeConfig); + updateSolverConfigWithRuntimeProperties(solverName, solverConfig); Map memberAccessorMap = new HashMap<>(); Map solutionClonerMap = new HashMap<>(); generatedGizmoMemberAccessorMap @@ -50,20 +53,8 @@ public Supplier solverConfigSupplier(Map config.termination().spentLimit()) - .ifPresent(terminationConfig::setSpentLimit); - timefoldRunTimeConfig.getSolverRuntimeConfig(solverName).flatMap(config -> config.termination().unimprovedSpentLimit()) - .ifPresent(terminationConfig::setUnimprovedSpentLimit); - timefoldRunTimeConfig.getSolverRuntimeConfig(solverName).flatMap(config -> config.termination().bestScoreLimit()) - .ifPresent(terminationConfig::setBestScoreLimit); - timefoldRunTimeConfig.getSolverRuntimeConfig(solverName).flatMap(SolverRuntimeConfig::moveThreadCount) - .ifPresent(solverConfig::setMoveThreadCount); + private void updateSolverConfigWithRuntimeProperties(String solverName, SolverConfig solverConfig) { + TimefoldRecorder.updateSolverConfigWithRuntimeProperties(solverConfig, + timefoldRuntimeConfig.getSolverRuntimeConfig(solverName).orElse(null)); } }