This is an automated email from the ASF dual-hosted git repository.
gnodet 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 98eacfb58be3 CAMEL-26237: camel-ehcache: fix shared CacheManager, null
body handling, consumer events, and type defaults (#28822)
98eacfb58be3 is described below
commit 98eacfb58be358bd34344fca7984914f029d43f1
Author: Guillaume Nodet <[email protected]>
AuthorDate: Mon Nov 2 14:59:09 2026 +0100
CAMEL-26237: camel-ehcache: fix shared CacheManager, null body handling,
consumer events, and type defaults (#28822)
* CAMEL-26237: camel-ehcache: fix shared CacheManager, null body, consumer
events, and type defaults
Four bugs fixed in the camel-ehcache component:
- EhcacheComponent: share a single default CacheManager across all endpoints
without explicit config via a sentinel key in the managers map
- EhcacheManager: replace UserManagedCache (no event executors) with a fully
managed cache so consumer event listeners fire correctly
- EhcacheProducer: fix null body storing the DefaultMessage instead of
throwing;
use message.getBody() instead of message.getBody(Object.class) for Object
type
- EhcacheManager: default keyType and valueType independently so setting
only
one of them does not cause NPE
The getCache() method now accepts an optional endpoint-specific
configuration
parameter so that a shared default CacheManager can still use per-endpoint
cache-level configurations (named or default CacheConfiguration instances).
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
* CAMEL-26237: address review: unbounded heap, test coverage, concurrent
createCache, docs and upgrade guide
* CAMEL-26237: fix tests per review: distinct URIs in EhcacheDefaultsTest,
assert refcount and use second component in EhcacheRemoveRouteTest
* CAMEL-26237: address review: add Status.UNINITIALIZED assertion and fix
stale comment in EhcacheRemoveRouteTest
* CAMEL-26237: remove unnecessary @SuppressWarnings("unchecked") from
createCacheOrGetExisting
* CAMEL-26237: fix doc NOTE wording and upgrade guide lifecycle sentence
* CAMEL-26237: strengthen EhcacheRemoveRouteTest: add Status.UNINITIALIZED
assertion and verify k1 gone after restart
---------
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
---
.../camel/catalog/docs/ehcache-component.adoc | 16 +-
.../src/main/docs/ehcache-component.adoc | 16 +-
.../camel/component/ehcache/EhcacheComponent.java | 5 +-
.../camel/component/ehcache/EhcacheConsumer.java | 2 +-
.../camel/component/ehcache/EhcacheManager.java | 95 +++++-----
.../camel/component/ehcache/EhcacheProducer.java | 4 +-
.../ehcache/EhcacheCacheConfigurationTest.java | 4 +-
.../component/ehcache/EhcacheDefaultsTest.java | 207 +++++++++++++++++++++
.../component/ehcache/EhcacheRemoveRouteTest.java | 77 ++++----
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 27 +++
10 files changed, 349 insertions(+), 104 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ehcache-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ehcache-component.adoc
index aa4e4cf710b5..2d01f34a06ee 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ehcache-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ehcache-component.adoc
@@ -99,14 +99,14 @@ The producer runs the action of the `CamelEhcacheAction`
header (or of the `acti
message: "Order ${header.orderId} is not in the cache"
----
-NOTE: Without a cache manager or a cache configuration, each endpoint creates
its own cache.
-`ehcache:orders?action=PUT` and `ehcache:orders?action=GET` are then two
different caches.
-Set the action in a header and use one endpoint, as above, or share a cache
manager with `configurationUri`,
-`cacheManager` or `cacheManagerConfiguration`.
-
-The consumer receives cache events only when the cache comes from a
configuration or a cache manager.
-The default cache of an endpoint has no events. In this example, both
endpoints use the same `ehcache.xml`,
-so they share its cache manager:
+NOTE: Endpoints without a `cacheManager`, `cacheManagerConfiguration` or
`configurationUri` share one
+`CacheManager` per component. `ehcache:orders?action=PUT` and
`ehcache:orders?action=GET` use the same
+cache, and all endpoints for the same cache name must use the same `keyType`
and `valueType`.
+A cache created on demand has no size limit; use a cache configuration or
`configurationUri` to bound it
+or to set an expiry.
+
+The consumer receives cache events for caches created on demand in the shared
manager. In this example, both
+endpoints use the same `ehcache.xml` for a custom capacity or expiry:
[source,yaml]
----
diff --git a/components/camel-ehcache/src/main/docs/ehcache-component.adoc
b/components/camel-ehcache/src/main/docs/ehcache-component.adoc
index aa4e4cf710b5..2d01f34a06ee 100644
--- a/components/camel-ehcache/src/main/docs/ehcache-component.adoc
+++ b/components/camel-ehcache/src/main/docs/ehcache-component.adoc
@@ -99,14 +99,14 @@ The producer runs the action of the `CamelEhcacheAction`
header (or of the `acti
message: "Order ${header.orderId} is not in the cache"
----
-NOTE: Without a cache manager or a cache configuration, each endpoint creates
its own cache.
-`ehcache:orders?action=PUT` and `ehcache:orders?action=GET` are then two
different caches.
-Set the action in a header and use one endpoint, as above, or share a cache
manager with `configurationUri`,
-`cacheManager` or `cacheManagerConfiguration`.
-
-The consumer receives cache events only when the cache comes from a
configuration or a cache manager.
-The default cache of an endpoint has no events. In this example, both
endpoints use the same `ehcache.xml`,
-so they share its cache manager:
+NOTE: Endpoints without a `cacheManager`, `cacheManagerConfiguration` or
`configurationUri` share one
+`CacheManager` per component. `ehcache:orders?action=PUT` and
`ehcache:orders?action=GET` use the same
+cache, and all endpoints for the same cache name must use the same `keyType`
and `valueType`.
+A cache created on demand has no size limit; use a cache configuration or
`configurationUri` to bound it
+or to set an expiry.
+
+The consumer receives cache events for caches created on demand in the shared
manager. In this example, both
+endpoints use the same `ehcache.xml` for a custom capacity or expiry:
[source,yaml]
----
diff --git
a/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheComponent.java
b/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheComponent.java
index e0ff6dbfa248..65e96005123e 100644
---
a/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheComponent.java
+++
b/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheComponent.java
@@ -110,7 +110,10 @@ public class EhcacheComponent extends DefaultComponent {
}
LOGGER.info("EhcacheManager configured with default builder");
- return new
EhcacheManager(CacheManagerBuilder.newCacheManagerBuilder().build(), true,
configuration);
+ return managers.computeIfAbsent(
+ EhcacheComponent.class,
+ k -> new EhcacheManager(
+ CacheManagerBuilder.newCacheManagerBuilder().build(),
true, new EhcacheConfiguration()));
}
// ****************************
diff --git
a/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheConsumer.java
b/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheConsumer.java
index 64c4e3e5f1c0..06324dbe1e9e 100644
---
a/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheConsumer.java
+++
b/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheConsumer.java
@@ -59,7 +59,7 @@ public class EhcacheConsumer extends DefaultConsumer
implements CacheEventListen
// the producer, as it opens the manager again if it was closed
endpointManager.incRef();
try {
- this.cache = endpointManager.getCache(cacheName, kt, vt);
+ this.cache = endpointManager.getCache(cacheName, kt, vt,
configuration);
this.cache.getRuntimeConfiguration().registerCacheEventListener(
this,
configuration.getEventOrdering(),
diff --git
a/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheManager.java
b/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheManager.java
index 4b7e4f9f38f6..4b1d5858a068 100644
---
a/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheManager.java
+++
b/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheManager.java
@@ -16,8 +16,6 @@
*/
package org.apache.camel.component.ehcache;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@@ -28,10 +26,9 @@ import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.ReferenceCount;
import org.ehcache.Cache;
import org.ehcache.CacheManager;
-import org.ehcache.UserManagedCache;
import org.ehcache.config.CacheConfiguration;
import org.ehcache.config.builders.CacheConfigurationBuilder;
-import org.ehcache.config.builders.UserManagedCacheBuilder;
+import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.spi.service.ServiceConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -41,7 +38,6 @@ public class EhcacheManager extends ServiceSupport {
private final EhcacheConfiguration configuration;
private final CacheManager cacheManager;
- private final ConcurrentMap<String, UserManagedCache<?, ?>> userCaches;
private final ReferenceCount refCount;
// the reference count changes together with the start and stop of the
manager, so that the last two users that
// stop at the same time do not both (or neither) see the count reach 0,
and a user that starts while the last one
@@ -50,15 +46,11 @@ public class EhcacheManager extends ServiceSupport {
public EhcacheManager(CacheManager cacheManager, boolean managed,
EhcacheConfiguration configuration) {
this.cacheManager = ObjectHelper.notNull(cacheManager, "cacheManager");
- this.userCaches = new ConcurrentHashMap<>();
this.configuration = configuration;
this.refCount = ReferenceCount.on(
managed ? cacheManager::init : () -> {
},
() -> {
- // the last user closes the caches created without a
configuration, as the endpoint that shuts
- // the manager down may have been shut down while other
routes still used them
- closeUserCaches();
if (managed) {
cacheManager.close();
}
@@ -113,47 +105,32 @@ public class EhcacheManager extends ServiceSupport {
}
}
- @Override
- protected void doShutdown() throws Exception {
- closeUserCaches();
- }
-
- private void closeUserCaches() {
- if (userCaches != null && !userCaches.isEmpty()) {
- for (UserManagedCache cache : userCaches.values()) {
- try {
- cache.close();
- } catch (Exception e) {
- // ignore
- }
- }
- userCaches.clear();
- }
+ public <K, V> Cache<K, V> getCache(String name, Class<K> keyType, Class<V>
valueType) throws Exception {
+ return getCache(name, keyType, valueType, configuration);
}
@SuppressWarnings("unchecked")
- public <K, V> Cache<K, V> getCache(String name, Class<K> keyType, Class<V>
valueType) throws Exception {
+ <K, V> Cache<K, V> getCache(String name, Class<K> keyType, Class<V>
valueType, EhcacheConfiguration endpointConfiguration)
+ throws Exception {
+ EhcacheConfiguration effectiveConfig = endpointConfiguration != null ?
endpointConfiguration : configuration;
CacheConfiguration<K, V> cacheConfiguration = null;
- if (configuration != null) {
- if (configuration.hasConfiguration(name)) {
+ if (effectiveConfig != null) {
+ if (effectiveConfig.hasConfiguration(name)) {
LOGGER.debug("Using custom cache configuration for cache {}",
name);
- cacheConfiguration =
CacheConfiguration.class.cast(configuration.getConfigurations().get(name));
- } else if (configuration.hasConfiguration()) {
+ cacheConfiguration =
CacheConfiguration.class.cast(effectiveConfig.getConfigurations().get(name));
+ } else if (effectiveConfig.hasConfiguration()) {
LOGGER.debug("Using global cache configuration for cache {}",
name);
- cacheConfiguration =
CacheConfiguration.class.cast(configuration.getConfiguration());
+ cacheConfiguration =
CacheConfiguration.class.cast(effectiveConfig.getConfiguration());
}
}
- if (keyType == null && valueType == null) {
- if (cacheConfiguration != null) {
- keyType = cacheConfiguration.getKeyType();
- valueType = cacheConfiguration.getValueType();
- } else {
- keyType = (Class<K>) Object.class;
- valueType = (Class<V>) Object.class;
- }
+ if (keyType == null) {
+ keyType = cacheConfiguration != null ?
cacheConfiguration.getKeyType() : (Class<K>) Object.class;
+ }
+ if (valueType == null) {
+ valueType = cacheConfiguration != null ?
cacheConfiguration.getValueType() : (Class<V>) Object.class;
}
Cache<K, V> cache = cacheManager.getCache(name, keyType, valueType);
- if (cache == null && configuration != null &&
configuration.isCreateCacheIfNotExist()) {
+ if (cache == null && effectiveConfig != null &&
effectiveConfig.isCreateCacheIfNotExist()) {
if (cacheConfiguration != null) {
if (keyType != cacheConfiguration.getKeyType() || valueType !=
cacheConfiguration.getValueType()) {
LOGGER.info("Mismatch keyType / valueType configuration
for cache {}", name);
@@ -167,16 +144,16 @@ public class EhcacheManager extends ServiceSupport {
}
cacheConfiguration = builder.build();
}
- cache = cacheManager.createCache(name, cacheConfiguration);
+ cache = createCacheOrGetExisting(name, cacheConfiguration,
keyType, valueType);
} else {
- // If a cache configuration is not provided, create a User
Managed
- // Cache
- LOGGER.debug("Using a UserManagedCache for cache {} as no
configuration has been found", name);
- Class<K> kt = keyType;
- Class<V> vt = valueType;
- cache = Cache.class.cast(userCaches.computeIfAbsent(
- name,
- key ->
UserManagedCacheBuilder.newUserManagedCacheBuilder(kt, vt).build(true)));
+ // If a cache configuration is not provided, create a managed
cache with default configuration
+ // so that event listeners work correctly (UserManagedCache
lacks event executor services)
+ LOGGER.debug("Creating a managed cache for {} with default
configuration as no configuration has been found",
+ name);
+ cache = createCacheOrGetExisting(name,
+ CacheConfigurationBuilder.newCacheConfigurationBuilder(
+ keyType, valueType,
ResourcePoolsBuilder.heap(Long.MAX_VALUE)).build(),
+ keyType, valueType);
}
}
@@ -187,6 +164,26 @@ public class EhcacheManager extends ServiceSupport {
return cache;
}
+ /**
+ * Creates a cache with the given configuration, or returns the existing
one if another thread raced to create it
+ * first. Ehcache throws {@link IllegalArgumentException} when two callers
create the same cache concurrently; this
+ * method catches that and falls back to {@link CacheManager#getCache}.
+ */
+ private <K, V> Cache<K, V> createCacheOrGetExisting(
+ String name, CacheConfiguration<K, V> cacheConfiguration, Class<K>
keyType, Class<V> valueType) {
+ try {
+ return cacheManager.createCache(name, cacheConfiguration);
+ } catch (IllegalArgumentException e) {
+ // another thread created the cache between our getCache() and
createCache() calls
+ LOGGER.debug("Cache {} was created concurrently; reusing the
existing one", name);
+ Cache<K, V> existing = cacheManager.getCache(name, keyType,
valueType);
+ if (existing == null) {
+ throw e;
+ }
+ return existing;
+ }
+ }
+
CacheManager getCacheManager() {
return this.cacheManager;
}
diff --git
a/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheProducer.java
b/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheProducer.java
index 8ac278211eb2..4059a0431635 100644
---
a/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheProducer.java
+++
b/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/EhcacheProducer.java
@@ -65,7 +65,7 @@ public class EhcacheProducer extends HeaderSelectorProducer {
manager.incRef();
retained = true;
// after the reference, which opens the manager again if it was closed
- cache = manager.getCache(cacheName, keyType, valueType);
+ cache = manager.getCache(cacheName, keyType, valueType, configuration);
}
@Override
@@ -193,7 +193,7 @@ public class EhcacheProducer extends HeaderSelectorProducer
{
if (type instanceof String) {
Class<?> clazz =
getEndpoint().getCamelContext().getClassResolver().resolveClass((String) type);
value = message.getBody(clazz);
- } else if (type instanceof Class) {
+ } else if (type instanceof Class && type != Object.class) {
value = message.getBody((Class) type);
} else {
value = message.getBody();
diff --git
a/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/EhcacheCacheConfigurationTest.java
b/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/EhcacheCacheConfigurationTest.java
index d410910f2db0..69290c3eca94 100644
---
a/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/EhcacheCacheConfigurationTest.java
+++
b/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/EhcacheCacheConfigurationTest.java
@@ -22,7 +22,6 @@ import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.test.junit6.CamelTestSupport;
import org.ehcache.Cache;
import org.ehcache.CacheManager;
-import org.ehcache.UserManagedCache;
import org.ehcache.config.CacheConfiguration;
import org.ehcache.config.ResourcePools;
import org.ehcache.config.ResourceType;
@@ -36,7 +35,6 @@ import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
public class EhcacheCacheConfigurationTest extends CamelTestSupport {
@EndpointInject("ehcache:myProgrammaticCacheConf?configuration=#myProgrammaticConfiguration")
@@ -111,7 +109,7 @@ public class EhcacheCacheConfigurationTest extends
CamelTestSupport {
Cache<Object, Object> cache =
ehcacheUserConf.getManager().getCache("myUserCacheConf", Object.class,
Object.class);
- assertTrue(cache instanceof UserManagedCache);
+ assertNotNull(cache);
assertEquals("user-val", cache.get("user-key"));
}
diff --git
a/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/EhcacheDefaultsTest.java
b/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/EhcacheDefaultsTest.java
new file mode 100644
index 000000000000..7c2b0ca20232
--- /dev/null
+++
b/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/EhcacheDefaultsTest.java
@@ -0,0 +1,207 @@
+/*
+ * 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.ehcache;
+
+import org.apache.camel.CamelExchangeException;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.ehcache.event.EventType;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+/**
+ * Tests for the default (no explicit cacheManager) behavior of camel-ehcache:
shared CacheManager across endpoints,
+ * consumer event delivery on unconfigured caches, null body handling, and
independent key/value type defaults.
+ */
+class EhcacheDefaultsTest extends CamelTestSupport {
+
+ /**
+ * Bug 1: Two endpoints sharing the same cache name without explicit
cacheManager should share data. A PUT on one
+ * endpoint followed by a GET on another endpoint must return the stored
value.
+ */
+ @Test
+ void testSharedDefaultCacheManager() throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:get-result");
+ mock.expectedMinimumMessageCount(1);
+ mock.expectedHeaderReceived(EhcacheConstants.ACTION_SUCCEEDED, true);
+ mock.expectedHeaderReceived(EhcacheConstants.ACTION_HAS_RESULT, true);
+
+ String key = "shared-key";
+ String val = "shared-value";
+
+ // Two distinct endpoint instances (different URIs due to action
param) must share the same manager,
+ // because both refer to the same cache name with no explicit
cacheManager option.
+ EhcacheEndpoint putEndpoint
+ = context.getEndpoint("ehcache://defaultTestCache?action=PUT",
EhcacheEndpoint.class);
+ EhcacheEndpoint getEndpoint
+ = context.getEndpoint("ehcache://defaultTestCache?action=GET",
EhcacheEndpoint.class);
+ assertSame(putEndpoint.getManager(), getEndpoint.getManager(),
+ "distinct endpoints for the same cache name must share the
default CacheManager");
+
+ // PUT via the first endpoint (action=PUT in the URI, distinct from
the GET route)
+ fluentTemplate()
+ .withHeader(EhcacheConstants.KEY, key)
+ .withBody(val)
+ .to("direct://put")
+ .send();
+
+ // GET via a different endpoint (action=GET — same cache name, no
explicit cacheManager, different URI)
+ fluentTemplate()
+ .withHeader(EhcacheConstants.KEY, key)
+ .to("direct://get")
+ .send();
+
+ MockEndpoint.assertIsSatisfied(context);
+
+ assertEquals(val,
mock.getExchanges().get(0).getIn().getBody(String.class));
+ }
+
+ /**
+ * Bug 2: A consumer on a cache without explicit configuration should
receive CREATED events when values are PUT via
+ * the producer. Previously, the UserManagedCache fallback lacked event
executor services, so listeners were
+ * silently never called.
+ */
+ @Test
+ void testConsumerReceivesEventsWithoutExplicitConfig() throws Exception {
+ MockEndpoint created = getMockEndpoint("mock:events");
+ created.expectedMinimumMessageCount(1);
+ created.expectedHeaderReceived(EhcacheConstants.EVENT_TYPE,
EventType.CREATED);
+
+ String key = "event-key";
+ String val = "event-value";
+
+ fluentTemplate()
+ .withHeader(EhcacheConstants.ACTION,
EhcacheConstants.ACTION_PUT)
+ .withHeader(EhcacheConstants.KEY, key)
+ .withBody(val)
+ .to("direct://put-events")
+ .send();
+
+ MockEndpoint.assertIsSatisfied(context);
+
+ assertEquals(key,
created.getExchanges().get(0).getIn().getHeader(EhcacheConstants.KEY));
+ assertEquals(val,
created.getExchanges().get(0).getIn().getBody(String.class));
+ }
+
+ /**
+ * Bug 3: A PUT with null body and no CamelEhcacheValue header must throw
CamelExchangeException, not silently store
+ * the DefaultMessage object.
+ */
+ @Test
+ void testPutWithNullBodyThrows() throws Exception {
+ Exchange result = fluentTemplate()
+ .withHeader(EhcacheConstants.ACTION,
EhcacheConstants.ACTION_PUT)
+ .withHeader(EhcacheConstants.KEY, "null-body-key")
+ .withBody((Object) null)
+ .to("direct://put")
+ .send();
+
+ assertNotNull(result.getException());
+ assertInstanceOf(CamelExchangeException.class, result.getException());
+ }
+
+ /**
+ * Bug 4: Setting valueType without keyType should not cause NPE. The
keyType must independently default to
+ * Object.class.
+ */
+ @Test
+ void testValueTypeWithoutKeyTypeDoesNotThrowNPE() throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:typed-result");
+ mock.expectedMinimumMessageCount(1);
+ mock.expectedHeaderReceived(EhcacheConstants.ACTION_SUCCEEDED, true);
+
+ String key = "typed-key";
+ String val = "typed-value";
+
+ fluentTemplate()
+ .withHeader(EhcacheConstants.ACTION,
EhcacheConstants.ACTION_PUT)
+ .withHeader(EhcacheConstants.KEY, key)
+ .withBody(val)
+ .to("direct://put-typed")
+ .send();
+
+ MockEndpoint.assertIsSatisfied(context);
+
+ // Verify we can GET the value back
+ MockEndpoint getMock = getMockEndpoint("mock:typed-get-result");
+ getMock.expectedMinimumMessageCount(1);
+ getMock.expectedHeaderReceived(EhcacheConstants.ACTION_HAS_RESULT,
true);
+
+ fluentTemplate()
+ .withHeader(EhcacheConstants.ACTION,
EhcacheConstants.ACTION_GET)
+ .withHeader(EhcacheConstants.KEY, key)
+ .to("direct://get-typed")
+ .send();
+
+ MockEndpoint.assertIsSatisfied(context);
+
+ assertEquals(val,
getMock.getExchanges().get(0).getIn().getBody(String.class));
+ }
+
+ /**
+ * Bug 3 (variant): PUT_IF_ABSENT with null body must also throw.
+ */
+ @Test
+ void testPutIfAbsentWithNullBodyThrows() throws Exception {
+ Exchange result = fluentTemplate()
+ .withHeader(EhcacheConstants.ACTION,
EhcacheConstants.ACTION_PUT_IF_ABSENT)
+ .withHeader(EhcacheConstants.KEY, "null-body-key-2")
+ .withBody((Object) null)
+ .to("direct://put")
+ .send();
+
+ assertNotNull(result.getException());
+ assertInstanceOf(CamelExchangeException.class, result.getException());
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ public void configure() {
+ // Bug 1: two endpoints with distinct URIs (action=PUT vs
action=GET) for the same cache name,
+ // no explicit cacheManager — they must share one CacheManager
and thus the same cache data.
+ from("direct://put")
+ .to("ehcache://defaultTestCache?action=PUT")
+ .to("mock:put-result");
+ from("direct://get")
+ .to("ehcache://defaultTestCache?action=GET")
+ .to("mock:get-result");
+
+ // Bug 2: consumer on a cache without explicit config
+ from("direct://put-events")
+ .to("ehcache://eventsCache")
+ .to("mock:put-events-result");
+
from("ehcache://eventsCache?eventTypes=CREATED&eventFiring=SYNCHRONOUS")
+ .to("mock:events");
+
+ // Bug 4: valueType set without keyType
+ from("direct://put-typed")
+ .to("ehcache://typedCache?valueType=java.lang.String")
+ .to("mock:typed-result");
+ from("direct://get-typed")
+ .to("ehcache://typedCache?valueType=java.lang.String")
+ .to("mock:typed-get-result");
+ }
+ };
+ }
+}
diff --git
a/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/EhcacheRemoveRouteTest.java
b/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/EhcacheRemoveRouteTest.java
index 8e5a7b9e3631..b7f363c95d60 100644
---
a/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/EhcacheRemoveRouteTest.java
+++
b/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/EhcacheRemoveRouteTest.java
@@ -26,9 +26,9 @@ import org.apache.camel.ProducerTemplate;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.test.junit6.CamelTestSupport;
+import org.ehcache.Cache;
import org.ehcache.CacheManager;
import org.ehcache.Status;
-import org.ehcache.UserManagedCache;
import org.ehcache.config.CacheConfiguration;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheManagerBuilder;
@@ -36,6 +36,7 @@ import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -144,42 +145,56 @@ class EhcacheRemoveRouteTest extends CamelTestSupport {
put("direct:plain", "k2", "v2");
assertEquals("v2", get("direct:plain", "k2"));
- // without a configuration the endpoint's own EhcacheManager creates a
user managed cache
+ // without a configuration the endpoints share the component's default
EhcacheManager, which creates the cache on demand
EhcacheEndpoint endpoint = context.getEndpoint("ehcache:plain",
EhcacheEndpoint.class);
- UserManagedCache<?, ?> cache
- = (UserManagedCache<?, ?>)
endpoint.getManager().getCache("plain", Object.class, Object.class);
- assertEquals(Status.AVAILABLE, cache.getStatus());
+ Cache<?, ?> cache = endpoint.getManager().getCache("plain",
Object.class, Object.class);
+ assertNotNull(cache);
- // the endpoint was shut down with the removed route while the
producer still used the cache: the producer, as
- // the last user, closes it
context.stop();
- assertEquals(Status.UNINITIALIZED, cache.getStatus());
+
+ // all endpoints were stopped with the context: the manager reference
count must have reached 0
+ assertEquals(0, endpoint.getManager().getReferenceCount().get());
+ // and the underlying CacheManager was closed (ref count hit 0 →
managed close)
+ assertEquals(Status.UNINITIALIZED,
endpoint.getManager().getCacheManager().getStatus());
}
@Test
void producerTakesTheCacheAgainWhenItsManagerWasClosedAndOpenedAgain()
throws Exception {
- EhcacheEndpoint endpoint = context.getEndpoint("ehcache:restarted",
EhcacheEndpoint.class);
- Producer producer = endpoint.createProducer();
- // as a route or a ProducerTemplate does with the producers they create
- CamelContextAware.trySetCamelContext(producer, context);
- producer.start();
- assertNull(send(endpoint, producer, EhcacheConstants.ACTION_PUT, "k1",
"v1").getException());
-
- // the last users stop: the manager closes the cache created on demand
- producer.stop();
- endpoint.stop();
- assertEquals(0, endpoint.getManager().getReferenceCount().get());
-
- // the same producer is started again after its endpoint, and uses the
cache the manager has now
- endpoint.start();
- producer.start();
+ // Register a second component instance so this endpoint gets its own
default CacheManager,
+ // independent of the default one shared by other test routes (which
would keep references
+ // and prevent the manager from closing when the producer and endpoint
below stop).
+ EhcacheComponent ehcache2 = new EhcacheComponent();
+ context.addComponent("ehcache2", ehcache2);
try {
- assertNull(send(endpoint, producer, EhcacheConstants.ACTION_PUT,
"k2", "v2").getException());
- Exchange got = send(endpoint, producer,
EhcacheConstants.ACTION_GET, "k2", null);
- assertNull(got.getException());
- assertEquals("v2", got.getMessage().getBody());
- } finally {
+ EhcacheEndpoint endpoint =
context.getEndpoint("ehcache2:restarted", EhcacheEndpoint.class);
+ Producer producer = endpoint.createProducer();
+ // as a route or a ProducerTemplate does with the producers they
create
+ CamelContextAware.trySetCamelContext(producer, context);
+ producer.start();
+ assertNull(send(endpoint, producer, EhcacheConstants.ACTION_PUT,
"k1", "v1").getException());
+
+ // the last users release their reference; the manager closes
because ref count reaches 0
producer.stop();
+ endpoint.stop();
+ assertEquals(0, endpoint.getManager().getReferenceCount().get());
+ // the underlying CacheManager was closed when the ref count hit 0
+ assertEquals(Status.UNINITIALIZED,
endpoint.getManager().getCacheManager().getStatus());
+
+ // the same producer is started again after its endpoint, and uses
the cache the manager has now
+ endpoint.start();
+ producer.start();
+ try {
+ // k1 is gone: the manager was closed and re-opened with a
fresh cache
+ assertNull(send(endpoint, producer,
EhcacheConstants.ACTION_GET, "k1", null).getMessage().getBody());
+ assertNull(send(endpoint, producer,
EhcacheConstants.ACTION_PUT, "k2", "v2").getException());
+ Exchange got = send(endpoint, producer,
EhcacheConstants.ACTION_GET, "k2", null);
+ assertNull(got.getException());
+ assertEquals("v2", got.getMessage().getBody());
+ } finally {
+ producer.stop();
+ }
+ } finally {
+ context.removeComponent("ehcache2");
}
}
@@ -191,14 +206,12 @@ class EhcacheRemoveRouteTest extends CamelTestSupport {
userTemplate.sendBodyAndHeaders("ehcache:fromTemplate", "v1",
Map.of(EhcacheConstants.ACTION, EhcacheConstants.ACTION_PUT,
EhcacheConstants.KEY, "k1"));
EhcacheEndpoint endpoint = context.getEndpoint("ehcache:fromTemplate",
EhcacheEndpoint.class);
- UserManagedCache<?, ?> cache
- = (UserManagedCache<?, ?>)
endpoint.getManager().getCache("fromTemplate", Object.class, Object.class);
- assertEquals(Status.AVAILABLE, cache.getStatus());
+ Cache<?, ?> cache = endpoint.getManager().getCache("fromTemplate",
Object.class, Object.class);
+ assertNotNull(cache);
context.stop();
assertEquals(0, endpoint.getManager().getReferenceCount().get());
- assertEquals(Status.UNINITIALIZED, cache.getStatus());
userTemplate.stop();
}
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 9becf9752ee5..08b16636c546 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
@@ -13,6 +13,33 @@ See the xref:camel-upgrade-recipes-tool.adoc[documentation]
page for details.
== Upgrading Camel 4.23 to 4.24
+=== camel-ehcache - shared CacheManager and changed defaults
+
+The following user-visible changes take effect in 4.24:
+
+* **Shared CacheManager.** Endpoints that have the same `cacheName` and no
explicit `cacheManager`,
+ `cacheManagerConfiguration`, or `configurationUri` now share one
`CacheManager` per component instance.
+ Previously each endpoint created its own unmanaged (`UserManagedCache`)
cache.
+ - The same name now means the same cache. `ehcache:foo?action=PUT` and
`ehcache:foo?action=GET`
+ now read and write the same entries.
+ - All endpoints for a cache name must use compatible key and value types.
Ehcache validates them
+ exactly, so `ehcache:foo` and `ehcache:foo?valueType=java.lang.String` now
fail at startup with
+ a type-mismatch error.
+ - The shared caches, and their entries, are closed when the last endpoint
that uses the shared
+ CacheManager stops (for example when the CamelContext stops).
+
+* **Event delivery on default caches.** A consumer on a cache without an
explicit configuration now
+ receives events. Previously, the `UserManagedCache` fallback lacked event
executor services, so
+ listeners were silently never called.
+
+* **Null body rejected.** PUT, PUT_IF_ABSENT and REPLACE without a body and
without a
+ `CamelEhcacheValue` header now fail with `CamelExchangeException` instead of
silently storing the
+ `Message` object.
+
+* **User-supplied `cacheManager` caches persist.** Caches created on demand
inside a user-supplied
+ `cacheManager` (e.g. `ehcache:foo?cacheManager=#myManager`) are no longer
removed when Camel stops;
+ the lifecycle of the caches follows the lifecycle of the supplied manager.
+
=== camel-a2a - consumer context is separate from producer headers
An A2A consumer no longer populates producer control headers with the incoming
operation and