From ce1360a8e61a9c2d02bd2000d8d8d0cc696edcfc Mon Sep 17 00:00:00 2001 From: rsynek Date: Fri, 17 Jan 2025 21:35:42 +0100 Subject: [PATCH 1/2] feat: expose ConstraintMetaModel at build time --- .../quarkus/deployment/pom.xml | 10 +++++ .../src/build/revapi-differences.json | 9 ++++ .../deployment/src/build/revapi-filter.json | 12 ++++++ .../deployment/SolverConfigBuildItem.java | 2 +- .../quarkus/deployment/TimefoldProcessor.java | 41 +++++++++++++++++++ .../api/ConstraintMetaModelBuildItem.java | 24 +++++++++++ .../quarkus/deployment/api/package-info.java | 7 ++++ 7 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 quarkus-integration/quarkus/deployment/src/build/revapi-differences.json create mode 100644 quarkus-integration/quarkus/deployment/src/build/revapi-filter.json create mode 100644 quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/api/ConstraintMetaModelBuildItem.java create mode 100644 quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/api/package-info.java diff --git a/quarkus-integration/quarkus/deployment/pom.xml b/quarkus-integration/quarkus/deployment/pom.xml index 411d9bf409..bbc8bcc5ab 100644 --- a/quarkus-integration/quarkus/deployment/pom.xml +++ b/quarkus-integration/quarkus/deployment/pom.xml @@ -101,6 +101,16 @@ + + org.revapi + revapi-maven-plugin + + + + ${project.groupId}:timefold-solver-quarkus-deployment:1.18.0 + + + maven-surefire-plugin diff --git a/quarkus-integration/quarkus/deployment/src/build/revapi-differences.json b/quarkus-integration/quarkus/deployment/src/build/revapi-differences.json new file mode 100644 index 0000000000..6441d68975 --- /dev/null +++ b/quarkus-integration/quarkus/deployment/src/build/revapi-differences.json @@ -0,0 +1,9 @@ +[ + { + "extension": "revapi.differences", + "configuration": { + "differences": [ + ] + } + } +] diff --git a/quarkus-integration/quarkus/deployment/src/build/revapi-filter.json b/quarkus-integration/quarkus/deployment/src/build/revapi-filter.json new file mode 100644 index 0000000000..6b4a8a5528 --- /dev/null +++ b/quarkus-integration/quarkus/deployment/src/build/revapi-filter.json @@ -0,0 +1,12 @@ +[ + { + "extension": "revapi.filter", + "configuration": { + "elements": { + "include": [ + "class ai\\.timefold\\.solver\\.quarkus\\.deployment\\.api.*" + ] + } + } + } +] \ No newline at end of file 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 f913479591..0d52e5b472 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 @@ -15,7 +15,7 @@ public final class SolverConfigBuildItem extends SimpleBuildItem { * Constructor for multiple solver configurations. */ public SolverConfigBuildItem(Map solverConfig, GeneratedGizmoClasses generatedGizmoClasses) { - this.solverConfigurations = solverConfig; + this.solverConfigurations = Map.copyOf(solverConfig); this.generatedGizmoClasses = generatedGizmoClasses; } 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 ad04eb4db8..f49857af30 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 @@ -30,6 +30,7 @@ import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty; 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.ConstraintMetaModel; 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; @@ -39,11 +40,14 @@ import ai.timefold.solver.core.impl.domain.common.ReflectionHelper; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.heuristic.selector.common.nearby.NearbyDistanceMeter; +import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamScoreDirectorFactory; +import ai.timefold.solver.core.impl.solver.DefaultSolverFactory; import ai.timefold.solver.quarkus.TimefoldRecorder; import ai.timefold.solver.quarkus.bean.DefaultTimefoldBeanProvider; import ai.timefold.solver.quarkus.bean.TimefoldSolverBannerBean; import ai.timefold.solver.quarkus.bean.UnavailableTimefoldBeanProvider; import ai.timefold.solver.quarkus.config.TimefoldRuntimeConfig; +import ai.timefold.solver.quarkus.deployment.api.ConstraintMetaModelBuildItem; import ai.timefold.solver.quarkus.deployment.config.SolverBuildTimeConfig; import ai.timefold.solver.quarkus.deployment.config.TimefoldBuildTimeConfig; import ai.timefold.solver.quarkus.devui.DevUISolverConfig; @@ -573,6 +577,43 @@ private SolverConfig loadSolverConfig(IndexView indexView, return solverConfig; } + @BuildStep + void buildConstraintMetaModel(SolverConfigBuildItem solverConfigBuildItem, + BuildProducer constraintMetaModelBuildItemBuildProducer) { + if (solverConfigBuildItem.getSolverConfigMap().isEmpty()) { + return; + } + + Map constraintMetaModelsBySolverNames = new HashMap<>(); + solverConfigBuildItem.getSolverConfigMap().forEach((solverName, solverConfig) -> { + // Gizmo-generated member accessors are not yet available at build time. + DomainAccessType originalDomainAccessType = solverConfig.getDomainAccessType(); + solverConfig.setDomainAccessType(DomainAccessType.REFLECTION); + + var solverFactory = (DefaultSolverFactory) SolverFactory.create(solverConfig); + ConstraintMetaModel constraintMetaModel = buildConstraintMetaModel(solverFactory); + // Avoid changing the original solver config. + solverConfig.setDomainAccessType(originalDomainAccessType); + constraintMetaModelsBySolverNames.put(solverName, constraintMetaModel); + }); + + constraintMetaModelBuildItemBuildProducer.produce(new ConstraintMetaModelBuildItem(constraintMetaModelsBySolverNames)); + } + + private static ConstraintMetaModel buildConstraintMetaModel(DefaultSolverFactory solverFactory) { + var scoreDirectorFactory = solverFactory.getScoreDirectorFactory(); + + ConstraintMetaModel constraintMetaModel; + if (scoreDirectorFactory instanceof AbstractConstraintStreamScoreDirectorFactory castScoreDirectorFactory) { + constraintMetaModel = castScoreDirectorFactory.getConstraintMetaModel(); + } else { + throw new IllegalStateException( + "Cannot provide %s because the score director does not use the Constraint Streams API." + .formatted(ConstraintMetaModel.class.getSimpleName())); + } + return constraintMetaModel; + } + @BuildStep @Record(RUNTIME_INIT) void recordAndRegisterRuntimeBeans(TimefoldRecorder recorder, RecorderContext recorderContext, diff --git a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/api/ConstraintMetaModelBuildItem.java b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/api/ConstraintMetaModelBuildItem.java new file mode 100644 index 0000000000..dc3a060db6 --- /dev/null +++ b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/api/ConstraintMetaModelBuildItem.java @@ -0,0 +1,24 @@ +package ai.timefold.solver.quarkus.deployment.api; + +import java.util.Map; + +import ai.timefold.solver.core.api.score.stream.ConstraintMetaModel; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * Represents a {@link ai.timefold.solver.core.api.score.stream.ConstraintMetaModel} at the build time for the purpose + * of Quarkus augmentation. + */ +public final class ConstraintMetaModelBuildItem extends SimpleBuildItem { + + private final Map constraintMetaModelsBySolverNames; + + public ConstraintMetaModelBuildItem(Map constraintMetaModelsBySolverNames) { + this.constraintMetaModelsBySolverNames = Map.copyOf(constraintMetaModelsBySolverNames); + } + + public Map constraintMetaModelsBySolverNames() { + return constraintMetaModelsBySolverNames; + } +} diff --git a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/api/package-info.java b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/api/package-info.java new file mode 100644 index 0000000000..1e7fa44728 --- /dev/null +++ b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/api/package-info.java @@ -0,0 +1,7 @@ +/** + * Public {@link io.quarkus.builder.item.BuildItem}s consumable by other Quarkus extensions. + *

+ * All classes in this package are part of the public API of this extension. + * Moreover, the extension is responsible for creating instances of these classes during the build phase. + */ +package ai.timefold.solver.quarkus.deployment.api; From cc1fe2570a9d1d29c279ab7d89e31bda3998f27a Mon Sep 17 00:00:00 2001 From: rsynek Date: Fri, 24 Jan 2025 17:40:24 +0100 Subject: [PATCH 2/2] chore: address review comments --- .../deployment/SolverConfigBuildItem.java | 1 + .../quarkus/deployment/TimefoldProcessor.java | 21 ++---------- .../solver/quarkus/bean/BeanUtil.java | 32 +++++++++++++++++++ .../bean/DefaultTimefoldBeanProvider.java | 13 ++------ 4 files changed, 39 insertions(+), 28 deletions(-) create mode 100644 quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/bean/BeanUtil.java 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 0d52e5b472..8329625898 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 @@ -15,6 +15,7 @@ public final class SolverConfigBuildItem extends SimpleBuildItem { * Constructor for multiple solver configurations. */ public SolverConfigBuildItem(Map solverConfig, GeneratedGizmoClasses generatedGizmoClasses) { + // Defensive copy to avoid changing the map in dependent build items. this.solverConfigurations = Map.copyOf(solverConfig); this.generatedGizmoClasses = generatedGizmoClasses; } 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 f49857af30..93c98cb679 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 @@ -40,9 +40,8 @@ import ai.timefold.solver.core.impl.domain.common.ReflectionHelper; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.heuristic.selector.common.nearby.NearbyDistanceMeter; -import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamScoreDirectorFactory; -import ai.timefold.solver.core.impl.solver.DefaultSolverFactory; import ai.timefold.solver.quarkus.TimefoldRecorder; +import ai.timefold.solver.quarkus.bean.BeanUtil; import ai.timefold.solver.quarkus.bean.DefaultTimefoldBeanProvider; import ai.timefold.solver.quarkus.bean.TimefoldSolverBannerBean; import ai.timefold.solver.quarkus.bean.UnavailableTimefoldBeanProvider; @@ -590,8 +589,8 @@ void buildConstraintMetaModel(SolverConfigBuildItem solverConfigBuildItem, DomainAccessType originalDomainAccessType = solverConfig.getDomainAccessType(); solverConfig.setDomainAccessType(DomainAccessType.REFLECTION); - var solverFactory = (DefaultSolverFactory) SolverFactory.create(solverConfig); - ConstraintMetaModel constraintMetaModel = buildConstraintMetaModel(solverFactory); + var solverFactory = SolverFactory.create(solverConfig); + ConstraintMetaModel constraintMetaModel = BeanUtil.buildConstraintMetaModel(solverFactory); // Avoid changing the original solver config. solverConfig.setDomainAccessType(originalDomainAccessType); constraintMetaModelsBySolverNames.put(solverName, constraintMetaModel); @@ -600,20 +599,6 @@ void buildConstraintMetaModel(SolverConfigBuildItem solverConfigBuildItem, constraintMetaModelBuildItemBuildProducer.produce(new ConstraintMetaModelBuildItem(constraintMetaModelsBySolverNames)); } - private static ConstraintMetaModel buildConstraintMetaModel(DefaultSolverFactory solverFactory) { - var scoreDirectorFactory = solverFactory.getScoreDirectorFactory(); - - ConstraintMetaModel constraintMetaModel; - if (scoreDirectorFactory instanceof AbstractConstraintStreamScoreDirectorFactory castScoreDirectorFactory) { - constraintMetaModel = castScoreDirectorFactory.getConstraintMetaModel(); - } else { - throw new IllegalStateException( - "Cannot provide %s because the score director does not use the Constraint Streams API." - .formatted(ConstraintMetaModel.class.getSimpleName())); - } - return constraintMetaModel; - } - @BuildStep @Record(RUNTIME_INIT) void recordAndRegisterRuntimeBeans(TimefoldRecorder recorder, RecorderContext recorderContext, diff --git a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/bean/BeanUtil.java b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/bean/BeanUtil.java new file mode 100644 index 0000000000..c762015861 --- /dev/null +++ b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/bean/BeanUtil.java @@ -0,0 +1,32 @@ +package ai.timefold.solver.quarkus.bean; + +import java.util.Objects; + +import ai.timefold.solver.core.api.score.stream.ConstraintMetaModel; +import ai.timefold.solver.core.api.solver.SolverFactory; +import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamScoreDirectorFactory; +import ai.timefold.solver.core.impl.solver.DefaultSolverFactory; + +public class BeanUtil { + + public static ConstraintMetaModel buildConstraintMetaModel(SolverFactory solverFactory) { + if (Objects.requireNonNull(solverFactory) instanceof DefaultSolverFactory defaultSolverFactory) { + var scoreDirectorFactory = defaultSolverFactory.getScoreDirectorFactory(); + if (scoreDirectorFactory instanceof AbstractConstraintStreamScoreDirectorFactory castScoreDirectorFactory) { + return castScoreDirectorFactory.getConstraintMetaModel(); + } else { + throw new IllegalStateException( + "Cannot provide %s because the score director does not use the Constraint Streams API." + .formatted(ConstraintMetaModel.class.getSimpleName())); + } + } else { + throw new IllegalStateException( + "%s is not supported by the solver factory (%s)." + .formatted(ConstraintMetaModel.class.getSimpleName(), solverFactory.getClass().getName())); + } + } + + private BeanUtil() { + throw new IllegalStateException("Utility class"); + } +} diff --git a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/bean/DefaultTimefoldBeanProvider.java b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/bean/DefaultTimefoldBeanProvider.java index c8dedd0c84..02645cbc35 100644 --- a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/bean/DefaultTimefoldBeanProvider.java +++ b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/bean/DefaultTimefoldBeanProvider.java @@ -1,5 +1,6 @@ package ai.timefold.solver.quarkus.bean; +import io.quarkus.runtime.annotations.Recorder; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.Dependent; import jakarta.enterprise.inject.Produces; @@ -24,8 +25,6 @@ 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 ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamScoreDirectorFactory; -import ai.timefold.solver.core.impl.solver.DefaultSolverFactory; import io.quarkus.arc.DefaultBean; import io.quarkus.arc.Lock; @@ -63,14 +62,8 @@ SolverFactory solverFactory(SolverConfig solverConfig) { @Produces ConstraintMetaModel constraintProviderMetaModel(SolverFactory solverFactory) { if (constraintMetaModel == null) { - var scoreDirectorFactory = ((DefaultSolverFactory) solverFactory).getScoreDirectorFactory(); - if (scoreDirectorFactory instanceof AbstractConstraintStreamScoreDirectorFactory castScoreDirectorFactory) { - constraintMetaModel = castScoreDirectorFactory.getConstraintMetaModel(); - } else { - throw new IllegalStateException( - "Cannot provide %s because the score director does not use the Constraint Streams API." - .formatted(ConstraintMetaModel.class.getSimpleName())); - } + // The metamodel is not compatible with Quarkus code recording, thus we need to rebuild it at runtime. + constraintMetaModel = BeanUtil.buildConstraintMetaModel(solverFactory); } return constraintMetaModel; }