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 86ffe1bd5575 CAMEL-24463: Add Redis and Infinispan KeyValueRepository 
implementations (#25818)
86ffe1bd5575 is described below

commit 86ffe1bd55758e6e30b8a7304d2db80193ebfc1e
Author: Guillaume Nodet <[email protected]>
AuthorDate: Wed Sep 2 10:42:32 2026 +0200

    CAMEL-24463: Add Redis and Infinispan KeyValueRepository implementations 
(#25818)
    
    CAMEL-24463: Add Redis and Infinispan KeyValueRepository implementations
    
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../org/apache/camel/catalog/beans.properties      |   2 +
 .../beans/InfinispanRemoteKeyValueRepository.json  |  16 ++
 .../catalog/beans/RedisKeyValueRepository.json     |  16 ++
 ...finispanRemoteKeyValueRepositoryConfigurer.java |  60 ++++
 .../services/org/apache/camel/bean.properties      |   2 +-
 .../bean/InfinispanRemoteKeyValueRepository.json   |  16 ++
 ...ispan.remote.InfinispanRemoteKeyValueRepository |   2 +
 .../remote/InfinispanRemoteKeyValueRepository.java | 266 ++++++++++++++++++
 .../InfinispanRemoteKeyValueRepositoryIT.java      | 308 +++++++++++++++++++++
 components/camel-redis/pom.xml                     |  11 +
 .../redis/RedisKeyValueRepositoryConfigurer.java   |  57 ++++
 .../services/org/apache/camel/bean.properties      |   2 +-
 .../apache/camel/bean/RedisKeyValueRepository.json |  16 ++
 ...e.camel.component.redis.RedisKeyValueRepository |   2 +
 .../component/redis/RedisKeyValueRepository.java   | 262 ++++++++++++++++++
 .../integration/RedisKeyValueRepositoryIT.java     | 299 ++++++++++++++++++++
 16 files changed, 1335 insertions(+), 2 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 ef75beaeb981..a7cd67fa6ec4 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
@@ -24,6 +24,7 @@ InfinispanEmbeddedIdempotentRepository
 InfinispanRemoteAggregationRepository
 InfinispanRemoteClusterService
 InfinispanRemoteIdempotentRepository
+InfinispanRemoteKeyValueRepository
 JCacheAggregationRepository
 JCacheIdempotentRepository
 JGroupsRaftClusterService
@@ -40,6 +41,7 @@ MemoryKeyValueRepository
 MongoDbIdempotentRepository
 OpensearchBulkRequestAggregationStrategy
 RedisAggregationRepository
+RedisKeyValueRepository
 SimpleScheduledRoutePolicy
 SpringCacheIdempotentRepository
 SpringRedisIdempotentRepository
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/InfinispanRemoteKeyValueRepository.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/InfinispanRemoteKeyValueRepository.json
new file mode 100644
index 000000000000..7a920e4f70b2
--- /dev/null
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/InfinispanRemoteKeyValueRepository.json
@@ -0,0 +1,16 @@
+{
+  "bean": {
+    "kind": "bean",
+    "name": "InfinispanRemoteKeyValueRepository",
+    "javaType": 
"org.apache.camel.component.infinispan.remote.InfinispanRemoteKeyValueRepository",
+    "interfaceType": "org.apache.camel.spi.KeyValueRepository",
+    "title": "Infinispan Remote Key Value Repository",
+    "description": "A KeyValueRepository backed by remote Infinispan (HotRod 
client).",
+    "deprecated": false,
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-infinispan",
+    "version": "4.23.0-SNAPSHOT",
+    "properties": { "cacheName": { "index": 0, "kind": "property", 
"displayName": "Cache Name", "required": true, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "autowired": false, "secret": false, 
"description": "Name of cache" }, "configuration": { "index": 1, "kind": 
"property", "displayName": "Configuration", "required": false, "type": 
"object", "javaType": 
"org.apache.camel.component.infinispan.remote.InfinispanRemoteConfiguration", 
"deprecated": false, "auto [...]
+  }
+}
+
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/RedisKeyValueRepository.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/RedisKeyValueRepository.json
new file mode 100644
index 000000000000..35fad0e6c77f
--- /dev/null
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/RedisKeyValueRepository.json
@@ -0,0 +1,16 @@
+{
+  "bean": {
+    "kind": "bean",
+    "name": "RedisKeyValueRepository",
+    "javaType": "org.apache.camel.component.redis.RedisKeyValueRepository",
+    "interfaceType": "org.apache.camel.spi.KeyValueRepository",
+    "title": "Redis Key Value Repository",
+    "description": "A KeyValueRepository backed by Redis (Redisson client).",
+    "deprecated": false,
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-redis",
+    "version": "4.23.0-SNAPSHOT",
+    "properties": { "redisson": { "index": 0, "kind": "property", 
"displayName": "Redisson", "label": "advanced", "required": false, "type": 
"object", "javaType": "org.redisson.api.RedissonClient", "deprecated": false, 
"autowired": false, "secret": false, "description": "To use an existing 
Redisson client to connect to Redis server" }, "endpoint": { "index": 1, 
"kind": "property", "displayName": "Endpoint", "required": true, "type": 
"string", "javaType": "java.lang.String", "deprecated": [...]
+  }
+}
+
diff --git 
a/components/camel-infinispan/camel-infinispan/src/generated/java/org/apache/camel/component/infinispan/remote/InfinispanRemoteKeyValueRepositoryConfigurer.java
 
b/components/camel-infinispan/camel-infinispan/src/generated/java/org/apache/camel/component/infinispan/remote/InfinispanRemoteKeyValueRepositoryConfigurer.java
new file mode 100644
index 000000000000..74a1b158fff4
--- /dev/null
+++ 
b/components/camel-infinispan/camel-infinispan/src/generated/java/org/apache/camel/component/infinispan/remote/InfinispanRemoteKeyValueRepositoryConfigurer.java
@@ -0,0 +1,60 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.component.infinispan.remote;
+
+import javax.annotation.processing.Generated;
+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.component.infinispan.remote.InfinispanRemoteKeyValueRepository;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo")
+@SuppressWarnings("unchecked")
+public class InfinispanRemoteKeyValueRepositoryConfigurer 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.component.infinispan.remote.InfinispanRemoteKeyValueRepository 
target = 
(org.apache.camel.component.infinispan.remote.InfinispanRemoteKeyValueRepository)
 obj;
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "cachecontainer":
+        case "cacheContainer": target.setCacheContainer(property(camelContext, 
org.infinispan.client.hotrod.RemoteCacheManager.class, value)); return true;
+        case "cachename":
+        case "cacheName": target.setCacheName(property(camelContext, 
java.lang.String.class, value)); return true;
+        case "configuration": target.setConfiguration(property(camelContext, 
org.apache.camel.component.infinispan.remote.InfinispanRemoteConfiguration.class,
 value)); return true;
+        default: return false;
+        }
+    }
+
+    @Override
+    public Class<?> getOptionType(String name, boolean ignoreCase) {
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "cachecontainer":
+        case "cacheContainer": return 
org.infinispan.client.hotrod.RemoteCacheManager.class;
+        case "cachename":
+        case "cacheName": return java.lang.String.class;
+        case "configuration": return 
org.apache.camel.component.infinispan.remote.InfinispanRemoteConfiguration.class;
+        default: return null;
+        }
+    }
+
+    @Override
+    public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
+        
org.apache.camel.component.infinispan.remote.InfinispanRemoteKeyValueRepository 
target = 
(org.apache.camel.component.infinispan.remote.InfinispanRemoteKeyValueRepository)
 obj;
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "cachecontainer":
+        case "cacheContainer": return target.getCacheContainer();
+        case "cachename":
+        case "cacheName": return target.getCacheName();
+        case "configuration": return target.getConfiguration();
+        default: return null;
+        }
+    }
+}
+
diff --git 
a/components/camel-infinispan/camel-infinispan/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
 
b/components/camel-infinispan/camel-infinispan/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
index 4266e31d6ea1..69f469416651 100644
--- 
a/components/camel-infinispan/camel-infinispan/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
+++ 
b/components/camel-infinispan/camel-infinispan/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=InfinispanRemoteAggregationRepository InfinispanRemoteClusterService 
InfinispanRemoteIdempotentRepository
+bean=InfinispanRemoteAggregationRepository InfinispanRemoteClusterService 
InfinispanRemoteIdempotentRepository InfinispanRemoteKeyValueRepository
 groupId=org.apache.camel
 artifactId=camel-infinispan
 version=4.23.0-SNAPSHOT
diff --git 
a/components/camel-infinispan/camel-infinispan/src/generated/resources/META-INF/services/org/apache/camel/bean/InfinispanRemoteKeyValueRepository.json
 
b/components/camel-infinispan/camel-infinispan/src/generated/resources/META-INF/services/org/apache/camel/bean/InfinispanRemoteKeyValueRepository.json
new file mode 100644
index 000000000000..7a920e4f70b2
--- /dev/null
+++ 
b/components/camel-infinispan/camel-infinispan/src/generated/resources/META-INF/services/org/apache/camel/bean/InfinispanRemoteKeyValueRepository.json
@@ -0,0 +1,16 @@
+{
+  "bean": {
+    "kind": "bean",
+    "name": "InfinispanRemoteKeyValueRepository",
+    "javaType": 
"org.apache.camel.component.infinispan.remote.InfinispanRemoteKeyValueRepository",
+    "interfaceType": "org.apache.camel.spi.KeyValueRepository",
+    "title": "Infinispan Remote Key Value Repository",
+    "description": "A KeyValueRepository backed by remote Infinispan (HotRod 
client).",
+    "deprecated": false,
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-infinispan",
+    "version": "4.23.0-SNAPSHOT",
+    "properties": { "cacheName": { "index": 0, "kind": "property", 
"displayName": "Cache Name", "required": true, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "autowired": false, "secret": false, 
"description": "Name of cache" }, "configuration": { "index": 1, "kind": 
"property", "displayName": "Configuration", "required": false, "type": 
"object", "javaType": 
"org.apache.camel.component.infinispan.remote.InfinispanRemoteConfiguration", 
"deprecated": false, "auto [...]
+  }
+}
+
diff --git 
a/components/camel-infinispan/camel-infinispan/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.infinispan.remote.InfinispanRemoteKeyValueRepository
 
b/components/camel-infinispan/camel-infinispan/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.infinispan.remote.InfinispanRemoteKeyValueRepository
new file mode 100644
index 000000000000..f3d4b4df7de4
--- /dev/null
+++ 
b/components/camel-infinispan/camel-infinispan/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.infinispan.remote.InfinispanRemoteKeyValueRepository
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.component.infinispan.remote.InfinispanRemoteKeyValueRepositoryConfigurer
diff --git 
a/components/camel-infinispan/camel-infinispan/src/main/java/org/apache/camel/component/infinispan/remote/InfinispanRemoteKeyValueRepository.java
 
b/components/camel-infinispan/camel-infinispan/src/main/java/org/apache/camel/component/infinispan/remote/InfinispanRemoteKeyValueRepository.java
new file mode 100644
index 000000000000..19dafea30149
--- /dev/null
+++ 
b/components/camel-infinispan/camel-infinispan/src/main/java/org/apache/camel/component/infinispan/remote/InfinispanRemoteKeyValueRepository.java
@@ -0,0 +1,266 @@
+/*
+ * 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.infinispan.remote;
+
+import java.time.Duration;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.CamelContextAware;
+import org.apache.camel.api.management.ManagedAttribute;
+import org.apache.camel.api.management.ManagedOperation;
+import org.apache.camel.api.management.ManagedResource;
+import org.apache.camel.spi.Configurer;
+import org.apache.camel.spi.KeyValueRepository;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.support.KeyValueRepositoryHelper;
+import org.apache.camel.support.service.ServiceHelper;
+import org.apache.camel.support.service.ServiceSupport;
+import org.apache.camel.util.ObjectHelper;
+import org.apache.camel.util.function.Suppliers;
+import org.infinispan.client.hotrod.Flag;
+import org.infinispan.client.hotrod.MetadataValue;
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.client.hotrod.RemoteCacheManager;
+import org.jspecify.annotations.Nullable;
+
+import static 
org.apache.camel.component.infinispan.remote.InfinispanRemoteUtil.getCacheWithFlags;
+
+/**
+ * A {@link KeyValueRepository} implementation backed by a remote Infinispan 
server via the HotRod client protocol.
+ * <p/>
+ * TTL is mapped from {@link Duration} to Infinispan's native lifespan via
+ * {@code BasicCache.put(key, value, lifespan, TimeUnit.MILLISECONDS)}. The 
cache is configured with
+ * {@link Flag#FORCE_RETURN_VALUE} to ensure that {@code put} and {@code 
remove} operations return previous values as
+ * required by the {@link KeyValueRepository} contract.
+ * <p/>
+ * Values are serialized through {@link KeyValueRepositoryHelper} (plain Java 
serialization) and stored as raw
+ * {@code byte[]} in the cache. This ensures a consistent serialization format 
across all persistent
+ * {@link KeyValueRepository} implementations, avoids coupling to Infinispan's 
marshaller configuration, and eliminates
+ * the need for ProtoStream schema registration for complex value types.
+ *
+ * @since 4.23
+ */
+@Metadata(label = "bean",
+          description = "A KeyValueRepository backed by remote Infinispan 
(HotRod client).",
+          annotations = { 
"interfaceName=org.apache.camel.spi.KeyValueRepository" })
+@Configurer(metadataOnly = true)
+@ManagedResource(description = "Infinispan Remote based key-value repository")
+public class InfinispanRemoteKeyValueRepository extends ServiceSupport 
implements KeyValueRepository, CamelContextAware {
+
+    private CamelContext camelContext;
+    private Supplier<RemoteCache<String, byte[]>> cache;
+    private InfinispanRemoteManager manager;
+
+    @Metadata(description = "Name of cache", required = true)
+    private String cacheName;
+    @Metadata(description = "Configuration for remote Infinispan")
+    private InfinispanRemoteConfiguration configuration;
+
+    public InfinispanRemoteKeyValueRepository() {
+    }
+
+    /**
+     * Creates a new Infinispan remote key-value repository for the given 
cache name.
+     *
+     * @param cacheName the name of the Infinispan cache to use
+     */
+    public InfinispanRemoteKeyValueRepository(String cacheName) {
+        this.cacheName = cacheName;
+    }
+
+    @Override
+    @ManagedOperation(description = "Get value by key")
+    public @Nullable Object get(String key) {
+        byte[] bytes = cache.get().get(key);
+        return bytes != null ? KeyValueRepositoryHelper.deserialize(bytes) : 
null;
+    }
+
+    @Override
+    @ManagedOperation(description = "Put a key-value pair with optional TTL")
+    public @Nullable Object put(String key, Object value, Duration ttl) {
+        byte[] serialized = KeyValueRepositoryHelper.serialize(value);
+        byte[] previous;
+        if (hasPositiveTtl(ttl)) {
+            previous = cache.get().put(key, serialized, ttl.toMillis(), 
TimeUnit.MILLISECONDS);
+        } else {
+            previous = cache.get().put(key, serialized);
+        }
+        return previous != null ? 
KeyValueRepositoryHelper.deserialize(previous) : null;
+    }
+
+    @Override
+    @ManagedOperation(description = "Delete a key")
+    public @Nullable Object delete(String key) {
+        byte[] bytes = cache.get().remove(key);
+        return bytes != null ? KeyValueRepositoryHelper.deserialize(bytes) : 
null;
+    }
+
+    @Override
+    @ManagedOperation(description = "Check if key exists")
+    public boolean contains(String key) {
+        return cache.get().containsKey(key);
+    }
+
+    @Override
+    public Set<String> keys() {
+        return cache.get().keySet();
+    }
+
+    @Override
+    @ManagedOperation(description = "Clear all entries")
+    public void clear() {
+        cache.get().clear();
+    }
+
+    @Override
+    public @Nullable Object putIfAbsent(String key, Object value, Duration 
ttl) {
+        byte[] serialized = KeyValueRepositoryHelper.serialize(value);
+        byte[] existing;
+        if (hasPositiveTtl(ttl)) {
+            existing = cache.get().putIfAbsent(key, serialized, 
ttl.toMillis(), TimeUnit.MILLISECONDS);
+        } else {
+            existing = cache.get().putIfAbsent(key, serialized);
+        }
+        return existing != null ? 
KeyValueRepositoryHelper.deserialize(existing) : null;
+    }
+
+    /**
+     * Atomically replaces the value for the given key using Infinispan's 
version-based optimistic locking.
+     * <p/>
+     * Because values are stored as serialized {@code byte[]}, Infinispan's 
value-based {@code replace(K, V, V)} cannot
+     * compare entries reliably through the marshaller. Instead, this method 
uses {@code getWithMetadata} to read the
+     * current value and its version, compares the deserialized value at the 
Java object level, and then calls
+     * {@code replaceWithVersion} to perform an atomic swap guarded by the 
entry version.
+     */
+    @Override
+    public boolean replace(String key, Object expectedOldValue, Object 
newValue, Duration ttl) {
+        MetadataValue<byte[]> metadata = cache.get().getWithMetadata(key);
+        if (metadata == null) {
+            return false;
+        }
+        Object currentObj = 
KeyValueRepositoryHelper.deserialize(metadata.getValue());
+        if (!Objects.equals(currentObj, expectedOldValue)) {
+            return false;
+        }
+        byte[] newBytes = KeyValueRepositoryHelper.serialize(newValue);
+        if (hasPositiveTtl(ttl)) {
+            return cache.get().replaceWithVersion(key, newBytes, 
metadata.getVersion(),
+                    ttl.toMillis(), TimeUnit.MILLISECONDS, -1, 
TimeUnit.MILLISECONDS);
+        }
+        return cache.get().replaceWithVersion(key, newBytes, 
metadata.getVersion());
+    }
+
+    /**
+     * Atomically removes the entry for the given key using Infinispan's 
version-based optimistic locking.
+     * <p/>
+     * Similar to {@link #replace}, this uses {@code getWithMetadata} and 
{@code removeWithVersion} to avoid relying on
+     * marshaller-level value comparison for serialized {@code byte[]} entries.
+     */
+    @Override
+    public boolean delete(String key, Object expectedValue) {
+        MetadataValue<byte[]> metadata = cache.get().getWithMetadata(key);
+        if (metadata == null) {
+            return false;
+        }
+        Object currentObj = 
KeyValueRepositoryHelper.deserialize(metadata.getValue());
+        if (!Objects.equals(currentObj, expectedValue)) {
+            return false;
+        }
+        return cache.get().removeWithVersion(key, metadata.getVersion());
+    }
+
+    @Override
+    @ManagedAttribute(description = "The number of entries in the repository")
+    public int size() {
+        return cache.get().size();
+    }
+
+    // ---- Configuration accessors ----
+
+    @ManagedAttribute(description = "The cache name")
+    public String getCacheName() {
+        return cacheName;
+    }
+
+    public void setCacheName(String cacheName) {
+        this.cacheName = cacheName;
+    }
+
+    public InfinispanRemoteConfiguration getConfiguration() {
+        return configuration;
+    }
+
+    public void setConfiguration(InfinispanRemoteConfiguration configuration) {
+        this.configuration = configuration;
+    }
+
+    public RemoteCacheManager getCacheContainer() {
+        return configuration != null ? configuration.getCacheContainer() : 
null;
+    }
+
+    public void setCacheContainer(RemoteCacheManager cacheContainer) {
+        if (this.configuration == null) {
+            this.configuration = new InfinispanRemoteConfiguration();
+        }
+        this.configuration.setCacheContainer(cacheContainer);
+    }
+
+    public InfinispanRemoteManager getManager() {
+        return manager;
+    }
+
+    @Override
+    public CamelContext getCamelContext() {
+        return camelContext;
+    }
+
+    @Override
+    public void setCamelContext(CamelContext camelContext) {
+        this.camelContext = camelContext;
+    }
+
+    // ---- Lifecycle ----
+
+    @Override
+    protected void doStart() throws Exception {
+        ObjectHelper.notNull(cacheName, "cacheName", this);
+
+        if (this.configuration == null) {
+            this.configuration = new InfinispanRemoteConfiguration();
+        }
+
+        this.manager = new InfinispanRemoteManager(camelContext, 
configuration);
+        this.cache = Suppliers.memorize(
+                () -> getCacheWithFlags(manager, cacheName, 
Flag.FORCE_RETURN_VALUE));
+        ServiceHelper.startService(manager);
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        ServiceHelper.stopAndShutdownService(manager);
+    }
+
+    // ---- Internal ----
+
+    private static boolean hasPositiveTtl(Duration ttl) {
+        return ttl != null && !ttl.isZero() && !ttl.isNegative();
+    }
+}
diff --git 
a/components/camel-infinispan/camel-infinispan/src/test/java/org/apache/camel/component/infinispan/remote/InfinispanRemoteKeyValueRepositoryIT.java
 
b/components/camel-infinispan/camel-infinispan/src/test/java/org/apache/camel/component/infinispan/remote/InfinispanRemoteKeyValueRepositoryIT.java
new file mode 100644
index 000000000000..64cafe5a80f3
--- /dev/null
+++ 
b/components/camel-infinispan/camel-infinispan/src/test/java/org/apache/camel/component/infinispan/remote/InfinispanRemoteKeyValueRepositoryIT.java
@@ -0,0 +1,308 @@
+/*
+ * 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.infinispan.remote;
+
+import java.time.Duration;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.test.infra.infinispan.common.InfinispanProperties;
+import org.apache.camel.test.infra.infinispan.services.InfinispanService;
+import 
org.apache.camel.test.infra.infinispan.services.InfinispanServiceFactory;
+import org.infinispan.client.hotrod.RemoteCacheManager;
+import org.infinispan.client.hotrod.configuration.ConfigurationBuilder;
+import org.infinispan.configuration.cache.CacheMode;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.awaitility.Awaitility.await;
+
+class InfinispanRemoteKeyValueRepositoryIT {
+
+    private static final String CACHE_NAME = "kvr-test";
+
+    @RegisterExtension
+    static InfinispanService service = 
InfinispanServiceFactory.createSingletonInfinispanService();
+
+    private CamelContext camelContext;
+    private RemoteCacheManager cacheManager;
+    private InfinispanRemoteKeyValueRepository repository;
+
+    @BeforeEach
+    void setUp() {
+        camelContext = new DefaultCamelContext();
+
+        cacheManager = new RemoteCacheManager(getConfiguration().build());
+
+        // Create the cache on the server
+        cacheManager.administration()
+                .getOrCreateCache(
+                        CACHE_NAME,
+                        new 
org.infinispan.configuration.cache.ConfigurationBuilder()
+                                .clustering()
+                                .cacheMode(CacheMode.DIST_SYNC)
+                                .build());
+
+        InfinispanRemoteConfiguration config = new 
InfinispanRemoteConfiguration();
+        config.setCacheContainer(cacheManager);
+
+        repository = new InfinispanRemoteKeyValueRepository(CACHE_NAME);
+        repository.setCamelContext(camelContext);
+        repository.setConfiguration(config);
+        repository.start();
+    }
+
+    @AfterEach
+    void tearDown() {
+        if (repository != null) {
+            try {
+                repository.clear();
+            } catch (Exception e) {
+                // ignore cleanup errors
+            }
+            repository.stop();
+        }
+        if (cacheManager != null) {
+            cacheManager.stop();
+        }
+        if (camelContext != null) {
+            try {
+                camelContext.close();
+            } catch (Exception e) {
+                // ignore cleanup errors
+            }
+        }
+    }
+
+    @Test
+    void testPutAndGet() {
+        Object previous = repository.put("key1", "value1", null);
+        assertThat(previous).isNull();
+
+        Object result = repository.get("key1");
+        assertThat(result).isEqualTo("value1");
+    }
+
+    @Test
+    void testPutReturnsOldValue() {
+        repository.put("key1", "first", null);
+        Object previous = repository.put("key1", "second", null);
+        assertThat(previous).isEqualTo("first");
+
+        Object result = repository.get("key1");
+        assertThat(result).isEqualTo("second");
+    }
+
+    @Test
+    void testGetNonExistent() {
+        Object result = repository.get("nonexistent");
+        assertThat(result).isNull();
+    }
+
+    @Test
+    void testDelete() {
+        repository.put("key1", "value1", null);
+        Object deleted = repository.delete("key1");
+        assertThat(deleted).isEqualTo("value1");
+        assertThat(repository.get("key1")).isNull();
+    }
+
+    @Test
+    void testDeleteNonExistent() {
+        Object deleted = repository.delete("nonexistent");
+        assertThat(deleted).isNull();
+    }
+
+    @Test
+    void testContains() {
+        repository.put("key1", "value1", null);
+        assertThat(repository.contains("key1")).isTrue();
+        assertThat(repository.contains("nonexistent")).isFalse();
+    }
+
+    @Test
+    void testKeys() {
+        repository.put("a", "1", null);
+        repository.put("b", "2", null);
+        repository.put("c", "3", null);
+
+        Set<String> keys = repository.keys();
+        assertThat(keys).containsExactlyInAnyOrder("a", "b", "c");
+    }
+
+    @Test
+    void testKeysEmpty() {
+        Set<String> keys = repository.keys();
+        assertThat(keys).isEmpty();
+    }
+
+    @Test
+    void testClear() {
+        repository.put("a", "1", null);
+        repository.put("b", "2", null);
+
+        repository.clear();
+
+        assertThat(repository.keys()).isEmpty();
+        assertThat(repository.contains("a")).isFalse();
+    }
+
+    @Test
+    void testPutIfAbsentNewKey() {
+        Object existing = repository.putIfAbsent("key1", "value1", null);
+        assertThat(existing).isNull();
+        assertThat(repository.get("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    void testPutIfAbsentExistingKey() {
+        repository.put("key1", "original", null);
+        Object existing = repository.putIfAbsent("key1", "replacement", null);
+        assertThat(existing).isEqualTo("original");
+        assertThat(repository.get("key1")).isEqualTo("original");
+    }
+
+    @Test
+    void testSize() {
+        assertThat(repository.size()).isZero();
+
+        repository.put("a", "1", null);
+        repository.put("b", "2", null);
+        assertThat(repository.size()).isEqualTo(2);
+
+        repository.delete("a");
+        assertThat(repository.size()).isEqualTo(1);
+    }
+
+    @Test
+    void testTtlExpiry() {
+        // Store with a short TTL (Infinispan lifespan)
+        repository.put("expiring", "value", Duration.ofSeconds(1));
+        assertThat(repository.contains("expiring")).isTrue();
+
+        // Wait for it to expire
+        await().atMost(10, TimeUnit.SECONDS)
+                .untilAsserted(() -> 
assertThat(repository.contains("expiring")).isFalse());
+    }
+
+    @Test
+    void testTtlPutIfAbsentExpiry() {
+        repository.putIfAbsent("expiring", "value", Duration.ofSeconds(1));
+        assertThat(repository.contains("expiring")).isTrue();
+
+        await().atMost(10, TimeUnit.SECONDS)
+                .untilAsserted(() -> 
assertThat(repository.contains("expiring")).isFalse());
+    }
+
+    @Test
+    void testReplaceMatchingValue() {
+        repository.put("key1", "value1", null);
+        boolean replaced = repository.replace("key1", "value1", "value2", 
null);
+        assertThat(replaced).isTrue();
+        assertThat(repository.get("key1")).isEqualTo("value2");
+    }
+
+    @Test
+    void testReplaceNonMatchingValue() {
+        repository.put("key1", "value1", null);
+        boolean replaced = repository.replace("key1", "wrong", "value2", null);
+        assertThat(replaced).isFalse();
+        assertThat(repository.get("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    void testReplaceMissingKey() {
+        boolean replaced = repository.replace("nonexistent", "value1", 
"value2", null);
+        assertThat(replaced).isFalse();
+    }
+
+    @Test
+    void testReplaceWithTtl() {
+        repository.put("key1", "value1", null);
+        boolean replaced = repository.replace("key1", "value1", "value2", 
Duration.ofSeconds(1));
+        assertThat(replaced).isTrue();
+        assertThat(repository.get("key1")).isEqualTo("value2");
+
+        await().atMost(10, TimeUnit.SECONDS)
+                .untilAsserted(() -> 
assertThat(repository.get("key1")).isNull());
+    }
+
+    @Test
+    void testDeleteWithMatchingValue() {
+        repository.put("key1", "value1", null);
+        boolean deleted = repository.delete("key1", "value1");
+        assertThat(deleted).isTrue();
+        assertThat(repository.get("key1")).isNull();
+    }
+
+    @Test
+    void testDeleteWithNonMatchingValue() {
+        repository.put("key1", "value1", null);
+        boolean deleted = repository.delete("key1", "wrong");
+        assertThat(deleted).isFalse();
+        assertThat(repository.get("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    void testDeleteWithMissingKey() {
+        boolean deleted = repository.delete("nonexistent", "value1");
+        assertThat(deleted).isFalse();
+    }
+
+    @Test
+    void testCacheNameRequired() {
+        InfinispanRemoteKeyValueRepository repo = new 
InfinispanRemoteKeyValueRepository();
+        repo.setCamelContext(camelContext);
+        
assertThatThrownBy(repo::start).isInstanceOf(IllegalArgumentException.class);
+    }
+
+    private ConfigurationBuilder getConfiguration() {
+        ConfigurationBuilder clientBuilder = new ConfigurationBuilder();
+
+        clientBuilder.forceReturnValues(true);
+
+        clientBuilder
+                .addServer()
+                .host(service.host())
+                .port(service.port());
+
+        clientBuilder
+                .socketTimeout(15000)
+                .connectionTimeout(15000)
+                .security()
+                .authentication()
+                .username(service.username())
+                .password(service.password())
+                .serverName("infinispan")
+                .saslMechanism("SCRAM-SHA-512")
+                .realm("default");
+
+        if 
(!Boolean.getBoolean(InfinispanProperties.INFINISPAN_CONTAINER_NETWORK_MODE_HOST))
 {
+            Properties properties = new Properties();
+            properties.put("infinispan.client.hotrod.client_intelligence", 
"BASIC");
+            clientBuilder.withProperties(properties);
+        }
+        return clientBuilder;
+    }
+}
diff --git a/components/camel-redis/pom.xml b/components/camel-redis/pom.xml
index fa1ea1977a31..0d01b321f306 100644
--- a/components/camel-redis/pom.xml
+++ b/components/camel-redis/pom.xml
@@ -56,6 +56,17 @@
             <artifactId>camel-test-junit6</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.awaitility</groupId>
+            <artifactId>awaitility</artifactId>
+            <version>${awaitility-version}</version>
+            <scope>test</scope>
+        </dependency>
 
         <!-- test infra -->
         <dependency>
diff --git 
a/components/camel-redis/src/generated/java/org/apache/camel/component/redis/RedisKeyValueRepositoryConfigurer.java
 
b/components/camel-redis/src/generated/java/org/apache/camel/component/redis/RedisKeyValueRepositoryConfigurer.java
new file mode 100644
index 000000000000..63e66174255e
--- /dev/null
+++ 
b/components/camel-redis/src/generated/java/org/apache/camel/component/redis/RedisKeyValueRepositoryConfigurer.java
@@ -0,0 +1,57 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.component.redis;
+
+import javax.annotation.processing.Generated;
+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.component.redis.RedisKeyValueRepository;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo")
+@SuppressWarnings("unchecked")
+public class RedisKeyValueRepositoryConfigurer 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.component.redis.RedisKeyValueRepository target = 
(org.apache.camel.component.redis.RedisKeyValueRepository) obj;
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "endpoint": target.setEndpoint(property(camelContext, 
java.lang.String.class, value)); return true;
+        case "keyprefix":
+        case "keyPrefix": target.setKeyPrefix(property(camelContext, 
java.lang.String.class, value)); return true;
+        case "redisson": target.setRedisson(property(camelContext, 
org.redisson.api.RedissonClient.class, value)); return true;
+        default: return false;
+        }
+    }
+
+    @Override
+    public Class<?> getOptionType(String name, boolean ignoreCase) {
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "endpoint": return java.lang.String.class;
+        case "keyprefix":
+        case "keyPrefix": return java.lang.String.class;
+        case "redisson": return org.redisson.api.RedissonClient.class;
+        default: return null;
+        }
+    }
+
+    @Override
+    public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
+        org.apache.camel.component.redis.RedisKeyValueRepository target = 
(org.apache.camel.component.redis.RedisKeyValueRepository) obj;
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "endpoint": return target.getEndpoint();
+        case "keyprefix":
+        case "keyPrefix": return target.getKeyPrefix();
+        case "redisson": return target.getRedisson();
+        default: return null;
+        }
+    }
+}
+
diff --git 
a/components/camel-redis/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
 
b/components/camel-redis/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
index 6291ead2f7c0..55e6cfc67360 100644
--- 
a/components/camel-redis/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
+++ 
b/components/camel-redis/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=RedisAggregationRepository
+bean=RedisAggregationRepository RedisKeyValueRepository
 groupId=org.apache.camel
 artifactId=camel-redis
 version=4.23.0-SNAPSHOT
diff --git 
a/components/camel-redis/src/generated/resources/META-INF/services/org/apache/camel/bean/RedisKeyValueRepository.json
 
b/components/camel-redis/src/generated/resources/META-INF/services/org/apache/camel/bean/RedisKeyValueRepository.json
new file mode 100644
index 000000000000..35fad0e6c77f
--- /dev/null
+++ 
b/components/camel-redis/src/generated/resources/META-INF/services/org/apache/camel/bean/RedisKeyValueRepository.json
@@ -0,0 +1,16 @@
+{
+  "bean": {
+    "kind": "bean",
+    "name": "RedisKeyValueRepository",
+    "javaType": "org.apache.camel.component.redis.RedisKeyValueRepository",
+    "interfaceType": "org.apache.camel.spi.KeyValueRepository",
+    "title": "Redis Key Value Repository",
+    "description": "A KeyValueRepository backed by Redis (Redisson client).",
+    "deprecated": false,
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-redis",
+    "version": "4.23.0-SNAPSHOT",
+    "properties": { "redisson": { "index": 0, "kind": "property", 
"displayName": "Redisson", "label": "advanced", "required": false, "type": 
"object", "javaType": "org.redisson.api.RedissonClient", "deprecated": false, 
"autowired": false, "secret": false, "description": "To use an existing 
Redisson client to connect to Redis server" }, "endpoint": { "index": 1, 
"kind": "property", "displayName": "Endpoint", "required": true, "type": 
"string", "javaType": "java.lang.String", "deprecated": [...]
+  }
+}
+
diff --git 
a/components/camel-redis/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.redis.RedisKeyValueRepository
 
b/components/camel-redis/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.redis.RedisKeyValueRepository
new file mode 100644
index 000000000000..acfa00a671c2
--- /dev/null
+++ 
b/components/camel-redis/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.redis.RedisKeyValueRepository
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.component.redis.RedisKeyValueRepositoryConfigurer
diff --git 
a/components/camel-redis/src/main/java/org/apache/camel/component/redis/RedisKeyValueRepository.java
 
b/components/camel-redis/src/main/java/org/apache/camel/component/redis/RedisKeyValueRepository.java
new file mode 100644
index 000000000000..3bedccad69fb
--- /dev/null
+++ 
b/components/camel-redis/src/main/java/org/apache/camel/component/redis/RedisKeyValueRepository.java
@@ -0,0 +1,262 @@
+/*
+ * 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.redis;
+
+import java.time.Duration;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.camel.api.management.ManagedAttribute;
+import org.apache.camel.api.management.ManagedOperation;
+import org.apache.camel.api.management.ManagedResource;
+import org.apache.camel.spi.Configurer;
+import org.apache.camel.spi.KeyValueRepository;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.support.KeyValueRepositoryHelper;
+import org.apache.camel.support.service.ServiceSupport;
+import org.apache.camel.util.StringHelper;
+import org.jspecify.annotations.Nullable;
+import org.redisson.Redisson;
+import org.redisson.api.RBucket;
+import org.redisson.api.RKeys;
+import org.redisson.api.RedissonClient;
+import org.redisson.api.options.KeysScanOptions;
+import org.redisson.client.codec.ByteArrayCodec;
+import org.redisson.config.Config;
+
+/**
+ * A {@link KeyValueRepository} implementation backed by Redis using the 
Redisson client.
+ * <p/>
+ * Keys are namespaced under a configurable prefix ({@link #keyPrefix}) to 
avoid collisions with other data in the same
+ * Redis instance. Values are serialized through {@link 
KeyValueRepositoryHelper} (plain Java serialization) and stored
+ * as raw {@code byte[]} using Redisson's {@link ByteArrayCodec}. This ensures 
a consistent serialization format across
+ * all persistent {@link KeyValueRepository} implementations and avoids 
coupling to Redisson's built-in codec.
+ * <p/>
+ * TTL is mapped from milliseconds to Redis native key expiry via
+ * {@code RBucket.set(value, ttl, TimeUnit.MILLISECONDS)}. Atomic {@link 
#putIfAbsent} is supported via
+ * {@code RBucket.setIfAbsent}.
+ *
+ * @since 4.23
+ */
+@Metadata(label = "bean",
+          description = "A KeyValueRepository backed by Redis (Redisson 
client).",
+          annotations = { 
"interfaceName=org.apache.camel.spi.KeyValueRepository" })
+@Configurer(metadataOnly = true)
+@ManagedResource(description = "Redis based key-value repository")
+public class RedisKeyValueRepository extends ServiceSupport implements 
KeyValueRepository {
+
+    private boolean shutdownRedisson;
+
+    @Metadata(label = "advanced", description = "To use an existing Redisson 
client to connect to Redis server")
+    private RedissonClient redisson;
+    @Metadata(description = "URL to remote Redis server (host:port)", required 
= true)
+    private String endpoint;
+    @Metadata(description = "Key prefix used to namespace entries in Redis", 
defaultValue = "camel-kvr:")
+    private String keyPrefix = "camel-kvr:";
+
+    public RedisKeyValueRepository() {
+    }
+
+    /**
+     * Creates a new Redis key-value repository connecting to the given 
endpoint.
+     *
+     * @param endpoint the Redis server address in {@code host:port} format
+     */
+    public RedisKeyValueRepository(String endpoint) {
+        this.endpoint = endpoint;
+    }
+
+    /**
+     * Creates a new Redis key-value repository connecting to the given 
endpoint with a custom key prefix.
+     *
+     * @param endpoint  the Redis server address in {@code host:port} format
+     * @param keyPrefix the prefix to prepend to all keys stored in Redis
+     */
+    public RedisKeyValueRepository(String endpoint, String keyPrefix) {
+        this.endpoint = endpoint;
+        this.keyPrefix = keyPrefix;
+    }
+
+    @Override
+    @ManagedOperation(description = "Get value by key")
+    public @Nullable Object get(String key) {
+        RBucket<byte[]> bucket = redisson.getBucket(toRedisKey(key), 
ByteArrayCodec.INSTANCE);
+        byte[] bytes = bucket.get();
+        return bytes != null ? KeyValueRepositoryHelper.deserialize(bytes) : 
null;
+    }
+
+    @Override
+    @ManagedOperation(description = "Put a key-value pair with optional TTL")
+    public @Nullable Object put(String key, Object value, Duration ttl) {
+        RBucket<byte[]> bucket = redisson.getBucket(toRedisKey(key), 
ByteArrayCodec.INSTANCE);
+        byte[] serialized = KeyValueRepositoryHelper.serialize(value);
+        byte[] previous = bucket.get();
+        if (hasPositiveTtl(ttl)) {
+            bucket.set(serialized, ttl);
+        } else {
+            bucket.set(serialized);
+        }
+        return previous != null ? 
KeyValueRepositoryHelper.deserialize(previous) : null;
+    }
+
+    @Override
+    @ManagedOperation(description = "Delete a key")
+    public @Nullable Object delete(String key) {
+        RBucket<byte[]> bucket = redisson.getBucket(toRedisKey(key), 
ByteArrayCodec.INSTANCE);
+        byte[] bytes = bucket.getAndDelete();
+        return bytes != null ? KeyValueRepositoryHelper.deserialize(bytes) : 
null;
+    }
+
+    @Override
+    @ManagedOperation(description = "Check if key exists")
+    public boolean contains(String key) {
+        RBucket<byte[]> bucket = redisson.getBucket(toRedisKey(key), 
ByteArrayCodec.INSTANCE);
+        return bucket.isExists();
+    }
+
+    @Override
+    public Set<String> keys() {
+        RKeys rKeys = redisson.getKeys();
+        int prefixLen = keyPrefix.length();
+        return 
rKeys.getKeysStream(KeysScanOptions.defaults().pattern(toRedisKey("*")))
+                .map(k -> k.substring(prefixLen))
+                .collect(Collectors.toUnmodifiableSet());
+    }
+
+    @Override
+    @ManagedOperation(description = "Clear all entries")
+    public void clear() {
+        RKeys rKeys = redisson.getKeys();
+        String pattern = toRedisKey("*");
+        rKeys.deleteByPattern(pattern);
+    }
+
+    @Override
+    public @Nullable Object putIfAbsent(String key, Object value, Duration 
ttl) {
+        RBucket<byte[]> bucket = redisson.getBucket(toRedisKey(key), 
ByteArrayCodec.INSTANCE);
+        byte[] serialized = KeyValueRepositoryHelper.serialize(value);
+        boolean wasSet;
+        if (hasPositiveTtl(ttl)) {
+            wasSet = bucket.setIfAbsent(serialized, ttl);
+        } else {
+            wasSet = bucket.setIfAbsent(serialized);
+        }
+        if (wasSet) {
+            return null;
+        }
+        // Key already existed; return the current value
+        byte[] existing = bucket.get();
+        return existing != null ? 
KeyValueRepositoryHelper.deserialize(existing) : null;
+    }
+
+    /**
+     * Atomically replaces the value for a key if the current value matches 
the expected one.
+     * <p/>
+     * <b>Note:</b> Redisson does not expose an atomic CAS-and-set-TTL 
primitive. When a TTL is specified, the
+     * replacement is performed in two steps: {@code compareAndSet} followed 
by {@code expire}. There is a brief window
+     * between the two calls where the new value exists without its TTL 
applied. A crash in that window would leave the
+     * entry without expiry. For callers requiring stricter consistency 
guarantees (e.g. idempotent repositories), be
+     * aware of this non-atomic TTL application.
+     */
+    @Override
+    public boolean replace(String key, Object expectedOldValue, Object 
newValue, Duration ttl) {
+        RBucket<byte[]> bucket = redisson.getBucket(toRedisKey(key), 
ByteArrayCodec.INSTANCE);
+        byte[] expectedBytes = 
KeyValueRepositoryHelper.serialize(expectedOldValue);
+        byte[] newBytes = KeyValueRepositoryHelper.serialize(newValue);
+        boolean swapped = bucket.compareAndSet(expectedBytes, newBytes);
+        if (swapped && hasPositiveTtl(ttl)) {
+            bucket.expire(ttl);
+        }
+        return swapped;
+    }
+
+    @Override
+    @ManagedAttribute(description = "The number of entries in the repository")
+    public int size() {
+        RKeys rKeys = redisson.getKeys();
+        String[] matchedKeys = 
rKeys.getKeysStream(KeysScanOptions.defaults().pattern(toRedisKey("*")))
+                .toArray(String[]::new);
+        if (matchedKeys.length == 0) {
+            return 0;
+        }
+        return (int) rKeys.countExists(matchedKeys);
+    }
+
+    // ---- Configuration accessors ----
+
+    public String getEndpoint() {
+        return endpoint;
+    }
+
+    public void setEndpoint(String endpoint) {
+        this.endpoint = endpoint;
+    }
+
+    @ManagedAttribute(description = "Key prefix used for namespacing")
+    public String getKeyPrefix() {
+        return keyPrefix;
+    }
+
+    public void setKeyPrefix(String keyPrefix) {
+        this.keyPrefix = keyPrefix;
+    }
+
+    public RedissonClient getRedisson() {
+        return redisson;
+    }
+
+    public void setRedisson(RedissonClient redisson) {
+        this.redisson = redisson;
+    }
+
+    // ---- Lifecycle ----
+
+    @Override
+    protected void doInit() throws Exception {
+        if (redisson == null) {
+            StringHelper.notEmpty(endpoint, "endpoint");
+        }
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        if (redisson == null) {
+            Config config = new Config();
+            config.useSingleServer().setAddress(String.format("redis://%s", 
endpoint));
+            redisson = Redisson.create(config);
+            shutdownRedisson = true;
+        }
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        if (redisson != null && shutdownRedisson) {
+            redisson.shutdown();
+            redisson = null;
+        }
+    }
+
+    // ---- Internal ----
+
+    private static boolean hasPositiveTtl(Duration ttl) {
+        return ttl != null && !ttl.isZero() && !ttl.isNegative();
+    }
+
+    private String toRedisKey(String key) {
+        return keyPrefix + key;
+    }
+}
diff --git 
a/components/camel-redis/src/test/java/org/apache/camel/component/redis/integration/RedisKeyValueRepositoryIT.java
 
b/components/camel-redis/src/test/java/org/apache/camel/component/redis/integration/RedisKeyValueRepositoryIT.java
new file mode 100644
index 000000000000..d0fd1a8f6f24
--- /dev/null
+++ 
b/components/camel-redis/src/test/java/org/apache/camel/component/redis/integration/RedisKeyValueRepositoryIT.java
@@ -0,0 +1,299 @@
+/*
+ * 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.redis.integration;
+
+import java.time.Duration;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.component.redis.RedisKeyValueRepository;
+import org.apache.camel.test.infra.redis.services.RedisService;
+import org.apache.camel.test.infra.redis.services.RedisServiceFactory;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.awaitility.Awaitility.await;
+
+class RedisKeyValueRepositoryIT {
+
+    @RegisterExtension
+    static RedisService service = RedisServiceFactory.createSingletonService();
+
+    private RedisKeyValueRepository repository;
+
+    @BeforeEach
+    void setUp() {
+        repository = new RedisKeyValueRepository(service.getServiceAddress(), 
"test-kvr:");
+        repository.start();
+    }
+
+    @AfterEach
+    void tearDown() {
+        if (repository != null) {
+            repository.clear();
+            repository.stop();
+        }
+    }
+
+    @Test
+    void testPutAndGet() {
+        Object previous = repository.put("key1", "value1", null);
+        assertThat(previous).isNull();
+
+        Object result = repository.get("key1");
+        assertThat(result).isEqualTo("value1");
+    }
+
+    @Test
+    void testPutReturnsOldValue() {
+        repository.put("key1", "first", null);
+        Object previous = repository.put("key1", "second", null);
+        assertThat(previous).isEqualTo("first");
+
+        Object result = repository.get("key1");
+        assertThat(result).isEqualTo("second");
+    }
+
+    @Test
+    void testGetNonExistent() {
+        Object result = repository.get("nonexistent");
+        assertThat(result).isNull();
+    }
+
+    @Test
+    void testDelete() {
+        repository.put("key1", "value1", null);
+        Object deleted = repository.delete("key1");
+        assertThat(deleted).isEqualTo("value1");
+        assertThat(repository.get("key1")).isNull();
+    }
+
+    @Test
+    void testDeleteNonExistent() {
+        Object deleted = repository.delete("nonexistent");
+        assertThat(deleted).isNull();
+    }
+
+    @Test
+    void testContains() {
+        repository.put("key1", "value1", null);
+        assertThat(repository.contains("key1")).isTrue();
+        assertThat(repository.contains("nonexistent")).isFalse();
+    }
+
+    @Test
+    void testKeys() {
+        repository.put("a", "1", null);
+        repository.put("b", "2", null);
+        repository.put("c", "3", null);
+
+        Set<String> keys = repository.keys();
+        assertThat(keys).containsExactlyInAnyOrder("a", "b", "c");
+    }
+
+    @Test
+    void testKeysEmpty() {
+        Set<String> keys = repository.keys();
+        assertThat(keys).isEmpty();
+    }
+
+    @Test
+    void testClear() {
+        repository.put("a", "1", null);
+        repository.put("b", "2", null);
+
+        repository.clear();
+
+        assertThat(repository.keys()).isEmpty();
+        assertThat(repository.contains("a")).isFalse();
+    }
+
+    @Test
+    void testPutIfAbsentNewKey() {
+        Object existing = repository.putIfAbsent("key1", "value1", null);
+        assertThat(existing).isNull();
+        assertThat(repository.get("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    void testPutIfAbsentExistingKey() {
+        repository.put("key1", "original", null);
+        Object existing = repository.putIfAbsent("key1", "replacement", null);
+        assertThat(existing).isEqualTo("original");
+        assertThat(repository.get("key1")).isEqualTo("original");
+    }
+
+    @Test
+    void testSize() {
+        assertThat(repository.size()).isZero();
+
+        repository.put("a", "1", null);
+        repository.put("b", "2", null);
+        assertThat(repository.size()).isEqualTo(2);
+
+        repository.delete("a");
+        assertThat(repository.size()).isEqualTo(1);
+    }
+
+    @Test
+    void testTtlExpiry() {
+        // Store with a short TTL
+        repository.put("expiring", "value", Duration.ofMillis(500));
+        assertThat(repository.contains("expiring")).isTrue();
+
+        // Wait for it to expire
+        await().atMost(5, TimeUnit.SECONDS)
+                .untilAsserted(() -> 
assertThat(repository.contains("expiring")).isFalse());
+    }
+
+    @Test
+    void testTtlPutIfAbsentExpiry() {
+        repository.putIfAbsent("expiring", "value", Duration.ofMillis(500));
+        assertThat(repository.contains("expiring")).isTrue();
+
+        await().atMost(5, TimeUnit.SECONDS)
+                .untilAsserted(() -> 
assertThat(repository.contains("expiring")).isFalse());
+    }
+
+    @Test
+    void testSerializableValues() {
+        // Test that non-String serializable objects work
+        repository.put("int-key", 42, null);
+        assertThat(repository.get("int-key")).isEqualTo(42);
+
+        repository.put("bool-key", Boolean.TRUE, null);
+        assertThat(repository.get("bool-key")).isEqualTo(Boolean.TRUE);
+    }
+
+    @Test
+    void testKeyPrefixIsolation() {
+        // Create a second repository with a different prefix
+        RedisKeyValueRepository other = new 
RedisKeyValueRepository(service.getServiceAddress(), "other-kvr:");
+        other.start();
+        try {
+            repository.put("shared-key", "from-repo1", null);
+            other.put("shared-key", "from-repo2", null);
+
+            assertThat(repository.get("shared-key")).isEqualTo("from-repo1");
+            assertThat(other.get("shared-key")).isEqualTo("from-repo2");
+
+            assertThat(repository.keys()).containsExactly("shared-key");
+            assertThat(other.keys()).containsExactly("shared-key");
+
+            // Clearing one should not affect the other
+            repository.clear();
+            assertThat(repository.keys()).isEmpty();
+            assertThat(other.get("shared-key")).isEqualTo("from-repo2");
+        } finally {
+            other.clear();
+            other.stop();
+        }
+    }
+
+    @Test
+    void testCustomRedissonClient() {
+        Config config = new Config();
+        config.useSingleServer().setAddress(String.format("redis://%s", 
service.getServiceAddress()));
+        RedissonClient customClient = Redisson.create(config);
+
+        try {
+            RedisKeyValueRepository customRepo = new RedisKeyValueRepository();
+            customRepo.setKeyPrefix("custom-kvr:");
+            customRepo.setRedisson(customClient);
+            customRepo.start();
+
+            customRepo.put("key1", "value1", null);
+            assertThat(customRepo.get("key1")).isEqualTo("value1");
+
+            customRepo.clear();
+            customRepo.stop();
+
+            // Custom client should not be shut down by the repository
+            assertThat(customClient.isShutdown()).isFalse();
+        } finally {
+            customClient.shutdown();
+        }
+    }
+
+    @Test
+    void testReplaceMatchingValue() {
+        repository.put("key1", "value1", null);
+        boolean replaced = repository.replace("key1", "value1", "value2", 
null);
+        assertThat(replaced).isTrue();
+        assertThat(repository.get("key1")).isEqualTo("value2");
+    }
+
+    @Test
+    void testReplaceNonMatchingValue() {
+        repository.put("key1", "value1", null);
+        boolean replaced = repository.replace("key1", "wrong", "value2", null);
+        assertThat(replaced).isFalse();
+        assertThat(repository.get("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    void testReplaceMissingKey() {
+        boolean replaced = repository.replace("nonexistent", "value1", 
"value2", null);
+        assertThat(replaced).isFalse();
+    }
+
+    @Test
+    void testReplaceWithTtl() {
+        repository.put("key1", "value1", null);
+        boolean replaced = repository.replace("key1", "value1", "value2", 
Duration.ofMillis(500));
+        assertThat(replaced).isTrue();
+        assertThat(repository.get("key1")).isEqualTo("value2");
+
+        await().atMost(5, TimeUnit.SECONDS)
+                .untilAsserted(() -> 
assertThat(repository.get("key1")).isNull());
+    }
+
+    @Test
+    void testDeleteWithMatchingValue() {
+        repository.put("key1", "value1", null);
+        boolean deleted = repository.delete("key1", "value1");
+        assertThat(deleted).isTrue();
+        assertThat(repository.get("key1")).isNull();
+    }
+
+    @Test
+    void testDeleteWithNonMatchingValue() {
+        repository.put("key1", "value1", null);
+        boolean deleted = repository.delete("key1", "wrong");
+        assertThat(deleted).isFalse();
+        assertThat(repository.get("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    void testDeleteWithMissingKey() {
+        boolean deleted = repository.delete("nonexistent", "value1");
+        assertThat(deleted).isFalse();
+    }
+
+    @Test
+    void testEndpointValidation() {
+        RedisKeyValueRepository repo = new RedisKeyValueRepository();
+        
assertThatThrownBy(repo::init).isInstanceOf(IllegalArgumentException.class);
+    }
+}

Reply via email to