This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 55754e0378c8 CAMEL-26753: KeyValueRepository - a very large TTL no
longer wraps around to a time in the past (#29534)
55754e0378c8 is described below
commit 55754e0378c828f55f539cf2654658e9fd4b532c
Author: allthingssecurity <[email protected]>
AuthorDate: Tue Nov 10 13:27:19 2026 +0530
CAMEL-26753: KeyValueRepository - a very large TTL no longer wraps around
to a time in the past (#29534)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../processor/CaffeineKeyValueRepository.java | 18 ++++++---
.../processor/CaffeineKeyValueRepositoryTest.java | 29 ++++++++++++++-
.../processor/EhcacheKeyValueRepository.java | 3 +-
.../processor/EhcacheKeyValueRepositoryTest.java | 29 ++++++++++++++-
.../jcache/processor/JCacheKeyValueRepository.java | 3 +-
.../processor/JCacheKeyValueRepositoryTest.java | 29 ++++++++++++++-
.../keyvalue/jpa/JpaKeyValueRepository.java | 2 +-
.../keyvalue/jpa/JpaKeyValueRepositoryTest.java | 43 ++++++++++++++++++++++
.../keyvalue/jdbc/JdbcKeyValueRepository.java | 2 +-
.../keyvalue/jdbc/JdbcKeyValueRepositoryTest.java | 27 ++++++++++++++
.../camel/support/KeyValueRepositoryHelper.java | 23 +++++++++++-
.../camel/support/MemoryKeyValueRepository.java | 2 +-
.../support/KeyValueRepositoryHelperTest.java | 23 ++++++++++++
.../support/MemoryKeyValueRepositoryTest.java | 26 +++++++++++++
14 files changed, 245 insertions(+), 14 deletions(-)
diff --git
a/components/camel-caffeine/src/main/java/org/apache/camel/component/caffeine/processor/CaffeineKeyValueRepository.java
b/components/camel-caffeine/src/main/java/org/apache/camel/component/caffeine/processor/CaffeineKeyValueRepository.java
index 9b38ef707b22..5e1ea90c326f 100644
---
a/components/camel-caffeine/src/main/java/org/apache/camel/component/caffeine/processor/CaffeineKeyValueRepository.java
+++
b/components/camel-caffeine/src/main/java/org/apache/camel/component/caffeine/processor/CaffeineKeyValueRepository.java
@@ -114,7 +114,7 @@ public class CaffeineKeyValueRepository extends
ServiceSupport implements KeyVal
@Override
@ManagedOperation(description = "Put a key-value pair with optional TTL")
public Object put(String key, Object value, Duration ttl) {
- long ttlNanos = hasPositiveTtl(ttl) ? ttl.toNanos() : Long.MAX_VALUE;
+ long ttlNanos = toTtlNanos(ttl);
TtlValue previous = cache.asMap().put(key, new TtlValue(value,
ttlNanos));
return previous != null ? previous.value() : null;
}
@@ -146,7 +146,7 @@ public class CaffeineKeyValueRepository extends
ServiceSupport implements KeyVal
@Override
public Object putIfAbsent(String key, Object value, Duration ttl) {
- long ttlNanos = hasPositiveTtl(ttl) ? ttl.toNanos() : Long.MAX_VALUE;
+ long ttlNanos = toTtlNanos(ttl);
ConcurrentMap<String, TtlValue> map = cache.asMap();
TtlValue newEntry = new TtlValue(value, ttlNanos);
TtlValue existing = map.putIfAbsent(key, newEntry);
@@ -155,7 +155,7 @@ public class CaffeineKeyValueRepository extends
ServiceSupport implements KeyVal
@Override
public boolean replace(String key, Object expectedOldValue, Object
newValue, Duration ttl) {
- long ttlNanos = hasPositiveTtl(ttl) ? ttl.toNanos() : Long.MAX_VALUE;
+ long ttlNanos = toTtlNanos(ttl);
boolean[] replaced = { false };
cache.asMap().computeIfPresent(key, (k, current) -> {
if (Objects.equals(current.value(), expectedOldValue)) {
@@ -187,8 +187,16 @@ public class CaffeineKeyValueRepository extends
ServiceSupport implements KeyVal
return (int) cache.estimatedSize();
}
- private static boolean hasPositiveTtl(Duration ttl) {
- return ttl != null && !ttl.isZero() && !ttl.isNegative();
+ private static long toTtlNanos(Duration ttl) {
+ if (ttl == null || ttl.isZero() || ttl.isNegative()) {
+ return Long.MAX_VALUE;
+ }
+ try {
+ return ttl.toNanos();
+ } catch (ArithmeticException e) {
+ // a TTL of about 292 years or more does not fit in a long of
nanoseconds: the entry does not expire
+ return Long.MAX_VALUE;
+ }
}
@Override
diff --git
a/components/camel-caffeine/src/test/java/org/apache/camel/component/caffeine/processor/CaffeineKeyValueRepositoryTest.java
b/components/camel-caffeine/src/test/java/org/apache/camel/component/caffeine/processor/CaffeineKeyValueRepositoryTest.java
index fe70ec6d794b..3f0c77cc6963 100644
---
a/components/camel-caffeine/src/test/java/org/apache/camel/component/caffeine/processor/CaffeineKeyValueRepositoryTest.java
+++
b/components/camel-caffeine/src/test/java/org/apache/camel/component/caffeine/processor/CaffeineKeyValueRepositoryTest.java
@@ -17,6 +17,7 @@
package org.apache.camel.component.caffeine.processor;
import java.time.Duration;
+import java.time.temporal.ChronoUnit;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -188,7 +189,7 @@ class CaffeineKeyValueRepositoryTest {
@Test
void testNoTtlWithZero() {
- repository.put("key1", "value1", null);
+ repository.put("key1", "value1", Duration.ZERO);
assertThat(repository.get("key1")).isEqualTo("value1");
assertThat(repository.contains("key1")).isTrue();
@@ -300,4 +301,30 @@ class CaffeineKeyValueRepositoryTest {
assertThat(deleted).isFalse();
}
+
+ @Test
+ void testTtlTooLargeToAddToTheCurrentTimeDoesNotExpire() {
+ // Duration.toNanos() overflows for this TTL
+ Duration huge = Duration.ofMillis(Long.MAX_VALUE);
+ assertThat(repository.put("key1", "value1", huge)).isNull();
+ assertThat(repository.putIfAbsent("key2", "value2", huge)).isNull();
+ repository.put("key3", "value3", huge);
+ assertThat(repository.replace("key3", "value3", "value3b",
huge)).isTrue();
+
+ assertThat(repository.get("key1")).isEqualTo("value1");
+ assertThat(repository.contains("key1")).isTrue();
+ assertThat(repository.putIfAbsent("key2", "other",
null)).isEqualTo("value2");
+ assertThat(repository.get("key3")).isEqualTo("value3b");
+ assertThat(repository.keys()).containsExactlyInAnyOrder("key1",
"key2", "key3");
+ assertThat(repository.size()).isEqualTo(3);
+ }
+
+ @Test
+ void testTtlLongerThanLongMaxValueMillisDoesNotExpire() {
+ // Duration.toNanos() overflows for this TTL
+ repository.put("key1", "value1", ChronoUnit.FOREVER.getDuration());
+
+ assertThat(repository.get("key1")).isEqualTo("value1");
+ assertThat(repository.keys()).containsExactly("key1");
+ }
}
diff --git
a/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/processor/EhcacheKeyValueRepository.java
b/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/processor/EhcacheKeyValueRepository.java
index 760372bc9739..18bb6e192a36 100644
---
a/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/processor/EhcacheKeyValueRepository.java
+++
b/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/processor/EhcacheKeyValueRepository.java
@@ -29,6 +29,7 @@ import org.apache.camel.component.ehcache.EhcacheManager;
import org.apache.camel.spi.Configurer;
import org.apache.camel.spi.KeyValueRepository;
import org.apache.camel.spi.Metadata;
+import org.apache.camel.support.KeyValueRepositoryHelper;
import org.apache.camel.support.KeyValueTtlValue;
import org.apache.camel.support.service.ServiceSupport;
import org.apache.camel.util.ObjectHelper;
@@ -239,7 +240,7 @@ public class EhcacheKeyValueRepository extends
ServiceSupport implements KeyValu
}
private static KeyValueTtlValue newEntry(Object value, Duration ttl) {
- long expiresAt = hasPositiveTtl(ttl) ? System.currentTimeMillis() +
ttl.toMillis() : Long.MAX_VALUE;
+ long expiresAt = hasPositiveTtl(ttl) ?
KeyValueRepositoryHelper.expiresAt(ttl) : Long.MAX_VALUE;
return new KeyValueTtlValue(value, expiresAt);
}
diff --git
a/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/processor/EhcacheKeyValueRepositoryTest.java
b/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/processor/EhcacheKeyValueRepositoryTest.java
index f892dd324549..a83544c1efdd 100644
---
a/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/processor/EhcacheKeyValueRepositoryTest.java
+++
b/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/processor/EhcacheKeyValueRepositoryTest.java
@@ -17,6 +17,7 @@
package org.apache.camel.component.ehcache.processor;
import java.time.Duration;
+import java.time.temporal.ChronoUnit;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -189,7 +190,7 @@ class EhcacheKeyValueRepositoryTest {
@Test
void testNoTtlWithZero() {
- repository.put("key1", "value1", null);
+ repository.put("key1", "value1", Duration.ZERO);
assertThat(repository.get("key1")).isEqualTo("value1");
assertThat(repository.contains("key1")).isTrue();
@@ -293,4 +294,30 @@ class EhcacheKeyValueRepositoryTest {
assertThat(deleted).isFalse();
}
+
+ @Test
+ void testTtlTooLargeToAddToTheCurrentTimeDoesNotExpire() {
+ // the current time plus this TTL is beyond Long.MAX_VALUE
+ Duration huge = Duration.ofMillis(Long.MAX_VALUE);
+ assertThat(repository.put("key1", "value1", huge)).isNull();
+ assertThat(repository.putIfAbsent("key2", "value2", huge)).isNull();
+ repository.put("key3", "value3", huge);
+ assertThat(repository.replace("key3", "value3", "value3b",
huge)).isTrue();
+
+ assertThat(repository.get("key1")).isEqualTo("value1");
+ assertThat(repository.contains("key1")).isTrue();
+ assertThat(repository.putIfAbsent("key2", "other",
null)).isEqualTo("value2");
+ assertThat(repository.get("key3")).isEqualTo("value3b");
+ assertThat(repository.keys()).containsExactlyInAnyOrder("key1",
"key2", "key3");
+ assertThat(repository.size()).isEqualTo(3);
+ }
+
+ @Test
+ void testTtlLongerThanLongMaxValueMillisDoesNotExpire() {
+ // Duration.toMillis() overflows for this TTL
+ repository.put("key1", "value1", ChronoUnit.FOREVER.getDuration());
+
+ assertThat(repository.get("key1")).isEqualTo("value1");
+ assertThat(repository.keys()).containsExactly("key1");
+ }
}
diff --git
a/components/camel-jcache/src/main/java/org/apache/camel/component/jcache/processor/JCacheKeyValueRepository.java
b/components/camel-jcache/src/main/java/org/apache/camel/component/jcache/processor/JCacheKeyValueRepository.java
index cae9ac7b9a2d..8ca817782c0c 100644
---
a/components/camel-jcache/src/main/java/org/apache/camel/component/jcache/processor/JCacheKeyValueRepository.java
+++
b/components/camel-jcache/src/main/java/org/apache/camel/component/jcache/processor/JCacheKeyValueRepository.java
@@ -35,6 +35,7 @@ import org.apache.camel.component.jcache.JCacheManager;
import org.apache.camel.spi.Configurer;
import org.apache.camel.spi.KeyValueRepository;
import org.apache.camel.spi.Metadata;
+import org.apache.camel.support.KeyValueRepositoryHelper;
import org.apache.camel.support.KeyValueTtlValue;
import org.apache.camel.support.service.ServiceSupport;
import org.apache.camel.util.ObjectHelper;
@@ -245,7 +246,7 @@ public class JCacheKeyValueRepository extends
ServiceSupport implements CamelCon
}
private static KeyValueTtlValue newEntry(Object value, Duration ttl) {
- long expiresAt = hasPositiveTtl(ttl) ? System.currentTimeMillis() +
ttl.toMillis() : Long.MAX_VALUE;
+ long expiresAt = hasPositiveTtl(ttl) ?
KeyValueRepositoryHelper.expiresAt(ttl) : Long.MAX_VALUE;
return new KeyValueTtlValue(value, expiresAt);
}
diff --git
a/components/camel-jcache/src/test/java/org/apache/camel/component/jcache/processor/JCacheKeyValueRepositoryTest.java
b/components/camel-jcache/src/test/java/org/apache/camel/component/jcache/processor/JCacheKeyValueRepositoryTest.java
index f6faa5579c4e..70c534b09063 100644
---
a/components/camel-jcache/src/test/java/org/apache/camel/component/jcache/processor/JCacheKeyValueRepositoryTest.java
+++
b/components/camel-jcache/src/test/java/org/apache/camel/component/jcache/processor/JCacheKeyValueRepositoryTest.java
@@ -17,6 +17,7 @@
package org.apache.camel.component.jcache.processor;
import java.time.Duration;
+import java.time.temporal.ChronoUnit;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -189,7 +190,7 @@ class JCacheKeyValueRepositoryTest extends CamelTestSupport
{
@Test
void testNoTtlWithZero() {
- repository.put("key1", "value1", null);
+ repository.put("key1", "value1", Duration.ZERO);
assertThat(repository.get("key1")).isEqualTo("value1");
assertThat(repository.contains("key1")).isTrue();
@@ -293,4 +294,30 @@ class JCacheKeyValueRepositoryTest extends
CamelTestSupport {
assertThat(deleted).isFalse();
}
+
+ @Test
+ void testTtlTooLargeToAddToTheCurrentTimeDoesNotExpire() {
+ // the current time plus this TTL is beyond Long.MAX_VALUE
+ Duration huge = Duration.ofMillis(Long.MAX_VALUE);
+ assertThat(repository.put("key1", "value1", huge)).isNull();
+ assertThat(repository.putIfAbsent("key2", "value2", huge)).isNull();
+ repository.put("key3", "value3", huge);
+ assertThat(repository.replace("key3", "value3", "value3b",
huge)).isTrue();
+
+ assertThat(repository.get("key1")).isEqualTo("value1");
+ assertThat(repository.contains("key1")).isTrue();
+ assertThat(repository.putIfAbsent("key2", "other",
null)).isEqualTo("value2");
+ assertThat(repository.get("key3")).isEqualTo("value3b");
+ assertThat(repository.keys()).containsExactlyInAnyOrder("key1",
"key2", "key3");
+ assertThat(repository.size()).isEqualTo(3);
+ }
+
+ @Test
+ void testTtlLongerThanLongMaxValueMillisDoesNotExpire() {
+ // Duration.toMillis() overflows for this TTL
+ repository.put("key1", "value1", ChronoUnit.FOREVER.getDuration());
+
+ assertThat(repository.get("key1")).isEqualTo("value1");
+ assertThat(repository.keys()).containsExactly("key1");
+ }
}
diff --git
a/components/camel-jpa/src/main/java/org/apache/camel/processor/keyvalue/jpa/JpaKeyValueRepository.java
b/components/camel-jpa/src/main/java/org/apache/camel/processor/keyvalue/jpa/JpaKeyValueRepository.java
index 882321343ee2..08e2ed6f67ae 100644
---
a/components/camel-jpa/src/main/java/org/apache/camel/processor/keyvalue/jpa/JpaKeyValueRepository.java
+++
b/components/camel-jpa/src/main/java/org/apache/camel/processor/keyvalue/jpa/JpaKeyValueRepository.java
@@ -528,7 +528,7 @@ public class JpaKeyValueRepository extends ServiceSupport
implements KeyValueRep
if (ttl == null || ttl.isZero() || ttl.isNegative()) {
return 0;
}
- return System.currentTimeMillis() + ttl.toMillis();
+ return KeyValueRepositoryHelper.expiresAt(ttl);
}
private static void closeEntityManager(EntityManager entityManager) {
diff --git
a/components/camel-jpa/src/test/java/org/apache/camel/processor/keyvalue/jpa/JpaKeyValueRepositoryTest.java
b/components/camel-jpa/src/test/java/org/apache/camel/processor/keyvalue/jpa/JpaKeyValueRepositoryTest.java
index d3ed2eca3f8e..f0abf23c3825 100644
---
a/components/camel-jpa/src/test/java/org/apache/camel/processor/keyvalue/jpa/JpaKeyValueRepositoryTest.java
+++
b/components/camel-jpa/src/test/java/org/apache/camel/processor/keyvalue/jpa/JpaKeyValueRepositoryTest.java
@@ -17,6 +17,7 @@
package org.apache.camel.processor.keyvalue.jpa;
import java.time.Duration;
+import java.time.temporal.ChronoUnit;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -253,4 +254,46 @@ class JpaKeyValueRepositoryTest {
assertEquals("value1", repository.putIfAbsent("key1", "value2"));
assertEquals("value1", repository.get("key1"));
}
+
+ @Test
+ void testNoTtlWithZero() {
+ repository.put("key1", "value1", Duration.ZERO);
+
+ assertEquals("value1", repository.get("key1"));
+ assertEquals(Set.of("key1"), repository.keys());
+ }
+
+ @Test
+ void testNoTtlWithNegative() {
+ repository.put("key1", "value1", Duration.ofMillis(-1));
+
+ assertEquals("value1", repository.get("key1"));
+ assertEquals(Set.of("key1"), repository.keys());
+ }
+
+ @Test
+ void testTtlTooLargeToAddToTheCurrentTimeDoesNotExpire() {
+ // the current time plus this TTL is beyond Long.MAX_VALUE
+ Duration huge = Duration.ofMillis(Long.MAX_VALUE);
+ assertNull(repository.put("key1", "value1", huge));
+ assertNull(repository.putIfAbsent("key2", "value2", huge));
+ repository.put("key3", "value3", huge);
+ assertTrue(repository.replace("key3", "value3", "value3b", huge));
+
+ assertEquals("value1", repository.get("key1"));
+ assertTrue(repository.contains("key1"));
+ assertEquals("value2", repository.putIfAbsent("key2", "other", null));
+ assertEquals("value3b", repository.get("key3"));
+ assertEquals(Set.of("key1", "key2", "key3"), repository.keys());
+ assertEquals(3, repository.size());
+ }
+
+ @Test
+ void testTtlLongerThanLongMaxValueMillisDoesNotExpire() {
+ // Duration.toMillis() overflows for this TTL
+ repository.put("key1", "value1", ChronoUnit.FOREVER.getDuration());
+
+ assertEquals("value1", repository.get("key1"));
+ assertEquals(Set.of("key1"), repository.keys());
+ }
}
diff --git
a/components/camel-sql/src/main/java/org/apache/camel/processor/keyvalue/jdbc/JdbcKeyValueRepository.java
b/components/camel-sql/src/main/java/org/apache/camel/processor/keyvalue/jdbc/JdbcKeyValueRepository.java
index 70fb6d0da8e3..ea7caaa7f0d0 100644
---
a/components/camel-sql/src/main/java/org/apache/camel/processor/keyvalue/jdbc/JdbcKeyValueRepository.java
+++
b/components/camel-sql/src/main/java/org/apache/camel/processor/keyvalue/jdbc/JdbcKeyValueRepository.java
@@ -428,7 +428,7 @@ public class JdbcKeyValueRepository extends ServiceSupport
implements KeyValueRe
if (ttl == null || ttl.isZero() || ttl.isNegative()) {
return 0;
}
- return System.currentTimeMillis() + ttl.toMillis();
+ return KeyValueRepositoryHelper.expiresAt(ttl);
}
// ---- Getters and Setters ----
diff --git
a/components/camel-sql/src/test/java/org/apache/camel/processor/keyvalue/jdbc/JdbcKeyValueRepositoryTest.java
b/components/camel-sql/src/test/java/org/apache/camel/processor/keyvalue/jdbc/JdbcKeyValueRepositoryTest.java
index 7766e6f7cdaa..ee12c95a8e6a 100644
---
a/components/camel-sql/src/test/java/org/apache/camel/processor/keyvalue/jdbc/JdbcKeyValueRepositoryTest.java
+++
b/components/camel-sql/src/test/java/org/apache/camel/processor/keyvalue/jdbc/JdbcKeyValueRepositoryTest.java
@@ -17,6 +17,7 @@
package org.apache.camel.processor.keyvalue.jdbc;
import java.time.Duration;
+import java.time.temporal.ChronoUnit;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -361,4 +362,30 @@ class JdbcKeyValueRepositoryTest {
repository.put("key1", "value1", null);
assertEquals("value1", repository.get("key1"));
}
+
+ @Test
+ void testTtlTooLargeToAddToTheCurrentTimeDoesNotExpire() {
+ // the current time plus this TTL is beyond Long.MAX_VALUE
+ Duration huge = Duration.ofMillis(Long.MAX_VALUE);
+ assertNull(repository.put("key1", "value1", huge));
+ assertNull(repository.putIfAbsent("key2", "value2", huge));
+ repository.put("key3", "value3", huge);
+ assertTrue(repository.replace("key3", "value3", "value3b", huge));
+
+ assertEquals("value1", repository.get("key1"));
+ assertTrue(repository.contains("key1"));
+ assertEquals("value2", repository.putIfAbsent("key2", "other", null));
+ assertEquals("value3b", repository.get("key3"));
+ assertEquals(Set.of("key1", "key2", "key3"), repository.keys());
+ assertEquals(3, repository.size());
+ }
+
+ @Test
+ void testTtlLongerThanLongMaxValueMillisDoesNotExpire() {
+ // Duration.toMillis() overflows for this TTL
+ repository.put("key1", "value1", ChronoUnit.FOREVER.getDuration());
+
+ assertEquals("value1", repository.get("key1"));
+ assertEquals(Set.of("key1"), repository.keys());
+ }
}
diff --git
a/core/camel-support/src/main/java/org/apache/camel/support/KeyValueRepositoryHelper.java
b/core/camel-support/src/main/java/org/apache/camel/support/KeyValueRepositoryHelper.java
index 87c143a0afb7..5f57621f580e 100644
---
a/core/camel-support/src/main/java/org/apache/camel/support/KeyValueRepositoryHelper.java
+++
b/core/camel-support/src/main/java/org/apache/camel/support/KeyValueRepositoryHelper.java
@@ -23,11 +23,12 @@ import java.io.ObjectInputFilter;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.nio.ByteBuffer;
+import java.time.Duration;
import org.apache.camel.RuntimeCamelException;
/**
- * Shared serialization utilities for {@link
org.apache.camel.spi.KeyValueRepository} implementations.
+ * Shared serialization and expiry utilities for {@link
org.apache.camel.spi.KeyValueRepository} implementations.
* <p/>
* All persistent {@code KeyValueRepository} implementations need to serialize
arbitrary Java objects to bytes (for BLOB
* columns, Kafka messages, etc.) and deserialize them back. This helper
centralises that logic to avoid the same
@@ -170,6 +171,26 @@ public final class KeyValueRepositoryHelper {
return deserialize(bytes, deserializationFilter);
}
+ /**
+ * Returns the time, in milliseconds since the epoch, at which an entry
stored now with the given time-to-live
+ * expires. When that time is beyond {@link Long#MAX_VALUE} (a
time-to-live of about {@link Long#MAX_VALUE}
+ * milliseconds or more), {@link Long#MAX_VALUE} is returned, so the entry
does not expire.
+ * <p/>
+ * The caller handles a {@code null}, zero or negative time-to-live (no
expiration) itself.
+ *
+ * @param ttl a positive time-to-live
+ * @return the expiration time in milliseconds since the epoch, at
most {@link Long#MAX_VALUE}
+ * @since 4.24
+ */
+ public static long expiresAt(Duration ttl) {
+ try {
+ return Math.addExact(System.currentTimeMillis(), ttl.toMillis());
+ } catch (ArithmeticException e) {
+ // toMillis() or the sum overflows: the entry would expire after
the end of the long time range
+ return Long.MAX_VALUE;
+ }
+ }
+
private static Object doDeserialize(ByteArrayInputStream bis, String
deserializationFilter)
throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(bis)) {
diff --git
a/core/camel-support/src/main/java/org/apache/camel/support/MemoryKeyValueRepository.java
b/core/camel-support/src/main/java/org/apache/camel/support/MemoryKeyValueRepository.java
index d6ce48680ca9..6b50f9641507 100644
---
a/core/camel-support/src/main/java/org/apache/camel/support/MemoryKeyValueRepository.java
+++
b/core/camel-support/src/main/java/org/apache/camel/support/MemoryKeyValueRepository.java
@@ -194,7 +194,7 @@ public class MemoryKeyValueRepository extends
ServiceSupport implements KeyValue
if (ttl == null || ttl.isZero() || ttl.isNegative()) {
return Long.MAX_VALUE;
}
- return System.currentTimeMillis() + ttl.toMillis();
+ return KeyValueRepositoryHelper.expiresAt(ttl);
}
private void evictExpired() {
diff --git
a/core/camel-support/src/test/java/org/apache/camel/support/KeyValueRepositoryHelperTest.java
b/core/camel-support/src/test/java/org/apache/camel/support/KeyValueRepositoryHelperTest.java
index 8ae3f86c6c3c..dc7688a45476 100644
---
a/core/camel-support/src/test/java/org/apache/camel/support/KeyValueRepositoryHelperTest.java
+++
b/core/camel-support/src/test/java/org/apache/camel/support/KeyValueRepositoryHelperTest.java
@@ -21,6 +21,8 @@ import java.io.Serial;
import java.io.Serializable;
import java.net.URI;
import java.nio.ByteBuffer;
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -345,6 +347,27 @@ class KeyValueRepositoryHelperTest {
.isEqualTo(original);
}
+ //
-------------------------------------------------------------------------
+ // expiresAt
+ //
-------------------------------------------------------------------------
+
+ @Test
+ void testExpiresAtAddsTtlToCurrentTime() {
+ long before = System.currentTimeMillis();
+ long expiresAt =
KeyValueRepositoryHelper.expiresAt(Duration.ofSeconds(10));
+ long after = System.currentTimeMillis();
+
+ assertThat(expiresAt).isBetween(before + 10_000, after + 10_000);
+ }
+
+ @Test
+ void testExpiresAtSaturatesInsteadOfWrappingAround() {
+ // the sum overflows
+
assertThat(KeyValueRepositoryHelper.expiresAt(Duration.ofMillis(Long.MAX_VALUE))).isEqualTo(Long.MAX_VALUE);
+ // Duration.toMillis() overflows
+
assertThat(KeyValueRepositoryHelper.expiresAt(ChronoUnit.FOREVER.getDuration())).isEqualTo(Long.MAX_VALUE);
+ }
+
//
-------------------------------------------------------------------------
// Helper types
//
-------------------------------------------------------------------------
diff --git
a/core/camel-support/src/test/java/org/apache/camel/support/MemoryKeyValueRepositoryTest.java
b/core/camel-support/src/test/java/org/apache/camel/support/MemoryKeyValueRepositoryTest.java
index aeefe7911957..28bf650e84a1 100644
---
a/core/camel-support/src/test/java/org/apache/camel/support/MemoryKeyValueRepositoryTest.java
+++
b/core/camel-support/src/test/java/org/apache/camel/support/MemoryKeyValueRepositoryTest.java
@@ -17,6 +17,7 @@
package org.apache.camel.support;
import java.time.Duration;
+import java.time.temporal.ChronoUnit;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -302,4 +303,29 @@ class MemoryKeyValueRepositoryTest {
.untilAsserted(() ->
assertThat(repository.get("key1")).isNull());
}
+ @Test
+ void testTtlTooLargeToAddToTheCurrentTimeDoesNotExpire() {
+ // the current time plus this TTL is beyond Long.MAX_VALUE
+ Duration huge = Duration.ofMillis(Long.MAX_VALUE);
+ assertThat(repository.put("key1", "value1", huge)).isNull();
+ assertThat(repository.putIfAbsent("key2", "value2", huge)).isNull();
+ repository.put("key3", "value3", huge);
+ assertThat(repository.replace("key3", "value3", "value3b",
huge)).isTrue();
+
+ assertThat(repository.get("key1")).isEqualTo("value1");
+ assertThat(repository.contains("key1")).isTrue();
+ assertThat(repository.putIfAbsent("key2", "other",
null)).isEqualTo("value2");
+ assertThat(repository.get("key3")).isEqualTo("value3b");
+ assertThat(repository.keys()).containsExactlyInAnyOrder("key1",
"key2", "key3");
+ assertThat(repository.size()).isEqualTo(3);
+ }
+
+ @Test
+ void testTtlLongerThanLongMaxValueMillisDoesNotExpire() {
+ // Duration.toMillis() overflows for this TTL
+ repository.put("key1", "value1", ChronoUnit.FOREVER.getDuration());
+
+ assertThat(repository.get("key1")).isEqualTo("value1");
+ assertThat(repository.keys()).containsExactly("key1");
+ }
}