Skip to content

Commit e3ba53c

Browse files
gnodetclaude
andcommitted
CAMEL-23239: Add camel-state-store component with pluggable key-value 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>
1 parent aed48e7 commit e3ba53c

File tree

57 files changed

+3400
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+3400
-0
lines changed

bom/camel-bom/pom.xml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2257,6 +2257,26 @@
22572257
<artifactId>camel-ssh</artifactId>
22582258
<version>4.19.0-SNAPSHOT</version>
22592259
</dependency>
2260+
<dependency>
2261+
<groupId>org.apache.camel</groupId>
2262+
<artifactId>camel-state-store</artifactId>
2263+
<version>4.19.0-SNAPSHOT</version>
2264+
</dependency>
2265+
<dependency>
2266+
<groupId>org.apache.camel</groupId>
2267+
<artifactId>camel-state-store-caffeine</artifactId>
2268+
<version>4.19.0-SNAPSHOT</version>
2269+
</dependency>
2270+
<dependency>
2271+
<groupId>org.apache.camel</groupId>
2272+
<artifactId>camel-state-store-infinispan</artifactId>
2273+
<version>4.19.0-SNAPSHOT</version>
2274+
</dependency>
2275+
<dependency>
2276+
<groupId>org.apache.camel</groupId>
2277+
<artifactId>camel-state-store-redis</artifactId>
2278+
<version>4.19.0-SNAPSHOT</version>
2279+
</dependency>
22602280
<dependency>
22612281
<groupId>org.apache.camel</groupId>
22622282
<artifactId>camel-stax</artifactId>

catalog/camel-allcomponents/pom.xml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2037,6 +2037,26 @@
20372037
<artifactId>camel-ssh</artifactId>
20382038
<version>${project.version}</version>
20392039
</dependency>
2040+
<dependency>
2041+
<groupId>org.apache.camel</groupId>
2042+
<artifactId>camel-state-store</artifactId>
2043+
<version>${project.version}</version>
2044+
</dependency>
2045+
<dependency>
2046+
<groupId>org.apache.camel</groupId>
2047+
<artifactId>camel-state-store-caffeine</artifactId>
2048+
<version>${project.version}</version>
2049+
</dependency>
2050+
<dependency>
2051+
<groupId>org.apache.camel</groupId>
2052+
<artifactId>camel-state-store-infinispan</artifactId>
2053+
<version>${project.version}</version>
2054+
</dependency>
2055+
<dependency>
2056+
<groupId>org.apache.camel</groupId>
2057+
<artifactId>camel-state-store-redis</artifactId>
2058+
<version>${project.version}</version>
2059+
</dependency>
20402060
<dependency>
20412061
<groupId>org.apache.camel</groupId>
20422062
<artifactId>camel-stax</artifactId>

catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ spring-ws
351351
sql
352352
sql-stored
353353
ssh
354+
state-store
354355
stax
355356
stitch
356357
stomp
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"component": {
3+
"kind": "component",
4+
"name": "state-store",
5+
"title": "State Store",
6+
"description": "Perform key-value operations against a pluggable state store backend.",
7+
"deprecated": false,
8+
"firstVersion": "4.19.0",
9+
"label": "cache",
10+
"javaType": "org.apache.camel.component.statestore.StateStoreComponent",
11+
"supportLevel": "Preview",
12+
"groupId": "org.apache.camel",
13+
"artifactId": "camel-state-store",
14+
"version": "4.19.0-SNAPSHOT",
15+
"scheme": "state-store",
16+
"extendsScheme": "",
17+
"syntax": "state-store:storeName",
18+
"async": false,
19+
"api": false,
20+
"consumerOnly": false,
21+
"producerOnly": true,
22+
"lenientProperties": false,
23+
"browsable": false,
24+
"remote": false
25+
},
26+
"componentProperties": {
27+
"lazyStartProducer": { "index": 0, "kind": "property", "displayName": "Lazy Start Producer", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." },
28+
"autowiredEnabled": { "index": 1, "kind": "property", "displayName": "Autowired Enabled", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether autowiring is enabled. This is used for automatic autowiring options (the option must be marked as autowired) by looking up in the registry to find if there is a single instance of matching type, which then gets configured on the component. This can be used for automatic configuring JDBC data sources, JMS connection factories, AWS Clients, etc." }
29+
},
30+
"headers": {
31+
"CamelStateStoreOperation": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "org.apache.camel.component.statestore.StateStoreOperations", "enum": [ "put", "putIfAbsent", "get", "delete", "contains", "keys", "size", "clear" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The operation to perform", "constantName": "org.apache.camel.component.statestore.StateStoreConstants#OPERATION" },
32+
"CamelStateStoreKey": { "index": 1, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The key to use for the operation", "constantName": "org.apache.camel.component.statestore.StateStoreConstants#KEY" },
33+
"CamelStateStoreTtl": { "index": 2, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Per-message TTL override in milliseconds. Takes precedence over the endpoint ttl option.", "constantName": "org.apache.camel.component.statestore.StateStoreConstants#TTL" }
34+
},
35+
"properties": {
36+
"storeName": { "index": 0, "kind": "path", "displayName": "Store Name", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The name of the state store" },
37+
"operation": { "index": 1, "kind": "parameter", "displayName": "Operation", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "org.apache.camel.component.statestore.StateStoreOperations", "enum": [ "put", "putIfAbsent", "get", "delete", "contains", "keys", "size", "clear" ], "deprecated": false, "autowired": false, "secret": false, "description": "The default operation to perform" },
38+
"ttl": { "index": 2, "kind": "parameter", "displayName": "Ttl", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "long", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 0, "description": "Time-to-live in milliseconds for entries. 0 means no expiry." },
39+
"lazyStartProducer": { "index": 3, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." },
40+
"backend": { "index": 4, "kind": "parameter", "displayName": "Backend", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.component.statestore.StateStoreBackend", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "memory", "description": "The backend to use. Default is an in-memory store. Set to a bean reference (e.g. #myBackend) for custom backends." }
41+
}
42+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
4+
Licensed to the Apache Software Foundation (ASF) under one or more
5+
contributor license agreements. See the NOTICE file distributed with
6+
this work for additional information regarding copyright ownership.
7+
The ASF licenses this file to You under the Apache License, Version 2.0
8+
(the "License"); you may not use this file except in compliance with
9+
the License. You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing, software
14+
distributed under the License is distributed on an "AS IS" BASIS,
15+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
See the License for the specific language governing permissions and
17+
limitations under the License.
18+
19+
-->
20+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
21+
<modelVersion>4.0.0</modelVersion>
22+
23+
<parent>
24+
<groupId>org.apache.camel</groupId>
25+
<artifactId>camel-state-store-parent</artifactId>
26+
<version>4.19.0-SNAPSHOT</version>
27+
</parent>
28+
29+
<artifactId>camel-state-store-caffeine</artifactId>
30+
<packaging>jar</packaging>
31+
32+
<name>Camel :: State Store :: Caffeine</name>
33+
<description>Camel State Store backend using Caffeine cache</description>
34+
35+
<properties>
36+
<firstVersion>4.19.0</firstVersion>
37+
</properties>
38+
39+
<dependencies>
40+
41+
<dependency>
42+
<groupId>org.apache.camel</groupId>
43+
<artifactId>camel-state-store</artifactId>
44+
</dependency>
45+
46+
<dependency>
47+
<groupId>com.github.ben-manes.caffeine</groupId>
48+
<artifactId>caffeine</artifactId>
49+
<version>${caffeine-version}</version>
50+
</dependency>
51+
52+
<!-- testing -->
53+
<dependency>
54+
<groupId>org.apache.camel</groupId>
55+
<artifactId>camel-test-junit5</artifactId>
56+
<scope>test</scope>
57+
</dependency>
58+
59+
</dependencies>
60+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Generated by camel build tools - do NOT edit this file!
2+
name=state-store-caffeine
3+
groupId=org.apache.camel
4+
artifactId=camel-state-store-caffeine
5+
version=4.19.0-SNAPSHOT
6+
projectName=Camel :: State Store :: Caffeine
7+
projectDescription=Camel State Store backend using Caffeine cache
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"other": {
3+
"kind": "other",
4+
"name": "state-store-caffeine",
5+
"title": "State Store Caffeine",
6+
"description": "Camel State Store backend using Caffeine cache",
7+
"deprecated": false,
8+
"firstVersion": "4.19.0",
9+
"supportLevel": "Preview",
10+
"groupId": "org.apache.camel",
11+
"artifactId": "camel-state-store-caffeine",
12+
"version": "4.19.0-SNAPSHOT"
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.camel.component.statestore.caffeine;
18+
19+
import java.time.Duration;
20+
import java.util.Set;
21+
22+
import com.github.benmanes.caffeine.cache.Cache;
23+
import com.github.benmanes.caffeine.cache.Caffeine;
24+
import com.github.benmanes.caffeine.cache.Expiry;
25+
import org.apache.camel.component.statestore.StateStoreBackend;
26+
27+
/**
28+
* A {@link StateStoreBackend} implementation backed by Caffeine cache. Supports per-entry TTL using Caffeine's variable
29+
* expiration.
30+
*/
31+
public class CaffeineStateStoreBackend implements StateStoreBackend {
32+
33+
private Cache<String, TimedValue> cache;
34+
private int maximumSize = 10_000;
35+
36+
@Override
37+
public Object put(String key, Object value, long ttlMillis) {
38+
TimedValue previous = cache.getIfPresent(key);
39+
cache.put(key, new TimedValue(value, ttlMillis));
40+
return previous != null ? previous.value() : null;
41+
}
42+
43+
@Override
44+
public Object get(String key) {
45+
TimedValue entry = cache.getIfPresent(key);
46+
return entry != null ? entry.value() : null;
47+
}
48+
49+
@Override
50+
public Object delete(String key) {
51+
TimedValue previous = cache.getIfPresent(key);
52+
cache.invalidate(key);
53+
return previous != null ? previous.value() : null;
54+
}
55+
56+
@Override
57+
public boolean contains(String key) {
58+
return cache.getIfPresent(key) != null;
59+
}
60+
61+
@Override
62+
public Object putIfAbsent(String key, Object value, long ttlMillis) {
63+
TimedValue existing = cache.getIfPresent(key);
64+
if (existing != null) {
65+
return existing.value();
66+
}
67+
cache.put(key, new TimedValue(value, ttlMillis));
68+
return null;
69+
}
70+
71+
@Override
72+
public int size() {
73+
cache.cleanUp();
74+
return (int) cache.estimatedSize();
75+
}
76+
77+
@Override
78+
public Set<String> keys() {
79+
return Set.copyOf(cache.asMap().keySet());
80+
}
81+
82+
@Override
83+
public void clear() {
84+
cache.invalidateAll();
85+
}
86+
87+
@Override
88+
public void start() {
89+
cache = Caffeine.newBuilder()
90+
.maximumSize(maximumSize)
91+
.expireAfter(new Expiry<String, TimedValue>() {
92+
@Override
93+
public long expireAfterCreate(String key, TimedValue value, long currentTime) {
94+
return value.ttlMillis() > 0
95+
? Duration.ofMillis(value.ttlMillis()).toNanos()
96+
: Long.MAX_VALUE;
97+
}
98+
99+
@Override
100+
public long expireAfterUpdate(String key, TimedValue value, long currentTime, long currentDuration) {
101+
return value.ttlMillis() > 0
102+
? Duration.ofMillis(value.ttlMillis()).toNanos()
103+
: Long.MAX_VALUE;
104+
}
105+
106+
@Override
107+
public long expireAfterRead(String key, TimedValue value, long currentTime, long currentDuration) {
108+
return currentDuration;
109+
}
110+
})
111+
.build();
112+
}
113+
114+
@Override
115+
public void stop() {
116+
if (cache != null) {
117+
cache.invalidateAll();
118+
cache.cleanUp();
119+
cache = null;
120+
}
121+
}
122+
123+
public int getMaximumSize() {
124+
return maximumSize;
125+
}
126+
127+
public void setMaximumSize(int maximumSize) {
128+
this.maximumSize = maximumSize;
129+
}
130+
131+
private record TimedValue(Object value, long ttlMillis) {
132+
}
133+
}

0 commit comments

Comments
 (0)