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");
+    }
 }

Reply via email to