This is an automated email from the ASF dual-hosted git repository.

Croway pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-spring-boot.git


The following commit(s) were added to refs/heads/main by this push:
     new f315101aa64 CAMEL-24757: camel-state-store-starter - document Redis 
immediate writes and fix flaky Redis IT
f315101aa64 is described below

commit f315101aa644dde6b36918b42ef29b45724572b8
Author: croway <[email protected]>
AuthorDate: Fri Oct 23 10:39:13 2026 +0200

    CAMEL-24757: camel-state-store-starter - document Redis immediate writes 
and fix flaky Redis IT
    
    Spring Data Redis 4 with Lettuce performs RedisCache.put asynchronously
    by default, so a get right after a put may not see the new value. The
    Spring Cache state store needs read-your-writes semantics: document the
    requirement and how to enable immediate writes, configure them in
    SpringCacheStateStoreRedisIT, and add a put/get visibility test.
---
 .../src/main/doc/limitations.adoc                  |  1 +
 .../src/main/doc/usage.adoc                        | 13 ++++++++
 .../cache/SpringCacheKeyValueRepository.java       |  5 ++++
 .../cache/SpringCacheStateStoreRedisIT.java        | 35 +++++++++++++++++++++-
 .../modules/ROOT/pages/starters/state-store.adoc   | 14 +++++++++
 5 files changed, 67 insertions(+), 1 deletion(-)

diff --git 
a/components-starter/camel-state-store-starter/src/main/doc/limitations.adoc 
b/components-starter/camel-state-store-starter/src/main/doc/limitations.adoc
index 880f77dcb5e..7111ef87394 100644
--- a/components-starter/camel-state-store-starter/src/main/doc/limitations.adoc
+++ b/components-starter/camel-state-store-starter/src/main/doc/limitations.adoc
@@ -3,6 +3,7 @@
 Spring Cache provides a common API, but some operations depend on the 
underlying cache provider.
 
 * **Per-entry TTL:** Spring Cache has no portable per-entry TTL API. A 
positive, non-zero TTL requested through the repository is not applied by the 
Spring adapter. Configure expiration using the underlying cache provider, for 
example with `spring.cache.redis.time-to-live` or a Caffeine specification.
+* **Write visibility:** Spring's `Cache.put` contract allows a provider to 
register an entry asynchronously, so a later lookup may not see it yet. The 
state store and the Camel EIPs that use it, such as the Aggregate EIP, expect a 
value to be readable right after it is written. Spring Data Redis 4 with 
Lettuce writes asynchronously by default; enable immediate writes as shown in 
the Redis configuration example.
 * **Atomic operations:** Spring Cache does not provide a portable atomic 
get-and-put operation. The adapter's `put` and `delete` read the previous value 
and then write or evict separately, so the returned previous value is not 
atomic. `putIfAbsent` delegates to the provider's Spring Cache implementation; 
its atomicity depends on that provider. Conditional `replace` and conditional 
`delete` may use non-atomic check-then-act operations.
 * **Key enumeration and size:** `keys()` and `size()` are supported only when 
`Cache.getNativeCache()` returns a `ConcurrentMap`, such as with the simple 
cache provider. They operate on a snapshot of current string keys. Other 
providers, including Caffeine, Redis, and JCache, do not support these 
operations through the Spring adapter.
 
diff --git 
a/components-starter/camel-state-store-starter/src/main/doc/usage.adoc 
b/components-starter/camel-state-store-starter/src/main/doc/usage.adoc
index b86d9ce7474..98b8a187f9a 100644
--- a/components-starter/camel-state-store-starter/src/main/doc/usage.adoc
+++ b/components-starter/camel-state-store-starter/src/main/doc/usage.adoc
@@ -27,6 +27,19 @@ camel.state-store.spring-cache.enabled=true
 
 Spring Boot creates the `CacheManager` from `spring.cache.*` only when the 
application enables caching with `@EnableCaching`; otherwise, provide your own 
`CacheManager` bean. Without a single eligible `CacheManager`, the factory is 
not registered, and stores may fall back to the default in-memory repository. 
If `spring.cache.cache-names` specifies a fixed set of names, include every 
state-store name you use. A dynamic cache manager can create caches on demand.
 
+When the cache provider is Redis with the Lettuce driver, Spring Data Redis 4 
writes cache entries asynchronously by default, so a value may not be readable 
right after the state store writes it. Enable immediate writes on the cache 
writer that Spring Boot uses for its `RedisCacheManager`:
+
+[source,java]
+----
+@Bean
+RedisCacheManagerBuilderCustomizer 
immediateWritesCustomizer(RedisConnectionFactory connectionFactory) {
+    return builder -> builder.cacheWriter(
+            RedisCacheWriter.create(connectionFactory, 
RedisCacheWriter.RedisCacheWriterConfigurer::immediateWrites));
+}
+----
+
+This setting applies to every cache of that `CacheManager`. If you define your 
own `RedisCacheManager`, create its `RedisCacheWriter` with `immediateWrites()` 
in the same way.
+
 To use a specific Camel repository, register it as a bean, for example under 
the name `redisRepo`, and select it on the endpoint:
 
 [source,java]
diff --git 
a/components-starter/camel-state-store-starter/src/main/java/org/apache/camel/component/statestore/springboot/cache/SpringCacheKeyValueRepository.java
 
b/components-starter/camel-state-store-starter/src/main/java/org/apache/camel/component/statestore/springboot/cache/SpringCacheKeyValueRepository.java
index a9f9a5d4429..ec95f18f51e 100644
--- 
a/components-starter/camel-state-store-starter/src/main/java/org/apache/camel/component/statestore/springboot/cache/SpringCacheKeyValueRepository.java
+++ 
b/components-starter/camel-state-store-starter/src/main/java/org/apache/camel/component/statestore/springboot/cache/SpringCacheKeyValueRepository.java
@@ -45,6 +45,11 @@ import org.springframework.util.Assert;
  *       requiring expiration must configure it through the underlying cache
  *       provider or {@link CacheManager}, for example using a Caffeine cache
  *       specification.</li>
+ *   <li><strong>Write visibility:</strong> {@link Cache#put(Object, Object)}
+ *       may register the entry asynchronously, so a following {@code get}
+ *       may not see it yet. Spring Data Redis 4 does this by default with
+ *       Lettuce; enable immediate writes on its {@code RedisCacheWriter} to
+ *       get the read-your-writes behavior that Camel EIPs expect.</li>
  *   <li><strong>Atomicity:</strong> Spring Cache has no atomic get-and-put
  *       API. {@code put} and {@code delete} read the previous value and then
  *       write or evict in a second call, so the returned old value is not
diff --git 
a/components-starter/camel-state-store-starter/src/test/java/org/apache/camel/component/statestore/springboot/cache/SpringCacheStateStoreRedisIT.java
 
b/components-starter/camel-state-store-starter/src/test/java/org/apache/camel/component/statestore/springboot/cache/SpringCacheStateStoreRedisIT.java
index c5c6978f0d3..8eaefc54bf3 100644
--- 
a/components-starter/camel-state-store-starter/src/test/java/org/apache/camel/component/statestore/springboot/cache/SpringCacheStateStoreRedisIT.java
+++ 
b/components-starter/camel-state-store-starter/src/test/java/org/apache/camel/component/statestore/springboot/cache/SpringCacheStateStoreRedisIT.java
@@ -39,11 +39,16 @@ import 
org.junit.jupiter.api.condition.DisabledIfSystemProperty;
 import org.junit.jupiter.api.extension.RegisterExtension;
 import org.springframework.boot.autoconfigure.AutoConfigurations;
 import org.springframework.boot.cache.autoconfigure.CacheAutoConfiguration;
+import 
org.springframework.boot.cache.autoconfigure.RedisCacheManagerBuilderCustomizer;
 import 
org.springframework.boot.data.redis.autoconfigure.DataRedisAutoConfiguration;
 import org.springframework.boot.test.context.runner.ApplicationContextRunner;
 import org.springframework.cache.CacheManager;
 import org.springframework.cache.annotation.ProxyCachingConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
 import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.cache.RedisCacheWriter;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -64,7 +69,8 @@ class SpringCacheStateStoreRedisIT {
                         CacheAutoConfiguration.class,
                         StateStoreComponentAutoConfiguration.class,
                         SpringCacheStateStoreAutoConfiguration.class))
-                .withUserConfiguration(ProxyCachingConfiguration.class, 
StateStoreComponentConverter.class)
+                .withUserConfiguration(ProxyCachingConfiguration.class, 
StateStoreComponentConverter.class,
+                        ImmediateWritesConfiguration.class)
                 .withPropertyValues(
                         "spring.main.banner-mode=off",
                         "camel.state-store.spring-cache.enabled=true",
@@ -167,6 +173,18 @@ class SpringCacheStateStoreRedisIT {
         });
     }
 
+    @Test
+    void putIsVisibleToSubsequentGet() {
+        runner().run(context -> {
+            StateStoreBackendFactory factory = 
context.getBean(StateStoreBackendFactory.class);
+            KeyValueRepository repository = factory.createBackend("orders");
+            for (int i = 0; i < 500; i++) {
+                repository.put("visible", "value-" + i);
+                assertThat(repository.get("visible")).isEqualTo("value-" + i);
+            }
+        });
+    }
+
     @Test
     void providerLevelTimeToLiveExpiresEntries() {
         runner()
@@ -182,4 +200,19 @@ class SpringCacheStateStoreRedisIT {
                             .untilAsserted(() -> 
assertThat(repository.get("expiring")).isNull());
                 });
     }
+
+    /**
+     * With Lettuce, Spring Data Redis writes cache entries asynchronously by 
default, so a get that follows a put may
+     * not see the new value. The state store needs read-your-writes 
semantics, so enable immediate writes as
+     * documented for the Spring Cache backend.
+     */
+    @Configuration(proxyBeanMethods = false)
+    static class ImmediateWritesConfiguration {
+
+        @Bean
+        RedisCacheManagerBuilderCustomizer 
immediateWritesCustomizer(RedisConnectionFactory connectionFactory) {
+            return builder -> builder.cacheWriter(
+                    RedisCacheWriter.create(connectionFactory, 
RedisCacheWriter.RedisCacheWriterConfigurer::immediateWrites));
+        }
+    }
 }
diff --git a/docs/spring-boot/modules/ROOT/pages/starters/state-store.adoc 
b/docs/spring-boot/modules/ROOT/pages/starters/state-store.adoc
index a9eeaba5253..3b8da5a2c88 100644
--- a/docs/spring-boot/modules/ROOT/pages/starters/state-store.adoc
+++ b/docs/spring-boot/modules/ROOT/pages/starters/state-store.adoc
@@ -52,6 +52,19 @@ camel.state-store.spring-cache.enabled=true
 
 Spring Boot creates the `CacheManager` from `spring.cache.*` only when the 
application enables caching with `@EnableCaching`; otherwise, provide your own 
`CacheManager` bean. Without a single eligible `CacheManager`, the factory is 
not registered, and stores may fall back to the default in-memory repository. 
If `spring.cache.cache-names` specifies a fixed set of names, include every 
state-store name you use. A dynamic cache manager can create caches on demand.
 
+When the cache provider is Redis with the Lettuce driver, Spring Data Redis 4 
writes cache entries asynchronously by default, so a value may not be readable 
right after the state store writes it. Enable immediate writes on the cache 
writer that Spring Boot uses for its `RedisCacheManager`:
+
+[source,java]
+----
+@Bean
+RedisCacheManagerBuilderCustomizer 
immediateWritesCustomizer(RedisConnectionFactory connectionFactory) {
+    return builder -> builder.cacheWriter(
+            RedisCacheWriter.create(connectionFactory, 
RedisCacheWriter.RedisCacheWriterConfigurer::immediateWrites));
+}
+----
+
+This setting applies to every cache of that `CacheManager`. If you define your 
own `RedisCacheManager`, create its `RedisCacheWriter` with `immediateWrites()` 
in the same way.
+
 To use a specific Camel repository, register it as a bean, for example under 
the name `redisRepo`, and select it on the endpoint:
 
 [source,java]
@@ -74,6 +87,7 @@ You can therefore use Spring Cache for ordinary stores and 
explicitly select a C
 Spring Cache provides a common API, but some operations depend on the 
underlying cache provider.
 
 * **Per-entry TTL:** Spring Cache has no portable per-entry TTL API. A 
positive, non-zero TTL requested through the repository is not applied by the 
Spring adapter. Configure expiration using the underlying cache provider, for 
example with `spring.cache.redis.time-to-live` or a Caffeine specification.
+* **Write visibility:** Spring's `Cache.put` contract allows a provider to 
register an entry asynchronously, so a later lookup may not see it yet. The 
state store and the Camel EIPs that use it, such as the Aggregate EIP, expect a 
value to be readable right after it is written. Spring Data Redis 4 with 
Lettuce writes asynchronously by default; enable immediate writes as shown in 
the Redis configuration example.
 * **Atomic operations:** Spring Cache does not provide a portable atomic 
get-and-put operation. The adapter's `put` and `delete` read the previous value 
and then write or evict separately, so the returned previous value is not 
atomic. `putIfAbsent` delegates to the provider's Spring Cache implementation; 
its atomicity depends on that provider. Conditional `replace` and conditional 
`delete` may use non-atomic check-then-act operations.
 * **Key enumeration and size:** `keys()` and `size()` are supported only when 
`Cache.getNativeCache()` returns a `ConcurrentMap`, such as with the simple 
cache provider. They operate on a snapshot of current string keys. Other 
providers, including Caffeine, Redis, and JCache, do not support these 
operations through the Spring adapter.
 

Reply via email to