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

Reply via email to