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. *
