Skip to content

Commit

Permalink
feat: expose ConstraintMetaModel at build time (#1343)
Browse files Browse the repository at this point in the history
Fixes #1323.
  • Loading branch information
rsynek authored Jan 28, 2025
1 parent 9f53d4a commit 58e14aa
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 11 deletions.
10 changes: 10 additions & 0 deletions quarkus-integration/quarkus/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.revapi</groupId>
<artifactId>revapi-maven-plugin</artifactId>
<configuration>
<!-- Introduction of API package to check -->
<oldArtifacts>
<artifact>${project.groupId}:timefold-solver-quarkus-deployment:1.18.0</artifact>
</oldArtifacts>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[
{
"extension": "revapi.differences",
"configuration": {
"differences": [
]
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"extension": "revapi.filter",
"configuration": {
"elements": {
"include": [
"class ai\\.timefold\\.solver\\.quarkus\\.deployment\\.api.*"
]
}
}
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public final class SolverConfigBuildItem extends SimpleBuildItem {
* Constructor for multiple solver configurations.
*/
public SolverConfigBuildItem(Map<String, SolverConfig> solverConfig, GeneratedGizmoClasses generatedGizmoClasses) {
this.solverConfigurations = solverConfig;
// Defensive copy to avoid changing the map in dependent build items.
this.solverConfigurations = Map.copyOf(solverConfig);
this.generatedGizmoClasses = generatedGizmoClasses;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -40,10 +41,12 @@
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.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;
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;
Expand Down Expand Up @@ -573,6 +576,29 @@ private SolverConfig loadSolverConfig(IndexView indexView,
return solverConfig;
}

@BuildStep
void buildConstraintMetaModel(SolverConfigBuildItem solverConfigBuildItem,
BuildProducer<ConstraintMetaModelBuildItem> constraintMetaModelBuildItemBuildProducer) {
if (solverConfigBuildItem.getSolverConfigMap().isEmpty()) {
return;
}

Map<String, ConstraintMetaModel> 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 = SolverFactory.create(solverConfig);
ConstraintMetaModel constraintMetaModel = BeanUtil.buildConstraintMetaModel(solverFactory);
// Avoid changing the original solver config.
solverConfig.setDomainAccessType(originalDomainAccessType);
constraintMetaModelsBySolverNames.put(solverName, constraintMetaModel);
});

constraintMetaModelBuildItemBuildProducer.produce(new ConstraintMetaModelBuildItem(constraintMetaModelsBySolverNames));
}

@BuildStep
@Record(RUNTIME_INIT)
void recordAndRegisterRuntimeBeans(TimefoldRecorder recorder, RecorderContext recorderContext,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, ConstraintMetaModel> constraintMetaModelsBySolverNames;

public ConstraintMetaModelBuildItem(Map<String, ConstraintMetaModel> constraintMetaModelsBySolverNames) {
this.constraintMetaModelsBySolverNames = Map.copyOf(constraintMetaModelsBySolverNames);
}

public Map<String, ConstraintMetaModel> constraintMetaModelsBySolverNames() {
return constraintMetaModelsBySolverNames;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Public {@link io.quarkus.builder.item.BuildItem}s consumable by other Quarkus extensions.
* <p>
* 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;
Original file line number Diff line number Diff line change
@@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,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;
Expand Down Expand Up @@ -63,14 +61,8 @@ <Solution_> SolverFactory<Solution_> 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;
}
Expand Down

0 comments on commit 58e14aa

Please sign in to comment.