diff --git a/benchmark/src/main/resources/benchmark.xsd b/benchmark/src/main/resources/benchmark.xsd
index 9ac3474560..d60a861541 100644
--- a/benchmark/src/main/resources/benchmark.xsd
+++ b/benchmark/src/main/resources/benchmark.xsd
@@ -446,6 +446,9 @@
+
+
+
diff --git a/core/constraint-streams/src/main/java/ai/timefold/solver/constraint/streams/bavet/BavetConstraintStreamScoreDirectorFactoryService.java b/core/constraint-streams/src/main/java/ai/timefold/solver/constraint/streams/bavet/BavetConstraintStreamScoreDirectorFactoryService.java
index 6855b8a3a4..242abbb61b 100644
--- a/core/constraint-streams/src/main/java/ai/timefold/solver/constraint/streams/bavet/BavetConstraintStreamScoreDirectorFactoryService.java
+++ b/core/constraint-streams/src/main/java/ai/timefold/solver/constraint/streams/bavet/BavetConstraintStreamScoreDirectorFactoryService.java
@@ -14,6 +14,7 @@
import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig;
import ai.timefold.solver.core.config.solver.EnvironmentMode;
import ai.timefold.solver.core.config.util.ConfigUtils;
+import ai.timefold.solver.core.enterprise.TimefoldSolverEnterpriseService;
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
import ai.timefold.solver.core.impl.score.director.AbstractScoreDirectorFactory;
import ai.timefold.solver.core.impl.score.director.ScoreDirectorType;
@@ -41,9 +42,19 @@ public Supplier> buildScoreDirec
"The constraintProviderClass (" + config.getConstraintProviderClass()
+ ") does not implement " + ConstraintProvider.class.getSimpleName() + ".");
}
+ final Class extends ConstraintProvider> constraintProviderClass;
+ if (Boolean.TRUE.equals(config.getConstraintStreamAutomaticNodeSharing())) {
+ TimefoldSolverEnterpriseService enterpriseService =
+ TimefoldSolverEnterpriseService
+ .loadOrFail(TimefoldSolverEnterpriseService.Feature.AUTOMATIC_NODE_SHARING);
+ constraintProviderClass =
+ enterpriseService.buildLambdaSharedConstraintProvider(config.getConstraintProviderClass());
+ } else {
+ constraintProviderClass = config.getConstraintProviderClass();
+ }
return () -> {
ConstraintProvider constraintProvider = ConfigUtils.newInstance(config,
- "constraintProviderClass", config.getConstraintProviderClass());
+ "constraintProviderClass", constraintProviderClass);
ConfigUtils.applyCustomProperties(constraintProvider, "constraintProviderClass",
config.getConstraintProviderCustomProperties(), "constraintProviderCustomProperties");
return buildScoreDirectorFactory(solutionDescriptor, constraintProvider, environmentMode);
diff --git a/core/core-impl/src/build/revapi-differences.json b/core/core-impl/src/build/revapi-differences.json
index 3013dd35f4..b6872627ec 100644
--- a/core/core-impl/src/build/revapi-differences.json
+++ b/core/core-impl/src/build/revapi-differences.json
@@ -69,8 +69,19 @@
"new": "class ai.timefold.solver.core.config.heuristic.selector.move.generic.list.kopt.KOptListMoveSelectorConfig",
"superClass": "ai.timefold.solver.core.config.heuristic.selector.move.NearbyAutoConfigurationMoveSelectorConfig",
"justification": "Adding support for Nearby Selection autoconfiguration"
+ },
+ {
+ "ignore": true,
+ "code": "java.annotation.attributeValueChanged",
+ "old": "class ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig",
+ "new": "class ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig",
+ "annotationType": "jakarta.xml.bind.annotation.XmlType",
+ "attribute": "propOrder",
+ "oldValue": "{\"easyScoreCalculatorClass\", \"easyScoreCalculatorCustomProperties\", \"constraintProviderClass\", \"constraintProviderCustomProperties\", \"constraintStreamImplType\", \"incrementalScoreCalculatorClass\", \"incrementalScoreCalculatorCustomProperties\", \"scoreDrlList\", \"initializingScoreTrend\", \"assertionScoreDirectorFactory\"}",
+ "newValue": "{\"easyScoreCalculatorClass\", \"easyScoreCalculatorCustomProperties\", \"constraintProviderClass\", \"constraintProviderCustomProperties\", \"constraintStreamImplType\", \"constraintStreamAutomaticNodeSharing\", \"incrementalScoreCalculatorClass\", \"incrementalScoreCalculatorCustomProperties\", \"scoreDrlList\", \"initializingScoreTrend\", \"assertionScoreDirectorFactory\"}",
+ "justification": "Add support for automatic constraint stream node sharing outside of Quarkus for Timefold Solver Enterprise edition users."
}
]
}
}
-]
\ No newline at end of file
+]
diff --git a/core/core-impl/src/main/java/ai/timefold/solver/core/config/score/director/ScoreDirectorFactoryConfig.java b/core/core-impl/src/main/java/ai/timefold/solver/core/config/score/director/ScoreDirectorFactoryConfig.java
index fbac8d1e59..4c161d6d02 100644
--- a/core/core-impl/src/main/java/ai/timefold/solver/core/config/score/director/ScoreDirectorFactoryConfig.java
+++ b/core/core-impl/src/main/java/ai/timefold/solver/core/config/score/director/ScoreDirectorFactoryConfig.java
@@ -23,6 +23,7 @@
"constraintProviderClass",
"constraintProviderCustomProperties",
"constraintStreamImplType",
+ "constraintStreamAutomaticNodeSharing",
"incrementalScoreCalculatorClass",
"incrementalScoreCalculatorCustomProperties",
"scoreDrlList",
@@ -41,6 +42,7 @@ public class ScoreDirectorFactoryConfig extends AbstractConfig constraintProviderCustomProperties = null;
protected ConstraintStreamImplType constraintStreamImplType;
+ protected Boolean constraintStreamAutomaticNodeSharing;
protected Class extends IncrementalScoreCalculator> incrementalScoreCalculatorClass = null;
@@ -101,6 +103,14 @@ public void setConstraintStreamImplType(ConstraintStreamImplType constraintStrea
this.constraintStreamImplType = constraintStreamImplType;
}
+ public Boolean getConstraintStreamAutomaticNodeSharing() {
+ return constraintStreamAutomaticNodeSharing;
+ }
+
+ public void setConstraintStreamAutomaticNodeSharing(Boolean constraintStreamAutomaticNodeSharing) {
+ this.constraintStreamAutomaticNodeSharing = constraintStreamAutomaticNodeSharing;
+ }
+
public Class extends IncrementalScoreCalculator> getIncrementalScoreCalculatorClass() {
return incrementalScoreCalculatorClass;
}
@@ -187,6 +197,11 @@ public ScoreDirectorFactoryConfig withConstraintStreamImplType(ConstraintStreamI
return this;
}
+ public ScoreDirectorFactoryConfig withConstraintStreamAutomaticNodeSharing(Boolean constraintStreamAutomaticNodeSharing) {
+ this.constraintStreamAutomaticNodeSharing = constraintStreamAutomaticNodeSharing;
+ return this;
+ }
+
public ScoreDirectorFactoryConfig
withIncrementalScoreCalculatorClass(Class extends IncrementalScoreCalculator> incrementalScoreCalculatorClass) {
this.incrementalScoreCalculatorClass = incrementalScoreCalculatorClass;
@@ -246,6 +261,8 @@ public ScoreDirectorFactoryConfig inherit(ScoreDirectorFactoryConfig inheritedCo
constraintProviderCustomProperties, inheritedConfig.getConstraintProviderCustomProperties());
constraintStreamImplType = ConfigUtils.inheritOverwritableProperty(
constraintStreamImplType, inheritedConfig.getConstraintStreamImplType());
+ constraintStreamAutomaticNodeSharing = ConfigUtils.inheritOverwritableProperty(constraintStreamAutomaticNodeSharing,
+ inheritedConfig.getConstraintStreamAutomaticNodeSharing());
incrementalScoreCalculatorClass = ConfigUtils.inheritOverwritableProperty(
incrementalScoreCalculatorClass, inheritedConfig.getIncrementalScoreCalculatorClass());
incrementalScoreCalculatorCustomProperties = ConfigUtils.inheritMergeableMapProperty(
diff --git a/core/core-impl/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java b/core/core-impl/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java
index 29a4b8201b..7896a017b3 100644
--- a/core/core-impl/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java
+++ b/core/core-impl/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java
@@ -3,6 +3,7 @@
import java.util.ServiceLoader;
import java.util.function.BiFunction;
+import ai.timefold.solver.core.api.score.stream.ConstraintProvider;
import ai.timefold.solver.core.api.solver.SolverFactory;
import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType;
import ai.timefold.solver.core.config.heuristic.selector.common.SelectionOrder;
@@ -57,6 +58,9 @@ static TimefoldSolverEnterpriseService loadOrFail(Feature feature) {
return service;
}
+ Class extends ConstraintProvider>
+ buildLambdaSharedConstraintProvider(Class extends ConstraintProvider> originalConstraintProvider);
+
ConstructionHeuristicDecider buildConstructionHeuristic(int moveThreadCount,
Termination termination, ConstructionHeuristicForager forager,
EnvironmentMode environmentMode, HeuristicConfigPolicy configPolicy);
@@ -90,7 +94,9 @@ DestinationSelector applyNearbySelection(DestinationSelec
enum Feature {
MULTITHREADED_SOLVING("Multi-threaded solving", "remove moveThreadCount from solver configuration"),
PARTITIONED_SEARCH("Partitioned search", "remove partitioned search phase from solver configuration"),
- NEARBY_SELECTION("Nearby selection", "remove nearby selection from solver configuration");
+ NEARBY_SELECTION("Nearby selection", "remove nearby selection from solver configuration"),
+ AUTOMATIC_NODE_SHARING("Automatic node sharing",
+ "remove automatic node sharing from solver configuration");
private final String name;
private final String workaround;
diff --git a/core/core-impl/src/main/resources/solver.xsd b/core/core-impl/src/main/resources/solver.xsd
index fc2058d7f1..99d3b5169e 100644
--- a/core/core-impl/src/main/resources/solver.xsd
+++ b/core/core-impl/src/main/resources/solver.xsd
@@ -107,6 +107,8 @@
+
+
diff --git a/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc b/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc
index ca19a17c4c..7bd4bc156c 100644
--- a/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc
+++ b/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc
@@ -89,7 +89,8 @@ The following features are only available in Timefold Solver Enterprise Edition:
* <>,
* <>,
-* and <>.
+* <>,
+* and <>.
[#nearbySelection]
@@ -759,5 +760,194 @@ the host is likely to hang or freeze,
unless there is an OS specific policy in place to avoid Timefold Solver from hogging all the CPU processors.
====
+[#automaticNodeSharing]
+=== Automatic node sharing
+[NOTE]
+====
+This feature is a commercial feature of Timefold Solver Enterprise Edition.
+It is not available in the Community Edition.
+====
+
+When a `ConstraintProvider` does an operation for multiple constraints (such as finding all shifts corresponding to an employee), that work can be shared.
+This can significantly improve score calculation speed if the repeated operation is computationally expensive.
+
+==== Configuration
+
+[tabs]
+======
+Plain Java::
+
+* Add `true` in your `solverConfig.xml`:
++
+[source,xml,options="nowrap"]
+----
+
+
+ org.acme.MyConstraintProvider
+ true
+
+
+----
+
+Spring Boot::
+
+Set the property `timefold.solver.constraint-stream-automatic-node-sharing` to `true` in `application.properties`:
++
+[source,properties,options="nowrap"]
+----
+timefold.solver.constraint-stream-automatic-node=true
+----
+
+Quarkus::
+
+Set the property `quarkus.timefold.solver.constraint-stream-automatic-node-sharing` to `true` in `application.properties`:
++
+[source,properties,options="nowrap"]
+----
+quarkus.timefold.solver.constraint-stream-automatic-node-sharing=true
+----
+======
+
+[IMPORTANT]
+====
+Debugging breakpoints put inside your constraints will not be respected, because the `ConstraintProvider` class will be transformed when this feature is enabled.
+====
+
+==== What is node sharing?
+
+When using xref:constraints-and-score/score-calculation.adoc#constraintStreams[constraint streams], each xref:constraints-and-score/score-calculation.adoc#constraintStreamsBuildingBlocks[building block] forms a node in the score calculation network.
+When two building blocks are functionally equivalent, they can share the same node in the network.
+Sharing nodes allows the operation to be performed only once instead of multiple times, improving the performance of the solver.
+To be functionally equivalent, the following must be true:
+
+* The building blocks must represent the same operation.
+
+* The building blocks must have functionally equivalent parent building blocks.
+
+* The building blocks must have functionally equivalent inputs.
+
+For example, the building blocks below are functionally equivalent:
+
+[source,java,options="nowrap"]
+----
+Predicate predicate = shift -> shift.getEmployee().getName().equals("Ann");
+
+var a = factory.forEach(Shift.class)
+ .filter(predicate);
+
+var b = factory.forEach(Shift.class)
+ .filter(predicate);
+----
+
+Whereas these building blocks are not functionally equivalent:
+
+[source,java,options="nowrap"]
+----
+Predicate predicate1 = shift -> shift.getEmployee().getName().equals("Ann");
+Predicate predicate2 = shift -> shift.getEmployee().getName().equals("Bob");
+
+// Different parents
+var a = factory.forEach(Shift.class)
+ .filter(predicate2);
+
+var b = factory.forEach(Shift.class)
+ .filter(predicate1)
+ .filter(predicate2);
+
+// Different operations
+var a = factory.forEach(Shift.class)
+ .ifExists(Employee.class);
+
+var b = factory.forEach(Shift.class)
+ .ifNotExists(Employee.class);
+
+// Different inputs
+var a = factory.forEach(Shift.class)
+ .filter(predicate1);
+
+var b = factory.forEach(Shift.class)
+ .filter(predicate2);
+----
+
+Counterintuitively, the building blocks produced by these (seemly) identical methods are not necessarily functionally equivalent:
+
+[source,java,options="nowrap"]
+----
+UniConstraintStream a(ConstraintFactory constraintFactory) {
+ return factory.forEach(Shift.class)
+ .filter(shift -> shift.getEmployee().getName().equals("Ann"));
+}
+
+UniConstraintStream b(ConstraintFactory constraintFactory) {
+ return factory.forEach(Shift.class)
+ .filter(shift -> shift.getEmployee().getName().equals("Ann"));
+}
+----
+
+The Java Virtual Machine is free to (and often does) create different instances of functionally equivalent lambdas.
+This severely limits the effectiveness of node sharing, since the only way to know two lambdas are equal is to compare their references.
+
+When automatic node sharing is used, the `ConstraintProvider` class is transformed so all lambdas are accessed via a static final field.
+Consider the following input class:
+
+[source,java,options="nowrap"]
+----
+public class MyConstraintProvider implements ConstraintProvider {
+
+ public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
+ return new Constraint[] {
+ a(constraintFactory),
+ b(constraintFactory)
+ };
+ }
+
+ Constraint a(ConstraintFactory constraintFactory) {
+ return factory.forEach(Shift.class)
+ .filter(shift -> shift.getEmployee().getName().equals("Ann"))
+ .penalize(SimpleScore.ONE)
+ .asConstraint("a");
+ }
+
+ Constraint b(ConstraintFactory constraintFactory) {
+ return factory.forEach(Shift.class)
+ .filter(shift -> shift.getEmployee().getName().equals("Ann"))
+ .penalize(SimpleScore.ONE)
+ .asConstraint("b");
+ }
+}
+----
+
+When automatic node sharing is enabled, the class will be transformed to look like this:
+
+[source,java,options="nowrap"]
+----
+public class MyConstraintProvider implements ConstraintProvider {
+ private static final Predicate $predicate1 = shift -> shift.getEmployee().getName().equals("Ann");
+
+ public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
+ return new Constraint[] {
+ a(constraintFactory),
+ b(constraintFactory)
+ };
+ }
+
+ Constraint a(ConstraintFactory constraintFactory) {
+ return factory.forEach(Shift.class)
+ .filter($predicate1)
+ .penalize(SimpleScore.ONE)
+ .asConstraint("a");
+ }
+
+ Constraint b(ConstraintFactory constraintFactory) {
+ return factory.forEach(Shift.class)
+ .filter($predicate1)
+ .penalize(SimpleScore.ONE)
+ .asConstraint("b");
+ }
+}
+----
+
+This transformation means that debugging breakpoints placed inside the original `ConstraintProvider` will not be honored in the transformed `ConstraintProvider`.
+From the above, you can see how this feature allows building blocks to share functionally equivalent parents, without needing the `ConstraintProvider` to be written in an awkward way.
diff --git a/quarkus-integration/quarkus-benchmark/deployment/src/main/java/ai/timefold/solver/benchmark/quarkus/deployment/TimefoldBenchmarkProcessor.java b/quarkus-integration/quarkus-benchmark/deployment/src/main/java/ai/timefold/solver/benchmark/quarkus/deployment/TimefoldBenchmarkProcessor.java
index 1696d29aba..12bc693912 100644
--- a/quarkus-integration/quarkus-benchmark/deployment/src/main/java/ai/timefold/solver/benchmark/quarkus/deployment/TimefoldBenchmarkProcessor.java
+++ b/quarkus-integration/quarkus-benchmark/deployment/src/main/java/ai/timefold/solver/benchmark/quarkus/deployment/TimefoldBenchmarkProcessor.java
@@ -44,7 +44,7 @@ BenchmarkConfigBuildItem registerAdditionalBeans(BuildProducer unremovableBeans,
SolverConfigBuildItem solverConfigBuildItem) {
// We don't support benchmarking for multiple solvers
- if (solverConfigBuildItem.getSolvetConfigMap().size() > 1) {
+ if (solverConfigBuildItem.getSolverConfigMap().size() > 1) {
throw new ConfigurationException("""
When defining multiple solvers, the benchmark feature is not enabled.
Consider using separate instances for evaluating different solver configurations.""");
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 9a33b5aad3..281646b80d 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
@@ -1,11 +1,8 @@
package ai.timefold.solver.quarkus.deployment;
import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
import ai.timefold.solver.core.config.solver.SolverConfig;
-import ai.timefold.solver.quarkus.deployment.config.TimefoldBuildTimeConfig;
import io.quarkus.builder.item.SimpleBuildItem;
@@ -21,28 +18,7 @@ public SolverConfigBuildItem(Map solverConfig, GeneratedGi
this.generatedGizmoClasses = generatedGizmoClasses;
}
- /**
- * Returns the configuration of a given solver name.
- *
- * @param solverName never null, the solver name
- */
- public SolverConfig getSolverConfig(String solverName) {
- return this.solverConfigurations
- .get(Objects.requireNonNull(solverName, "Invalid solverName (null) given to SolverConfigBuildItem."));
- }
-
- /**
- * Returns the configuration of the default solver.
- */
- public SolverConfig getSolverConfig() {
- return this.solverConfigurations.get(TimefoldBuildTimeConfig.DEFAULT_SOLVER_NAME);
- }
-
- public Set getSolverNames() {
- return this.solverConfigurations.keySet();
- }
-
- public Map getSolvetConfigMap() {
+ 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 438d4d84bd..13e6d4a211 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
@@ -575,7 +575,7 @@ void recordAndRegisterRuntimeBeans(TimefoldRecorder recorder, RecorderContext re
// 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.getSolvetConfigMap().forEach((key, value) -> {
+ solverConfigBuildItem.getSolverConfigMap().forEach((key, value) -> {
if (timefoldBuildTimeConfig.isDefaultSolverConfig(key)) {
// The two configuration resources are required for DefaultTimefoldBeanProvider produce all available
// managed beans for the default solver
@@ -635,7 +635,7 @@ public void recordAndRegisterDevUIBean(
BuildProducer syntheticBeans) {
syntheticBeans.produce(SyntheticBeanBuildItem.configure(DevUISolverConfig.class)
.scope(ApplicationScoped.class)
- .supplier(devUIRecorder.solverConfigSupplier(solverConfigBuildItem.getSolvetConfigMap(), runtimeConfig,
+ .supplier(devUIRecorder.solverConfigSupplier(solverConfigBuildItem.getSolverConfigMap(), runtimeConfig,
GizmoMemberAccessorEntityEnhancer.getGeneratedGizmoMemberAccessorMap(recorderContext,
solverConfigBuildItem
.getGeneratedGizmoClasses().generatedGizmoMemberAccessorClassSet),
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 29c6b4f33a..cb44d06dfc 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
@@ -48,6 +48,9 @@ public interface SolverBuildTimeConfig {
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
@@ -73,4 +76,15 @@ public interface SolverBuildTimeConfig {
@Deprecated(forRemoval = true, since = "1.4.0")
Optional constraintStreamImplType();
+ /**
+ * Note: this setting is only available
+ * for Timefold Solver
+ * Enterprise Edition.
+ * Enable rewriting the {@link ai.timefold.solver.core.api.score.stream.ConstraintProvider} class
+ * so nodes share lambdas when possible, improving performance.
+ * When enabled, breakpoints placed in the {@link ai.timefold.solver.core.api.score.stream.ConstraintProvider}
+ * will no longer be triggered.
+ * Defaults to "false".
+ */
+ Optional constraintStreamAutomaticNodeSharing();
}
diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java
index e882c1ad23..70c5b93984 100644
--- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java
+++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java
@@ -61,6 +61,7 @@
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Configuration;
+import org.springframework.core.NativeDetector;
import org.springframework.core.env.Environment;
@Configuration(proxyBeanMethods = false)
@@ -250,29 +251,42 @@ private void loadSolverConfig(IncludeAbstractClassesEntityScanner entityScanner,
.collect(joining(", "))));
}
}
+ timefoldProperties.getSolverConfig(solverName)
+ .ifPresentOrElse(
+ solverProperties -> applyScoreDirectorFactoryProperties(entityScanner, solverConfig, solverProperties),
+ () -> applyScoreDirectorFactoryProperties(entityScanner, solverConfig));
+ }
+
+ private void applyScoreDirectorFactoryProperties(IncludeAbstractClassesEntityScanner entityScanner,
+ SolverConfig solverConfig, SolverProperties solverProperties) {
applyScoreDirectorFactoryProperties(entityScanner, solverConfig);
- Optional solverProperties = timefoldProperties.getSolverConfig(solverName);
- if (solverProperties.isPresent()) {
- if (solverProperties.get().getEnvironmentMode() != null) {
- solverConfig.setEnvironmentMode(solverProperties.get().getEnvironmentMode());
- }
- if (solverProperties.get().getDomainAccessType() != null) {
- solverConfig.setDomainAccessType(solverProperties.get().getDomainAccessType());
- }
- if (solverProperties.get().getNearbyDistanceMeterClass() != null) {
- solverConfig.setNearbyDistanceMeterClass(solverProperties.get().getNearbyDistanceMeterClass());
- }
- if (solverProperties.get().getDaemon() != null) {
- solverConfig.setDaemon(solverProperties.get().getDaemon());
- }
- if (solverProperties.get().getMoveThreadCount() != null) {
- solverConfig.setMoveThreadCount(solverProperties.get().getMoveThreadCount());
+ if (solverProperties.getConstraintStreamAutomaticNodeSharing() != null
+ && solverProperties.getConstraintStreamAutomaticNodeSharing()) {
+ if (NativeDetector.inNativeImage()) {
+ throw new UnsupportedOperationException(
+ "Constraint stream automatic node sharing is unsupported in a Spring native image.");
}
- applyTerminationProperties(solverConfig, solverProperties.get().getTermination());
+ solverConfig.getScoreDirectorFactoryConfig().setConstraintStreamAutomaticNodeSharing(true);
+ }
+ if (solverProperties.getEnvironmentMode() != null) {
+ solverConfig.setEnvironmentMode(solverProperties.getEnvironmentMode());
+ }
+ if (solverProperties.getDomainAccessType() != null) {
+ solverConfig.setDomainAccessType(solverProperties.getDomainAccessType());
+ }
+ if (solverProperties.getNearbyDistanceMeterClass() != null) {
+ solverConfig.setNearbyDistanceMeterClass(solverProperties.getNearbyDistanceMeterClass());
+ }
+ if (solverProperties.getDaemon() != null) {
+ solverConfig.setDaemon(solverProperties.getDaemon());
+ }
+ if (solverProperties.getMoveThreadCount() != null) {
+ solverConfig.setMoveThreadCount(solverProperties.getMoveThreadCount());
}
+ applyTerminationProperties(solverConfig, solverProperties.getTermination());
}
- protected void applyScoreDirectorFactoryProperties(IncludeAbstractClassesEntityScanner entityScanner,
+ private void applyScoreDirectorFactoryProperties(IncludeAbstractClassesEntityScanner entityScanner,
SolverConfig solverConfig) {
if (solverConfig.getScoreDirectorFactoryConfig() == null) {
solverConfig.setScoreDirectorFactoryConfig(defaultScoreDirectoryFactoryConfig(entityScanner));
diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperties.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperties.java
index 598289bb18..186d3b7f98 100644
--- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperties.java
+++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperties.java
@@ -1,9 +1,7 @@
package ai.timefold.solver.spring.boot.autoconfigure.config;
-import static java.util.stream.Collectors.joining;
-
import java.util.Map;
-import java.util.Set;
+import java.util.TreeSet;
import ai.timefold.solver.core.api.domain.common.DomainAccessType;
import ai.timefold.solver.core.api.score.stream.ConstraintStreamImplType;
@@ -13,21 +11,6 @@
import org.springframework.boot.context.properties.NestedConfigurationProperty;
public class SolverProperties {
-
- public static final String SOLVER_CONFIG_XML_PROPERTY_NAME = "solver-config-xml";
- public static final String ENVIRONMENT_MODE_PROPERTY_NAME = "environment-mode";
- public static final String DAEMON_PROPERTY_NAME = "daemon";
- public static final String MOVE_THREAD_COUNT_PROPERTY_NAME = "move-thread-count";
- public static final String DOMAIN_ACCESS_TYPE_PROPERTY_NAME = "domain-access-type";
- public static final String NEARBY_DISTANCE_METER_CLASS_PROPERTY_NAME = "nearby-distance-meter-class";
- public static final String CONSTRAINT_STREAM_IMPL_TYPE_PROPERTY_NAME = "constraint-stream-impl-type";
- public static final String TERMINATION_PROPERTY_NAME = "termination";
- public static final Set VALID_FIELD_NAMES_SET =
- Set.of(SOLVER_CONFIG_XML_PROPERTY_NAME, ENVIRONMENT_MODE_PROPERTY_NAME, DAEMON_PROPERTY_NAME,
- MOVE_THREAD_COUNT_PROPERTY_NAME, DOMAIN_ACCESS_TYPE_PROPERTY_NAME,
- NEARBY_DISTANCE_METER_CLASS_PROPERTY_NAME, CONSTRAINT_STREAM_IMPL_TYPE_PROPERTY_NAME,
- TERMINATION_PROPERTY_NAME);
-
/**
* A classpath resource to read the specific solver configuration XML.
* If this property isn't specified, that solverConfig.xml is optional.
@@ -46,7 +29,11 @@ public class SolverProperties {
* Defaults to "false".
*/
private Boolean 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 "NONE".
* Other options include "AUTO", a number or formula based on the available processor count.
@@ -75,6 +62,18 @@ public class SolverProperties {
@Deprecated(forRemoval = true, since = "1.4.0")
private ConstraintStreamImplType constraintStreamImplType;
+ /**
+ * Note: this setting is only available
+ * for Timefold Solver
+ * Enterprise Edition.
+ * Enable rewriting the {@link ai.timefold.solver.core.api.score.stream.ConstraintProvider} class
+ * so nodes share lambdas when possible, improving performance.
+ * When enabled, breakpoints placed in the {@link ai.timefold.solver.core.api.score.stream.ConstraintProvider}
+ * will no longer be triggered.
+ * Defaults to "false".
+ */
+ private Boolean constraintStreamAutomaticNodeSharing;
+
@NestedConfigurationProperty
private TerminationProperties termination;
@@ -146,6 +145,14 @@ public void setConstraintStreamImplType(ConstraintStreamImplType constraintStrea
this.constraintStreamImplType = constraintStreamImplType;
}
+ public Boolean getConstraintStreamAutomaticNodeSharing() {
+ return constraintStreamAutomaticNodeSharing;
+ }
+
+ public void setConstraintStreamAutomaticNodeSharing(Boolean constraintStreamAutomaticNodeSharing) {
+ this.constraintStreamAutomaticNodeSharing = constraintStreamAutomaticNodeSharing;
+ }
+
public TerminationProperties getTermination() {
return termination;
}
@@ -156,17 +163,15 @@ public void setTermination(TerminationProperties termination) {
public void loadProperties(Map properties) {
// Check if the keys are valid
- String invalidKeys = properties.entrySet().stream()
- .filter(e -> !VALID_FIELD_NAMES_SET.contains(e.getKey()))
- .map(Map.Entry::getKey)
- .collect(joining(", "));
+ var invalidKeySet = new TreeSet<>(properties.keySet());
+ invalidKeySet.removeAll(SolverProperty.getValidPropertyNames());
- if (!invalidKeys.isBlank()) {
+ if (!invalidKeySet.isEmpty()) {
throw new IllegalStateException("""
The properties [%s] are not valid.
Maybe try changing the property name to kebab-case.
Here is the list of valid properties: %s"""
- .formatted(invalidKeys, String.join(", ", VALID_FIELD_NAMES_SET)));
+ .formatted(invalidKeySet, String.join(", ", SolverProperty.getValidPropertyNames())));
}
properties.forEach(this::loadProperty);
}
@@ -175,56 +180,8 @@ private void loadProperty(String key, Object value) {
if (value == null) {
return;
}
- switch (key) {
- case SOLVER_CONFIG_XML_PROPERTY_NAME:
- setSolverConfigXml(value.toString());
- break;
- case ENVIRONMENT_MODE_PROPERTY_NAME:
- setEnvironmentMode(EnvironmentMode.valueOf((String) value));
- break;
- case DAEMON_PROPERTY_NAME:
- setDaemon(Boolean.parseBoolean(value.toString()));
- break;
- case MOVE_THREAD_COUNT_PROPERTY_NAME:
- setMoveThreadCount(value.toString());
- break;
- case DOMAIN_ACCESS_TYPE_PROPERTY_NAME:
- setDomainAccessType(DomainAccessType.valueOf((String) value));
- break;
- case NEARBY_DISTANCE_METER_CLASS_PROPERTY_NAME:
- try {
- Class> nearbyClass = Class.forName(value.toString(), false,
- Thread.currentThread().getContextClassLoader());
-
- if (!NearbyDistanceMeter.class.isAssignableFrom(nearbyClass)) {
- throw new IllegalStateException(
- "The Nearby Selection Meter class (%s) does not implement NearbyDistanceMeter."
- .formatted(value.toString()));
- }
- setNearbyDistanceMeterClass((Class extends NearbyDistanceMeter, ?>>) nearbyClass);
- } catch (ClassNotFoundException e) {
- throw new IllegalStateException(
- "Cannot find the Nearby Selection Meter class (%s).".formatted(value.toString()));
- }
- break;
- case CONSTRAINT_STREAM_IMPL_TYPE_PROPERTY_NAME:
- setConstraintStreamImplType(ConstraintStreamImplType.valueOf((String) value));
- break;
- case TERMINATION_PROPERTY_NAME: {
- if (value instanceof TerminationProperties terminationProperties) {
- setTermination(terminationProperties);
- } else if (value instanceof Map, ?>) {
- TerminationProperties terminationProperties = new TerminationProperties();
- terminationProperties.loadProperties((Map) value);
- setTermination(terminationProperties);
- } else {
- throw new IllegalStateException("The termination value is not valid.");
- }
- break;
- }
- default:
- throw new IllegalStateException("The property %s is not valid.".formatted(key));
- }
+ SolverProperty property = SolverProperty.forPropertyName(key);
+ property.update(this, value);
}
}
diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperty.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperty.java
new file mode 100644
index 0000000000..32547d3b2b
--- /dev/null
+++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperty.java
@@ -0,0 +1,99 @@
+package ai.timefold.solver.spring.boot.autoconfigure.config;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+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.impl.heuristic.selector.common.nearby.NearbyDistanceMeter;
+
+public enum SolverProperty {
+ SOLVER_CONFIG_XML("solver-config-xml", SolverProperties::setSolverConfigXml, Object::toString),
+ ENVIRONMENT_MODE("environment-mode", SolverProperties::setEnvironmentMode,
+ value -> EnvironmentMode.valueOf(value.toString())),
+ DAEMON("daemon", SolverProperties::setDaemon, value -> Boolean.valueOf(value.toString())),
+ MOVE_THREAD_COUNT("move-thread-count", SolverProperties::setMoveThreadCount, Object::toString),
+ DOMAIN_ACCESS_TYPE("domain-access-type", SolverProperties::setDomainAccessType,
+ value -> DomainAccessType.valueOf(value.toString())),
+ NEARBY_DISTANCE_METER_CLASS("nearby-distance-meter-class", SolverProperties::setNearbyDistanceMeterClass,
+ value -> {
+ try {
+ @SuppressWarnings("rawtypes")
+ Class nearbyClass = Class.forName(value.toString(), false,
+ Thread.currentThread().getContextClassLoader());
+
+ if (!NearbyDistanceMeter.class.isAssignableFrom(nearbyClass)) {
+ throw new IllegalStateException(
+ "The Nearby Selection Meter class (%s) does not implement NearbyDistanceMeter."
+ .formatted(value.toString()));
+ }
+ return nearbyClass;
+ } catch (ClassNotFoundException e) {
+ throw new IllegalStateException(
+ "Cannot find the Nearby Selection Meter class (%s).".formatted(value.toString()));
+ }
+ }),
+ /**
+ * @deprecated No longer used.
+ */
+ @Deprecated(forRemoval = true, since = "1.4.0")
+ CONSTRAINT_STREAM_IMPL_TYPE("constraint-stream-impl-type", SolverProperties::setConstraintStreamImplType,
+ value -> ConstraintStreamImplType.valueOf(value.toString())),
+ CONSTRAINT_STREAM_AUTOMATIC_NODE_SHARING("constraint-stream-automatic-node-sharing",
+ SolverProperties::setConstraintStreamAutomaticNodeSharing, value -> Boolean.valueOf(value.toString())),
+ TERMINATION("termination", SolverProperties::setTermination, value -> {
+ if (value instanceof TerminationProperties terminationProperties) {
+ return terminationProperties;
+ } else if (value instanceof Map, ?> map) {
+ TerminationProperties terminationProperties = new TerminationProperties();
+ terminationProperties.loadProperties((Map) map);
+ return terminationProperties;
+ } else {
+ throw new IllegalStateException(
+ "The termination value (%s) is not valid. Expected an instance of %s or %s, but got an instance of %s."
+ .formatted(value, Map.class.getSimpleName(), TerminationProperties.class.getSimpleName(),
+ value.getClass().getName()));
+ }
+ });
+
+ private final String propertyName;
+ private final BiConsumer propertyUpdater;
+ private static final Set PROPERTY_NAMES = Stream.of(SolverProperty.values())
+ .map(SolverProperty::getPropertyName)
+ .collect(Collectors.toCollection(TreeSet::new));
+
+ SolverProperty(String propertyName, BiConsumer propertySetter,
+ Function