This is an automated email from the ASF dual-hosted git repository.
oscerd 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 2948bb6308fb CAMEL-26776: Ensure KeyValueRepository instances isolate
namespaces across components and patterns (#29479)
2948bb6308fb is described below
commit 2948bb6308fbb2a9ce662dfb57fd8d27f5c37d33
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Nov 9 19:53:06 2026 +0100
CAMEL-26776: Ensure KeyValueRepository instances isolate namespaces across
components and patterns (#29479)
- Added NamespacedKeyValueRepository in camel-support, which provides a
repository wrapper that partitions all keys under a designated namespace
prefix, restricts clear() to delete only entries in that namespace, streams key
counts in size(), and returns un-prefixed keys in keys().
- Updated StateStoreComponent to isolate shared backend repositories under
statestore:<storeName>:, escaping delimiter characters to prevent prefix
collision between store names, and logging a warning if conflicting namespaced
options are requested for the same store name.
- Added a namespaced configuration option (default true) on
StateStoreComponent and StateStoreEndpoint to allow opting out when preserving
un-namespaced keys written in Camel 4.23.
- Dedicated backends from StateStoreBackendFactory and fallback in-memory
stores are kept un-namespaced.
- Retained un-prefixed keys in CacheProcessor to preserve documented cache
invalidation recipes (cache.delete(key) and cache.clear()).
- Hardened KeyValueAggregationRepository to safely verify that entries are
instances of DefaultExchangeHolder before unmarshalling or completing, logging
a warning and returning null if an unexpected type is encountered.
- Added documentation in state-store-component.adoc and a migration note in
camel-4x-upgrade-guide-4_24.adoc, including an advisory for shared backends
with Cache EIP.
Co-authored-by: Antigravity <[email protected]>
---
.../org/apache/camel/catalog/beans.properties | 1 +
.../beans/NamespacedKeyValueRepository.json | 15 ++
.../camel/catalog/components/state-store.json | 14 +-
.../camel/catalog/docs/state-store-component.adoc | 16 ++
.../camel-state-store/camel-state-store/pom.xml | 2 +-
.../statestore/StateStoreComponentConfigurer.java | 3 +
.../statestore/StateStoreEndpointConfigurer.java | 3 +
.../statestore/StateStoreEndpointUriFactory.java | 3 +-
.../camel/component/statestore/state-store.json | 14 +-
.../src/main/docs/state-store-component.adoc | 16 ++
.../component/statestore/StateStoreComponent.java | 59 ++++-
.../component/statestore/StateStoreEndpoint.java | 14 +-
.../statestore/StateStoreAutoDiscoveryTest.java | 5 +-
.../statestore/StateStoreBackendFactoryTest.java | 6 +-
.../StateStoreNamespaceIsolationTest.java | 148 +++++++++++
.../statestore/StateStoreStreamBodyTest.java | 2 +-
.../apache/camel/processor/CacheProcessorTest.java | 40 +++
.../support/KeyValueAggregationRepositoryTest.java | 16 ++
.../NamespacedKeyValueRepositoryConfigurer.java | 49 ++++
.../services/org/apache/camel/bean.properties | 2 +-
.../camel/bean/NamespacedKeyValueRepository.json | 15 ++
...ache.camel.support.NamespacedKeyValueRepository | 2 +
.../support/KeyValueAggregationRepository.java | 19 +-
.../support/NamespacedKeyValueRepository.java | 271 +++++++++++++++++++++
.../support/NamespacedKeyValueRepositoryTest.java | 185 ++++++++++++++
.../java/org/apache/camel/util/ArtifactUtils.java | 2 +
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 12 +
.../dsl/StateStoreComponentBuilderFactory.java | 19 ++
.../dsl/StateStoreEndpointBuilderFactory.java | 32 +++
29 files changed, 949 insertions(+), 36 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans.properties
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans.properties
index b54d21b7ebc8..dc3b21b7dcaf 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans.properties
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans.properties
@@ -47,6 +47,7 @@ MemoryAggregationRepository
MemoryIdempotentRepository
MemoryKeyValueRepository
MongoDbIdempotentRepository
+NamespacedKeyValueRepository
OpaSecurityPolicy
OpenFgaSecurityPolicy
OpensearchBulkRequestAggregationStrategy
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/NamespacedKeyValueRepository.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/NamespacedKeyValueRepository.json
new file mode 100644
index 000000000000..55274077a939
--- /dev/null
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/NamespacedKeyValueRepository.json
@@ -0,0 +1,15 @@
+{
+ "bean": {
+ "kind": "bean",
+ "name": "NamespacedKeyValueRepository",
+ "javaType": "org.apache.camel.support.NamespacedKeyValueRepository",
+ "interfaceType": "org.apache.camel.spi.KeyValueRepository",
+ "title": "Namespaced Key Value Repository",
+ "description": "A KeyValueRepository that partitions keys using a
namespace prefix.",
+ "deprecated": false,
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-support",
+ "version": "4.24.0-SNAPSHOT"
+ }
+}
+
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/state-store.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/state-store.json
index 6ba0cb8d60af..da8e8ec118e0 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/state-store.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/state-store.json
@@ -25,8 +25,9 @@
},
"componentProperties": {
"lazyStartProducer": { "index": 0, "kind": "property", "displayName":
"Lazy Start Producer", "group": "producer", "label": "producer", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": false, "description":
"Whether the producer should be started lazy (on the first message). By
starting lazy you can use this to allow CamelContext and routes to startup in
situations where a producer may otherwise fail [...]
- "autowiredEnabled": { "index": 1, "kind": "property", "displayName":
"Autowired Enabled", "group": "advanced", "label": "advanced", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether autowiring is enabled. This is used for automatic autowiring options
(the option must be marked as autowired) by looking up in the registry to find
if there is a single instance of matching t [...]
- "backendFactory": { "index": 2, "kind": "property", "displayName":
"Backend Factory", "group": "advanced", "label": "advanced", "required": false,
"type": "object", "javaType":
"org.apache.camel.component.statestore.StateStoreBackendFactory", "deprecated":
false, "autowired": true, "secret": false, "description": "Factory that creates
a dedicated backend for each store name. When set, it takes precedence over
registry auto-discovery for endpoints without an explicit backend." }
+ "namespaced": { "index": 1, "kind": "property", "displayName":
"Namespaced", "group": "producer", "label": "", "required": false, "type":
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": true, "description": "Whether to use a
store-specific namespace prefix to isolate keys on a shared backend
repository." },
+ "autowiredEnabled": { "index": 2, "kind": "property", "displayName":
"Autowired Enabled", "group": "advanced", "label": "advanced", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether autowiring is enabled. This is used for automatic autowiring options
(the option must be marked as autowired) by looking up in the registry to find
if there is a single instance of matching t [...]
+ "backendFactory": { "index": 3, "kind": "property", "displayName":
"Backend Factory", "group": "advanced", "label": "advanced", "required": false,
"type": "object", "javaType":
"org.apache.camel.component.statestore.StateStoreBackendFactory", "deprecated":
false, "autowired": true, "secret": false, "description": "Factory that creates
a dedicated backend for each store name. When set, it takes precedence over
registry auto-discovery for endpoints without an explicit backend." }
},
"headers": {
"CamelStateStoreOperation": { "index": 0, "kind": "header", "displayName":
"", "group": "producer", "label": "producer", "required": false, "javaType":
"org.apache.camel.component.statestore.StateStoreOperations", "enum": [ "put",
"putIfAbsent", "get", "delete", "contains", "keys", "size", "clear" ],
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The operation to perform", "constantName":
"org.apache.camel.component.statestore.StateSt [...]
@@ -39,9 +40,10 @@
},
"properties": {
"storeName": { "index": 0, "kind": "path", "displayName": "Store Name",
"group": "producer", "label": "", "required": true, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "The name of the state
store" },
- "operation": { "index": 1, "kind": "parameter", "displayName":
"Operation", "group": "producer", "label": "", "required": false, "type":
"enum", "javaType":
"org.apache.camel.component.statestore.StateStoreOperations", "enum": [ "put",
"putIfAbsent", "get", "delete", "contains", "keys", "size", "clear" ],
"deprecated": false, "autowired": false, "secret": false, "description": "The
default operation to perform" },
- "ttl": { "index": 2, "kind": "parameter", "displayName": "Ttl", "group":
"producer", "label": "", "required": false, "type": "integer", "javaType":
"long", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": 0, "description": "Time-to-live in milliseconds for entries. 0
means no expiry." },
- "lazyStartProducer": { "index": 3, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
- "backend": { "index": 4, "kind": "parameter", "displayName": "Backend",
"group": "advanced", "label": "advanced", "required": false, "type": "object",
"javaType": "org.apache.camel.spi.KeyValueRepository", "deprecated": false,
"autowired": false, "secret": false, "description": "The backend to use. If not
set, auto-discovers a single KeyValueRepository from the registry, or falls
back to an in-memory store." }
+ "namespaced": { "index": 1, "kind": "parameter", "displayName":
"Namespaced", "group": "producer", "label": "", "required": false, "type":
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": true, "description": "Whether to use a
store-specific namespace prefix to isolate keys on a shared backend
repository." },
+ "operation": { "index": 2, "kind": "parameter", "displayName":
"Operation", "group": "producer", "label": "", "required": false, "type":
"enum", "javaType":
"org.apache.camel.component.statestore.StateStoreOperations", "enum": [ "put",
"putIfAbsent", "get", "delete", "contains", "keys", "size", "clear" ],
"deprecated": false, "autowired": false, "secret": false, "description": "The
default operation to perform" },
+ "ttl": { "index": 3, "kind": "parameter", "displayName": "Ttl", "group":
"producer", "label": "", "required": false, "type": "integer", "javaType":
"long", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": 0, "description": "Time-to-live in milliseconds for entries. 0
means no expiry." },
+ "lazyStartProducer": { "index": 4, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
+ "backend": { "index": 5, "kind": "parameter", "displayName": "Backend",
"group": "advanced", "label": "advanced", "required": false, "type": "object",
"javaType": "org.apache.camel.spi.KeyValueRepository", "deprecated": false,
"autowired": false, "secret": false, "description": "The backend to use. If not
set, auto-discovers a single KeyValueRepository from the registry, or falls
back to an in-memory store." }
}
}
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/state-store-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/state-store-component.adoc
index 5cdd3f1b04d6..8bddc34dbb7c 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/state-store-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/state-store-component.adoc
@@ -206,3 +206,19 @@ for the State Store component, the Idempotent Consumer,
the Aggregator, and the
See the KeyValueRepository section in the User Manual for the full list of
available
backends, configuration examples, and instructions for writing a custom
backend.
+
+== Namespace Isolation
+
+When using a shared `KeyValueRepository` (such as a single bean
auto-discovered from the registry or referenced via `backend=#shared`),
`state-store` automatically isolates entries per store name under the prefix
`statestore:<storeName>:`.
+
+This ensures that:
+
+* Multiple state stores sharing the same backend do not collide on keys.
+* Calling `clear()`, `keys()`, or `size()` on one store is strictly scoped to
that store's entries.
+* State store operations cannot interfere with entries from other patterns
sharing the repository.
+
+To opt out of namespacing (for example, to read existing un-namespaced keys
written in Camel 4.23 without key migration), set `namespaced=false` on the
endpoint or component:
+
+----
+state-store:orders?namespaced=false
+----
diff --git a/components/camel-state-store/camel-state-store/pom.xml
b/components/camel-state-store/camel-state-store/pom.xml
index a130d1351b02..025c6dffc4dd 100644
--- a/components/camel-state-store/camel-state-store/pom.xml
+++ b/components/camel-state-store/camel-state-store/pom.xml
@@ -46,7 +46,7 @@
<!-- testing -->
<dependency>
<groupId>org.apache.camel</groupId>
- <artifactId>camel-test-main-junit5</artifactId>
+ <artifactId>camel-test-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
diff --git
a/components/camel-state-store/camel-state-store/src/generated/java/org/apache/camel/component/statestore/StateStoreComponentConfigurer.java
b/components/camel-state-store/camel-state-store/src/generated/java/org/apache/camel/component/statestore/StateStoreComponentConfigurer.java
index cefdabbdcd4f..0b89deeb1866 100644
---
a/components/camel-state-store/camel-state-store/src/generated/java/org/apache/camel/component/statestore/StateStoreComponentConfigurer.java
+++
b/components/camel-state-store/camel-state-store/src/generated/java/org/apache/camel/component/statestore/StateStoreComponentConfigurer.java
@@ -30,6 +30,7 @@ public class StateStoreComponentConfigurer extends
PropertyConfigurerSupport imp
case "backendFactory": target.setBackendFactory(property(camelContext,
org.apache.camel.component.statestore.StateStoreBackendFactory.class, value));
return true;
case "lazystartproducer":
case "lazyStartProducer":
target.setLazyStartProducer(property(camelContext, boolean.class, value));
return true;
+ case "namespaced": target.setNamespaced(property(camelContext,
boolean.class, value)); return true;
default: return false;
}
}
@@ -48,6 +49,7 @@ public class StateStoreComponentConfigurer extends
PropertyConfigurerSupport imp
case "backendFactory": return
org.apache.camel.component.statestore.StateStoreBackendFactory.class;
case "lazystartproducer":
case "lazyStartProducer": return boolean.class;
+ case "namespaced": return boolean.class;
default: return null;
}
}
@@ -62,6 +64,7 @@ public class StateStoreComponentConfigurer extends
PropertyConfigurerSupport imp
case "backendFactory": return target.getBackendFactory();
case "lazystartproducer":
case "lazyStartProducer": return target.isLazyStartProducer();
+ case "namespaced": return target.isNamespaced();
default: return null;
}
}
diff --git
a/components/camel-state-store/camel-state-store/src/generated/java/org/apache/camel/component/statestore/StateStoreEndpointConfigurer.java
b/components/camel-state-store/camel-state-store/src/generated/java/org/apache/camel/component/statestore/StateStoreEndpointConfigurer.java
index 2084b9a60356..746e40080497 100644
---
a/components/camel-state-store/camel-state-store/src/generated/java/org/apache/camel/component/statestore/StateStoreEndpointConfigurer.java
+++
b/components/camel-state-store/camel-state-store/src/generated/java/org/apache/camel/component/statestore/StateStoreEndpointConfigurer.java
@@ -27,6 +27,7 @@ public class StateStoreEndpointConfigurer extends
PropertyConfigurerSupport impl
case "backend": target.setBackend(property(camelContext,
org.apache.camel.spi.KeyValueRepository.class, value)); return true;
case "lazystartproducer":
case "lazyStartProducer":
target.setLazyStartProducer(property(camelContext, boolean.class, value));
return true;
+ case "namespaced": target.setNamespaced(property(camelContext,
boolean.class, value)); return true;
case "operation": target.setOperation(property(camelContext,
org.apache.camel.component.statestore.StateStoreOperations.class, value));
return true;
case "ttl": target.setTtl(property(camelContext, long.class, value));
return true;
default: return false;
@@ -39,6 +40,7 @@ public class StateStoreEndpointConfigurer extends
PropertyConfigurerSupport impl
case "backend": return org.apache.camel.spi.KeyValueRepository.class;
case "lazystartproducer":
case "lazyStartProducer": return boolean.class;
+ case "namespaced": return boolean.class;
case "operation": return
org.apache.camel.component.statestore.StateStoreOperations.class;
case "ttl": return long.class;
default: return null;
@@ -52,6 +54,7 @@ public class StateStoreEndpointConfigurer extends
PropertyConfigurerSupport impl
case "backend": return target.getBackend();
case "lazystartproducer":
case "lazyStartProducer": return target.isLazyStartProducer();
+ case "namespaced": return target.isNamespaced();
case "operation": return target.getOperation();
case "ttl": return target.getTtl();
default: return null;
diff --git
a/components/camel-state-store/camel-state-store/src/generated/java/org/apache/camel/component/statestore/StateStoreEndpointUriFactory.java
b/components/camel-state-store/camel-state-store/src/generated/java/org/apache/camel/component/statestore/StateStoreEndpointUriFactory.java
index a3e003b0b405..82bdc1b1b98c 100644
---
a/components/camel-state-store/camel-state-store/src/generated/java/org/apache/camel/component/statestore/StateStoreEndpointUriFactory.java
+++
b/components/camel-state-store/camel-state-store/src/generated/java/org/apache/camel/component/statestore/StateStoreEndpointUriFactory.java
@@ -24,9 +24,10 @@ public class StateStoreEndpointUriFactory extends
org.apache.camel.support.compo
private static final Set<String> ENDPOINT_IDENTITY_PROPERTY_NAMES;
private static final Map<String, String> MULTI_VALUE_PREFIXES;
static {
- Set<String> props = new HashSet<>(5);
+ Set<String> props = new HashSet<>(6);
props.add("backend");
props.add("lazyStartProducer");
+ props.add("namespaced");
props.add("operation");
props.add("storeName");
props.add("ttl");
diff --git
a/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/org/apache/camel/component/statestore/state-store.json
b/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/org/apache/camel/component/statestore/state-store.json
index 6ba0cb8d60af..da8e8ec118e0 100644
---
a/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/org/apache/camel/component/statestore/state-store.json
+++
b/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/org/apache/camel/component/statestore/state-store.json
@@ -25,8 +25,9 @@
},
"componentProperties": {
"lazyStartProducer": { "index": 0, "kind": "property", "displayName":
"Lazy Start Producer", "group": "producer", "label": "producer", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": false, "description":
"Whether the producer should be started lazy (on the first message). By
starting lazy you can use this to allow CamelContext and routes to startup in
situations where a producer may otherwise fail [...]
- "autowiredEnabled": { "index": 1, "kind": "property", "displayName":
"Autowired Enabled", "group": "advanced", "label": "advanced", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether autowiring is enabled. This is used for automatic autowiring options
(the option must be marked as autowired) by looking up in the registry to find
if there is a single instance of matching t [...]
- "backendFactory": { "index": 2, "kind": "property", "displayName":
"Backend Factory", "group": "advanced", "label": "advanced", "required": false,
"type": "object", "javaType":
"org.apache.camel.component.statestore.StateStoreBackendFactory", "deprecated":
false, "autowired": true, "secret": false, "description": "Factory that creates
a dedicated backend for each store name. When set, it takes precedence over
registry auto-discovery for endpoints without an explicit backend." }
+ "namespaced": { "index": 1, "kind": "property", "displayName":
"Namespaced", "group": "producer", "label": "", "required": false, "type":
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": true, "description": "Whether to use a
store-specific namespace prefix to isolate keys on a shared backend
repository." },
+ "autowiredEnabled": { "index": 2, "kind": "property", "displayName":
"Autowired Enabled", "group": "advanced", "label": "advanced", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether autowiring is enabled. This is used for automatic autowiring options
(the option must be marked as autowired) by looking up in the registry to find
if there is a single instance of matching t [...]
+ "backendFactory": { "index": 3, "kind": "property", "displayName":
"Backend Factory", "group": "advanced", "label": "advanced", "required": false,
"type": "object", "javaType":
"org.apache.camel.component.statestore.StateStoreBackendFactory", "deprecated":
false, "autowired": true, "secret": false, "description": "Factory that creates
a dedicated backend for each store name. When set, it takes precedence over
registry auto-discovery for endpoints without an explicit backend." }
},
"headers": {
"CamelStateStoreOperation": { "index": 0, "kind": "header", "displayName":
"", "group": "producer", "label": "producer", "required": false, "javaType":
"org.apache.camel.component.statestore.StateStoreOperations", "enum": [ "put",
"putIfAbsent", "get", "delete", "contains", "keys", "size", "clear" ],
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The operation to perform", "constantName":
"org.apache.camel.component.statestore.StateSt [...]
@@ -39,9 +40,10 @@
},
"properties": {
"storeName": { "index": 0, "kind": "path", "displayName": "Store Name",
"group": "producer", "label": "", "required": true, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "The name of the state
store" },
- "operation": { "index": 1, "kind": "parameter", "displayName":
"Operation", "group": "producer", "label": "", "required": false, "type":
"enum", "javaType":
"org.apache.camel.component.statestore.StateStoreOperations", "enum": [ "put",
"putIfAbsent", "get", "delete", "contains", "keys", "size", "clear" ],
"deprecated": false, "autowired": false, "secret": false, "description": "The
default operation to perform" },
- "ttl": { "index": 2, "kind": "parameter", "displayName": "Ttl", "group":
"producer", "label": "", "required": false, "type": "integer", "javaType":
"long", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": 0, "description": "Time-to-live in milliseconds for entries. 0
means no expiry." },
- "lazyStartProducer": { "index": 3, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
- "backend": { "index": 4, "kind": "parameter", "displayName": "Backend",
"group": "advanced", "label": "advanced", "required": false, "type": "object",
"javaType": "org.apache.camel.spi.KeyValueRepository", "deprecated": false,
"autowired": false, "secret": false, "description": "The backend to use. If not
set, auto-discovers a single KeyValueRepository from the registry, or falls
back to an in-memory store." }
+ "namespaced": { "index": 1, "kind": "parameter", "displayName":
"Namespaced", "group": "producer", "label": "", "required": false, "type":
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": true, "description": "Whether to use a
store-specific namespace prefix to isolate keys on a shared backend
repository." },
+ "operation": { "index": 2, "kind": "parameter", "displayName":
"Operation", "group": "producer", "label": "", "required": false, "type":
"enum", "javaType":
"org.apache.camel.component.statestore.StateStoreOperations", "enum": [ "put",
"putIfAbsent", "get", "delete", "contains", "keys", "size", "clear" ],
"deprecated": false, "autowired": false, "secret": false, "description": "The
default operation to perform" },
+ "ttl": { "index": 3, "kind": "parameter", "displayName": "Ttl", "group":
"producer", "label": "", "required": false, "type": "integer", "javaType":
"long", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": 0, "description": "Time-to-live in milliseconds for entries. 0
means no expiry." },
+ "lazyStartProducer": { "index": 4, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
+ "backend": { "index": 5, "kind": "parameter", "displayName": "Backend",
"group": "advanced", "label": "advanced", "required": false, "type": "object",
"javaType": "org.apache.camel.spi.KeyValueRepository", "deprecated": false,
"autowired": false, "secret": false, "description": "The backend to use. If not
set, auto-discovers a single KeyValueRepository from the registry, or falls
back to an in-memory store." }
}
}
diff --git
a/components/camel-state-store/camel-state-store/src/main/docs/state-store-component.adoc
b/components/camel-state-store/camel-state-store/src/main/docs/state-store-component.adoc
index 5cdd3f1b04d6..8bddc34dbb7c 100644
---
a/components/camel-state-store/camel-state-store/src/main/docs/state-store-component.adoc
+++
b/components/camel-state-store/camel-state-store/src/main/docs/state-store-component.adoc
@@ -206,3 +206,19 @@ for the State Store component, the Idempotent Consumer,
the Aggregator, and the
See the KeyValueRepository section in the User Manual for the full list of
available
backends, configuration examples, and instructions for writing a custom
backend.
+
+== Namespace Isolation
+
+When using a shared `KeyValueRepository` (such as a single bean
auto-discovered from the registry or referenced via `backend=#shared`),
`state-store` automatically isolates entries per store name under the prefix
`statestore:<storeName>:`.
+
+This ensures that:
+
+* Multiple state stores sharing the same backend do not collide on keys.
+* Calling `clear()`, `keys()`, or `size()` on one store is strictly scoped to
that store's entries.
+* State store operations cannot interfere with entries from other patterns
sharing the repository.
+
+To opt out of namespacing (for example, to read existing un-namespaced keys
written in Camel 4.23 without key migration), set `namespaced=false` on the
endpoint or component:
+
+----
+state-store:orders?namespaced=false
+----
diff --git
a/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreComponent.java
b/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreComponent.java
index 7f466e69f19b..c5ef6ec2561a 100644
---
a/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreComponent.java
+++
b/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreComponent.java
@@ -27,6 +27,7 @@ import org.apache.camel.spi.Metadata;
import org.apache.camel.spi.annotations.Component;
import org.apache.camel.support.DefaultComponent;
import org.apache.camel.support.MemoryKeyValueRepository;
+import org.apache.camel.support.NamespacedKeyValueRepository;
import org.apache.camel.support.service.ServiceHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -49,33 +50,56 @@ public class StateStoreComponent extends DefaultComponent {
+ " registry auto-discovery for endpoints without
an explicit backend.")
private StateStoreBackendFactory backendFactory;
+ @Metadata(description = "Whether to use a store-specific namespace prefix
to isolate keys on a shared backend repository.",
+ defaultValue = "true")
+ private boolean namespaced = true;
+
@Override
protected Endpoint createEndpoint(String uri, String remaining,
Map<String, Object> parameters) throws Exception {
StateStoreEndpoint endpoint = new StateStoreEndpoint(uri, this);
endpoint.setStoreName(remaining);
+ endpoint.setNamespaced(isNamespaced());
setProperties(endpoint, parameters);
return endpoint;
}
+ private static String toNamespace(String storeName) {
+ return "statestore:" + storeName.replace("%", "%25").replace(":",
"%3A");
+ }
+
/**
* Returns the backend for the given store name, creating one if needed.
Uses first-one-wins semantics: if a backend
* already exists for this store name, subsequent calls return the
existing one regardless of the explicit backend
* passed.
*/
KeyValueRepository getOrCreateBackend(String storeName, KeyValueRepository
explicitBackend) {
+ return getOrCreateBackend(storeName, explicitBackend, isNamespaced());
+ }
+
+ KeyValueRepository getOrCreateBackend(String storeName, KeyValueRepository
explicitBackend, boolean namespaced) {
if (explicitBackend != null) {
- KeyValueRepository existing = backends.putIfAbsent(storeName,
explicitBackend);
+ KeyValueRepository target = namespaced
+ ? NamespacedKeyValueRepository.namespaced(explicitBackend,
toNamespace(storeName))
+ : explicitBackend;
+ KeyValueRepository existing = backends.putIfAbsent(storeName,
target);
if (existing != null) {
- if (existing != explicitBackend) {
+ KeyValueRepository existingDelegate
+ = existing instanceof NamespacedKeyValueRepository n ?
n.getDelegate() : existing;
+ if (existingDelegate != explicitBackend) {
LOG.warn("Store '{}' already has a backend ({}). Ignoring
explicit backend=# reference ({}).",
- storeName, existing.getClass().getSimpleName(),
explicitBackend.getClass().getSimpleName());
+ storeName,
existingDelegate.getClass().getSimpleName(),
explicitBackend.getClass().getSimpleName());
+ }
+ boolean existingNamespaced = existing instanceof
NamespacedKeyValueRepository;
+ if (existingNamespaced != namespaced) {
+ LOG.warn("Store '{}' was already initialized with
namespaced={}. Ignoring namespaced={} for this endpoint.",
+ storeName, existingNamespaced, namespaced);
}
return existing;
}
- ServiceHelper.startService(explicitBackend);
- return explicitBackend;
+ ServiceHelper.startService(target);
+ return target;
}
- return backends.computeIfAbsent(storeName, k -> {
+ KeyValueRepository result = backends.computeIfAbsent(storeName, k -> {
if (backendFactory == null &&
backendFactoryChecked.compareAndSet(false, true)) {
// checked lazily as autowiring happens after the component is
initialized
int count =
getCamelContext().getRegistry().findByType(StateStoreBackendFactory.class).size();
@@ -98,6 +122,11 @@ public class StateStoreComponent extends DefaultComponent {
backend = found.iterator().next();
LOG.debug("Auto-discovered KeyValueRepository '{}' for store
'{}'",
backend.getClass().getSimpleName(), storeName);
+ KeyValueRepository target = namespaced
+ ? NamespacedKeyValueRepository.namespaced(backend,
toNamespace(storeName))
+ : backend;
+ ServiceHelper.startService(target);
+ return target;
} else {
if (found.size() > 1) {
LOG.warn(
@@ -110,10 +139,24 @@ public class StateStoreComponent extends DefaultComponent
{
storeName);
}
backend = new MemoryKeyValueRepository();
+ ServiceHelper.startService(backend);
+ return backend;
}
- ServiceHelper.startService(backend);
- return backend;
});
+ boolean existingNamespaced = result instanceof
NamespacedKeyValueRepository;
+ if (existingNamespaced != namespaced && backendFactory == null &&
!(result instanceof MemoryKeyValueRepository)) {
+ LOG.warn("Store '{}' was already initialized with namespaced={}.
Ignoring namespaced={} for this endpoint.",
+ storeName, existingNamespaced, namespaced);
+ }
+ return result;
+ }
+
+ public boolean isNamespaced() {
+ return namespaced;
+ }
+
+ public void setNamespaced(boolean namespaced) {
+ this.namespaced = namespaced;
}
public StateStoreBackendFactory getBackendFactory() {
diff --git
a/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreEndpoint.java
b/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreEndpoint.java
index afedd7cc04c9..6e4996087848 100644
---
a/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreEndpoint.java
+++
b/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreEndpoint.java
@@ -76,6 +76,10 @@ public class StateStoreEndpoint extends DefaultEndpoint {
@UriParam(description = "Time-to-live in milliseconds for entries. 0 means
no expiry.", defaultValue = "0")
private long ttl;
+ @UriParam(description = "Whether to use a store-specific namespace prefix
to isolate keys on a shared backend repository.",
+ defaultValue = "true")
+ private boolean namespaced = true;
+
public StateStoreEndpoint(String endpointUri, StateStoreComponent
component) {
super(endpointUri, component);
}
@@ -94,7 +98,7 @@ public class StateStoreEndpoint extends DefaultEndpoint {
protected void doStart() throws Exception {
super.doStart();
StateStoreComponent comp = (StateStoreComponent) getComponent();
- backend = comp.getOrCreateBackend(storeName, backend);
+ backend = comp.getOrCreateBackend(storeName, backend, namespaced);
}
public String getStoreName() {
@@ -128,4 +132,12 @@ public class StateStoreEndpoint extends DefaultEndpoint {
public void setTtl(long ttl) {
this.ttl = ttl;
}
+
+ public boolean isNamespaced() {
+ return namespaced;
+ }
+
+ public void setNamespaced(boolean namespaced) {
+ this.namespaced = namespaced;
+ }
}
diff --git
a/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreAutoDiscoveryTest.java
b/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreAutoDiscoveryTest.java
index 8f1cfba27b4b..bb54888f293d 100644
---
a/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreAutoDiscoveryTest.java
+++
b/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreAutoDiscoveryTest.java
@@ -52,9 +52,10 @@ class StateStoreAutoDiscoveryTest extends CamelTestSupport {
Map.of(StateStoreConstants.KEY, "key1"));
assertThat(result).isEqualTo("hello");
- // Verify our tracking backend was used, not the default
MemoryKeyValueRepository
+ // Verify our tracking backend was used, not the default
MemoryKeyValueRepository,
+ // and keys are namespaced to isolate this store from other stores and
patterns
assertThat(trackingBackend).isInstanceOf(TrackingKeyValueRepository.class);
- assertThat(trackingBackend.keys()).containsExactly("key1");
+
assertThat(trackingBackend.keys()).containsExactly("statestore:myStore:key1");
}
@Override
diff --git
a/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreBackendFactoryTest.java
b/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreBackendFactoryTest.java
index c35b4cc723b1..d3002a48ed76 100644
---
a/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreBackendFactoryTest.java
+++
b/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreBackendFactoryTest.java
@@ -63,6 +63,8 @@ class StateStoreBackendFactoryTest extends CamelTestSupport {
assertThat(created).containsOnlyKeys("orders", "users");
assertThat(created.get("orders").isStarted()).isTrue();
+ assertThat(created.get("orders").get("1")).isEqualTo("order-1");
+ assertThat(created.get("users").get("1")).isEqualTo("user-1");
assertThat(registryBackend.keys()).isEmpty();
}
@@ -71,7 +73,7 @@ class StateStoreBackendFactoryTest extends CamelTestSupport {
template.requestBodyAndHeader("state-store:fallback?operation=put",
"v", StateStoreConstants.KEY, "k");
assertThat(created).doesNotContainKey("fallback");
- assertThat(registryBackend.get("k")).isEqualTo("v");
+
assertThat(registryBackend.get("statestore:fallback:k")).isEqualTo("v");
}
@Test
@@ -79,7 +81,7 @@ class StateStoreBackendFactoryTest extends CamelTestSupport {
template.requestBodyAndHeader("direct:explicit", "v",
StateStoreConstants.KEY, "k");
assertThat(created).doesNotContainKey("explicit");
- assertThat(explicitBackend.get("k")).isEqualTo("v");
+
assertThat(explicitBackend.get("statestore:explicit:k")).isEqualTo("v");
}
@Override
diff --git
a/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreNamespaceIsolationTest.java
b/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreNamespaceIsolationTest.java
new file mode 100644
index 000000000000..b83e8a9be605
--- /dev/null
+++
b/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreNamespaceIsolationTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.statestore;
+
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.camel.BindToRegistry;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.KeyValueRepository;
+import org.apache.camel.support.MemoryKeyValueRepository;
+import org.apache.camel.test.junit5.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests that multiple state stores sharing the same underlying {@link
KeyValueRepository} keep their keys and
+ * operations isolated within their respective namespaces.
+ */
+class StateStoreNamespaceIsolationTest extends CamelTestSupport {
+
+ private final MemoryKeyValueRepository sharedBackend = new
MemoryKeyValueRepository();
+
+ @BindToRegistry("sharedBackend")
+ public KeyValueRepository getSharedBackend() {
+ return sharedBackend;
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ void testCrossStoreIsolationAndClear() {
+ // Store entries in storeA
+ template.requestBodyAndHeaders("direct:putA", "valA1",
Map.of(StateStoreConstants.KEY, "item1"));
+ template.requestBodyAndHeaders("direct:putA", "valA2",
Map.of(StateStoreConstants.KEY, "item2"));
+
+ // Store entry in storeB with identical key name
+ template.requestBodyAndHeaders("direct:putB", "valB1",
Map.of(StateStoreConstants.KEY, "item1"));
+
+ // Pre-existing external key in shared backend (e.g. from an
aggregation repository)
+ sharedBackend.put("aggregate:grp1", "holderData");
+
+ // Verify isolation
+ assertThat(template.requestBodyAndHeaders("direct:getA", null,
Map.of(StateStoreConstants.KEY, "item1")))
+ .isEqualTo("valA1");
+ assertThat(template.requestBodyAndHeaders("direct:getB", null,
Map.of(StateStoreConstants.KEY, "item1")))
+ .isEqualTo("valB1");
+
+ // Verify keys in storeA
+ Set<String> keysA = (Set<String>) template.requestBody("direct:keysA",
(Object) null);
+ assertThat(keysA).containsExactlyInAnyOrder("item1", "item2");
+
+ // Clear storeA
+ template.requestBody("direct:clearA", (Object) null);
+
+ // storeA is now empty
+ assertThat((Set<String>) template.requestBody("direct:keysA", (Object)
null)).isEmpty();
+ assertThat(template.requestBodyAndHeaders("direct:getA", null,
Map.of(StateStoreConstants.KEY, "item1")))
+ .isNull();
+
+ // storeB and shared backend entries remain intact
+ assertThat(template.requestBodyAndHeaders("direct:getB", null,
Map.of(StateStoreConstants.KEY, "item1")))
+ .isEqualTo("valB1");
+
assertThat(sharedBackend.get("aggregate:grp1")).isEqualTo("holderData");
+ }
+
+ @Test
+ void testPatternKeyNamesDoNotCollide() {
+ // Store a key with a prefix that resembles aggregation or idempotent
pattern prefixes
+ template.requestBodyAndHeaders("direct:putA", "customPayload",
Map.of(StateStoreConstants.KEY, "aggregate:grp1"));
+
+ // Ensure key in underlying repository is safely prefixed under the
state store namespace
+
assertThat(sharedBackend.get("statestore:storeA:aggregate:grp1")).isEqualTo("customPayload");
+
+ // Underlying raw aggregate:grp1 is not overwritten
+ assertThat(sharedBackend.get("aggregate:grp1")).isNull();
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ void testStoreNamesSharingAPrefixAreIsolated() {
+ template.requestBodyAndHeaders("direct:putDynamic", "archived",
+ Map.of("reproStore", "orders:archive",
StateStoreConstants.KEY, "x"));
+ template.requestBodyAndHeaders("direct:putDynamic", "live",
+ Map.of("reproStore", "orders", StateStoreConstants.KEY, "y"));
+
+ Set<String> keys = (Set<String>)
template.requestBodyAndHeaders("direct:keysDynamic", null,
+ Map.of("reproStore", "orders"));
+ assertThat(keys).containsExactly("y");
+
+ // Clear on 'orders' must not wipe 'orders:archive'
+ template.requestBodyAndHeaders("direct:clearDynamic", null,
+ Map.of("reproStore", "orders"));
+
+ assertThat((Set<String>)
template.requestBodyAndHeaders("direct:keysDynamic", null,
+ Map.of("reproStore", "orders"))).isEmpty();
+ assertThat(template.requestBodyAndHeaders("direct:getDynamic", null,
+ Map.of("reproStore", "orders:archive",
StateStoreConstants.KEY, "x")))
+ .isEqualTo("archived");
+ }
+
+ @Test
+ void testOptOutNamespacingForBackwardCompatibility() {
+ // Data written by 4.23 without namespacing can be accessed by opting
out of namespacing
+ sharedBackend.put("legacyKey", "legacyValue");
+
+ Object value =
template.requestBodyAndHeaders("direct:getUnnamespaced", null,
+ Map.of(StateStoreConstants.KEY, "legacyKey"));
+ assertThat(value).isEqualTo("legacyValue");
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:putA").to("state-store:storeA?operation=put");
+ from("direct:getA").to("state-store:storeA?operation=get");
+ from("direct:keysA").to("state-store:storeA?operation=keys");
+ from("direct:clearA").to("state-store:storeA?operation=clear");
+
+ from("direct:putB").to("state-store:storeB?operation=put");
+ from("direct:getB").to("state-store:storeB?operation=get");
+
+
from("direct:putDynamic").toD("state-store:${header.reproStore}?operation=put");
+
from("direct:getDynamic").toD("state-store:${header.reproStore}?operation=get");
+
from("direct:keysDynamic").toD("state-store:${header.reproStore}?operation=keys");
+
from("direct:clearDynamic").toD("state-store:${header.reproStore}?operation=clear");
+
+
from("direct:getUnnamespaced").to("state-store:legacyStore?namespaced=false&operation=get");
+ }
+ };
+ }
+}
diff --git
a/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreStreamBodyTest.java
b/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreStreamBodyTest.java
index 69e1820fc302..2c591322c53e 100644
---
a/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreStreamBodyTest.java
+++
b/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreStreamBodyTest.java
@@ -85,7 +85,7 @@ class StateStoreStreamBodyTest extends CamelTestSupport {
// again, with the stream cache reset
put("direct:putRedelivery", new ByteArrayInputStream(BINARY),
"redelivered");
- Object value = redeliveryBackend.get("redelivered");
+ Object value =
redeliveryBackend.get("statestore:redeliveryStore:redelivered");
assertThat(value).isInstanceOf(byte[].class);
assertThat((byte[]) value).containsExactly(BINARY);
}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/processor/CacheProcessorTest.java
b/core/camel-core/src/test/java/org/apache/camel/processor/CacheProcessorTest.java
index eb3e385e3881..35cfec932e68 100644
---
a/core/camel-core/src/test/java/org/apache/camel/processor/CacheProcessorTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/processor/CacheProcessorTest.java
@@ -16,11 +16,14 @@
*/
package org.apache.camel.processor;
+import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.apache.camel.ContextTestSupport;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.spi.KeyValueRepository;
+import org.apache.camel.support.MemoryKeyValueRepository;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@@ -173,6 +176,43 @@ class CacheProcessorTest extends ContextTestSupport {
MockEndpoint.assertIsSatisfied(context);
}
+ @Test
+ void testDocumentedInvalidationRecipeDropsCachedEntry() throws Exception {
+ KeyValueRepository repo = new MemoryKeyValueRepository();
+ context.getRegistry().bind("productCache", repo);
+
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:getProduct")
+
.cache(simple("${header.productId}")).keyValueRepository("productCache")
+ .process(e -> e.getMessage().setBody("v" +
e.getIn().getHeader("version", Integer.class)))
+ .end();
+
+ from("direct:invalidate")
+ .process(exchange -> {
+ String productId =
exchange.getIn().getHeader("productId", String.class);
+ KeyValueRepository cache =
exchange.getContext().getRegistry()
+ .lookupByNameAndType("productCache",
KeyValueRepository.class);
+ cache.delete(productId);
+ });
+ }
+ });
+
+ assertThat(template.requestBodyAndHeaders("direct:getProduct", null,
+ Map.of("productId", "42", "version", 1))).isEqualTo("v1");
+ // cached — version 2 ignored
+ assertThat(template.requestBodyAndHeaders("direct:getProduct", null,
+ Map.of("productId", "42", "version", 2))).isEqualTo("v1");
+
+ // invalidate via recipe from docs
+ template.sendBodyAndHeader("direct:invalidate", null, "productId",
"42");
+
+ // cache miss — reloads with version 2
+ assertThat(template.requestBodyAndHeaders("direct:getProduct", null,
+ Map.of("productId", "42", "version", 2))).isEqualTo("v2");
+ }
+
@Override
protected RouteBuilder createRouteBuilder() {
return new RouteBuilder() {
diff --git
a/core/camel-core/src/test/java/org/apache/camel/support/KeyValueAggregationRepositoryTest.java
b/core/camel-core/src/test/java/org/apache/camel/support/KeyValueAggregationRepositoryTest.java
index 63157cfb31f3..8e3aea6fd455 100644
---
a/core/camel-core/src/test/java/org/apache/camel/support/KeyValueAggregationRepositoryTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/support/KeyValueAggregationRepositoryTest.java
@@ -251,6 +251,22 @@ class KeyValueAggregationRepositoryTest {
assertThat(invoices.scan(camelContext)).containsExactly(invoice.getExchangeId());
}
+ @Test
+ void testUnexpectedPayloadHandledGracefully() {
+ kvRepository.put("aggregate:corruptKey", "plainStringPayload");
+ kvRepository.put("completed:corruptExchangeId", "plainStringPayload");
+
+ Exchange retrieved = aggregationRepository.get(camelContext,
"corruptKey");
+ assertThat(retrieved).isNull();
+
+ Exchange newEx = createExchange("recovered");
+ Exchange oldEx = aggregationRepository.add(camelContext, "corruptKey",
newEx);
+ assertThat(oldEx).isNull();
+
+ Exchange recovered = aggregationRepository.recover(camelContext,
"corruptExchangeId");
+ assertThat(recovered).isNull();
+ }
+
private Exchange createExchange(String body) {
Exchange exchange = new DefaultExchange(camelContext);
exchange.getIn().setBody(body);
diff --git
a/core/camel-support/src/generated/java/org/apache/camel/support/NamespacedKeyValueRepositoryConfigurer.java
b/core/camel-support/src/generated/java/org/apache/camel/support/NamespacedKeyValueRepositoryConfigurer.java
new file mode 100644
index 000000000000..3d440a1e920c
--- /dev/null
+++
b/core/camel-support/src/generated/java/org/apache/camel/support/NamespacedKeyValueRepositoryConfigurer.java
@@ -0,0 +1,49 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.support;
+
+import javax.annotation.processing.Generated;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.ExtendedPropertyConfigurerGetter;
+import org.apache.camel.spi.PropertyConfigurerGetter;
+import org.apache.camel.spi.ConfigurerStrategy;
+import org.apache.camel.spi.GeneratedPropertyConfigurer;
+import org.apache.camel.util.CaseInsensitiveMap;
+import org.apache.camel.support.NamespacedKeyValueRepository;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo")
+@SuppressWarnings("unchecked")
+public class NamespacedKeyValueRepositoryConfigurer extends
org.apache.camel.support.component.PropertyConfigurerSupport implements
GeneratedPropertyConfigurer, PropertyConfigurerGetter {
+
+ @Override
+ public boolean configure(CamelContext camelContext, Object obj, String
name, Object value, boolean ignoreCase) {
+ org.apache.camel.support.NamespacedKeyValueRepository target =
(org.apache.camel.support.NamespacedKeyValueRepository) obj;
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "namespace": target.setNamespace(property(camelContext,
java.lang.String.class, value)); return true;
+ default: return false;
+ }
+ }
+
+ @Override
+ public Class<?> getOptionType(String name, boolean ignoreCase) {
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "namespace": return java.lang.String.class;
+ default: return null;
+ }
+ }
+
+ @Override
+ public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
+ org.apache.camel.support.NamespacedKeyValueRepository target =
(org.apache.camel.support.NamespacedKeyValueRepository) obj;
+ switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) {
+ case "namespace": return target.getNamespace();
+ default: return null;
+ }
+ }
+}
+
diff --git
a/core/camel-support/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
b/core/camel-support/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
index 0234c0b2afeb..e00906d89041 100644
---
a/core/camel-support/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
+++
b/core/camel-support/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
@@ -1,5 +1,5 @@
# Generated by camel build tools - do NOT edit this file!
-bean=AcceptAllHeaderFilterStrategy DefaultHeaderFilterStrategy
FileIdempotentRepository KeyValueAggregationRepository
KeyValueIdempotentRepository MemoryIdempotentRepository
MemoryKeyValueRepository ThrottlingExceptionRoutePolicy
ThrottlingInflightRoutePolicy
+bean=AcceptAllHeaderFilterStrategy DefaultHeaderFilterStrategy
FileIdempotentRepository KeyValueAggregationRepository
KeyValueIdempotentRepository MemoryIdempotentRepository
MemoryKeyValueRepository NamespacedKeyValueRepository
ThrottlingExceptionRoutePolicy ThrottlingInflightRoutePolicy
groupId=org.apache.camel
artifactId=camel-support
version=4.24.0-SNAPSHOT
diff --git
a/core/camel-support/src/generated/resources/META-INF/services/org/apache/camel/bean/NamespacedKeyValueRepository.json
b/core/camel-support/src/generated/resources/META-INF/services/org/apache/camel/bean/NamespacedKeyValueRepository.json
new file mode 100644
index 000000000000..55274077a939
--- /dev/null
+++
b/core/camel-support/src/generated/resources/META-INF/services/org/apache/camel/bean/NamespacedKeyValueRepository.json
@@ -0,0 +1,15 @@
+{
+ "bean": {
+ "kind": "bean",
+ "name": "NamespacedKeyValueRepository",
+ "javaType": "org.apache.camel.support.NamespacedKeyValueRepository",
+ "interfaceType": "org.apache.camel.spi.KeyValueRepository",
+ "title": "Namespaced Key Value Repository",
+ "description": "A KeyValueRepository that partitions keys using a
namespace prefix.",
+ "deprecated": false,
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-support",
+ "version": "4.24.0-SNAPSHOT"
+ }
+}
+
diff --git
a/core/camel-support/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.support.NamespacedKeyValueRepository
b/core/camel-support/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.support.NamespacedKeyValueRepository
new file mode 100644
index 000000000000..3250ddb766d4
--- /dev/null
+++
b/core/camel-support/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.support.NamespacedKeyValueRepository
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.support.NamespacedKeyValueRepositoryConfigurer
diff --git
a/core/camel-support/src/main/java/org/apache/camel/support/KeyValueAggregationRepository.java
b/core/camel-support/src/main/java/org/apache/camel/support/KeyValueAggregationRepository.java
index 393492249da2..ea63ab6cb881 100644
---
a/core/camel-support/src/main/java/org/apache/camel/support/KeyValueAggregationRepository.java
+++
b/core/camel-support/src/main/java/org/apache/camel/support/KeyValueAggregationRepository.java
@@ -135,20 +135,20 @@ public class KeyValueAggregationRepository extends
ServiceSupport
public Exchange add(CamelContext camelContext, String key, Exchange
exchange) {
LOG.trace("Adding an Exchange with ID {} for key {}",
exchange.getExchangeId(), key);
DefaultExchangeHolder newHolder =
DefaultExchangeHolder.marshal(exchange, true, allowSerializedHeaders);
- DefaultExchangeHolder oldHolder = (DefaultExchangeHolder)
repository.put(AGGREGATE_PREFIX + key, newHolder);
+ Object oldHolder = repository.put(AGGREGATE_PREFIX + key, newHolder);
return unmarshallExchange(camelContext, oldHolder);
}
@Override
public Exchange get(CamelContext camelContext, String key) {
- return unmarshallExchange(camelContext, (DefaultExchangeHolder)
repository.get(AGGREGATE_PREFIX + key));
+ return unmarshallExchange(camelContext,
repository.get(AGGREGATE_PREFIX + key));
}
@Override
public void remove(CamelContext camelContext, String key, Exchange
exchange) {
LOG.trace("Removing an Exchange with ID {} for key {}",
exchange.getExchangeId(), key);
- DefaultExchangeHolder holder = (DefaultExchangeHolder)
repository.delete(AGGREGATE_PREFIX + key);
- if (useRecovery && holder != null) {
+ Object holder = repository.delete(AGGREGATE_PREFIX + key);
+ if (useRecovery && holder instanceof DefaultExchangeHolder) {
// Store the given exchange under the exchangeId for potential
recovery. The removed holder is not used as
// it can be older than the exchange: when a group is completed by
an incoming exchange, the last
// aggregated exchange is not added to the repository before it is
removed.
@@ -190,7 +190,7 @@ public class KeyValueAggregationRepository extends
ServiceSupport
if (!useRecovery) {
return null;
}
- return unmarshallExchange(camelContext, (DefaultExchangeHolder)
repository.get(completedPrefix + exchangeId));
+ return unmarshallExchange(camelContext, repository.get(completedPrefix
+ exchangeId));
}
@Override
@@ -285,12 +285,17 @@ public class KeyValueAggregationRepository extends
ServiceSupport
ServiceHelper.stopService(repository);
}
- private Exchange unmarshallExchange(CamelContext camelContext,
DefaultExchangeHolder holder) {
+ private Exchange unmarshallExchange(CamelContext camelContext, Object
holder) {
if (holder == null) {
return null;
}
+ if (!(holder instanceof DefaultExchangeHolder exchangeHolder)) {
+ LOG.warn("Ignoring entry of type {} in
KeyValueAggregationRepository as it is not a DefaultExchangeHolder",
+ holder.getClass().getName());
+ return null;
+ }
Exchange exchange = new DefaultExchange(camelContext);
- DefaultExchangeHolder.unmarshal(exchange, holder);
+ DefaultExchangeHolder.unmarshal(exchange, exchangeHolder);
return exchange;
}
}
diff --git
a/core/camel-support/src/main/java/org/apache/camel/support/NamespacedKeyValueRepository.java
b/core/camel-support/src/main/java/org/apache/camel/support/NamespacedKeyValueRepository.java
new file mode 100644
index 000000000000..f759360c10d5
--- /dev/null
+++
b/core/camel-support/src/main/java/org/apache/camel/support/NamespacedKeyValueRepository.java
@@ -0,0 +1,271 @@
+/*
+ * 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.support;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.camel.spi.Configurer;
+import org.apache.camel.spi.KeyValueRepository;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.support.service.ServiceHelper;
+import org.apache.camel.support.service.ServiceSupport;
+import org.apache.camel.util.ObjectHelper;
+
+/**
+ * A {@link KeyValueRepository} wrapper that isolates entries by prepending a
namespace prefix to all keys.
+ * <p/>
+ * This adapter ensures that multiple components, patterns (e.g. Cache EIP,
State Store, Aggregator, Idempotent
+ * Consumer), or named stores sharing a single underlying {@link
KeyValueRepository} operate in isolated key partitions
+ * without key collisions or unintended cross-namespace mutations during
operations such as {@link #clear()} or
+ * {@link #keys()}.
+ *
+ * @since 4.24
+ */
+@Metadata(label = "bean",
+ description = "A KeyValueRepository that partitions keys using a
namespace prefix.",
+ annotations = {
"interfaceName=org.apache.camel.spi.KeyValueRepository" })
+@Configurer(metadataOnly = true)
+public class NamespacedKeyValueRepository extends ServiceSupport implements
KeyValueRepository {
+
+ private KeyValueRepository delegate;
+ private String prefix;
+
+ /**
+ * Creates an uninitialized namespaced repository adapter. The delegate
repository and namespace should be set
+ * before starting.
+ */
+ public NamespacedKeyValueRepository() {
+ }
+
+ /**
+ * Creates a new namespaced wrapper around the given repository.
+ *
+ * @param delegate the underlying key-value repository; must not be
{@code null}
+ * @param namespace the namespace or prefix to isolate keys; must not be
{@code null}
+ */
+ public NamespacedKeyValueRepository(KeyValueRepository delegate, String
namespace) {
+ this.delegate = ObjectHelper.notNull(delegate, "delegate");
+ setNamespace(namespace);
+ }
+
+ /**
+ * Creates a new {@link NamespacedKeyValueRepository} wrapping the given
repository, or returns the repository if it
+ * is already wrapped with the identical prefix.
+ *
+ * @param repository the key-value repository to wrap
+ * @param namespace the namespace prefix to apply
+ * @return the namespaced repository wrapper
+ */
+ public static KeyValueRepository namespaced(KeyValueRepository repository,
String namespace) {
+ if (repository == null) {
+ return null;
+ }
+ ObjectHelper.notNull(namespace, "namespace");
+ String p = namespace.endsWith(":") ? namespace : namespace + ":";
+ if (repository instanceof NamespacedKeyValueRepository nkvr) {
+ if (nkvr.getNamespace().equals(p)) {
+ return repository;
+ }
+ }
+ return new NamespacedKeyValueRepository(repository, p);
+ }
+
+ /**
+ * Returns the underlying delegated {@link KeyValueRepository}.
+ */
+ public KeyValueRepository getDelegate() {
+ return delegate;
+ }
+
+ /**
+ * Sets the underlying delegated {@link KeyValueRepository}.
+ */
+ public void setDelegate(KeyValueRepository delegate) {
+ this.delegate = delegate;
+ }
+
+ /**
+ * Returns the namespace applied to all keys.
+ */
+ public String getNamespace() {
+ return prefix;
+ }
+
+ /**
+ * Sets the namespace prefix to isolate keys. If the namespace does not
end with ':', a trailing colon is added.
+ */
+ public void setNamespace(String namespace) {
+ ObjectHelper.notNull(namespace, "namespace");
+ this.prefix = namespace.endsWith(":") ? namespace : namespace + ":";
+ }
+
+ @Override
+ public Object get(String key) {
+ ObjectHelper.notNull(delegate, "delegate");
+ ObjectHelper.notNull(prefix, "prefix");
+ ObjectHelper.notNull(key, "key");
+ return delegate.get(prefix + key);
+ }
+
+ @Override
+ public Object put(String key, Object value) {
+ ObjectHelper.notNull(delegate, "delegate");
+ ObjectHelper.notNull(prefix, "prefix");
+ ObjectHelper.notNull(key, "key");
+ return delegate.put(prefix + key, value);
+ }
+
+ @Override
+ public Object put(String key, Object value, Duration ttl) {
+ ObjectHelper.notNull(delegate, "delegate");
+ ObjectHelper.notNull(prefix, "prefix");
+ ObjectHelper.notNull(key, "key");
+ return delegate.put(prefix + key, value, ttl);
+ }
+
+ @Override
+ public Object delete(String key) {
+ ObjectHelper.notNull(delegate, "delegate");
+ ObjectHelper.notNull(prefix, "prefix");
+ ObjectHelper.notNull(key, "key");
+ return delegate.delete(prefix + key);
+ }
+
+ @Override
+ public boolean contains(String key) {
+ ObjectHelper.notNull(delegate, "delegate");
+ ObjectHelper.notNull(prefix, "prefix");
+ ObjectHelper.notNull(key, "key");
+ return delegate.contains(prefix + key);
+ }
+
+ @Override
+ public Set<String> keys() {
+ ObjectHelper.notNull(delegate, "delegate");
+ ObjectHelper.notNull(prefix, "prefix");
+ Set<String> allKeys = delegate.keys();
+ if (allKeys == null || allKeys.isEmpty()) {
+ return Collections.emptySet();
+ }
+ Set<String> filtered = new LinkedHashSet<>();
+ for (String k : allKeys) {
+ if (k != null && k.startsWith(prefix)) {
+ filtered.add(k.substring(prefix.length()));
+ }
+ }
+ return Collections.unmodifiableSet(filtered);
+ }
+
+ /**
+ * Clears all entries within this repository's namespace by deleting
matching keys individually from the underlying
+ * delegate repository. Note that on shared backends this operation is
non-atomic and executes in O(n) relative to
+ * the total number of keys in the delegate.
+ */
+ @Override
+ public void clear() {
+ ObjectHelper.notNull(delegate, "delegate");
+ ObjectHelper.notNull(prefix, "prefix");
+ Set<String> allKeys = delegate.keys();
+ if (allKeys != null && !allKeys.isEmpty()) {
+ List<String> toDelete = new ArrayList<>();
+ for (String k : allKeys) {
+ if (k != null && k.startsWith(prefix)) {
+ toDelete.add(k);
+ }
+ }
+ for (String k : toDelete) {
+ delegate.delete(k);
+ }
+ }
+ }
+
+ @Override
+ public Object putIfAbsent(String key, Object value) {
+ ObjectHelper.notNull(delegate, "delegate");
+ ObjectHelper.notNull(prefix, "prefix");
+ ObjectHelper.notNull(key, "key");
+ return delegate.putIfAbsent(prefix + key, value);
+ }
+
+ @Override
+ public Object putIfAbsent(String key, Object value, Duration ttl) {
+ ObjectHelper.notNull(delegate, "delegate");
+ ObjectHelper.notNull(prefix, "prefix");
+ ObjectHelper.notNull(key, "key");
+ return delegate.putIfAbsent(prefix + key, value, ttl);
+ }
+
+ @Override
+ public boolean replace(String key, Object expectedOldValue, Object
newValue, Duration ttl) {
+ ObjectHelper.notNull(delegate, "delegate");
+ ObjectHelper.notNull(prefix, "prefix");
+ ObjectHelper.notNull(key, "key");
+ return delegate.replace(prefix + key, expectedOldValue, newValue, ttl);
+ }
+
+ @Override
+ public boolean delete(String key, Object expectedValue) {
+ ObjectHelper.notNull(delegate, "delegate");
+ ObjectHelper.notNull(prefix, "prefix");
+ ObjectHelper.notNull(key, "key");
+ return delegate.delete(prefix + key, expectedValue);
+ }
+
+ @Override
+ public int size() {
+ ObjectHelper.notNull(delegate, "delegate");
+ ObjectHelper.notNull(prefix, "prefix");
+ Set<String> allKeys = delegate.keys();
+ if (allKeys == null || allKeys.isEmpty()) {
+ return 0;
+ }
+ int count = 0;
+ for (String k : allKeys) {
+ if (k != null && k.startsWith(prefix)) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ @Override
+ protected void doStart() throws Exception {
+ ObjectHelper.notNull(delegate, "delegate");
+ ObjectHelper.notNull(prefix, "prefix");
+ ServiceHelper.startService(delegate);
+ }
+
+ @Override
+ protected void doStop() throws Exception {
+ ServiceHelper.stopService(delegate);
+ }
+
+ @Override
+ protected void doShutdown() throws Exception {
+ ServiceHelper.stopAndShutdownService(delegate);
+ }
+
+ @Override
+ public String toString() {
+ return "NamespacedKeyValueRepository[" + prefix + " -> " + delegate +
"]";
+ }
+}
diff --git
a/core/camel-support/src/test/java/org/apache/camel/support/NamespacedKeyValueRepositoryTest.java
b/core/camel-support/src/test/java/org/apache/camel/support/NamespacedKeyValueRepositoryTest.java
new file mode 100644
index 000000000000..10d5ccd555a7
--- /dev/null
+++
b/core/camel-support/src/test/java/org/apache/camel/support/NamespacedKeyValueRepositoryTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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.support;
+
+import java.time.Duration;
+import java.util.Set;
+
+import org.apache.camel.spi.KeyValueRepository;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class NamespacedKeyValueRepositoryTest {
+
+ private MemoryKeyValueRepository delegate;
+ private NamespacedKeyValueRepository repoA;
+ private NamespacedKeyValueRepository repoB;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ delegate = new MemoryKeyValueRepository();
+ delegate.start();
+
+ repoA = new NamespacedKeyValueRepository(delegate, "nsA");
+ repoA.start();
+
+ repoB = new NamespacedKeyValueRepository(delegate, "nsB:");
+ repoB.start();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ repoA.stop();
+ repoB.stop();
+ delegate.stop();
+ }
+
+ @Test
+ void testPutAndGet() {
+ repoA.put("key1", "valueA");
+ repoB.put("key1", "valueB");
+
+ assertThat(repoA.get("key1")).isEqualTo("valueA");
+ assertThat(repoB.get("key1")).isEqualTo("valueB");
+
+ assertThat(delegate.get("nsA:key1")).isEqualTo("valueA");
+ assertThat(delegate.get("nsB:key1")).isEqualTo("valueB");
+ }
+
+ @Test
+ void testContains() {
+ repoA.put("key1", "valueA");
+
+ assertThat(repoA.contains("key1")).isTrue();
+ assertThat(repoB.contains("key1")).isFalse();
+ assertThat(repoA.contains("key2")).isFalse();
+ }
+
+ @Test
+ void testDelete() {
+ repoA.put("key1", "valueA");
+ repoB.put("key1", "valueB");
+
+ Object deleted = repoA.delete("key1");
+ assertThat(deleted).isEqualTo("valueA");
+
+ assertThat(repoA.get("key1")).isNull();
+ assertThat(repoB.get("key1")).isEqualTo("valueB");
+ assertThat(delegate.get("nsA:key1")).isNull();
+ assertThat(delegate.get("nsB:key1")).isEqualTo("valueB");
+ }
+
+ @Test
+ void testKeys() {
+ repoA.put("key1", "v1");
+ repoA.put("key2", "v2");
+ repoB.put("key3", "v3");
+
+ Set<String> keysA = repoA.keys();
+ assertThat(keysA).containsExactlyInAnyOrder("key1", "key2");
+
+ Set<String> keysB = repoB.keys();
+ assertThat(keysB).containsExactly("key3");
+ }
+
+ @Test
+ void testSize() {
+ repoA.put("key1", "v1");
+ repoA.put("key2", "v2");
+ repoB.put("key3", "v3");
+
+ assertThat(repoA.size()).isEqualTo(2);
+ assertThat(repoB.size()).isEqualTo(1);
+ }
+
+ @Test
+ void testClearOnlyClearsOwnNamespace() {
+ repoA.put("key1", "v1");
+ repoA.put("key2", "v2");
+ repoB.put("key1", "vB1");
+ repoB.put("key2", "vB2");
+
+ repoA.clear();
+
+ assertThat(repoA.keys()).isEmpty();
+ assertThat(repoA.size()).isZero();
+
+ assertThat(repoB.keys()).containsExactlyInAnyOrder("key1", "key2");
+ assertThat(repoB.size()).isEqualTo(2);
+ assertThat(repoB.get("key1")).isEqualTo("vB1");
+ }
+
+ @Test
+ void testPutIfAbsent() {
+ Object first = repoA.putIfAbsent("key1", "val1");
+ assertThat(first).isNull();
+
+ Object second = repoA.putIfAbsent("key1", "val2");
+ assertThat(second).isEqualTo("val1");
+ assertThat(repoA.get("key1")).isEqualTo("val1");
+
+ Object bResult = repoB.putIfAbsent("key1", "valB");
+ assertThat(bResult).isNull();
+ assertThat(repoB.get("key1")).isEqualTo("valB");
+ }
+
+ @Test
+ void testReplace() {
+ repoA.put("key1", "oldVal");
+
+ boolean failed = repoA.replace("key1", "wrong", "newVal",
Duration.ofMinutes(1));
+ assertThat(failed).isFalse();
+ assertThat(repoA.get("key1")).isEqualTo("oldVal");
+
+ boolean replaced = repoA.replace("key1", "oldVal", "newVal",
Duration.ofMinutes(1));
+ assertThat(replaced).isTrue();
+ assertThat(repoA.get("key1")).isEqualTo("newVal");
+ }
+
+ @Test
+ void testDeleteWithValue() {
+ repoA.put("key1", "val1");
+
+ boolean failed = repoA.delete("key1", "wrong");
+ assertThat(failed).isFalse();
+ assertThat(repoA.get("key1")).isEqualTo("val1");
+
+ boolean deleted = repoA.delete("key1", "val1");
+ assertThat(deleted).isTrue();
+ assertThat(repoA.get("key1")).isNull();
+ }
+
+ @Test
+ void testFactoryMethodAvoidsRedundantWrapping() {
+ KeyValueRepository wrapped =
NamespacedKeyValueRepository.namespaced(delegate, "myNs");
+ KeyValueRepository wrappedAgain =
NamespacedKeyValueRepository.namespaced(wrapped, "myNs");
+
+ assertThat(wrappedAgain).isSameAs(wrapped);
+ }
+
+ @Test
+ void testNullKeyThrows() {
+ assertThatThrownBy(() -> repoA.get(null))
+ .isInstanceOf(IllegalArgumentException.class);
+ assertThatThrownBy(() -> repoA.put(null, "val"))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+}
diff --git
a/core/camel-util/src/main/java/org/apache/camel/util/ArtifactUtils.java
b/core/camel-util/src/main/java/org/apache/camel/util/ArtifactUtils.java
index f445cb9985a9..336f01a1ae4c 100644
--- a/core/camel-util/src/main/java/org/apache/camel/util/ArtifactUtils.java
+++ b/core/camel-util/src/main/java/org/apache/camel/util/ArtifactUtils.java
@@ -657,6 +657,8 @@ public final class ArtifactUtils {
"org.apache.camel.support.MemoryKeyValueRepository|org.apache.camel.spi.KeyValueRepository|camel-support"),
Map.<String, String> entry("MongoDbIdempotentRepository",
"org.apache.camel.component.mongodb.processor.idempotent.MongoDbIdempotentRepository|org.apache.camel.spi.IdempotentRepository|camel-mongodb"),
+ Map.<String, String> entry("NamespacedKeyValueRepository",
+
"org.apache.camel.support.NamespacedKeyValueRepository|org.apache.camel.spi.KeyValueRepository|camel-support"),
Map.<String, String> entry("OpaSecurityPolicy",
"org.apache.camel.component.opa.security.OpaSecurityPolicy|org.apache.camel.spi.AuthorizationPolicy|camel-opa"),
Map.<String, String> entry("OpenFgaSecurityPolicy",
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 3bd88a9ea434..b53a6ee48f2b 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
@@ -17,6 +17,18 @@ See the xref:camel-upgrade-recipes-tool.adoc[documentation]
page for details.
The `camel-observability-services` component has been promoted from _Preview_
to _Stable_ support level.
+=== KeyValueRepository and State Store - namespace partitioning on shared
backends
+
+In Camel 4.23, `state-store` endpoints stored keys directly into the
underlying `KeyValueRepository` without namespacing. When a single
`KeyValueRepository` was shared between multiple state stores, the Cache EIP,
or repository adapters (such as `KeyValueAggregationRepository` or
`KeyValueIdempotentRepository`), operations on one store (such as `clear()`) or
key collisions could affect entries of another component or pattern.
+
+Starting in 4.24:
+
+* `StateStoreComponent` automatically namespaces keys on shared backend
repositories (auto-discovered from the registry or referenced via
`backend=#ref`) under `statestore:<storeName>:<key>`. Delimiter characters in
store names are escaped to guarantee that store names sharing a prefix remain
strictly isolated. Operations such as `clear()`, `keys()`, and `size()` are
scoped to the store's namespace.
+* Dedicated backends created by `StateStoreBackendFactory` or the default
per-store in-memory fallback are not namespaced.
+* To maintain backward compatibility with un-namespaced entries written by
Camel 4.23 on a shared backend without migrating keys, users can opt out of
namespacing by configuring `namespaced=false` on the endpoint
(`state-store:myStore?namespaced=false`) or on the component
(`camel.component.state-store.namespaced=false`).
+* The Cache EIP stores keys directly without a namespace prefix, preserving
documented cache invalidation recipes (`cache.delete(key)` and
`cache.clear()`). Note that because cache keys are un-prefixed, calling
`clear()` on a `KeyValueRepository` shared with the Cache EIP will clear all
entries in that repository, including state-store entries. It is recommended to
use a dedicated repository for the Cache EIP when co-located with state-store.
+* `KeyValueAggregationRepository` safely verifies entry types before
unmarshalling, ignoring unexpected types gracefully instead of failing with a
class cast exception.
+
=== camel-ehcache - shared CacheManager and changed defaults
The following user-visible changes take effect in 4.24:
diff --git
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/StateStoreComponentBuilderFactory.java
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/StateStoreComponentBuilderFactory.java
index 99ce12088646..38fcac0c94e5 100644
---
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/StateStoreComponentBuilderFactory.java
+++
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/StateStoreComponentBuilderFactory.java
@@ -77,6 +77,24 @@ public interface StateStoreComponentBuilderFactory {
}
+ /**
+ * Whether to use a store-specific namespace prefix to isolate keys on
a
+ * shared backend repository.
+ *
+ * The option is a: <code>boolean</code> type.
+ *
+ * Default: true
+ * Group: producer
+ *
+ * @param namespaced the value to set
+ * @return the dsl builder
+ */
+ default StateStoreComponentBuilder namespaced(boolean namespaced) {
+ doSetProperty("namespaced", namespaced);
+ return this;
+ }
+
+
/**
* Whether autowiring is enabled. This is used for automatic autowiring
* options (the option must be marked as autowired) by looking up in
the
@@ -131,6 +149,7 @@ public interface StateStoreComponentBuilderFactory {
Object value) {
switch (name) {
case "lazyStartProducer": ((StateStoreComponent)
component).setLazyStartProducer((boolean) value); return true;
+ case "namespaced": ((StateStoreComponent)
component).setNamespaced((boolean) value); return true;
case "autowiredEnabled": ((StateStoreComponent)
component).setAutowiredEnabled((boolean) value); return true;
case "backendFactory": ((StateStoreComponent)
component).setBackendFactory((org.apache.camel.component.statestore.StateStoreBackendFactory)
value); return true;
default: return false;
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/StateStoreEndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/StateStoreEndpointBuilderFactory.java
index 884023f13c62..07e647f8339f 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/StateStoreEndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/StateStoreEndpointBuilderFactory.java
@@ -44,6 +44,38 @@ public interface StateStoreEndpointBuilderFactory {
return (AdvancedStateStoreEndpointBuilder) this;
}
+ /**
+ * Whether to use a store-specific namespace prefix to isolate keys on
a
+ * shared backend repository.
+ *
+ * The option is a: <code>boolean</code> type.
+ *
+ * Default: true
+ * Group: producer
+ *
+ * @param namespaced the value to set
+ * @return the dsl builder
+ */
+ default StateStoreEndpointBuilder namespaced(boolean namespaced) {
+ doSetProperty("namespaced", namespaced);
+ return this;
+ }
+ /**
+ * Whether to use a store-specific namespace prefix to isolate keys on
a
+ * shared backend repository.
+ *
+ * The option will be converted to a <code>boolean</code> type.
+ *
+ * Default: true
+ * Group: producer
+ *
+ * @param namespaced the value to set
+ * @return the dsl builder
+ */
+ default StateStoreEndpointBuilder namespaced(String namespaced) {
+ doSetProperty("namespaced", namespaced);
+ return this;
+ }
/**
* The default operation to perform.
*