This is an automated email from the ASF dual-hosted git repository. gnodet pushed a commit to branch keyvaluerepository-spi-improvements in repository https://gitbox.apache.org/repos/asf/camel.git
commit 48cb1774aee45fcb20104d26f4867645a7005f16 Author: Guillaume Nodet <[email protected]> AuthorDate: Tue Sep 1 13:28:12 2026 +0200 CAMEL-24463: Improve KeyValueRepository SPI - Change TTL parameter type from Duration to long millis for simpler implementation across all backends (JDBC, JPA, Cassandra, Kafka) - Add put(key, value) and putIfAbsent(key, value) convenience defaults that delegate to the TTL variants with 0 - Remove replace() and delete(key, expectedValue) CAS methods that no current implementation overrides with a real atomic operation - Extract shared serialization logic into KeyValueRepositoryHelper to eliminate duplication across persistent implementations - Update adapters and tests to use the simplified API Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../org/apache/camel/spi/KeyValueRepository.java | 90 ++++++-------- .../support/KeyValueAggregationRepository.java | 4 +- .../support/KeyValueIdempotentRepository.java | 2 +- .../camel/support/KeyValueRepositoryHelper.java | 125 ++++++++++++++++++++ .../camel/support/MemoryKeyValueRepository.java | 44 +------ .../support/KeyValueIdempotentRepositoryTest.java | 4 +- .../support/MemoryKeyValueRepositoryTest.java | 131 +++++---------------- 7 files changed, 196 insertions(+), 204 deletions(-) diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/KeyValueRepository.java b/core/camel-api/src/main/java/org/apache/camel/spi/KeyValueRepository.java index fef877edda4c..299f095490df 100644 --- a/core/camel-api/src/main/java/org/apache/camel/spi/KeyValueRepository.java +++ b/core/camel-api/src/main/java/org/apache/camel/spi/KeyValueRepository.java @@ -16,8 +16,6 @@ */ package org.apache.camel.spi; -import java.time.Duration; -import java.util.Objects; import java.util.Set; import org.apache.camel.Service; @@ -35,8 +33,8 @@ import org.jspecify.annotations.Nullable; * storage technology (Redis, Hazelcast, Infinispan, JDBC, etc.), a single {@code KeyValueRepository} implementation can * be wrapped by the appropriate adapter. * <p/> - * Implementations must be thread-safe. Entries may optionally have a time-to-live (TTL); a {@code null}, zero, or - * negative TTL means the entry does not expire. + * Implementations must be thread-safe. Entries may optionally have a time-to-live (TTL); a TTL of {@code 0} or less + * means the entry does not expire. * * @since 4.23 */ @@ -52,15 +50,27 @@ public interface KeyValueRepository extends Service { Object get(String key); /** - * Stores a value under the given key with an optional time-to-live. + * Stores a value under the given key with no expiration. * * @param key the key * @param value the value to store - * @param ttl the time-to-live; {@code null}, zero, or negative means no expiration * @return the previous value associated with the key, or {@code null} if there was no mapping */ @Nullable - Object put(String key, Object value, @Nullable Duration ttl); + default Object put(String key, Object value) { + return put(key, value, 0); + } + + /** + * Stores a value under the given key with an optional time-to-live. + * + * @param key the key + * @param value the value to store + * @param ttlMillis the time-to-live in milliseconds; {@code 0} or negative means no expiration + * @return the previous value associated with the key, or {@code null} if there was no mapping + */ + @Nullable + Object put(String key, Object value, long ttlMillis); /** * Removes the entry for the given key. @@ -92,68 +102,38 @@ public interface KeyValueRepository extends Service { void clear(); /** - * Stores the value under the given key only if no non-expired mapping already exists. + * Stores the value under the given key only if no non-expired mapping already exists, with no expiration. * <p/> - * The default implementation is not atomic. Implementations backed by stores that support atomic compare-and-set - * operations should override this method for better concurrency guarantees. + * Equivalent to {@code putIfAbsent(key, value, 0)}. * * @param key the key * @param value the value to store - * @param ttl the time-to-live; {@code null}, zero, or negative means no expiration * @return the existing value if the key was already present, or {@code null} if the put succeeded */ @Nullable - default Object putIfAbsent(String key, Object value, @Nullable Duration ttl) { - Object existing = get(key); - if (existing != null) { - return existing; - } - put(key, value, ttl); - return null; + default Object putIfAbsent(String key, Object value) { + return putIfAbsent(key, value, 0); } /** - * Atomically replaces the value for the given key only if the current value equals the expected old value - * (compare-and-swap). - * <p/> - * The default implementation is not atomic. Implementations backed by stores that support atomic compare-and-swap - * operations (e.g., {@code ConcurrentMap.replace}, Hazelcast {@code IMap.replace}) should override this method for - * better concurrency guarantees. - * - * @param key the key - * @param expectedOldValue the value that must currently be associated with the key - * @param newValue the new value to store - * @param ttl the time-to-live for the new entry; {@code null}, zero, or negative means no expiration - * @return {@code true} if the value was replaced, {@code false} if the current value did not match - */ - default boolean replace(String key, Object expectedOldValue, Object newValue, @Nullable Duration ttl) { - Object current = get(key); - if (current != null && Objects.equals(current, expectedOldValue)) { - put(key, newValue, ttl); - return true; - } - return false; - } - - /** - * Removes the entry for the given key only if the current value equals the expected value (compare-and-swap). + * Stores the value under the given key only if no non-expired mapping already exists. * <p/> - * The default implementation is not atomic. Implementations backed by stores that support atomic compare-and-remove - * operations (e.g., {@code ConcurrentMap.remove(key, value)}, Hazelcast {@code IMap.remove(key, value)}) should - * override this method for better concurrency guarantees. + * The default implementation is not atomic. Implementations backed by stores that support atomic compare-and-set + * operations should override this method for better concurrency guarantees. * - * @param key the key to remove - * @param expectedValue the value that must currently be associated with the key - * @return {@code true} if the entry was removed, {@code false} if the current value did not match or - * the key was not present + * @param key the key + * @param value the value to store + * @param ttlMillis the time-to-live in milliseconds; {@code 0} or negative means no expiration + * @return the existing value if the key was already present, or {@code null} if the put succeeded */ - default boolean delete(String key, Object expectedValue) { - Object current = get(key); - if (current != null && Objects.equals(current, expectedValue)) { - delete(key); - return true; + @Nullable + default Object putIfAbsent(String key, Object value, long ttlMillis) { + Object existing = get(key); + if (existing != null) { + return existing; } - return false; + put(key, value, ttlMillis); + return null; } /** diff --git a/core/camel-support/src/main/java/org/apache/camel/support/KeyValueAggregationRepository.java b/core/camel-support/src/main/java/org/apache/camel/support/KeyValueAggregationRepository.java index 95fb22141b5b..38a4aaf24301 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/KeyValueAggregationRepository.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/KeyValueAggregationRepository.java @@ -110,7 +110,7 @@ public class KeyValueAggregationRepository extends ServiceSupport public Exchange add(CamelContext camelContext, String key, Exchange exchange) { LOG.trace("Adding an Exchange with ID {} for key {}", exchange.getExchangeId(), key); DefaultExchangeHolder newHolder = DefaultExchangeHolder.marshal(exchange, true, allowSerializedHeaders); - DefaultExchangeHolder oldHolder = (DefaultExchangeHolder) repository.put(AGGREGATE_PREFIX + key, newHolder, null); + DefaultExchangeHolder oldHolder = (DefaultExchangeHolder) repository.put(AGGREGATE_PREFIX + key, newHolder); return unmarshallExchange(camelContext, oldHolder); } @@ -126,7 +126,7 @@ public class KeyValueAggregationRepository extends ServiceSupport if (useRecovery && holder != null) { // Store under the exchangeId for potential recovery LOG.trace("Moving Exchange with ID {} to completed (pending confirmation)", exchange.getExchangeId()); - repository.put(COMPLETED_PREFIX + exchange.getExchangeId(), holder, null); + repository.put(COMPLETED_PREFIX + exchange.getExchangeId(), holder); } } diff --git a/core/camel-support/src/main/java/org/apache/camel/support/KeyValueIdempotentRepository.java b/core/camel-support/src/main/java/org/apache/camel/support/KeyValueIdempotentRepository.java index d11b656641e1..02d87434d725 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/KeyValueIdempotentRepository.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/KeyValueIdempotentRepository.java @@ -84,7 +84,7 @@ public class KeyValueIdempotentRepository extends ServiceSupport implements Idem @Override public boolean add(String key) { // putIfAbsent returns null if the key was successfully added (not already present) - return repository.putIfAbsent(IDEMPOTENT_PREFIX + key, Boolean.TRUE, null) == null; + return repository.putIfAbsent(IDEMPOTENT_PREFIX + key, Boolean.TRUE) == null; } @Override 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 new file mode 100644 index 000000000000..13d3ff696456 --- /dev/null +++ b/core/camel-support/src/main/java/org/apache/camel/support/KeyValueRepositoryHelper.java @@ -0,0 +1,125 @@ +/* + * 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.support; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.nio.ByteBuffer; + +import org.apache.camel.RuntimeCamelException; + +/** + * Shared serialization 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 + * try/catch boilerplate in every implementation. + * <p/> + * <b>Security note:</b> These methods use plain Java serialization + * ({@link ObjectOutputStream}/{@link ObjectInputStream}). The stored data is trusted — it was written by the same + * application instance or cluster. Do not expose a repository's raw byte store to untrusted input. + * + * @since 4.23 + */ +public final class KeyValueRepositoryHelper { + + private KeyValueRepositoryHelper() { + // utility class + } + + /** + * Serializes an object to a byte array using Java object serialization. + * + * @param value the object to serialize (must be {@link java.io.Serializable}) + * @return the serialized bytes + * @throws RuntimeCamelException if serialization fails + */ + public static byte[] serialize(Object value) { + try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos)) { + oos.writeObject(value); + oos.flush(); + return bos.toByteArray(); + } catch (IOException e) { + throw new RuntimeCamelException("Failed to serialize value", e); + } + } + + /** + * Serializes an object to a {@link ByteBuffer} using Java object serialization. Useful for drivers that work with + * {@code ByteBuffer} (e.g. Cassandra). + * + * @param value the object to serialize (must be {@link java.io.Serializable}) + * @return a ByteBuffer wrapping the serialized bytes + * @throws RuntimeCamelException if serialization fails + */ + public static ByteBuffer serializeToByteBuffer(Object value) { + return ByteBuffer.wrap(serialize(value)); + } + + /** + * Deserializes a byte array back into an object using Java object serialization. + * + * @param bytes the bytes to deserialize + * @return the deserialized object + * @throws RuntimeCamelException if deserialization fails + */ + public static Object deserialize(byte[] bytes) { + try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bis)) { + return ois.readObject(); + } catch (IOException | ClassNotFoundException e) { + throw new RuntimeCamelException("Failed to deserialize value", e); + } + } + + /** + * Deserializes an object from a portion of a byte array using Java object serialization. Useful when the serialized + * data starts at an offset (e.g. after a protocol header). + * + * @param bytes the byte array containing the serialized data + * @param offset the start offset within the array + * @param length the number of bytes to read + * @return the deserialized object + * @throws RuntimeCamelException if deserialization fails + */ + public static Object deserialize(byte[] bytes, int offset, int length) { + try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes, offset, length); + ObjectInputStream ois = new ObjectInputStream(bis)) { + return ois.readObject(); + } catch (IOException | ClassNotFoundException e) { + throw new RuntimeCamelException("Failed to deserialize value", e); + } + } + + /** + * Deserializes an object from a {@link ByteBuffer} using Java object serialization. The buffer's remaining bytes + * are consumed. + * + * @param buffer the ByteBuffer containing the serialized bytes + * @return the deserialized object + * @throws RuntimeCamelException if deserialization fails + */ + public static Object deserialize(ByteBuffer buffer) { + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + return deserialize(bytes); + } +} 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..f32527ea9094 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 @@ -18,10 +18,8 @@ package org.apache.camel.support; import java.io.Serial; import java.io.Serializable; -import java.time.Duration; import java.util.Iterator; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @@ -75,8 +73,8 @@ public class MemoryKeyValueRepository extends ServiceSupport implements KeyValue @Override @ManagedOperation(description = "Put a key-value pair with optional TTL") - public Object put(String key, Object value, Duration ttl) { - long expiresAt = toExpiresAt(ttl); + public Object put(String key, Object value, long ttlMillis) { + long expiresAt = ttlMillis > 0 ? System.currentTimeMillis() + ttlMillis : Long.MAX_VALUE; Entry previous = store.put(key, new Entry(value, expiresAt)); if (previous == null) { return null; @@ -132,8 +130,8 @@ public class MemoryKeyValueRepository extends ServiceSupport implements KeyValue } @Override - public Object putIfAbsent(String key, Object value, Duration ttl) { - long expiresAt = toExpiresAt(ttl); + public Object putIfAbsent(String key, Object value, long ttlMillis) { + long expiresAt = ttlMillis > 0 ? System.currentTimeMillis() + ttlMillis : Long.MAX_VALUE; Entry newEntry = new Entry(value, expiresAt); Entry existing = store.putIfAbsent(key, newEntry); if (existing == null) { @@ -151,33 +149,6 @@ public class MemoryKeyValueRepository extends ServiceSupport implements KeyValue return existing.value(); } - @Override - public boolean replace(String key, Object expectedOldValue, Object newValue, Duration ttl) { - long expiresAt = toExpiresAt(ttl); - boolean[] replaced = { false }; - store.computeIfPresent(key, (k, current) -> { - if (!current.isExpired() && Objects.equals(current.value(), expectedOldValue)) { - replaced[0] = true; - return new Entry(newValue, expiresAt); - } - return current; - }); - return replaced[0]; - } - - @Override - public boolean delete(String key, Object expectedValue) { - boolean[] removed = { false }; - store.computeIfPresent(key, (k, current) -> { - if (!current.isExpired() && Objects.equals(current.value(), expectedValue)) { - removed[0] = true; - return null; // returning null removes the entry from the map - } - return current; - }); - return removed[0]; - } - @Override @ManagedAttribute(description = "The number of entries in the repository") public int size() { @@ -190,13 +161,6 @@ public class MemoryKeyValueRepository extends ServiceSupport implements KeyValue store.clear(); } - private static long toExpiresAt(Duration ttl) { - if (ttl == null || ttl.isZero() || ttl.isNegative()) { - return Long.MAX_VALUE; - } - return System.currentTimeMillis() + ttl.toMillis(); - } - private void evictExpired() { Iterator<Map.Entry<String, Entry>> it = store.entrySet().iterator(); while (it.hasNext()) { diff --git a/core/camel-support/src/test/java/org/apache/camel/support/KeyValueIdempotentRepositoryTest.java b/core/camel-support/src/test/java/org/apache/camel/support/KeyValueIdempotentRepositoryTest.java index c1853e4c172a..3b78af19a580 100644 --- a/core/camel-support/src/test/java/org/apache/camel/support/KeyValueIdempotentRepositoryTest.java +++ b/core/camel-support/src/test/java/org/apache/camel/support/KeyValueIdempotentRepositoryTest.java @@ -145,7 +145,7 @@ class KeyValueIdempotentRepositoryTest { @Test void testClearDoesNotAffectOtherPrefixes() { // Simulate another adapter storing entries under a different prefix - kvRepository.put("aggregate:order-1", "exchange-holder", null); + kvRepository.put("aggregate:order-1", "exchange-holder"); // Add idempotent entries and clear them idempotentRepository.add("msg-001"); @@ -166,7 +166,7 @@ class KeyValueIdempotentRepositoryTest { idempotentRepository.add("order-1"); // A different adapter storing under its own prefix should not collide - kvRepository.put("aggregate:order-1", "exchange-data", null); + kvRepository.put("aggregate:order-1", "exchange-data"); // The idempotent entry should still resolve correctly assertThat(idempotentRepository.contains("order-1")).isTrue(); 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 e72a8f71ae26..f8f9465cb255 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 @@ -16,7 +16,6 @@ */ package org.apache.camel.support; -import java.time.Duration; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -44,7 +43,7 @@ class MemoryKeyValueRepositoryTest { @Test void testPutAndGet() { - repository.put("key1", "value1", null); + repository.put("key1", "value1", 0); assertThat(repository.get("key1")).isEqualTo("value1"); } @@ -56,15 +55,15 @@ class MemoryKeyValueRepositoryTest { @Test void testPutOverwritesExistingValue() { - repository.put("key1", "value1", null); - repository.put("key1", "value2", null); + repository.put("key1", "value1", 0); + repository.put("key1", "value2", 0); assertThat(repository.get("key1")).isEqualTo("value2"); } @Test void testDelete() { - repository.put("key1", "value1", null); + repository.put("key1", "value1", 0); Object deleted = repository.delete("key1"); @@ -79,7 +78,7 @@ class MemoryKeyValueRepositoryTest { @Test void testContains() { - repository.put("key1", "value1", null); + repository.put("key1", "value1", 0); assertThat(repository.contains("key1")).isTrue(); assertThat(repository.contains("nonexistent")).isFalse(); @@ -87,9 +86,9 @@ class MemoryKeyValueRepositoryTest { @Test void testKeys() { - repository.put("key1", "value1", null); - repository.put("key2", "value2", null); - repository.put("key3", "value3", null); + repository.put("key1", "value1", 0); + repository.put("key2", "value2", 0); + repository.put("key3", "value3", 0); Set<String> keys = repository.keys(); @@ -103,8 +102,8 @@ class MemoryKeyValueRepositoryTest { @Test void testClear() { - repository.put("key1", "value1", null); - repository.put("key2", "value2", null); + repository.put("key1", "value1", 0); + repository.put("key2", "value2", 0); repository.clear(); @@ -117,10 +116,10 @@ class MemoryKeyValueRepositoryTest { void testSize() { assertThat(repository.size()).isZero(); - repository.put("key1", "value1", null); + repository.put("key1", "value1", 0); assertThat(repository.size()).isEqualTo(1); - repository.put("key2", "value2", null); + repository.put("key2", "value2", 0); assertThat(repository.size()).isEqualTo(2); repository.delete("key1"); @@ -129,7 +128,7 @@ class MemoryKeyValueRepositoryTest { @Test void testPutIfAbsentNewKey() { - Object result = repository.putIfAbsent("key1", "value1", null); + Object result = repository.putIfAbsent("key1", "value1", 0); assertThat(result).isNull(); assertThat(repository.get("key1")).isEqualTo("value1"); @@ -137,9 +136,9 @@ class MemoryKeyValueRepositoryTest { @Test void testPutIfAbsentExistingKey() { - repository.put("key1", "value1", null); + repository.put("key1", "value1", 0); - Object result = repository.putIfAbsent("key1", "value2", null); + Object result = repository.putIfAbsent("key1", "value2", 0); assertThat(result).isEqualTo("value1"); assertThat(repository.get("key1")).isEqualTo("value1"); @@ -148,7 +147,7 @@ class MemoryKeyValueRepositoryTest { @Test void testTtlExpiration() { // Use a very short TTL - repository.put("key1", "value1", Duration.ofMillis(50)); + repository.put("key1", "value1", 50); assertThat(repository.get("key1")).isEqualTo("value1"); assertThat(repository.contains("key1")).isTrue(); @@ -163,8 +162,8 @@ class MemoryKeyValueRepositoryTest { @Test void testTtlExpirationOnKeys() { - repository.put("key1", "value1", Duration.ofMillis(50)); - repository.put("key2", "value2", null); // no expiration + repository.put("key1", "value1", 50); + repository.put("key2", "value2", 0); // no expiration await().atMost(500, TimeUnit.MILLISECONDS) .untilAsserted(() -> { @@ -175,7 +174,7 @@ class MemoryKeyValueRepositoryTest { @Test void testTtlExpirationOnDelete() { - repository.put("key1", "value1", Duration.ofMillis(50)); + repository.put("key1", "value1", 50); await().atMost(500, TimeUnit.MILLISECONDS) .untilAsserted(() -> { @@ -186,38 +185,29 @@ class MemoryKeyValueRepositoryTest { @Test void testPutIfAbsentWithExpiredEntry() { - repository.put("key1", "value1", Duration.ofMillis(50)); + repository.put("key1", "value1", 50); await().atMost(500, TimeUnit.MILLISECONDS) .untilAsserted(() -> { // The entry has expired, so putIfAbsent should succeed - Object result = repository.putIfAbsent("key1", "value2", null); + Object result = repository.putIfAbsent("key1", "value2", 0); assertThat(result).isNull(); assertThat(repository.get("key1")).isEqualTo("value2"); }); } - @Test - void testNoTtlWithNull() { - repository.put("key1", "value1", null); - - // Entry with null TTL should not expire - assertThat(repository.get("key1")).isEqualTo("value1"); - assertThat(repository.contains("key1")).isTrue(); - } - @Test void testNoTtlWithZero() { - repository.put("key1", "value1", Duration.ZERO); + repository.put("key1", "value1", 0); - // Entry with zero TTL should not expire + // Entry with TTL=0 should not expire assertThat(repository.get("key1")).isEqualTo("value1"); assertThat(repository.contains("key1")).isTrue(); } @Test void testNoTtlWithNegative() { - repository.put("key1", "value1", Duration.ofMillis(-1)); + repository.put("key1", "value1", -1); // Entry with negative TTL should not expire assertThat(repository.get("key1")).isEqualTo("value1"); @@ -226,79 +216,12 @@ class MemoryKeyValueRepositoryTest { @Test void testStoresDifferentValueTypes() { - repository.put("string", "hello", null); - repository.put("integer", 42, null); - repository.put("boolean", Boolean.TRUE, null); + repository.put("string", "hello", 0); + repository.put("integer", 42, 0); + repository.put("boolean", Boolean.TRUE, 0); assertThat(repository.get("string")).isEqualTo("hello"); assertThat(repository.get("integer")).isEqualTo(42); assertThat(repository.get("boolean")).isEqualTo(Boolean.TRUE); } - - @Test - void testReplaceMatchingValue() { - repository.put("key1", "value1", null); - - boolean replaced = repository.replace("key1", "value1", "value2", null); - - assertThat(replaced).isTrue(); - assertThat(repository.get("key1")).isEqualTo("value2"); - } - - @Test - void testReplaceNonMatchingValue() { - repository.put("key1", "value1", null); - - boolean replaced = repository.replace("key1", "wrong", "value2", null); - - assertThat(replaced).isFalse(); - assertThat(repository.get("key1")).isEqualTo("value1"); - } - - @Test - void testReplaceMissingKey() { - boolean replaced = repository.replace("nonexistent", "value1", "value2", null); - - assertThat(replaced).isFalse(); - } - - @Test - void testReplaceWithTtl() { - repository.put("key1", "value1", null); - - boolean replaced = repository.replace("key1", "value1", "value2", Duration.ofMillis(500)); - - assertThat(replaced).isTrue(); - assertThat(repository.get("key1")).isEqualTo("value2"); - - await().atMost(5, TimeUnit.SECONDS) - .untilAsserted(() -> assertThat(repository.get("key1")).isNull()); - } - - @Test - void testDeleteWithMatchingValue() { - repository.put("key1", "value1", null); - - boolean deleted = repository.delete("key1", "value1"); - - assertThat(deleted).isTrue(); - assertThat(repository.get("key1")).isNull(); - } - - @Test - void testDeleteWithNonMatchingValue() { - repository.put("key1", "value1", null); - - boolean deleted = repository.delete("key1", "wrong"); - - assertThat(deleted).isFalse(); - assertThat(repository.get("key1")).isEqualTo("value1"); - } - - @Test - void testDeleteWithMissingKey() { - boolean deleted = repository.delete("nonexistent", "value1"); - - assertThat(deleted).isFalse(); - } }
