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.