This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch CAMEL-26123-optaplanner-solver-leak
in repository https://gitbox.apache.org/repos/asf/camel.git

commit a2a2e68a26110047d566c12b6c09c4cdc6e924b5
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 2 16:39:57 2026 +0100

    CAMEL-26123: camel-optaplanner - one SolverManager per endpoint, 
per-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 1bc188419d61..e96232cc08ce 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
@@ -850,6 +850,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.
          * 

Reply via email to