davsclaus commented on code in PR #2046: URL: https://github.com/apache/camel-spring-boot/pull/2046#discussion_r4302150154
########## core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/statestore/SpringCacheKeyValueRepositoryAutoConfiguration.java: ########## @@ -0,0 +1,39 @@ +/* + * 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.spring.boot.statestore; + +import org.apache.camel.spi.KeyValueRepository; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cache.CacheManager; +import org.springframework.context.annotation.Bean; + +@AutoConfiguration +@ConditionalOnClass({ CacheManager.class, KeyValueRepository.class }) +@ConditionalOnBean(CacheManager.class) +@AutoConfigureAfter(name = { "org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration" }) Review Comment: This is the Spring Boot 3 class name. camel-spring-boot builds on Spring Boot 4.1.1, where the cache auto-configuration is `org.springframework.boot.cache.autoconfigure.CacheAutoConfiguration` (in `spring-boot-cache`). An `afterName` that does not resolve is silently ignored. This class then sorts alphabetically ahead of `CacheAutoConfiguration`, so `@ConditionalOnBean(CacheManager.class)` is false for a `CacheManager` that Spring Boot creates from `spring.cache.type`. Only a user-defined `@Bean CacheManager` passes, which is what all the tests use. A test with `AutoConfigurations.of(CacheAutoConfiguration.class, SpringCacheKeyValueRepositoryAutoConfiguration.class)` and `spring.cache.type=simple` would cover the documented setup. ```suggestion @AutoConfigureAfter(name = { "org.springframework.boot.cache.autoconfigure.CacheAutoConfiguration" }) ``` ########## core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/statestore/SpringCacheKeyValueRepository.java: ########## @@ -0,0 +1,160 @@ +/* + * 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.spring.boot.statestore; + +import java.time.Duration; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.camel.spi.KeyValueRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.util.Assert; + +/** + * An Apache Camel {@link KeyValueRepository} adapter backed by Spring's {@link CacheManager}. + * + * <h3>Design Constraints & Backend Behavior:</h3> + * <ul> + * <li><strong>TTL Support:</strong> Spring Cache has no standard per-entry TTL API. Providing a TTL argument + * triggers a single {@code WARN} log per repository instance and stores the entry without expiration. + * Users requiring TTL must configure it at the underlying provider/CacheManager spec level (e.g., Caffeine spec).</li> + * <li><strong>Atomicity:</strong> CAS operations ({@code putIfAbsent}, {@code replace}, conditional delete) Review Comment: Only `putIfAbsent` delegates to `Cache.putIfAbsent()`. `replace(key, expected, new, ttl)` and `delete(key, expected)` are not overridden, so they use the non-atomic SPI defaults (`get`, then `put`/`evict`) with every provider, including Caffeine. Suggest describing them that way. ########## core/camel-spring-boot/src/main/docs/spring-boot.adoc: ########## @@ -610,6 +610,56 @@ public class MyRoute extends RouteBuilder { } ---- +== Spring Cache State Store Support Review Comment: This section is inserted between `==== Platform HTTP Component` (line 596) and its child `===== Undertow Metrics` (now line 663). Undertow Metrics ends up under `=== Design Considerations & Constraints`, and the heading jumps two levels. Could it move after the Virtual Threads section, or into a section of its own? ########## core/camel-spring-boot/src/main/docs/spring-boot.adoc: ########## @@ -610,6 +610,56 @@ public class MyRoute extends RouteBuilder { } ---- +== Spring Cache State Store Support + +The `camel-spring-boot` module provides an auto-configured Apache Camel `KeyValueRepository` backed by Spring's `CacheManager` abstraction (`org.springframework.cache.CacheManager`). + +When a `CacheManager` bean is present in the Spring application context and no explicit `KeyValueRepository` bean is defined, a `SpringCacheKeyValueRepository` is automatically registered. This allows any caching backend supported by Spring Boot (such as Caffeine, Redis via Lettuce, Ehcache, Hazelcast, or JCache) to seamlessly function as an Apache Camel state store backend without requiring extra component-specific dependencies. + +=== Configuration Example (Caffeine) + +[source,yaml] +---- +spring: + cache: + type: caffeine + cache-names: myStore + caffeine: + spec: maximumSize=10000,expireAfterWrite=600s +---- + +[source,java] +---- +from("direct:store") + .setHeader("CamelStateStoreKey", constant("myKey")) + .to("state-store:myStore?operation=put"); + +from("direct:retrieve") + .setHeader("CamelStateStoreKey", constant("myKey")) + .to("state-store:myStore?operation=get"); +---- + +=== Configuration Example (Redis / Lettuce) + +[source,yaml] +---- +spring: + cache: + type: redis + cache-names: myStore + data: + redis: + host: localhost + port: 6379 +---- + +=== Design Considerations & Constraints + +* **Store Name Mapping:** Each Camel `state-store:storeName` endpoint maps 1:1 to the Spring Cache name looked up via `CacheManager.getCache(storeName)`. Review Comment: This mapping is not what happens. The auto-configured repository always uses the `camel-kvr` cache, and the State Store component shares that one bean across all store names. A real 1:1 mapping needs `state-store` to resolve its backend per store name, which is a change in apache/camel. Without that, this bullet should describe the single shared cache. ########## core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/statestore/SpringCacheKeyValueRepository.java: ########## @@ -0,0 +1,160 @@ +/* + * 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.spring.boot.statestore; + +import java.time.Duration; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.camel.spi.KeyValueRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.util.Assert; + +/** + * An Apache Camel {@link KeyValueRepository} adapter backed by Spring's {@link CacheManager}. + * + * <h3>Design Constraints & Backend Behavior:</h3> + * <ul> + * <li><strong>TTL Support:</strong> Spring Cache has no standard per-entry TTL API. Providing a TTL argument + * triggers a single {@code WARN} log per repository instance and stores the entry without expiration. + * Users requiring TTL must configure it at the underlying provider/CacheManager spec level (e.g., Caffeine spec).</li> + * <li><strong>Atomicity:</strong> CAS operations ({@code putIfAbsent}, {@code replace}, conditional delete) + * delegate to Spring Cache's {@code Cache.putIfAbsent()}. Atomicity guarantees are provider-dependent + * (e.g., atomic in Caffeine, but potentially non-atomic in distributed backends like Lettuce/Redis).</li> + * <li><strong>Enumeration:</strong> {@link #keys()} and {@link #size()} intentionally throw + * {@link UnsupportedOperationException} as Spring Cache does not provide a portable key enumeration API.</li> + * <li><strong>Scope & Multi-Store:</strong> Backed by a single global {@link CacheManager}. Multiple endpoints + * can isolate state by specifying distinct cache/store names while sharing the same underlying manager.</li> Review Comment: With the auto-configured bean, endpoints cannot isolate state by store name. The instance is bound to `camel-kvr`, and `StateStoreComponent.getOrCreateBackend` gives the same discovered bean to every `state-store:*` endpoint. So all stores share one cache (keys collide, `clear` clears every store). Isolation works only when separate instances are bound per endpoint with `backend=#...`. ########## core/camel-spring-boot/src/main/docs/spring-boot.adoc: ########## @@ -610,6 +610,56 @@ public class MyRoute extends RouteBuilder { } ---- +== Spring Cache State Store Support + +The `camel-spring-boot` module provides an auto-configured Apache Camel `KeyValueRepository` backed by Spring's `CacheManager` abstraction (`org.springframework.cache.CacheManager`). + +When a `CacheManager` bean is present in the Spring application context and no explicit `KeyValueRepository` bean is defined, a `SpringCacheKeyValueRepository` is automatically registered. This allows any caching backend supported by Spring Boot (such as Caffeine, Redis via Lettuce, Ehcache, Hazelcast, or JCache) to seamlessly function as an Apache Camel state store backend without requiring extra component-specific dependencies. + +=== Configuration Example (Caffeine) + +[source,yaml] +---- +spring: + cache: + type: caffeine + cache-names: myStore + caffeine: + spec: maximumSize=10000,expireAfterWrite=600s +---- + +[source,java] +---- +from("direct:store") + .setHeader("CamelStateStoreKey", constant("myKey")) + .to("state-store:myStore?operation=put"); + +from("direct:retrieve") + .setHeader("CamelStateStoreKey", constant("myKey")) + .to("state-store:myStore?operation=get"); +---- + +=== Configuration Example (Redis / Lettuce) + +[source,yaml] +---- +spring: + cache: + type: redis + cache-names: myStore + data: + redis: + host: localhost + port: 6379 +---- + +=== Design Considerations & Constraints + +* **Store Name Mapping:** Each Camel `state-store:storeName` endpoint maps 1:1 to the Spring Cache name looked up via `CacheManager.getCache(storeName)`. +* **TTL Support:** Per-entry TTL arguments in the SPI are acknowledged with a one-time warning log and stored without expiration. To enforce TTL, configure it at the underlying provider/CacheManager spec level. +* **Atomicity:** CAS operations (`putIfAbsent`, `replace`, etc.) delegate to Spring Cache's `putIfAbsent()`, whose atomicity guarantees depend on the underlying provider (e.g., atomic in Caffeine, potentially non-atomic in distributed stores like Redis). Review Comment: Same as the javadoc: `replace` and conditional `delete` are not overridden. They use the non-atomic SPI defaults, so they are not atomic with any provider. Only `putIfAbsent` goes through `Cache.putIfAbsent()`. ########## core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/statestore/SpringCacheKeyValueRepositoryAutoConfigurationTest.java: ########## @@ -0,0 +1,304 @@ +/* + * 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.spring.boot.statestore; + +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.apache.camel.spi.KeyValueRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cache.CacheManager; +import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class SpringCacheKeyValueRepositoryAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of( + SpringCacheKeyValueRepositoryAutoConfiguration.class + )) + .withUserConfiguration(DefaultCacheManagerConfig.class); + + @Configuration + static class DefaultCacheManagerConfig { + @Bean + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager("camel-kvr"); + } + } + + @Test + @DisplayName("Should auto-configure SpringCacheKeyValueRepository when CacheManager is present") + void shouldAutoConfigureWhenCacheManagerPresent() { + contextRunner.run(context -> { + assertThat(context).hasBean("springCacheKeyValueRepository"); + assertThat(context).hasSingleBean(KeyValueRepository.class); + + KeyValueRepository repo = context.getBean(KeyValueRepository.class); + assertThat(repo).isInstanceOf(SpringCacheKeyValueRepository.class); + + SpringCacheKeyValueRepository springRepo = (SpringCacheKeyValueRepository) repo; + assertThat(springRepo.getCacheName()).isEqualTo("camel-kvr"); + }); + } + + @Test + @DisplayName("Should NOT auto-configure repository when CacheManager is strictly absent") + void shouldNotAutoConfigureWhenCacheManagerAbsent() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(SpringCacheKeyValueRepositoryAutoConfiguration.class)) + .run(context -> { + assertThat(context).doesNotHaveBean("springCacheKeyValueRepository"); + assertThat(context).doesNotHaveBean(KeyValueRepository.class); + }); + } + + @Test + @DisplayName("Should respect user-defined KeyValueRepository bean precedence") + void shouldRespectUserDefinedRepositoryPrecedence() { + contextRunner.withUserConfiguration(CustomRepositoryConfig.class).run(context -> { + assertThat(context).hasBean("customRepo"); + assertThat(context).hasSingleBean(KeyValueRepository.class); + + KeyValueRepository repo = context.getBean(KeyValueRepository.class); + assertThat(repo).isInstanceOf(StubKeyValueRepository.class); + assertThat(repo).isNotInstanceOf(SpringCacheKeyValueRepository.class); + }); + } + + @Test + @DisplayName("Should correctly handle full CRUD lifecycle and sequential previous/removed return value semantics") + void shouldHandleCrudLifecycleAndReturnValues() { + CacheManager cacheManager = new ConcurrentMapCacheManager("camel-kvr"); + SpringCacheKeyValueRepository repo = new SpringCacheKeyValueRepository(cacheManager); + + assertThat(repo.contains("key1")).isFalse(); + assertThat(repo.get("key1")).isNull(); + + Object previous = repo.put("key1", "val1"); + assertThat(previous).isNull(); + assertThat(repo.contains("key1")).isTrue(); + assertThat(repo.get("key1")).isEqualTo("val1"); + + Object updatedPrevious = repo.put("key1", "val2"); + assertThat(updatedPrevious).isEqualTo("val1"); + assertThat(repo.get("key1")).isEqualTo("val2"); + + Object deletedValue = repo.delete("key1"); + assertThat(deletedValue).isEqualTo("val2"); + assertThat(repo.contains("key1")).isFalse(); + + assertThat(repo.delete("non-existent")).isNull(); + } + + @Test + @DisplayName("Should handle putIfAbsent correctly") + void shouldHandlePutIfAbsent() { + CacheManager cacheManager = new ConcurrentMapCacheManager("camel-kvr"); + SpringCacheKeyValueRepository repo = new SpringCacheKeyValueRepository(cacheManager); + + Object existing1 = repo.putIfAbsent("key1", "val1"); + assertThat(existing1).isNull(); + assertThat(repo.get("key1")).isEqualTo("val1"); + + Object existing2 = repo.putIfAbsent("key1", "newVal"); + assertThat(existing2).isEqualTo("val1"); + assertThat(repo.get("key1")).isEqualTo("val1"); + } + + @Test + @DisplayName("Should clear all entries in the target cache") + void shouldClearCache() { + CacheManager cacheManager = new ConcurrentMapCacheManager("camel-kvr"); + SpringCacheKeyValueRepository repo = new SpringCacheKeyValueRepository(cacheManager); + + repo.put("key1", "val1"); + repo.put("key2", "val2"); + repo.clear(); + + assertThat(repo.contains("key1")).isFalse(); + assertThat(repo.contains("key2")).isFalse(); + } + + @Test + @DisplayName("Should inherit and execute SPI default conditional operations (replace and delete)") + void shouldSupportInheritedConditionalOperations() { + CacheManager cacheManager = new ConcurrentMapCacheManager("camel-kvr"); + SpringCacheKeyValueRepository repo = new SpringCacheKeyValueRepository(cacheManager); + + repo.put("key1", "val1"); + + boolean replaced = repo.replace("key1", "val1", "val2", null); + assertThat(replaced).isTrue(); + assertThat(repo.get("key1")).isEqualTo("val2"); + + boolean deletedMismatch = repo.delete("key1", "wrongVal"); + assertThat(deletedMismatch).isFalse(); + assertThat(repo.contains("key1")).isTrue(); + + boolean deletedMatch = repo.delete("key1", "val2"); + assertThat(deletedMatch).isTrue(); + assertThat(repo.contains("key1")).isFalse(); + } + + @Test + @DisplayName("Should throw IllegalStateException when requested named cache is missing in strict CacheManager") + void shouldThrowExceptionWhenCacheIsMissing() { + SimpleCacheManager strictCacheManager = new SimpleCacheManager(); + strictCacheManager.setCaches(Collections.singletonList(new ConcurrentMapCache("other-cache"))); + strictCacheManager.initializeCaches(); + + SpringCacheKeyValueRepository repo = new SpringCacheKeyValueRepository(strictCacheManager); + + assertThatThrownBy(() -> repo.get("key1")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Spring Cache 'camel-kvr' not found in CacheManager"); + } + + @Test + @DisplayName("Should throw UnsupportedOperationException for non-portable keys() and size()") + void shouldThrowUnsupportedForKeysAndSize() { + CacheManager cacheManager = new ConcurrentMapCacheManager("camel-kvr"); + KeyValueRepository repo = new SpringCacheKeyValueRepository(cacheManager); + + assertThatThrownBy(repo::keys) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContaining("portable key enumeration API"); + + assertThatThrownBy(repo::size) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContaining("portable size API"); + } + + @Test + @DisplayName("Should execute TTL-bearing operations successfully without throwing errors") + void shouldExecuteTtlOperationsSuccessfully() { + CacheManager cacheManager = new ConcurrentMapCacheManager("camel-kvr"); + SpringCacheKeyValueRepository repo = new SpringCacheKeyValueRepository(cacheManager); + + repo.put("k1", "v1", Duration.ofMinutes(5)); + repo.putIfAbsent("k2", "v2", Duration.ofMinutes(10)); + repo.put("k1", "v3", Duration.ofMinutes(2)); + + assertThat(repo.get("k1")).isEqualTo("v3"); + assertThat(repo.get("k2")).isEqualTo("v2"); + } + + @Test + @DisplayName("Should maintain complete multi-store isolation between separate repository instances") + void shouldMaintainMultiStoreIsolation() { + CacheManager cacheManager = new ConcurrentMapCacheManager("orders", "inventory"); + + SpringCacheKeyValueRepository ordersRepo = new SpringCacheKeyValueRepository(cacheManager, "orders"); + SpringCacheKeyValueRepository inventoryRepo = new SpringCacheKeyValueRepository(cacheManager, "inventory"); + + ordersRepo.put("item123", "ORDER_PLACED"); + inventoryRepo.put("item123", "IN_STOCK"); + + assertThat(ordersRepo.get("item123")).isEqualTo("ORDER_PLACED"); + assertThat(inventoryRepo.get("item123")).isEqualTo("IN_STOCK"); + + ordersRepo.clear(); + + assertThat(ordersRepo.contains("item123")).isFalse(); + assertThat(inventoryRepo.contains("item123")).isTrue(); + } + + @Test + @DisplayName("Should support custom storeName routing mapped 1:1 to Spring Cache names") + void shouldSupportCustomStoreNameMapping() { + CacheManager cacheManager = new ConcurrentMapCacheManager("myStore"); + SpringCacheKeyValueRepository repo = new SpringCacheKeyValueRepository(cacheManager, "myStore"); + + repo.put("myKey", "myVal"); + assertThat(repo.get("myKey")).isEqualTo("myVal"); + assertThat(repo.getCacheName()).isEqualTo("myStore"); + } + + @Test + @DisplayName("Should auto-configure SpringCacheKeyValueRepository with Caffeine-backed or custom multi-cache Manager") Review Comment: Nit: the display name says "Caffeine-backed", but `CaffeineCacheConfig` creates a `ConcurrentMapCacheManager`. Renaming it (and the config class) would avoid suggesting that Caffeine is tested. ########## core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/statestore/SpringCacheKeyValueRepositoryAutoConfiguration.java: ########## @@ -0,0 +1,39 @@ +/* + * 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.spring.boot.statestore; + +import org.apache.camel.spi.KeyValueRepository; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cache.CacheManager; +import org.springframework.context.annotation.Bean; + +@AutoConfiguration +@ConditionalOnClass({ CacheManager.class, KeyValueRepository.class }) +@ConditionalOnBean(CacheManager.class) +@AutoConfigureAfter(name = { "org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration" }) +public class SpringCacheKeyValueRepositoryAutoConfiguration { + + @Bean Review Comment: Registering this bean whenever a `CacheManager` exists reaches beyond `state-store`. When no repository is configured, `AggregateProcessor`, `IdempotentConsumerReifier` and `CacheReifier` in Camel core also use a single `KeyValueRepository` bean from the registry. For example, an aggregator with `completionTimeout` would no longer start, because `getKeys()` ends in `keys()`, which throws `UnsupportedOperationException` here. Details in the review summary. Could this be made opt-in (for example enabled by a property)? ########## core/camel-spring-boot/src/main/docs/spring-boot.adoc: ########## @@ -610,6 +610,56 @@ public class MyRoute extends RouteBuilder { } ---- +== Spring Cache State Store Support + +The `camel-spring-boot` module provides an auto-configured Apache Camel `KeyValueRepository` backed by Spring's `CacheManager` abstraction (`org.springframework.cache.CacheManager`). + +When a `CacheManager` bean is present in the Spring application context and no explicit `KeyValueRepository` bean is defined, a `SpringCacheKeyValueRepository` is automatically registered. This allows any caching backend supported by Spring Boot (such as Caffeine, Redis via Lettuce, Ehcache, Hazelcast, or JCache) to seamlessly function as an Apache Camel state store backend without requiring extra component-specific dependencies. + +=== Configuration Example (Caffeine) + +[source,yaml] +---- +spring: + cache: + type: caffeine + cache-names: myStore Review Comment: With `spring.cache.cache-names` set, Spring Boot configures `CaffeineCacheManager` with a fixed set of caches (static mode). `getCache("camel-kvr")` then returns `null`, and every `state-store` operation fails with `IllegalStateException`. The routes below use `state-store:myStore`, but the repository never looks up `myStore` (see the comment on line 658). -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
