This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 1635587b1f90 CAMEL-26123: camel-optaplanner - one SolverManager per
endpoint, per-context consumers and sync events (#28902)
1635587b1f90 is described below
commit 1635587b1f9096fef07c59985ca0bb906d70fe7d
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 2 20:39:42 2026 +0100
CAMEL-26123: camel-optaplanner - one SolverManager per endpoint,
per-context consumers and sync events (#28902)
…context consumers and sync events
With configFile and no SolverManager, the producer created a new
SolverManager per
exchange and never closed it, leaking its solver threads. The endpoint now
creates
it once on first use and closes it in doStop.
The solution listeners were in a static map keyed only by problemId, so a
consumer
got the best solutions of producers in every CamelContext of the JVM. They
are now
kept on the component, and the solvers map is per endpoint instead of
static.
A producer with async=false solved without a listener, so consumers got no
events.
It now uses solveAndListen like the async path.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../camel/catalog/components/optaplanner.json | 4 +-
.../camel/catalog/docs/optaplanner-component.adoc | 12 ++-
.../camel/component/optaplanner/optaplanner.json | 4 +-
.../src/main/docs/optaplanner-component.adoc | 12 ++-
.../optaplanner/OptaPlannerComponent.java | 29 +++++++
.../optaplanner/OptaPlannerConfiguration.java | 3 +-
.../component/optaplanner/OptaPlannerEndpoint.java | 75 +++++++++++++-----
.../component/optaplanner/OptaPlannerProducer.java | 14 ++--
.../OptaPlannerConfigFileSolverManagerTest.java | 79 +++++++++++++++++++
.../OptaPlannerConsumerCamelContextTest.java | 88 ++++++++++++++++++++++
.../optaplanner/OptaPlannerSyncConsumerTest.java | 63 ++++++++++++++++
.../component/optaplanner/solverConfigQuick.xml | 38 ++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 13 ++++
.../dsl/OptaPlannerEndpointBuilderFactory.java | 9 ++-
14 files changed, 402 insertions(+), 41 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/optaplanner.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/optaplanner.json
index 262598ea1771..d7ad87d76f64 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/optaplanner.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/optaplanner.json
@@ -36,13 +36,13 @@
"CamelOptaPlannerSolverManager": { "index": 4, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType": "org.optaplanner.core.api.solver.SolverManager", "deprecated":
false, "deprecationNote": "", "autowired": false, "secret": false,
"description": "The Solver Manager.", "constantName":
"org.apache.camel.component.optaplanner.OptaPlannerConstants#SOLVER_MANAGER" }
},
"bodyTypes": {
- "consumerOut": { "carriers": [ "null" ], "description": "The body is
empty: the new best solution is in the CamelOptaPlannerBestSolution header.
Events come only from producers that solve asynchronously, with the same
problemId" },
+ "consumerOut": { "carriers": [ "null" ], "description": "The body is
empty: the new best solution is in the CamelOptaPlannerBestSolution header.
Events come from the producers of the same CamelContext that solve a problem
with the same problemId" },
"producerIn": { "carriers": [ "pojo" ], "description": "The planning
problem: an instance of the PlanningSolution class of the solver configuration,
or a ProblemChange to apply to the problem being solved. Any other body fails
to solve, and no body fails" },
"producerOut": { "carriers": [ "pojo", "unchanged" ], "description": "The
best solution found, a new instance of the problem class, with the
CamelOptaPlannerIsSolving header false (also with async, the exchange waits for
it); a ProblemChange leaves the body unchanged" }
},
"properties": {
"problemName": { "index": 0, "kind": "path", "displayName": "Problem
Name", "group": "common", "label": "", "required": true, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.optaplanner.OptaPlannerConfiguration",
"configurationField": "configuration", "description": "Problem name" },
- "configFile": { "index": 1, "kind": "parameter", "displayName": "Config
File", "group": "common", "label": "common", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": false, "configurationClass":
"org.apache.camel.component.optaplanner.OptaPlannerConfiguration",
"configurationField": "configuration", "description": "If SolverManager is
absent from the header OptaPlannerConstants.SOLVER_MANAGER then a SolverManager
[...]
+ "configFile": { "index": 1, "kind": "parameter", "displayName": "Config
File", "group": "common", "label": "common", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": false, "configurationClass":
"org.apache.camel.component.optaplanner.OptaPlannerConfiguration",
"configurationField": "configuration", "description": "If SolverManager is
absent from the header OptaPlannerConstants.SOLVER_MANAGER then a SolverManager
[...]
"problemId": { "index": 2, "kind": "parameter", "displayName": "Problem
Id", "group": "common", "label": "common", "required": false, "type":
"integer", "javaType": "long", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": 1, "configurationClass":
"org.apache.camel.component.optaplanner.OptaPlannerConfiguration",
"configurationField": "configuration", "description": "In case of using
SolverManager : the problem id" },
"solverId": { "index": 3, "kind": "parameter", "displayName": "Solver Id",
"group": "common", "label": "common", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": "DEFAULT_SOLVER", "configurationClass":
"org.apache.camel.component.optaplanner.OptaPlannerConfiguration",
"configurationField": "configuration", "description": "Specifies the solverId
to user for the solver instance key" },
"bridgeErrorHandler": { "index": 4, "kind": "parameter", "displayName":
"Bridge Error Handler", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Allows for bridging the consumer to the
Camel routing Error Handler, which mean any exceptions (if possible) occurred
while the Camel consumer is trying to pickup incoming [...]
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/optaplanner-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/optaplanner-component.adoc
index 9e16f6d14e52..8bc6313bd75a 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/optaplanner-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/optaplanner-component.adoc
@@ -59,9 +59,15 @@ with the best solution as the body.
with the same `problemId`, and the body is left unchanged.
* Any other body fails the exchange, as the solver cannot solve it.
-The consumer receives an exchange each time a producer solving asynchronously,
with the same
-`problemId`, finds a new best solution. Its body is empty: the best solution
is in the
-`CamelOptaPlannerBestSolution` header.
+The consumer receives an exchange each time a producer of the same
CamelContext, solving a problem
+with the same `problemId`, finds a new best solution. This applies to
producers solving synchronously
+and asynchronously. Its body is empty: the best solution is in the
`CamelOptaPlannerBestSolution` header.
+
+When no `SolverManager` is given, neither with the `solverManager` option nor
with the
+`CamelOptaPlannerSolverManager` header, the endpoint creates one
`SolverManager` from the `configFile`
+on first use, and closes it when the endpoint stops. As the exchanges of the
endpoint share this
+`SolverManager`, a `ProblemChange` sent to the same endpoint reaches the
problem being solved, and
+solving a problem while another one with the same `problemId` is still being
solved fails.
=== Examples
diff --git
a/components/camel-optaplanner/src/generated/resources/META-INF/org/apache/camel/component/optaplanner/optaplanner.json
b/components/camel-optaplanner/src/generated/resources/META-INF/org/apache/camel/component/optaplanner/optaplanner.json
index 262598ea1771..d7ad87d76f64 100644
---
a/components/camel-optaplanner/src/generated/resources/META-INF/org/apache/camel/component/optaplanner/optaplanner.json
+++
b/components/camel-optaplanner/src/generated/resources/META-INF/org/apache/camel/component/optaplanner/optaplanner.json
@@ -36,13 +36,13 @@
"CamelOptaPlannerSolverManager": { "index": 4, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType": "org.optaplanner.core.api.solver.SolverManager", "deprecated":
false, "deprecationNote": "", "autowired": false, "secret": false,
"description": "The Solver Manager.", "constantName":
"org.apache.camel.component.optaplanner.OptaPlannerConstants#SOLVER_MANAGER" }
},
"bodyTypes": {
- "consumerOut": { "carriers": [ "null" ], "description": "The body is
empty: the new best solution is in the CamelOptaPlannerBestSolution header.
Events come only from producers that solve asynchronously, with the same
problemId" },
+ "consumerOut": { "carriers": [ "null" ], "description": "The body is
empty: the new best solution is in the CamelOptaPlannerBestSolution header.
Events come from the producers of the same CamelContext that solve a problem
with the same problemId" },
"producerIn": { "carriers": [ "pojo" ], "description": "The planning
problem: an instance of the PlanningSolution class of the solver configuration,
or a ProblemChange to apply to the problem being solved. Any other body fails
to solve, and no body fails" },
"producerOut": { "carriers": [ "pojo", "unchanged" ], "description": "The
best solution found, a new instance of the problem class, with the
CamelOptaPlannerIsSolving header false (also with async, the exchange waits for
it); a ProblemChange leaves the body unchanged" }
},
"properties": {
"problemName": { "index": 0, "kind": "path", "displayName": "Problem
Name", "group": "common", "label": "", "required": true, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.optaplanner.OptaPlannerConfiguration",
"configurationField": "configuration", "description": "Problem name" },
- "configFile": { "index": 1, "kind": "parameter", "displayName": "Config
File", "group": "common", "label": "common", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": false, "configurationClass":
"org.apache.camel.component.optaplanner.OptaPlannerConfiguration",
"configurationField": "configuration", "description": "If SolverManager is
absent from the header OptaPlannerConstants.SOLVER_MANAGER then a SolverManager
[...]
+ "configFile": { "index": 1, "kind": "parameter", "displayName": "Config
File", "group": "common", "label": "common", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": false, "configurationClass":
"org.apache.camel.component.optaplanner.OptaPlannerConfiguration",
"configurationField": "configuration", "description": "If SolverManager is
absent from the header OptaPlannerConstants.SOLVER_MANAGER then a SolverManager
[...]
"problemId": { "index": 2, "kind": "parameter", "displayName": "Problem
Id", "group": "common", "label": "common", "required": false, "type":
"integer", "javaType": "long", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": 1, "configurationClass":
"org.apache.camel.component.optaplanner.OptaPlannerConfiguration",
"configurationField": "configuration", "description": "In case of using
SolverManager : the problem id" },
"solverId": { "index": 3, "kind": "parameter", "displayName": "Solver Id",
"group": "common", "label": "common", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": "DEFAULT_SOLVER", "configurationClass":
"org.apache.camel.component.optaplanner.OptaPlannerConfiguration",
"configurationField": "configuration", "description": "Specifies the solverId
to user for the solver instance key" },
"bridgeErrorHandler": { "index": 4, "kind": "parameter", "displayName":
"Bridge Error Handler", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Allows for bridging the consumer to the
Camel routing Error Handler, which mean any exceptions (if possible) occurred
while the Camel consumer is trying to pickup incoming [...]
diff --git
a/components/camel-optaplanner/src/main/docs/optaplanner-component.adoc
b/components/camel-optaplanner/src/main/docs/optaplanner-component.adoc
index 9e16f6d14e52..8bc6313bd75a 100644
--- a/components/camel-optaplanner/src/main/docs/optaplanner-component.adoc
+++ b/components/camel-optaplanner/src/main/docs/optaplanner-component.adoc
@@ -59,9 +59,15 @@ with the best solution as the body.
with the same `problemId`, and the body is left unchanged.
* Any other body fails the exchange, as the solver cannot solve it.
-The consumer receives an exchange each time a producer solving asynchronously,
with the same
-`problemId`, finds a new best solution. Its body is empty: the best solution
is in the
-`CamelOptaPlannerBestSolution` header.
+The consumer receives an exchange each time a producer of the same
CamelContext, solving a problem
+with the same `problemId`, finds a new best solution. This applies to
producers solving synchronously
+and asynchronously. Its body is empty: the best solution is in the
`CamelOptaPlannerBestSolution` header.
+
+When no `SolverManager` is given, neither with the `solverManager` option nor
with the
+`CamelOptaPlannerSolverManager` header, the endpoint creates one
`SolverManager` from the `configFile`
+on first use, and closes it when the endpoint stops. As the exchanges of the
endpoint share this
+`SolverManager`, a `ProblemChange` sent to the same endpoint reaches the
problem being solved, and
+solving a problem while another one with the same `problemId` is still being
solved fails.
=== Examples
diff --git
a/components/camel-optaplanner/src/main/java/org/apache/camel/component/optaplanner/OptaPlannerComponent.java
b/components/camel-optaplanner/src/main/java/org/apache/camel/component/optaplanner/OptaPlannerComponent.java
index 35e7e6d64e5a..8db280c5a94b 100644
---
a/components/camel-optaplanner/src/main/java/org/apache/camel/component/optaplanner/OptaPlannerComponent.java
+++
b/components/camel-optaplanner/src/main/java/org/apache/camel/component/optaplanner/OptaPlannerComponent.java
@@ -17,6 +17,9 @@
package org.apache.camel.component.optaplanner;
import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArraySet;
import org.apache.camel.Endpoint;
import org.apache.camel.spi.annotations.Component;
@@ -27,6 +30,9 @@ import org.kie.api.KieServices;
@Component("optaplanner")
public class OptaPlannerComponent extends DefaultComponent {
+ // the consumers listening for the best solutions, per problemId, in this
CamelContext
+ private final Map<Long, Set<OptaplannerSolutionEventListener>>
solutionListeners = new ConcurrentHashMap<>();
+
@Override
protected Endpoint createEndpoint(String uri, String remaining,
Map<String, Object> parameters) throws Exception {
OptaPlannerConfiguration configuration = new
OptaPlannerConfiguration();
@@ -47,4 +53,27 @@ public class OptaPlannerComponent extends DefaultComponent {
return answer;
}
+ protected Set<OptaplannerSolutionEventListener>
getSolutionEventListeners(Long problemId) {
+ return solutionListeners.get(problemId);
+ }
+
+ protected void addSolutionEventListener(Long problemId,
OptaplannerSolutionEventListener listener) {
+ solutionListeners.compute(problemId, (k, listeners) -> {
+ if (listeners == null) {
+ listeners = new CopyOnWriteArraySet<>();
+ }
+ listeners.add(listener);
+ return listeners;
+ });
+ }
+
+ protected void removeSolutionEventListener(Long problemId,
OptaplannerSolutionEventListener listener) {
+ solutionListeners.computeIfPresent(problemId, (k, listeners) -> {
+ listeners.remove(listener);
+ if (listeners.isEmpty()) {
+ return null;
+ }
+ return listeners;
+ });
+ }
}
diff --git
a/components/camel-optaplanner/src/main/java/org/apache/camel/component/optaplanner/OptaPlannerConfiguration.java
b/components/camel-optaplanner/src/main/java/org/apache/camel/component/optaplanner/OptaPlannerConfiguration.java
index 15e2839fb10a..9accbed34313 100644
---
a/components/camel-optaplanner/src/main/java/org/apache/camel/component/optaplanner/OptaPlannerConfiguration.java
+++
b/components/camel-optaplanner/src/main/java/org/apache/camel/component/optaplanner/OptaPlannerConfiguration.java
@@ -47,7 +47,8 @@ public class OptaPlannerConfiguration {
/**
* If SolverManager is absent from the header
OptaPlannerConstants.SOLVER_MANAGER then a SolverManager will be
- * created using this Optaplanner config file.
+ * created using this Optaplanner config file. The endpoint creates it
once, on first use, and closes it when the
+ * endpoint stops.
*/
public void setConfigFile(String configFile) {
this.configFile = configFile;
diff --git
a/components/camel-optaplanner/src/main/java/org/apache/camel/component/optaplanner/OptaPlannerEndpoint.java
b/components/camel-optaplanner/src/main/java/org/apache/camel/component/optaplanner/OptaPlannerEndpoint.java
index 9d142114c3db..5ace583d7855 100644
---
a/components/camel-optaplanner/src/main/java/org/apache/camel/component/optaplanner/OptaPlannerEndpoint.java
+++
b/components/camel-optaplanner/src/main/java/org/apache/camel/component/optaplanner/OptaPlannerEndpoint.java
@@ -16,10 +16,11 @@
*/
package org.apache.camel.component.optaplanner;
-import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
import org.apache.camel.Category;
import org.apache.camel.Component;
@@ -34,6 +35,9 @@ import org.apache.camel.spi.UriParam;
import org.apache.camel.support.DefaultEndpoint;
import org.optaplanner.core.api.solver.Solver;
import org.optaplanner.core.api.solver.SolverFactory;
+import org.optaplanner.core.api.solver.SolverManager;
+import org.optaplanner.core.config.solver.SolverConfig;
+import org.optaplanner.core.config.solver.SolverManagerConfig;
/**
* Solve planning problems with OptaPlanner.
@@ -42,8 +46,8 @@ import org.optaplanner.core.api.solver.SolverFactory;
category = { Category.WORKFLOW }, headersClass =
OptaPlannerConstants.class)
@BodyTypes(consumerOut = @BodyType(value = Carrier.NULL,
description = "The body is empty: the new
best solution is in the CamelOptaPlannerBestSolution"
- + " header. Events come only
from producers that solve asynchronously, with the"
- + " same problemId"),
+ + " header. Events come from
the producers of the same CamelContext that solve"
+ + " a problem with the same
problemId"),
producerIn = @BodyType(value = Carrier.POJO,
description = "The planning problem: an
instance of the PlanningSolution class of the solver"
+ " configuration, or a
ProblemChange to apply to the problem being solved. Any"
@@ -53,8 +57,10 @@ import org.optaplanner.core.api.solver.SolverFactory;
+ " CamelOptaPlannerIsSolving
header false (also with async, the exchange waits"
+ " for it); a ProblemChange
leaves the body unchanged"))
public class OptaPlannerEndpoint extends DefaultEndpoint {
- private static final Map<String, Solver<Object>> SOLVERS = new
ConcurrentHashMap<>();
- private static final Map<Long, Set<OptaplannerSolutionEventListener>>
SOLUTION_LISTENER = new ConcurrentHashMap<>();
+ private final Map<String, Solver<Object>> solvers = new
ConcurrentHashMap<>();
+ private final Lock solverManagerLock = new ReentrantLock();
+ // the SolverManager created from the configFile, which this endpoint
closes when it stops
+ private volatile SolverManager<?, Long> solverManager;
@UriParam
private OptaPlannerConfiguration configuration;
@@ -69,7 +75,7 @@ public class OptaPlannerEndpoint extends DefaultEndpoint {
}
protected Solver<Object> getOrCreateSolver(String solverId) {
- return SOLVERS.computeIfAbsent(solverId, k -> createSolver());
+ return solvers.computeIfAbsent(solverId, k -> createSolver());
}
protected Solver<Object> createSolver() {
@@ -79,7 +85,30 @@ public class OptaPlannerEndpoint extends DefaultEndpoint {
}
protected Solver<Object> getSolver(String solverId) {
- return SOLVERS.get(solverId);
+ return solvers.get(solverId);
+ }
+
+ /**
+ * Gets the SolverManager created from the configFile, creating it on
first use. It is shared by the exchanges of
+ * this endpoint, and closed when the endpoint stops.
+ */
+ protected SolverManager<?, Long> getOrCreateSolverManager() {
+ SolverManager<?, Long> answer = solverManager;
+ if (answer == null) {
+ solverManagerLock.lock();
+ try {
+ answer = solverManager;
+ if (answer == null) {
+ SolverConfig solverConfig =
SolverConfig.createFromXmlResource(configuration.getConfigFile());
+ SolverFactory<Object> solverFactory =
SolverFactory.create(solverConfig);
+ answer = SolverManager.create(solverFactory, new
SolverManagerConfig());
+ solverManager = answer;
+ }
+ } finally {
+ solverManagerLock.unlock();
+ }
+ }
+ return answer;
}
@Override
@@ -96,28 +125,38 @@ public class OptaPlannerEndpoint extends DefaultEndpoint {
@Override
protected void doStop() throws Exception {
- for (Map.Entry<String, Solver<Object>> solver : SOLVERS.entrySet()) {
+ for (Map.Entry<String, Solver<Object>> solver : solvers.entrySet()) {
solver.getValue().terminateEarly();
- SOLVERS.remove(solver.getKey());
+ solvers.remove(solver.getKey());
+ }
+ solverManagerLock.lock();
+ try {
+ if (solverManager != null) {
+ solverManager.close();
+ solverManager = null;
+ }
+ } finally {
+ solverManagerLock.unlock();
}
super.doStop();
}
+ @Override
+ public OptaPlannerComponent getComponent() {
+ return (OptaPlannerComponent) super.getComponent();
+ }
+
+ // the listeners are kept on the component, so a consumer gets the events
of the producers of its CamelContext only
+
protected Set<OptaplannerSolutionEventListener>
getSolutionEventListeners(Long problemId) {
- return SOLUTION_LISTENER.get(problemId);
+ return getComponent().getSolutionEventListeners(problemId);
}
protected void addSolutionEventListener(Long problemId,
OptaplannerSolutionEventListener listener) {
- SOLUTION_LISTENER.computeIfAbsent(problemId, k -> new
HashSet<>()).add(listener);
+ getComponent().addSolutionEventListener(problemId, listener);
}
protected void removeSolutionEventListener(Long problemId,
OptaplannerSolutionEventListener listener) {
- SOLUTION_LISTENER.computeIfPresent(problemId, (k, listeners) -> {
- listeners.remove(listener);
- if (listeners.isEmpty()) {
- return null;
- }
- return listeners;
- });
+ getComponent().removeSolutionEventListener(problemId, listener);
}
}
diff --git
a/components/camel-optaplanner/src/main/java/org/apache/camel/component/optaplanner/OptaPlannerProducer.java
b/components/camel-optaplanner/src/main/java/org/apache/camel/component/optaplanner/OptaPlannerProducer.java
index 5b2573f41a19..ebb5347b5957 100644
---
a/components/camel-optaplanner/src/main/java/org/apache/camel/component/optaplanner/OptaPlannerProducer.java
+++
b/components/camel-optaplanner/src/main/java/org/apache/camel/component/optaplanner/OptaPlannerProducer.java
@@ -22,12 +22,9 @@ import java.util.concurrent.ExecutorService;
import org.apache.camel.AsyncCallback;
import org.apache.camel.Exchange;
import org.apache.camel.support.DefaultAsyncProducer;
-import org.optaplanner.core.api.solver.SolverFactory;
import org.optaplanner.core.api.solver.SolverJob;
import org.optaplanner.core.api.solver.SolverManager;
import org.optaplanner.core.api.solver.change.ProblemChange;
-import org.optaplanner.core.config.solver.SolverConfig;
-import org.optaplanner.core.config.solver.SolverManagerConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -97,8 +94,9 @@ public class OptaPlannerProducer extends DefaultAsyncProducer
{
});
return false;
} else {
- // no need for a consumer for sync call
- SolverJob solverJob = solverManager.solve(problemId, body);
+ // the consumers of the same problemId also get the best
solutions of a sync call
+ OptaplannerEventSupport eventSupport = new
OptaplannerEventSupport(endpoint, problemId);
+ SolverJob solverJob = solverManager.solveAndListen(problemId,
t -> body, eventSupport::updateBestSolution);
// wait for result
populateResult(exchange, solverJob);
}
@@ -139,10 +137,8 @@ public class OptaPlannerProducer extends
DefaultAsyncProducer {
if (configuration.getConfigFile() == null) {
return null;
}
- SolverConfig solverConfig
- =
SolverConfig.createFromXmlResource(configuration.getConfigFile());
- SolverFactory<?> solverFactory =
SolverFactory.create(solverConfig);
- solverManager = SolverManager.create(solverFactory, new
SolverManagerConfig());
+ // created once per endpoint, which closes it when it stops
+ solverManager = endpoint.getOrCreateSolverManager();
}
return solverManager;
}
diff --git
a/components/camel-optaplanner/src/test/java/org/apache/camel/component/optaplanner/OptaPlannerConfigFileSolverManagerTest.java
b/components/camel-optaplanner/src/test/java/org/apache/camel/component/optaplanner/OptaPlannerConfigFileSolverManagerTest.java
new file mode 100644
index 000000000000..c3856d548f80
--- /dev/null
+++
b/components/camel-optaplanner/src/test/java/org/apache/camel/component/optaplanner/OptaPlannerConfigFileSolverManagerTest.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.optaplanner;
+
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.optaplanner.core.config.solver.SolverManagerConfig;
+import org.optaplanner.examples.cloudbalancing.domain.CloudBalance;
+import
org.optaplanner.examples.cloudbalancing.persistence.CloudBalancingGenerator;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * With a configFile, the endpoint creates one SolverManager, which it closes
when it stops.
+ */
+public class OptaPlannerConfigFileSolverManagerTest extends CamelTestSupport {
+
+ // the SolverManager runs its solvers on a JDK fixed thread pool, whose
threads are named pool-N-thread-M
+ private static final Pattern POOL_THREAD =
Pattern.compile("pool-\\d+-thread-\\d+");
+
+ @Test
+ public void testOneSolverManagerPerEndpoint() throws Exception {
+ int parallelSolverCount = new
SolverManagerConfig().resolveParallelSolverCount();
+ long before = countPoolThreads();
+
+ // more exchanges than one SolverManager has solver threads
+ for (int i = 0; i < parallelSolverCount + 2; i++) {
+ CloudBalance problem = new
CloudBalancingGenerator(true).createCloudBalance(4, 12);
+ CloudBalance bestSolution = template.requestBody("direct:in",
problem, CloudBalance.class);
+ assertNotNull(bestSolution.getScore());
+ }
+
+ // a SolverManager per exchange would keep a solver thread alive per
exchange
+ await().atMost(10, TimeUnit.SECONDS)
+ .until(() -> countPoolThreads() - before <=
parallelSolverCount);
+
+ // the endpoint closes its SolverManager when it stops
+ context.stop();
+ await().atMost(10, TimeUnit.SECONDS)
+ .until(() -> countPoolThreads() <= before);
+ }
+
+ private static long countPoolThreads() {
+ return Thread.getAllStackTraces().keySet().stream()
+ .filter(Thread::isAlive)
+ .filter(t -> POOL_THREAD.matcher(t.getName()).matches())
+ .count();
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ public void configure() {
+ from("direct:in")
+
.to("optaplanner:myName?configFile=org/apache/camel/component/optaplanner/solverConfigQuick.xml");
+ }
+ };
+ }
+
+}
diff --git
a/components/camel-optaplanner/src/test/java/org/apache/camel/component/optaplanner/OptaPlannerConsumerCamelContextTest.java
b/components/camel-optaplanner/src/test/java/org/apache/camel/component/optaplanner/OptaPlannerConsumerCamelContextTest.java
new file mode 100644
index 000000000000..0c3922d2e282
--- /dev/null
+++
b/components/camel-optaplanner/src/test/java/org/apache/camel/component/optaplanner/OptaPlannerConsumerCamelContextTest.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.optaplanner;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.optaplanner.examples.cloudbalancing.domain.CloudBalance;
+import
org.optaplanner.examples.cloudbalancing.persistence.CloudBalancingGenerator;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * A consumer gets the best solutions of the producers of its own CamelContext
only.
+ */
+public class OptaPlannerConsumerCamelContextTest extends CamelTestSupport {
+
+ private static final String CONFIG_FILE =
"org/apache/camel/component/optaplanner/solverConfigQuick.xml";
+
+ private CamelContext otherContext;
+
+ @AfterEach
+ public void stopOtherContext() {
+ if (otherContext != null) {
+ otherContext.stop();
+ }
+ }
+
+ @Test
+ public void testConsumerOfOtherCamelContextGetsNoEvents() throws Exception
{
+ // a consumer in another CamelContext, with the same (default)
problemId
+ otherContext = new DefaultCamelContext();
+ otherContext.addRoutes(new RouteBuilder() {
+ public void configure() {
+ from("optaplanner:myName?configFile=" + CONFIG_FILE)
+ .to("mock:other");
+ }
+ });
+ otherContext.start();
+ MockEndpoint other = otherContext.getEndpoint("mock:other",
MockEndpoint.class);
+
+ MockEndpoint mock = getMockEndpoint("mock:result");
+ mock.expectedMinimumMessageCount(1);
+
+ CloudBalance problem = new
CloudBalancingGenerator(true).createCloudBalance(4, 12);
+ CloudBalance bestSolution = template.requestBody("direct:in", problem,
CloudBalance.class);
+ assertNotNull(bestSolution.getScore());
+
+ mock.assertIsSatisfied();
+ await().during(500, TimeUnit.MILLISECONDS).atMost(5, TimeUnit.SECONDS)
+ .until(() -> other.getReceivedCounter() == 0);
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ public void configure() {
+ from("direct:in")
+ .to("optaplanner:myName?async=true&configFile=" +
CONFIG_FILE);
+
+ from("optaplanner:myName?configFile=" + CONFIG_FILE)
+ .to("mock:result");
+ }
+ };
+ }
+
+}
diff --git
a/components/camel-optaplanner/src/test/java/org/apache/camel/component/optaplanner/OptaPlannerSyncConsumerTest.java
b/components/camel-optaplanner/src/test/java/org/apache/camel/component/optaplanner/OptaPlannerSyncConsumerTest.java
new file mode 100644
index 000000000000..3fb1009b0956
--- /dev/null
+++
b/components/camel-optaplanner/src/test/java/org/apache/camel/component/optaplanner/OptaPlannerSyncConsumerTest.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.optaplanner;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.optaplanner.examples.cloudbalancing.domain.CloudBalance;
+import
org.optaplanner.examples.cloudbalancing.persistence.CloudBalancingGenerator;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * A consumer also gets the best solutions of a producer that solves
synchronously.
+ */
+public class OptaPlannerSyncConsumerTest extends CamelTestSupport {
+
+ private static final String CONFIG_FILE =
"org/apache/camel/component/optaplanner/solverConfigQuick.xml";
+
+ @Test
+ public void testConsumerGetsEventsOfSyncProducer() throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:result");
+ mock.expectedMinimumMessageCount(1);
+
+ CloudBalance problem = new
CloudBalancingGenerator(true).createCloudBalance(4, 12);
+ CloudBalance bestSolution = template.requestBody("direct:in", problem,
CloudBalance.class);
+ assertNotNull(bestSolution.getScore());
+
+ mock.assertIsSatisfied();
+ CloudBalance event = mock.getReceivedExchanges().get(0).getMessage()
+ .getHeader(OptaPlannerConstants.BEST_SOLUTION,
CloudBalance.class);
+ assertNotNull(event);
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ public void configure() {
+ from("direct:in")
+ .to("optaplanner:myName?configFile=" + CONFIG_FILE);
+
+ from("optaplanner:myName?configFile=" + CONFIG_FILE)
+ .to("mock:result");
+ }
+ };
+ }
+
+}
diff --git
a/components/camel-optaplanner/src/test/resources/org/apache/camel/component/optaplanner/solverConfigQuick.xml
b/components/camel-optaplanner/src/test/resources/org/apache/camel/component/optaplanner/solverConfigQuick.xml
new file mode 100644
index 000000000000..a1aab4838549
--- /dev/null
+++
b/components/camel-optaplanner/src/test/resources/org/apache/camel/component/optaplanner/solverConfigQuick.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<solver xmlns="https://www.optaplanner.org/xsd/solver"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="https://www.optaplanner.org/xsd/solver
https://www.optaplanner.org/xsd/solver/solver.xsd">
+
+ <!-- Domain model configuration -->
+
<solutionClass>org.optaplanner.examples.cloudbalancing.domain.CloudBalance</solutionClass>
+
<entityClass>org.optaplanner.examples.cloudbalancing.domain.CloudProcess</entityClass>
+
+ <!-- Score configuration -->
+ <scoreDirectorFactory>
+
<constraintProviderClass>org.optaplanner.examples.cloudbalancing.score.CloudBalancingConstraintProvider</constraintProviderClass>
+ <initializingScoreTrend>ONLY_DOWN</initializingScoreTrend>
+ </scoreDirectorFactory>
+
+ <!-- Optimization algorithms configuration -->
+ <termination>
+ <millisecondsSpentLimit>200</millisecondsSpentLimit>
+ </termination>
+
+</solver>
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index b3c96ca6168d..87cacf8cb5cc 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -889,6 +889,19 @@ The generated Endpoint DSL header accessors on the Swift
`HeaderNameBuilder` are
`openstackSwiftVersionsLocation()`, `xContainerRead()` ->
`openstackSwiftContainerRead()`, `xContainerWrite()` ->
`openstackSwiftContainerWrite()`).
+=== camel-optaplanner - one SolverManager per endpoint, and consumers get the
events of their own CamelContext
+
+With `configFile` and no `SolverManager` (neither the `solverManager` option
nor the `CamelOptaPlannerSolverManager`
+header), the producer created a new `SolverManager` for every exchange and
never closed it, so its threads were never
+released. The endpoint now creates one `SolverManager` on first use and closes
it when the endpoint stops. As the
+exchanges of the endpoint share it, solving a problem while another one with
the same `problemId` is still being
+solved now fails with `The problemId (1) is already solving`, as it already
did with a `SolverManager` you configure.
+Use a different `problemId` (an endpoint per problem) to solve several
problems at the same time.
+
+A consumer now gets the best solutions of the producers of its own
CamelContext only. Before, it also got the ones of
+producers in any other CamelContext of the JVM with the same `problemId`. A
consumer now also gets the best solutions
+of a producer that solves synchronously (`async=false`), not only of the ones
that solve asynchronously.
+
=== camel-file - the checksum of checksumFileAlgorithm is the checksum of the
written file
With `checksumFileAlgorithm`, the file producer calculated the checksum from
the body before it was written. With
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OptaPlannerEndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OptaPlannerEndpointBuilderFactory.java
index 25c51d820cf4..bcd84df9a6d4 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OptaPlannerEndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OptaPlannerEndpointBuilderFactory.java
@@ -46,7 +46,8 @@ public interface OptaPlannerEndpointBuilderFactory {
/**
* If SolverManager is absent from the header
* OptaPlannerConstants.SOLVER_MANAGER then a SolverManager will be
- * created using this Optaplanner config file.
+ * created using this Optaplanner config file. The endpoint creates it
+ * once, on first use, and closes it when the endpoint stops.
*
* The option is a: <code>java.lang.String</code> type.
*
@@ -277,7 +278,8 @@ public interface OptaPlannerEndpointBuilderFactory {
/**
* If SolverManager is absent from the header
* OptaPlannerConstants.SOLVER_MANAGER then a SolverManager will be
- * created using this Optaplanner config file.
+ * created using this Optaplanner config file. The endpoint creates it
+ * once, on first use, and closes it when the endpoint stops.
*
* The option is a: <code>java.lang.String</code> type.
*
@@ -497,7 +499,8 @@ public interface OptaPlannerEndpointBuilderFactory {
/**
* If SolverManager is absent from the header
* OptaPlannerConstants.SOLVER_MANAGER then a SolverManager will be
- * created using this Optaplanner config file.
+ * created using this Optaplanner config file. The endpoint creates it
+ * once, on first use, and closes it when the endpoint stops.
*
* The option is a: <code>java.lang.String</code> type.
*