CAMEL-23239: Add camel-state-store component with pluggable key-value store#22158
CAMEL-23239: Add camel-state-store component with pluggable key-value store#22158gnodet wants to merge 6 commits intoapache:mainfrom
Conversation
|
🌟 Thank you for your contribution to the Apache Camel project! 🌟 🐫 Apache Camel Committers, please review the following items:
|
a434f02 to
1e1a769
Compare
|
Hm, this issue doesn't seem to exist on ASF Jira. |
|
The title refers to the wrong issue, CAMEL-23228 is "Add DataWeave to DataSonnet transpiler in camel-jbang". |
|
the commit message also needs to be updated with the correct jira issue number |
apupier
left a comment
There was a problem hiding this comment.
Can you elaborate on the difference between these components and the existing Camel caffeine cache component? https://camel.apache.org/components/4.18.x/caffeine-cache-component.html
… store - New state-store component providing a unified key-value store API - Operations: put, putIfAbsent, get, delete, contains, keys, size, clear - Per-message TTL override via CamelStateStoreTtl header - Multi-module structure with pluggable backends: - camel-state-store: core + in-memory backend (ConcurrentHashMap, lazy TTL) - camel-state-store-caffeine: Caffeine cache with per-entry variable expiry - camel-state-store-redis: Redisson RMapCache with native TTL - camel-state-store-infinispan: Hot Rod client with lifespan TTL - Registered in MojoHelper, parent BOM, allcomponents, catalog - 22 unit tests (core + caffeine) and 15 integration tests (Redis + Infinispan) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1e1a769 to
e3ba53c
Compare
|
Thanks for the reviews! I've pushed an update:
@apupier — regarding the difference with Claude Code on behalf of Guillaume Nodet |
Design note:
|
| [source,java] | ||
| ---- | ||
| @BindToRegistry("infinispanBackend") | ||
| public InfinispanStateStoreBackend infinispan() { |
There was a problem hiding this comment.
@gnodet is this kind of configuration doable only via java beans? would it be possible to provide this configuration via properties?
There was a problem hiding this comment.
Good point! I've pushed an update that addresses this:
Property-based configuration — backends can now be fully configured via application.properties using Camel's camel.beans.* syntax, without writing any Java code:
# Caffeine example
camel.beans.caffeineBackend = #class:org.apache.camel.component.statestore.caffeine.CaffeineStateStoreBackend
camel.beans.caffeineBackend.maximumSize = 50000# Redis example
camel.beans.redisBackend = #class:org.apache.camel.component.statestore.redis.RedisStateStoreBackend
camel.beans.redisBackend.redisUrl = redis://myhost:6379
camel.beans.redisBackend.mapName = my-app-state# Infinispan example
camel.beans.infinispanBackend = #class:org.apache.camel.component.statestore.infinispan.InfinispanStateStoreBackend
camel.beans.infinispanBackend.hosts = myhost:11222
camel.beans.infinispanBackend.cacheName = my-cacheAuto-discovery — if a single StateStoreBackend bean is found in the registry (whether registered via Java or via properties), it is automatically used by all state-store endpoints. No backend=#beanName reference needed:
- route:
from:
uri: direct:store
steps:
- setHeader:
name: CamelStateStoreKey
constant: myKey
- to:
uri: state-store:myStore?operation=putBoth features are covered by new tests (CaffeineStateStorePropertiesTest and StateStoreAutoDiscoveryTest).
Claude Code on behalf of Guillaume Nodet
…tion - Auto-discover a single StateStoreBackend from the registry when no explicit backend is specified on the endpoint - Add test for auto-discovery from registry (StateStoreAutoDiscoveryTest) - Add test for property-based configuration via camel.beans.* (CaffeineStateStorePropertiesTest) using CamelMainTestSupport - Update documentation with property-based configuration examples for all backends (Caffeine, Redis, Infinispan) and auto-discovery section - Add camel-test-main-junit5 dependency to caffeine module for Main tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add RedisStateStorePropertiesIT: fully property-based configuration via camel.beans.* with auto-discovery (no backend=# in routes) - Add InfinispanStateStorePropertiesIT: property-based backend creation with programmatic RemoteCacheManager injection (auth requires Java) - Use camel-test-main-junit5 in all modules for CamelMainTestSupport Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix CaffeineStateStoreBackend.putIfAbsent() race condition (use atomic asMap().putIfAbsent()) - Guard start() in Caffeine/Infinispan backends to prevent data loss on repeated calls - Move backend.start() from endpoint to component (called once per backend in computeIfAbsent) - Add WARN log when multiple backends found in registry (falls back to InMemory) - Add retry logging in InfinispanStateStoreBackend.start() - Remove misleading remote=false and incorrect defaultValue="memory" from @UriEndpoint/@UriParam - Add firstVersion property to core pom.xml - Add Javadoc warning on StateStoreBackend.putIfAbsent() default (not atomic) - Add error case tests (missing key, missing operation, multiple backends fallback) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9acd8bf to
8bbae8d
Compare
| String key = requireKey(message); | ||
| Object value = message.getBody(); | ||
| Object existing = backend.putIfAbsent(key, value, ttl); | ||
| message.setBody(existing); |
There was a problem hiding this comment.
maybe normal but I was surprised when reading it that when performing and operation to store a state, we change the message body with the previous value
There was a problem hiding this comment.
I'm even wondering why modifying the body when setting something in the state store?
There was a problem hiding this comment.
The return-previous-value behavior follows java.util.Map.put() / Map.putIfAbsent() semantics:
put— returns the previous value (ornullif key was new), same asMap.put()putIfAbsent— returns the existing value if key already existed (meaning nothing was stored), ornullif the value was stored successfully
This is intentional — it's the only way to communicate the result back to the route, and it enables patterns like idempotent deduplication:
from("kafka:orders")
.setHeader(StateStoreConstants.KEY, simple("${header.orderId}"))
.to("state-store:processed?operation=putIfAbsent")
.choice()
.when(body().isNotNull()) // non-null means key already existed = duplicate
.log("Duplicate order, skipping").stop()
.end()
.to("direct:process-order");This is consistent with how camel-caffeine-cache handles it (the PUT action also returns the previous value in the body).
Claude Code on behalf of Guillaume Nodet
apupier
left a comment
There was a problem hiding this comment.
Seems fine but I'm really not an expert on this piece of code and what it applies. i think it would be nice to have someone more experimented reviewing it.
- Fix Infinispan backend lifecycle: nullify cache in stop() to allow restart - Fix InMemory putIfAbsent race condition: use compute() for atomicity - Fix Caffeine put()/delete() atomicity: use asMap() for atomic previous-value return - Fix Redis stop(): nullify mapCache for lifecycle hygiene - Increase TTL test sleep margins to 5x to prevent CI flakiness - Document first-one-wins semantics on getOrCreateBackend() Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| lastException = e; | ||
| LOG.warn("Failed to access cache '{}' (attempt {}/10): {}", cacheName, i + 1, e.getMessage()); | ||
| try { | ||
| Thread.sleep(1000); |
There was a problem hiding this comment.
what about using resilience4j? would it be overkill?
This sounds fine, but from a usability perspective, it would be nice to be able to reuse the existing Camel Infinispan or Redis components. This is not the first time we are "reinventing the wheel" for this kind of use case. Given that we already have Infinispan and Redis components with their own configurations — which are supposed to be more feature-rich than the current state store implementation — I was wondering whether we could find a way to reuse those components (or at least part of their logic) for these use cases. |
I'm failing to see the reason why we should have another component of this kind, it will cause another dose of entropy. |
Pros:
Cons:
Conclusion: So, I am not opposed to have this one on the code base and I think there is potential for making stateful operations/caching a bit more elegant. The consistency of the operations make the behavior a bit more predictable regardless of the backend in use. However, I believe that we should have a greater discussion about what our vision and/or medium term goals for the problem is. Even if the API is elegant, it may not be enough if we don't collectively share the same vision about how we would like this to be ... As such, I'm currently leaning towards 0 on this one and would like to hear more feedback from the community before having a decision on -1 or +1 from my side. |
JIRA: CAMEL-23239
Motivation
Camel provides dedicated components for specific caching/store technologies (Caffeine, Redis, Infinispan, etc.), each with its own API surface. This works well when users need the full feature set of a specific technology, but it creates friction when the actual need is simple: store and retrieve key-value pairs.
The
camel-state-storecomponent follows the same "choose the problem, not the technology" pattern that Camel already uses successfully in other areas:camel-sql/camel-jdbc— generic SQL over any JDBC database, without locking into a vendorcamel-jms— generic messaging over any JMS provider (withcamel-activemq,camel-amqpas pre-configured variants)camel-jcache— generic caching via JSR-107 over any compliant implementationSimilarly,
camel-state-storeprovides a unified key-value API where:Difference from existing cache components
camel-state-storecamel-caffeine-cache/camel-infinispan/ etc.StateStoreBackendinterfaceSummary
camel-state-storecomponent providing a simple, unified key-value store API with pluggable backendsput,putIfAbsent,get,delete,contains,keys,size,clearCamelStateStoreTtlheaderStateStoreBackendbean is in the registry, it is used automatically — nobackend=#beanNameneeded on endpoints. Logs a WARN when multiple backends are found and falls back to in-memory.application.propertiesusingcamel.beans.*syntax — no Java code requiredcamel-state-store: core + in-memory backend (ConcurrentHashMap, lazy TTL)camel-state-store-caffeine: Caffeine cache with per-entry variable expirycamel-state-store-redis: RedissonRMapCachewith native TTLcamel-state-store-infinispan: Hot Rod client with lifespan TTLStateStoreBackendinterface and bean referencesstart()called once per backend inside the component, idempotent guards in all backendsRoute examples
Simple put/get with in-memory backend (Java DSL)
Caching HTTP responses with TTL (Java DSL)
Idempotent deduplication pattern (Java DSL)
YAML DSL with property-configured Redis backend
Dynamic operation via header
Configuration examples
Java bean registration
Property-based configuration (no Java required)
Design decision: StateStoreBackend interface location
We considered moving the
StateStoreBackendinterface tocamel-apialongside existing SPIs (IdempotentRepository,AggregationRepository,StateRepository). We decided against it because:camel-apiSPIs are there because core EIPs consume them (idempotent consumer, aggregator).StateStoreBackendis only consumed by the component itself.If a future core EIP needs a generic key-value store, the interface can be promoted then.
Test plan
StateStoreTest— 13 tests)StateStoreTtlTest— 2 tests)StateStoreAutoDiscoveryTest— 1 test)StateStoreMultiBackendFallbackTest— 1 test)StateStoreTest— 2 tests)CaffeineStateStoreBackendTest— 9 tests)CaffeineStateStorePropertiesTest— 3 tests)RedisStateStoreBackendIT— 8 tests)RedisStateStorePropertiesIT— 2 tests)InfinispanStateStoreBackendIT— 7 tests)InfinispanStateStorePropertiesIT— 2 tests)formatter:formatandimpsort:sort