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 2d385549fb39 CAMEL-26287: camel-jcache - stopping an endpoint no
longer closes the caches, cache manager and caching provider that other
endpoints use (#28640)
2d385549fb39 is described below
commit 2d385549fb39e0a45e83fcc01eafffb93452cb1f
Author: allthingssecurity <[email protected]>
AuthorDate: Thu Oct 29 18:09:31 2026 +0530
CAMEL-26287: camel-jcache - stopping an endpoint no longer closes the
caches, cache manager and caching provider that other endpoints use (#28640)
* CAMEL-26287: camel-jcache - stopping an endpoint no longer closes the
caches, cache manager and caching provider that other endpoints use
JCacheManager.close() (called when an endpoint stops, such as when the only
route that uses it is removed) closed its cache, its cache manager and the
caching provider. The provider is a JVM singleton, and it gives the same
cache
manager and cache to every endpoint of the same cache, so every other jcache
endpoint (and repository) failed with "The cache closed" afterwards. Each of
them is now closed by the last manager that uses it; a manager releases
only what
it counted, so one whose cache could not be got
(createCacheIfNotExists=false)
does not close the provider and cache manager that others use.
A closed manager no longer opens its cache again: getCache() throws an
IllegalStateException. The routes that use the same jcache uri share one
endpoint, and removing one of them stops that endpoint while the producer
of another route still uses it; its getCache() would otherwise open the
cache again (a new, empty one when the provider was closed) and count it,
and nothing would ever release it. Such a producer fails, as before. The
consumer deregisters its listener on the cache it registered it on (not
on a closed one) instead of asking the manager again, as the manager may
be closed by then.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* CAMEL-26287: camel-jcache - upgrade-guide note for JCacheManager.close,
and the same-uri test is labelled a known limitation
- The 4.24 upgrade guide says that a JCacheManager closes its cache, cache
manager and caching provider only when no
other JCacheManager uses them, and that getCache() of a closed manager
now throws IllegalStateException (before,
it returned the closed cache, whose operations throw it).
- The JCacheRemoveRouteTest test that asserts that the producer of the same
uri fails after the consumer route is
removed is renamed knownLimitation... and its javadoc says it pins a
known limitation that a later fix must flip.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* CAMEL-26287: camel-jcache - document that every JCacheManager must be
closed
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* CAMEL-26287: camel-jcache - tests that concurrent first getCache() calls
count a manager once, and that a restarted endpoint uses a new manager
JCacheProducer.process() calls getCache() for every exchange on the one
manager of its endpoint. getCache() already holds the manager's lock
around the init and the retain calls; a comment now says why, and a
test with 16 threads calling getCache() together on a fresh manager
(20 rounds) checks that its close() closes the cache. Without the lock
the test fails in every run.
A route stopped and started again keeps its endpoint's manager, and an
endpoint stopped and started again creates a new one instead of using
the closed one; a test pins both.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
.../camel/component/jcache/JCacheConsumer.java | 28 +++-
.../camel/component/jcache/JCacheManager.java | 82 +++++++++-
.../camel/component/jcache/JCacheManagerTest.java | 88 +++++++++++
.../component/jcache/JCacheRemoveRouteTest.java | 175 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 9 ++
5 files changed, 371 insertions(+), 11 deletions(-)
diff --git
a/components/camel-jcache/src/main/java/org/apache/camel/component/jcache/JCacheConsumer.java
b/components/camel-jcache/src/main/java/org/apache/camel/component/jcache/JCacheConsumer.java
index 554a6440dcff..14a8c02dbf7a 100644
---
a/components/camel-jcache/src/main/java/org/apache/camel/component/jcache/JCacheConsumer.java
+++
b/components/camel-jcache/src/main/java/org/apache/camel/component/jcache/JCacheConsumer.java
@@ -35,6 +35,8 @@ import org.apache.camel.support.DefaultConsumer;
public class JCacheConsumer extends DefaultConsumer {
private CacheEntryListenerConfiguration<Object, Object>
entryListenerConfiguration;
+ // the cache the listener is registered on: the manager of the endpoint
may be closed when this consumer stops
+ private Cache<Object, Object> listenerCache;
public JCacheConsumer(final JCacheEndpoint endpoint, Processor processor) {
super(endpoint, processor);
@@ -43,31 +45,41 @@ public class JCacheConsumer extends DefaultConsumer {
@Override
protected void doStart() throws Exception {
- if (entryListenerConfiguration != null) {
-
getCache().deregisterCacheEntryListener(entryListenerConfiguration);
- }
+ deregisterListener();
+ Cache<Object, Object> cache = getCache();
entryListenerConfiguration = createEntryListenerConfiguration();
- getCache().registerCacheEntryListener(entryListenerConfiguration);
+ cache.registerCacheEntryListener(entryListenerConfiguration);
+ listenerCache = cache;
super.doStart();
}
@Override
protected void doStop() throws Exception {
+ deregisterListener();
+
+ super.doStop();
+ }
+
+ private void deregisterListener() {
if (entryListenerConfiguration != null) {
-
getCache().deregisterCacheEntryListener(entryListenerConfiguration);
+ Cache<Object, Object> cache = listenerCache;
+ // a closed cache has no listeners any more (its manager was
closed with the endpoint of another route
+ // that used the same uri)
+ if (cache != null && !cache.isClosed()) {
+ cache.deregisterCacheEntryListener(entryListenerConfiguration);
+ }
entryListenerConfiguration = null;
+ listenerCache = null;
}
-
- super.doStop();
}
private JCacheEndpoint getJCacheEndpoint() {
return (JCacheEndpoint) getEndpoint();
}
- private Cache getCache() throws Exception {
+ private Cache<Object, Object> getCache() throws Exception {
return getJCacheEndpoint().getManager().getCache();
}
diff --git
a/components/camel-jcache/src/main/java/org/apache/camel/component/jcache/JCacheManager.java
b/components/camel-jcache/src/main/java/org/apache/camel/component/jcache/JCacheManager.java
index 5810ce890e7f..4b4d4c306685 100644
---
a/components/camel-jcache/src/main/java/org/apache/camel/component/jcache/JCacheManager.java
+++
b/components/camel-jcache/src/main/java/org/apache/camel/component/jcache/JCacheManager.java
@@ -20,7 +20,9 @@ import java.io.Closeable;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@@ -36,14 +38,31 @@ import javax.cache.spi.CachingProvider;
import org.apache.camel.CamelContext;
import org.apache.camel.util.ObjectHelper;
+/**
+ * Gives access to a JCache {@link Cache}, with a reference count shared by
the whole JVM for the caching provider, the
+ * cache manager and the cache it uses, so that one user closing them does not
close them for the others.
+ * <p/>
+ * Every user of a manager must call {@link #close()} when done with it, to
release its share. A manager that is never
+ * closed keeps the provider, the cache manager and the cache open for the
lifetime of the JVM.
+ */
public class JCacheManager<K, V> implements Closeable {
+ // The caching provider is a JVM singleton, and it returns the same cache
manager for the same uri, which returns
+ // the same cache for the same name: other managers (the other endpoints
of the cache, other caches, repositories,
+ // other CamelContexts) use them too, so each one is closed by the last
manager that uses it
+ private static final Map<Object, Integer> USERS = new HashMap<>();
+ private static final Lock USERS_LOCK = new ReentrantLock();
+
private final JCacheConfiguration configuration;
private final String cacheName;
private final CamelContext camelContext;
private final Lock lock = new ReentrantLock();
+ // what this manager counted in USERS, so that close() releases only its
own uses
+ private final List<Object> retained = new ArrayList<>(3);
private CachingProvider provider;
private CacheManager manager;
private Cache<K, V> cache;
+ // set by close(): the cache is not opened again, as nothing would release
it (see getCache)
+ private boolean closed;
public JCacheManager(JCacheConfiguration configuration) {
this.configuration = configuration;
@@ -72,11 +91,22 @@ public class JCacheManager<K, V> implements Closeable {
}
public Cache<K, V> getCache() throws Exception {
+ // the producer calls this for every exchange, so the first calls can
run together: the lock opens and counts
+ // the cache once per manager (retain is not thread safe on its own)
lock.lock();
try {
+ if (closed) {
+ // such as the producer of a route that uses the endpoint of a
removed route with the same uri: its
+ // endpoint has stopped, so a cache opened again here would
never be released (and with its caching
+ // provider closed, it would be a new, empty cache)
+ throw new IllegalStateException("The JCache manager of cache "
+ cacheName + " is closed");
+ }
if (cache == null) {
JCacheProvider provider =
JCacheProviders.lookup(configuration.getCachingProvider());
cache = doGetCache(provider);
+ retain(this.provider);
+ retain(this.manager);
+ retain(cacheKey());
}
return cache;
@@ -90,23 +120,69 @@ public class JCacheManager<K, V> implements Closeable {
lock.lock();
try {
if (configuration != null) {
- if (cache != null) {
+ if (cache != null && release(cacheKey())) {
cache.close();
}
- if (manager != null) {
+ if (manager != null && release(manager)) {
manager.close();
}
- if (provider != null) {
+ if (provider != null && release(provider)) {
provider.close();
}
+ cache = null;
+ manager = null;
+ provider = null;
+ closed = true;
}
} finally {
lock.unlock();
}
}
+ private Object cacheKey() {
+ return cache != null ? new CacheKey(manager, cache.getName()) : null;
+ }
+
+ private void retain(Object resource) {
+ if (resource != null && !retained.contains(resource)) {
+ USERS_LOCK.lock();
+ try {
+ USERS.merge(resource, 1, Integer::sum);
+ retained.add(resource);
+ } finally {
+ USERS_LOCK.unlock();
+ }
+ }
+ }
+
+ /**
+ * Whether the resource is no longer used by any manager, so it can be
closed.
+ */
+ private boolean release(Object resource) {
+ USERS_LOCK.lock();
+ try {
+ Integer count = USERS.get(resource);
+ if (!retained.remove(resource)) {
+ // not counted by this manager, such as the provider and cache
manager of a getCache that failed (the
+ // cache does not exist and createCacheIfNotExists=false):
closed as before, unless others use it
+ return count == null;
+ }
+ if (count == null || count <= 1) {
+ USERS.remove(resource);
+ return true;
+ }
+ USERS.put(resource, count - 1);
+ return false;
+ } finally {
+ USERS_LOCK.unlock();
+ }
+ }
+
+ private record CacheKey(CacheManager manager, String name) {
+ }
+
protected CacheEntryEventFilter getEventFilter() {
if (configuration.getEventFilters() != null) {
return new
JCacheEntryEventFilters.Chained(configuration.getEventFilters());
diff --git
a/components/camel-jcache/src/test/java/org/apache/camel/component/jcache/JCacheManagerTest.java
b/components/camel-jcache/src/test/java/org/apache/camel/component/jcache/JCacheManagerTest.java
index 9e576ed5b60c..1f8bd42da2ea 100644
---
a/components/camel-jcache/src/test/java/org/apache/camel/component/jcache/JCacheManagerTest.java
+++
b/components/camel-jcache/src/test/java/org/apache/camel/component/jcache/JCacheManagerTest.java
@@ -16,10 +16,24 @@
*/
package org.apache.camel.component.jcache;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import javax.cache.Cache;
+
import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
public class JCacheManagerTest extends JCacheComponentTestSupport {
@@ -47,4 +61,78 @@ public class JCacheManagerTest extends
JCacheComponentTestSupport {
objectObjectJCacheManager.getCache();
});
}
+
+ @Test
+ void
aManagerThatCouldNotGetItsCacheDoesNotCloseTheCacheManagerOfTheOthers() throws
Exception {
+ JCacheConfiguration conf = new JCacheConfiguration();
+ conf.setCacheName(randomString());
+ JCacheManager<Object, Object> user = new JCacheManager<>(conf);
+ Cache<Object, Object> cache = user.getCache();
+
+ JCacheConfiguration missing = new JCacheConfiguration();
+ missing.setCacheName(randomString());
+ missing.setCreateCacheIfNotExists(false);
+ JCacheManager<Object, Object> failing = new JCacheManager<>(missing);
+ assertThrows(IllegalStateException.class, failing::getCache);
+ // such as when the endpoint stops after its route failed to start
+ failing.close();
+
+ assertFalse(cache.isClosed());
+ cache.put("k", "v");
+ assertEquals("v", cache.get("k"));
+
+ user.close();
+ assertTrue(cache.isClosed());
+ }
+
+ @Test
+ void aClosedManagerDoesNotOpenItsCacheAgain() throws Exception {
+ JCacheConfiguration conf = new JCacheConfiguration();
+ conf.setCacheName(randomString());
+ JCacheManager<Object, Object> manager = new JCacheManager<>(conf);
+ Cache<Object, Object> cache = manager.getCache();
+
+ manager.close();
+ assertTrue(cache.isClosed());
+ // nothing would close a cache opened again by a closed manager
+ assertThrows(IllegalStateException.class, manager::getCache);
+ }
+
+ @Test
+ void concurrentFirstCallsOfGetCacheCountTheManagerOnce() throws Exception {
+ // the producer calls getCache() for every exchange, on the one
manager of its endpoint
+ int threads = 16;
+ ExecutorService pool = Executors.newFixedThreadPool(threads);
+ // keeps the caching provider and the cache manager open, so that each
round closes only its own cache
+ JCacheConfiguration other = new JCacheConfiguration();
+ other.setCacheName(randomString());
+ JCacheManager<Object, Object> holder = new JCacheManager<>(other);
+ holder.getCache();
+ try {
+ for (int round = 0; round < 20; round++) {
+ JCacheConfiguration conf = new JCacheConfiguration();
+ conf.setCacheName(randomString());
+ JCacheManager<Object, Object> manager = new
JCacheManager<>(conf);
+ CyclicBarrier start = new CyclicBarrier(threads);
+ List<Future<Cache<Object, Object>>> calls = new ArrayList<>();
+ for (int i = 0; i < threads; i++) {
+ calls.add(pool.submit(() -> {
+ start.await(10, TimeUnit.SECONDS);
+ return manager.getCache();
+ }));
+ }
+ Cache<Object, Object> cache = calls.get(0).get(10,
TimeUnit.SECONDS);
+ for (Future<Cache<Object, Object>> call : calls) {
+ assertSame(cache, call.get(10, TimeUnit.SECONDS));
+ }
+
+ manager.close();
+ // its only user closed it: counted once, not once for each
first call
+ assertTrue(cache.isClosed(), "the cache is still open after
round " + round);
+ }
+ } finally {
+ pool.shutdownNow();
+ holder.close();
+ }
+ }
}
diff --git
a/components/camel-jcache/src/test/java/org/apache/camel/component/jcache/JCacheRemoveRouteTest.java
b/components/camel-jcache/src/test/java/org/apache/camel/component/jcache/JCacheRemoveRouteTest.java
new file mode 100644
index 000000000000..dbfa59af472f
--- /dev/null
+++
b/components/camel-jcache/src/test/java/org/apache/camel/component/jcache/JCacheRemoveRouteTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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.jcache;
+
+import java.util.Map;
+
+import javax.cache.Cache;
+
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.Consumer;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Removing a route that uses a jcache endpoint must not close the caches that
the routes of other endpoints still use.
+ */
+class JCacheRemoveRouteTest extends JCacheComponentTestSupport {
+
+ @Test
+ void removingARouteKeepsTheCachesOfTheOtherRoutes() throws Exception {
+ put("direct:a", "k", "v");
+ put("direct:temp", "t", "x");
+
+ context.getRouteController().stopRoute("temp");
+ context.removeRoute("temp");
+
+ assertEquals("v", get("direct:a", "k"));
+ }
+
+ @Test
+ void removingTheConsumerRouteOfACacheKeepsItsProducer() throws Exception {
+ MockEndpoint events = getMockEndpoint("mock:events");
+ events.expectedMessageCount(1);
+ put("direct:b", "k1", "v1");
+ events.assertIsSatisfied();
+
+ context.getRouteController().stopRoute("events");
+ context.removeRoute("events");
+
+ put("direct:b", "k2", "v2");
+ assertEquals("v2", get("direct:b", "k2"));
+ }
+
+ /**
+ * KNOWN LIMITATION, not the wanted behaviour: this pins what happens
today so that a change to it is noticed.
+ * Removing a route stops the endpoint of its consumer, also when another
route still produces to the same uri
+ * (camel-core RouteService.doShutdown), and this fix does not change
that: the producer's sends then fail. When the
+ * endpoint is kept for its producer (a core fix, or producers and
consumers that hold the cache, as CAMEL-26302
+ * does in camel-ehcache), this test must expect the sends to work instead.
+ */
+ @Test
+ void
knownLimitationRemovingTheConsumerRouteOfTheSameUriStopsTheEndpointOfItsProducer()
throws Exception {
+ put("direct:same", "k", "v");
+
+ // the producer of route "same" uses the endpoint of the removed
route, which stops with it (as before): its
+ // sends fail, rather than opening the cache again where nothing would
close it
+ context.getRouteController().stopRoute("sameEvents");
+ context.removeRoute("sameEvents");
+
+ Exception e = assertThrows(CamelExecutionException.class, () ->
put("direct:same", "k2", "v2"));
+ assertInstanceOf(IllegalStateException.class, e.getCause());
+ }
+
+ @Test
+ void consumerStopsAfterTheEndpointWasStoppedWithAnotherRoute() throws
Exception {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("jcache://remove-route-standby").routeId("standby").autoStartup(false).to("mock:standby");
+ }
+ });
+
+ // removing the route that never started stops the endpoint the
running route consumes from too
+ context.removeRoute("standby");
+
+ // the running consumer still stops: a failure in its doStop would be
thrown here (the route controller only
+ // logs it)
+ Consumer consumer = context.getRoute("standbyRunning").getConsumer();
+ assertDoesNotThrow(consumer::stop);
+ }
+
+ @Test
+ void routesAndEndpointsThatAreStartedAgainKeepWorking() throws Exception {
+ put("direct:a", "k", "v");
+ JCacheEndpoint endpoint =
context.getEndpoint("jcache://remove-route-a", JCacheEndpoint.class);
+
+ // stopping a route does not stop its endpoint: the manager stays open
+ context.getRouteController().stopRoute("a");
+ context.getRouteController().startRoute("a");
+ assertEquals("v", get("direct:a", "k"));
+
+ // an endpoint that stops (as with its context) and starts again uses
a new manager, not the closed one
+ JCacheManager<?, ?> closed = endpoint.getManager();
+ context.getRouteController().stopRoute("a");
+ endpoint.stop();
+ endpoint.start();
+ context.getRouteController().startRoute("a");
+
+ assertNotSame(closed, endpoint.getManager());
+ assertThrows(IllegalStateException.class, closed::getCache);
+ put("direct:a", "k2", "v2");
+ assertEquals("v2", get("direct:a", "k2"));
+ }
+
+ @Test
+ void theLastManagerOfACacheClosesIt() throws Exception {
+ JCacheConfiguration conf = new JCacheConfiguration();
+ conf.setCacheName(randomString());
+ JCacheManager<Object, Object> first = new JCacheManager<>(conf);
+ JCacheManager<Object, Object> second = new JCacheManager<>(conf);
+ Cache<Object, Object> cache = first.getCache();
+ assertEquals(cache, second.getCache());
+
+ first.close();
+ assertFalse(cache.isClosed());
+ cache.put("k", "v");
+
+ second.close();
+ assertTrue(cache.isClosed());
+ }
+
+ private void put(String uri, String key, String value) {
+ template.sendBodyAndHeaders(uri, value,
+ Map.of(JCacheConstants.ACTION, "PUT", JCacheConstants.KEY,
key));
+ }
+
+ private Object get(String uri, String key) {
+ return template.requestBodyAndHeaders(uri, null,
+ Map.of(JCacheConstants.ACTION, "GET", JCacheConstants.KEY,
key));
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:a").routeId("a").to("jcache://remove-route-a");
+
from("direct:temp").routeId("temp").to("jcache://remove-route-temp");
+
+ from("direct:b").routeId("b").to("jcache://remove-route-b");
+
from("jcache://remove-route-b?filteredEvents=UPDATED,REMOVED,EXPIRED").routeId("events")
+ .to("mock:events");
+
+ // a consumer and a producer of the same uri share one endpoint
+
from("direct:same").routeId("same").to("jcache://remove-route-same");
+
from("jcache://remove-route-same").routeId("sameEvents").to("mock:same");
+
+
from("jcache://remove-route-standby").routeId("standbyRunning").to("mock:standbyRunning");
+ }
+ };
+ }
+}
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 7ea1e78ba5bd..719673edee95 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
@@ -396,6 +396,15 @@ configuration (the `camel.component.google-mail.*`
options), kept until the comp
client the component had already created, for example the one of a
`google-mail:messages/modify` endpoint, so
credentials set only on an endpoint URI no longer reach the transformer: set
them on the component as well.
+=== camel-jcache - a JCacheManager closes only what no other manager uses
+
+`JCacheManager.close()`, which a `jcache` endpoint calls when it stops, closed
its cache, its `CacheManager` and the
+`CachingProvider`, which is shared by the whole JVM, and so the caches of
every other endpoint, repository and
+CamelContext of that provider. It now closes each of them only when no other
`JCacheManager` uses it. A
+`JCacheManager` that was closed no longer opens its cache: `getCache()` then
throws `IllegalStateException`; before,
+it returned the closed cache, whose operations throw `IllegalStateException`.
This matters for code that calls a
+`JCacheManager` (for example one created by a custom `JCacheManagerFactory`)
after it was closed.
+
=== camel-spring-rabbitmq - confirm=auto waits for simple publisher confirms
With the default `confirm=auto`, an InOnly producer now also waits for the
publisher confirm when the connection