From f66697f29b0534e19e2b481813b6c0a4ce8368c1 Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Thu, 25 Jan 2024 01:05:58 +0600 Subject: [PATCH 01/16] Support TTL in client side caching (using Caffeine library) --- pom.xml | 6 +++ .../redis/clients/jedis/ClientSideCache.java | 48 ++++++------------ .../redis/clients/jedis/UnifiedJedis.java | 4 +- .../redis/clients/jedis/util/CaffeineCSC.java | 50 +++++++++++++++++++ .../JedisClusterClientSideCacheTest.java | 9 ++-- .../jedis/JedisPooledClientSideCacheTest.java | 9 ++-- .../JedisSentineledClientSideCacheTest.java | 18 ++----- .../java/redis/clients/jedis/util/MapCSC.java | 39 +++++++++++++++ 8 files changed, 128 insertions(+), 55 deletions(-) create mode 100644 src/main/java/redis/clients/jedis/util/CaffeineCSC.java create mode 100644 src/test/java/redis/clients/jedis/util/MapCSC.java diff --git a/pom.xml b/pom.xml index c9fd15d2cb..96a4e122f7 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,12 @@ gson 2.10.1 + + com.github.ben-manes.caffeine + caffeine + 2.9.3 + true + diff --git a/src/main/java/redis/clients/jedis/ClientSideCache.java b/src/main/java/redis/clients/jedis/ClientSideCache.java index 62c5be28c2..482c9c0681 100644 --- a/src/main/java/redis/clients/jedis/ClientSideCache.java +++ b/src/main/java/redis/clients/jedis/ClientSideCache.java @@ -1,63 +1,47 @@ package redis.clients.jedis; import java.nio.ByteBuffer; -import java.util.HashMap; import java.util.List; -import java.util.Map; - import redis.clients.jedis.exceptions.JedisException; import redis.clients.jedis.util.SafeEncoder; -public class ClientSideCache { +public abstract class ClientSideCache { - private final Map cache; + protected ClientSideCache() { } - public ClientSideCache() { - this.cache = new HashMap<>(); - } + public abstract void clear(); - /** - * For testing purpose only. - * @param map - */ - ClientSideCache(Map map) { - this.cache = map; - } + protected abstract void remove(ByteBuffer key); - public final void clear() { - cache.clear(); - } + protected abstract void put(ByteBuffer key, Object value); + + protected abstract Object get(ByteBuffer key); - public final void invalidateKeys(List list) { + final void invalidateKeys(List list) { if (list == null) { clear(); - return; + } else { + list.forEach(this::invalidateKey); } - - list.forEach(this::invalidateKey); } private void invalidateKey(Object key) { if (key instanceof byte[]) { - cache.remove(convertKey((byte[]) key)); + remove(convertKey((byte[]) key)); } else { throw new JedisException("" + key.getClass().getSimpleName() + " is not supported. Value: " + String.valueOf(key)); } } - protected void setKey(Object key, Object value) { - cache.put(getMapKey(key), value); - } - - protected T getValue(Object key) { - return (T) getMapValue(key); + final void set(Object key, Object value) { + put(makeKey(key), value); } - private Object getMapValue(Object key) { - return cache.get(getMapKey(key)); + final T get(Object key) { + return (T) get(makeKey(key)); } - private ByteBuffer getMapKey(Object key) { + private ByteBuffer makeKey(Object key) { if (key instanceof byte[]) { return convertKey((byte[]) key); } else { diff --git a/src/main/java/redis/clients/jedis/UnifiedJedis.java b/src/main/java/redis/clients/jedis/UnifiedJedis.java index ba7b36b134..343b24c842 100644 --- a/src/main/java/redis/clients/jedis/UnifiedJedis.java +++ b/src/main/java/redis/clients/jedis/UnifiedJedis.java @@ -750,11 +750,11 @@ public String set(String key, String value, SetParams params) { @Override public String get(String key) { if (clientSideCache != null) { - String cachedValue = clientSideCache.getValue(key); + String cachedValue = clientSideCache.get(key); if (cachedValue != null) return cachedValue; String value = executeCommand(commandObjects.get(key)); - if (value != null) clientSideCache.setKey(key, value); + if (value != null) clientSideCache.set(key, value); return value; } return executeCommand(commandObjects.get(key)); diff --git a/src/main/java/redis/clients/jedis/util/CaffeineCSC.java b/src/main/java/redis/clients/jedis/util/CaffeineCSC.java new file mode 100644 index 0000000000..7a205f40ab --- /dev/null +++ b/src/main/java/redis/clients/jedis/util/CaffeineCSC.java @@ -0,0 +1,50 @@ +package redis.clients.jedis.util; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; +import redis.clients.jedis.ClientSideCache; + +public class CaffeineCSC extends ClientSideCache { + + private static final int DEFAULT_MAXIMUM_SIZE = 10_000; + + private final Cache cache; + + public CaffeineCSC() { + this(DEFAULT_MAXIMUM_SIZE); + } + + public CaffeineCSC(int maximumSize) { + this(Caffeine.newBuilder().maximumSize(maximumSize).build()); + } + + public CaffeineCSC(int maximumSize, int ttlSeconds) { + this(Caffeine.newBuilder().maximumSize(maximumSize).expireAfterWrite(ttlSeconds, TimeUnit.SECONDS).build()); + } + + public CaffeineCSC(Cache caffeineCache) { + this.cache = caffeineCache; + } + + @Override + public final void clear() { + cache.invalidateAll(); + } + + @Override + protected void remove(ByteBuffer key) { + cache.invalidate(key); + } + + @Override + protected void put(ByteBuffer key, Object value) { + cache.put(key, value); + } + + @Override + protected Object get(ByteBuffer key) { + return cache.getIfPresent(key); + } +} diff --git a/src/test/java/redis/clients/jedis/JedisClusterClientSideCacheTest.java b/src/test/java/redis/clients/jedis/JedisClusterClientSideCacheTest.java index 3c8bc18c5c..8e276d9665 100644 --- a/src/test/java/redis/clients/jedis/JedisClusterClientSideCacheTest.java +++ b/src/test/java/redis/clients/jedis/JedisClusterClientSideCacheTest.java @@ -14,6 +14,7 @@ import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.hamcrest.Matchers; import org.junit.Test; +import redis.clients.jedis.util.MapCSC; public class JedisClusterClientSideCacheTest extends JedisClusterTestBase { @@ -31,7 +32,7 @@ public class JedisClusterClientSideCacheTest extends JedisClusterTestBase { @Test public void simple() { - try (JedisCluster jedis = new JedisCluster(hnp, clientConfig.get(), new ClientSideCache())) { + try (JedisCluster jedis = new JedisCluster(hnp, clientConfig.get(), new MapCSC())) { jedis.set("foo", "bar"); assertEquals("bar", jedis.get("foo")); jedis.del("foo"); @@ -42,7 +43,7 @@ public void simple() { @Test public void simpleWithSimpleMap() { HashMap map = new HashMap<>(); - try (JedisCluster jedis = new JedisCluster(hnp, clientConfig.get(), new ClientSideCache(map), singleConnectionPoolConfig.get())) { + try (JedisCluster jedis = new JedisCluster(hnp, clientConfig.get(), new MapCSC(map), singleConnectionPoolConfig.get())) { jedis.set("foo", "bar"); assertThat(map, Matchers.aMapWithSize(0)); assertEquals("bar", jedis.get("foo")); @@ -60,7 +61,7 @@ public void simpleWithSimpleMap() { @Test public void flushAll() { - try (JedisCluster jedis = new JedisCluster(hnp, clientConfig.get(), new ClientSideCache())) { + try (JedisCluster jedis = new JedisCluster(hnp, clientConfig.get(), new MapCSC())) { jedis.set("foo", "bar"); assertEquals("bar", jedis.get("foo")); jedis.flushAll(); @@ -71,7 +72,7 @@ public void flushAll() { @Test public void flushAllWithSimpleMap() { HashMap map = new HashMap<>(); - try (JedisCluster jedis = new JedisCluster(hnp, clientConfig.get(), new ClientSideCache(map), singleConnectionPoolConfig.get())) { + try (JedisCluster jedis = new JedisCluster(hnp, clientConfig.get(), new MapCSC(map), singleConnectionPoolConfig.get())) { jedis.set("foo", "bar"); assertThat(map, Matchers.aMapWithSize(0)); assertEquals("bar", jedis.get("foo")); diff --git a/src/test/java/redis/clients/jedis/JedisPooledClientSideCacheTest.java b/src/test/java/redis/clients/jedis/JedisPooledClientSideCacheTest.java index ad4313a4b7..805d9738d7 100644 --- a/src/test/java/redis/clients/jedis/JedisPooledClientSideCacheTest.java +++ b/src/test/java/redis/clients/jedis/JedisPooledClientSideCacheTest.java @@ -12,6 +12,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import redis.clients.jedis.util.MapCSC; public class JedisPooledClientSideCacheTest { @@ -42,7 +43,7 @@ public void tearDown() throws Exception { @Test public void simple() { - try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), new ClientSideCache())) { + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), new MapCSC())) { control.set("foo", "bar"); assertEquals("bar", jedis.get("foo")); control.del("foo"); @@ -53,7 +54,7 @@ public void simple() { @Test public void simpleWithSimpleMap() { HashMap map = new HashMap<>(); - try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), new ClientSideCache(map), singleConnectionPoolConfig.get())) { + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), new MapCSC(map), singleConnectionPoolConfig.get())) { control.set("foo", "bar"); assertThat(map, Matchers.aMapWithSize(0)); assertEquals("bar", jedis.get("foo")); @@ -71,7 +72,7 @@ public void simpleWithSimpleMap() { @Test public void flushAll() { - try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), new ClientSideCache())) { + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), new MapCSC())) { control.set("foo", "bar"); assertEquals("bar", jedis.get("foo")); control.flushAll(); @@ -82,7 +83,7 @@ public void flushAll() { @Test public void flushAllWithSimpleMap() { HashMap map = new HashMap<>(); - try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), new ClientSideCache(map), singleConnectionPoolConfig.get())) { + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), new MapCSC(map), singleConnectionPoolConfig.get())) { control.set("foo", "bar"); assertThat(map, Matchers.aMapWithSize(0)); assertEquals("bar", jedis.get("foo")); diff --git a/src/test/java/redis/clients/jedis/JedisSentineledClientSideCacheTest.java b/src/test/java/redis/clients/jedis/JedisSentineledClientSideCacheTest.java index 9af243ffc7..5fa5a36f4b 100644 --- a/src/test/java/redis/clients/jedis/JedisSentineledClientSideCacheTest.java +++ b/src/test/java/redis/clients/jedis/JedisSentineledClientSideCacheTest.java @@ -8,12 +8,11 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Set; -import java.util.function.Supplier; import java.util.stream.Collectors; -import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.hamcrest.Matchers; import org.junit.Test; +import redis.clients.jedis.util.MapCSC; public class JedisSentineledClientSideCacheTest { @@ -28,16 +27,9 @@ public class JedisSentineledClientSideCacheTest { private static final JedisClientConfig sentinelClientConfig = DefaultJedisClientConfig.builder().resp3().build(); - private static final Supplier> singleConnectionPoolConfig - = () -> { - ConnectionPoolConfig poolConfig = new ConnectionPoolConfig(); - poolConfig.setMaxTotal(1); - return poolConfig; - }; - @Test public void simple() { - try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, masterClientConfig, new ClientSideCache(), sentinels, sentinelClientConfig)) { + try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, masterClientConfig, new MapCSC(), sentinels, sentinelClientConfig)) { jedis.set("foo", "bar"); assertEquals("bar", jedis.get("foo")); jedis.del("foo"); @@ -48,7 +40,7 @@ public void simple() { @Test public void simpleWithSimpleMap() { HashMap map = new HashMap<>(); - try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, masterClientConfig, new ClientSideCache(map), sentinels, sentinelClientConfig)) { + try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, masterClientConfig, new MapCSC(map), sentinels, sentinelClientConfig)) { jedis.set("foo", "bar"); assertThat(map, Matchers.aMapWithSize(0)); assertEquals("bar", jedis.get("foo")); @@ -66,7 +58,7 @@ public void simpleWithSimpleMap() { @Test public void flushAll() { - try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, masterClientConfig, new ClientSideCache(), sentinels, sentinelClientConfig)) { + try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, masterClientConfig, new MapCSC(), sentinels, sentinelClientConfig)) { jedis.set("foo", "bar"); assertEquals("bar", jedis.get("foo")); jedis.flushAll(); @@ -77,7 +69,7 @@ public void flushAll() { @Test public void flushAllWithSimpleMap() { HashMap map = new HashMap<>(); - try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, masterClientConfig, new ClientSideCache(map), sentinels, sentinelClientConfig)) { + try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, masterClientConfig, new MapCSC(map), sentinels, sentinelClientConfig)) { jedis.set("foo", "bar"); assertThat(map, Matchers.aMapWithSize(0)); assertEquals("bar", jedis.get("foo")); diff --git a/src/test/java/redis/clients/jedis/util/MapCSC.java b/src/test/java/redis/clients/jedis/util/MapCSC.java new file mode 100644 index 0000000000..a9d1ef6f37 --- /dev/null +++ b/src/test/java/redis/clients/jedis/util/MapCSC.java @@ -0,0 +1,39 @@ +package redis.clients.jedis.util; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; +import redis.clients.jedis.ClientSideCache; + +public class MapCSC extends ClientSideCache { + + private final Map cache; + + public MapCSC() { + this(new HashMap<>()); + } + + public MapCSC(Map map) { + this.cache = map; + } + + @Override + public final void clear() { + cache.clear(); + } + + @Override + protected void remove(ByteBuffer key) { + cache.remove(key); + } + + @Override + protected void put(ByteBuffer key, Object value) { + cache.put(key, value); + } + + @Override + protected Object get(ByteBuffer key) { + return cache.get(key); + } +} From 322ffeb8a876ec1fabfe40145fc1cb32c94f863e Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Thu, 25 Jan 2024 19:34:08 +0600 Subject: [PATCH 02/16] Also Guava cache --- pom.xml | 12 ++++- .../redis/clients/jedis/util/CaffeineCSC.java | 3 +- .../redis/clients/jedis/util/GuavaCSC.java | 51 +++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 src/main/java/redis/clients/jedis/util/GuavaCSC.java diff --git a/pom.xml b/pom.xml index 96a4e122f7..c8c8d8149d 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,15 @@ gson 2.10.1 + + + + + com.google.guava + guava + 33.0.0-jre + true + com.github.ben-manes.caffeine caffeine @@ -96,7 +105,8 @@ 1.19.0 test - + + junit junit diff --git a/src/main/java/redis/clients/jedis/util/CaffeineCSC.java b/src/main/java/redis/clients/jedis/util/CaffeineCSC.java index 7a205f40ab..d5537cc73b 100644 --- a/src/main/java/redis/clients/jedis/util/CaffeineCSC.java +++ b/src/main/java/redis/clients/jedis/util/CaffeineCSC.java @@ -21,7 +21,8 @@ public CaffeineCSC(int maximumSize) { } public CaffeineCSC(int maximumSize, int ttlSeconds) { - this(Caffeine.newBuilder().maximumSize(maximumSize).expireAfterWrite(ttlSeconds, TimeUnit.SECONDS).build()); + this(Caffeine.newBuilder().maximumSize(maximumSize) + .expireAfterWrite(ttlSeconds, TimeUnit.SECONDS).build()); } public CaffeineCSC(Cache caffeineCache) { diff --git a/src/main/java/redis/clients/jedis/util/GuavaCSC.java b/src/main/java/redis/clients/jedis/util/GuavaCSC.java new file mode 100644 index 0000000000..b4e31caa62 --- /dev/null +++ b/src/main/java/redis/clients/jedis/util/GuavaCSC.java @@ -0,0 +1,51 @@ +package redis.clients.jedis.util; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; +import redis.clients.jedis.ClientSideCache; + +public class GuavaCSC extends ClientSideCache { + + private static final int DEFAULT_MAXIMUM_SIZE = 10_000; + + private final Cache cache; + + public GuavaCSC() { + this(DEFAULT_MAXIMUM_SIZE); + } + + public GuavaCSC(int maximumSize) { + this(CacheBuilder.newBuilder().maximumSize(maximumSize).build()); + } + + public GuavaCSC(int maximumSize, int ttlSeconds) { + this(CacheBuilder.newBuilder().maximumSize(maximumSize) + .expireAfterWrite(ttlSeconds, TimeUnit.SECONDS).build()); + } + + public GuavaCSC(Cache caffeineCache) { + this.cache = caffeineCache; + } + + @Override + public final void clear() { + cache.invalidateAll(); + } + + @Override + protected void remove(ByteBuffer key) { + cache.invalidate(key); + } + + @Override + protected void put(ByteBuffer key, Object value) { + cache.put(key, value); + } + + @Override + protected Object get(ByteBuffer key) { + return cache.getIfPresent(key); + } +} From 2ef437622c218088dcb0bc66e677714dd1544925 Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Thu, 25 Jan 2024 20:29:05 +0600 Subject: [PATCH 03/16] format pom.xml --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c8c8d8149d..1fcb758640 100644 --- a/pom.xml +++ b/pom.xml @@ -106,7 +106,7 @@ test - + junit junit From 9f160e2192a1e6eb5e8df701b11f58f7483cd94c Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Sun, 28 Jan 2024 13:58:10 +0600 Subject: [PATCH 04/16] Client-side caching by command arguments TODO: Compute hash code. --- .../redis/clients/jedis/ClientSideCache.java | 103 +++++++++++++----- .../clients/jedis/JedisClusterInfoCache.java | 2 +- .../redis/clients/jedis/UnifiedJedis.java | 18 +-- .../SentineledConnectionProvider.java | 2 +- .../redis/clients/jedis/util/CaffeineCSC.java | 19 ++-- .../redis/clients/jedis/util/GuavaCSC.java | 21 ++-- .../JedisClusterClientSideCacheTest.java | 4 +- .../jedis/JedisPooledClientSideCacheTest.java | 4 +- .../JedisSentineledClientSideCacheTest.java | 4 +- .../java/redis/clients/jedis/util/MapCSC.java | 23 ++-- 10 files changed, 125 insertions(+), 75 deletions(-) diff --git a/src/main/java/redis/clients/jedis/ClientSideCache.java b/src/main/java/redis/clients/jedis/ClientSideCache.java index 482c9c0681..063087cff5 100644 --- a/src/main/java/redis/clients/jedis/ClientSideCache.java +++ b/src/main/java/redis/clients/jedis/ClientSideCache.java @@ -1,55 +1,108 @@ package redis.clients.jedis; import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.HashSet; import java.util.List; -import redis.clients.jedis.exceptions.JedisException; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Function; import redis.clients.jedis.util.SafeEncoder; public abstract class ClientSideCache { - protected ClientSideCache() { } + private final Map> keyHashes; + private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); + private final Lock readLock = rwl.readLock(); + private final Lock writeLock = rwl.writeLock(); - public abstract void clear(); + protected ClientSideCache() { + this.keyHashes = new ConcurrentHashMap<>(); + } + + protected ClientSideCache(Map> keyHashes) { + this.keyHashes = keyHashes; + } + + public abstract void invalidateAll(); - protected abstract void remove(ByteBuffer key); + protected abstract void invalidateAll(Iterable hashes); - protected abstract void put(ByteBuffer key, Object value); + protected abstract void put(long hash, Object value); - protected abstract Object get(ByteBuffer key); + protected abstract Object get(long hash); final void invalidateKeys(List list) { if (list == null) { - clear(); - } else { - list.forEach(this::invalidateKey); + invalidateAll(); + return; } + + Set hashes = new HashSet<>(); + list.forEach(key -> hashes.addAll(getHashes(key))); + invalidateAll(hashes); } - private void invalidateKey(Object key) { - if (key instanceof byte[]) { - remove(convertKey((byte[]) key)); - } else { - throw new JedisException("" + key.getClass().getSimpleName() + " is not supported. Value: " + String.valueOf(key)); + private Set getHashes(Object key) { + if (!(key instanceof byte[])) { + throw new AssertionError("" + key.getClass().getSimpleName() + " is not supported. Value: " + String.valueOf(key)); + } + + final ByteBuffer mapKey = makeKey((byte[]) key); + readLock.lock(); + try { + Set hashes = keyHashes.get(mapKey); + return hashes != null ? hashes : Collections.emptySet(); + } finally { + readLock.unlock(); } } - final void set(Object key, Object value) { - put(makeKey(key), value); + final T getValue(Function, T> loader, CommandObject command, String... keys) { + + final long hash = getHash(command); + + T value = (T) get(hash); + if (value != null) { + return value; + } + + value = loader.apply(command); + if (value != null) { + writeLock.lock(); + try { + put(hash, value); + for (String key : keys) { + ByteBuffer mapKey = makeKey(key); + if (keyHashes.containsKey(mapKey)) { + keyHashes.get(mapKey).add(hash); + } else { + Set set = new HashSet<>(); + set.add(hash); + keyHashes.put(mapKey, set); + } + } + } finally { + writeLock.unlock(); + } + } + + return value; } - final T get(Object key) { - return (T) get(makeKey(key)); + private long getHash(CommandObject command) { + // TODO: + return 0; } - private ByteBuffer makeKey(Object key) { - if (key instanceof byte[]) { - return convertKey((byte[]) key); - } else { - return convertKey(SafeEncoder.encode(String.valueOf(key))); - } + private ByteBuffer makeKey(String key) { + return makeKey(SafeEncoder.encode(key)); } - private static ByteBuffer convertKey(byte[] b) { + private static ByteBuffer makeKey(byte[] b) { return ByteBuffer.wrap(b); } } diff --git a/src/main/java/redis/clients/jedis/JedisClusterInfoCache.java b/src/main/java/redis/clients/jedis/JedisClusterInfoCache.java index 5646dbfb59..9402b150ba 100644 --- a/src/main/java/redis/clients/jedis/JedisClusterInfoCache.java +++ b/src/main/java/redis/clients/jedis/JedisClusterInfoCache.java @@ -227,7 +227,7 @@ private void discoverClusterSlots(Connection jedis) { Arrays.fill(slots, null); Arrays.fill(slotNodes, null); if (clientSideCache != null) { - clientSideCache.clear(); + clientSideCache.invalidateAll(); } Set hostAndPortKeys = new HashSet<>(); diff --git a/src/main/java/redis/clients/jedis/UnifiedJedis.java b/src/main/java/redis/clients/jedis/UnifiedJedis.java index 343b24c842..c7936bfb62 100644 --- a/src/main/java/redis/clients/jedis/UnifiedJedis.java +++ b/src/main/java/redis/clients/jedis/UnifiedJedis.java @@ -295,6 +295,14 @@ public void setBroadcastAndRoundRobinConfig(JedisBroadcastAndRoundRobinConfig co this.commandObjects.setBroadcastAndRoundRobinConfig(this.broadcastAndRoundRobinConfig); } + private T executeClientSideCacheCommand(CommandObject command) { + if (clientSideCache == null) { + return executeCommand(command); + } + + return clientSideCache.getValue((cmd) -> executeCommand(cmd), command); + } + public String ping() { return checkAndBroadcastCommand(commandObjects.ping()); } @@ -749,15 +757,7 @@ public String set(String key, String value, SetParams params) { @Override public String get(String key) { - if (clientSideCache != null) { - String cachedValue = clientSideCache.get(key); - if (cachedValue != null) return cachedValue; - - String value = executeCommand(commandObjects.get(key)); - if (value != null) clientSideCache.set(key, value); - return value; - } - return executeCommand(commandObjects.get(key)); + return executeClientSideCacheCommand(commandObjects.get(key)); } @Override diff --git a/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java b/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java index e335803b62..ae883e2af3 100644 --- a/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java +++ b/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java @@ -128,7 +128,7 @@ private void initMaster(HostAndPort master) { pool = newPool; LOG.info("Created connection pool to master at {}.", master); if (clientSideCache != null) { - clientSideCache.clear(); + clientSideCache.invalidateAll(); } if (existingPool != null) { diff --git a/src/main/java/redis/clients/jedis/util/CaffeineCSC.java b/src/main/java/redis/clients/jedis/util/CaffeineCSC.java index d5537cc73b..c3364168c7 100644 --- a/src/main/java/redis/clients/jedis/util/CaffeineCSC.java +++ b/src/main/java/redis/clients/jedis/util/CaffeineCSC.java @@ -2,7 +2,6 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; -import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit; import redis.clients.jedis.ClientSideCache; @@ -10,7 +9,7 @@ public class CaffeineCSC extends ClientSideCache { private static final int DEFAULT_MAXIMUM_SIZE = 10_000; - private final Cache cache; + private final Cache cache; public CaffeineCSC() { this(DEFAULT_MAXIMUM_SIZE); @@ -25,27 +24,27 @@ public CaffeineCSC(int maximumSize, int ttlSeconds) { .expireAfterWrite(ttlSeconds, TimeUnit.SECONDS).build()); } - public CaffeineCSC(Cache caffeineCache) { + public CaffeineCSC(Cache caffeineCache) { this.cache = caffeineCache; } @Override - public final void clear() { + public final void invalidateAll() { cache.invalidateAll(); } @Override - protected void remove(ByteBuffer key) { - cache.invalidate(key); + protected void invalidateAll(Iterable hashes) { + cache.invalidateAll(hashes); } @Override - protected void put(ByteBuffer key, Object value) { - cache.put(key, value); + protected void put(long hash, Object value) { + cache.put(hash, value); } @Override - protected Object get(ByteBuffer key) { - return cache.getIfPresent(key); + protected Object get(long hash) { + return cache.getIfPresent(hash); } } diff --git a/src/main/java/redis/clients/jedis/util/GuavaCSC.java b/src/main/java/redis/clients/jedis/util/GuavaCSC.java index b4e31caa62..e51e0c25f8 100644 --- a/src/main/java/redis/clients/jedis/util/GuavaCSC.java +++ b/src/main/java/redis/clients/jedis/util/GuavaCSC.java @@ -2,7 +2,6 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; -import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit; import redis.clients.jedis.ClientSideCache; @@ -10,7 +9,7 @@ public class GuavaCSC extends ClientSideCache { private static final int DEFAULT_MAXIMUM_SIZE = 10_000; - private final Cache cache; + private final Cache cache; public GuavaCSC() { this(DEFAULT_MAXIMUM_SIZE); @@ -25,27 +24,27 @@ public GuavaCSC(int maximumSize, int ttlSeconds) { .expireAfterWrite(ttlSeconds, TimeUnit.SECONDS).build()); } - public GuavaCSC(Cache caffeineCache) { - this.cache = caffeineCache; + public GuavaCSC(Cache guavaCache) { + this.cache = guavaCache; } @Override - public final void clear() { + public final void invalidateAll() { cache.invalidateAll(); } @Override - protected void remove(ByteBuffer key) { - cache.invalidate(key); + protected void invalidateAll(Iterable hashes) { + cache.invalidateAll(hashes); } @Override - protected void put(ByteBuffer key, Object value) { - cache.put(key, value); + protected void put(long hash, Object value) { + cache.put(hash, value); } @Override - protected Object get(ByteBuffer key) { - return cache.getIfPresent(key); + protected Object get(long hash) { + return cache.getIfPresent(hash); } } diff --git a/src/test/java/redis/clients/jedis/JedisClusterClientSideCacheTest.java b/src/test/java/redis/clients/jedis/JedisClusterClientSideCacheTest.java index 8e276d9665..2f11af837b 100644 --- a/src/test/java/redis/clients/jedis/JedisClusterClientSideCacheTest.java +++ b/src/test/java/redis/clients/jedis/JedisClusterClientSideCacheTest.java @@ -42,7 +42,7 @@ public void simple() { @Test public void simpleWithSimpleMap() { - HashMap map = new HashMap<>(); + HashMap map = new HashMap<>(); try (JedisCluster jedis = new JedisCluster(hnp, clientConfig.get(), new MapCSC(map), singleConnectionPoolConfig.get())) { jedis.set("foo", "bar"); assertThat(map, Matchers.aMapWithSize(0)); @@ -71,7 +71,7 @@ public void flushAll() { @Test public void flushAllWithSimpleMap() { - HashMap map = new HashMap<>(); + HashMap map = new HashMap<>(); try (JedisCluster jedis = new JedisCluster(hnp, clientConfig.get(), new MapCSC(map), singleConnectionPoolConfig.get())) { jedis.set("foo", "bar"); assertThat(map, Matchers.aMapWithSize(0)); diff --git a/src/test/java/redis/clients/jedis/JedisPooledClientSideCacheTest.java b/src/test/java/redis/clients/jedis/JedisPooledClientSideCacheTest.java index 805d9738d7..b5741454db 100644 --- a/src/test/java/redis/clients/jedis/JedisPooledClientSideCacheTest.java +++ b/src/test/java/redis/clients/jedis/JedisPooledClientSideCacheTest.java @@ -53,7 +53,7 @@ public void simple() { @Test public void simpleWithSimpleMap() { - HashMap map = new HashMap<>(); + HashMap map = new HashMap<>(); try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), new MapCSC(map), singleConnectionPoolConfig.get())) { control.set("foo", "bar"); assertThat(map, Matchers.aMapWithSize(0)); @@ -82,7 +82,7 @@ public void flushAll() { @Test public void flushAllWithSimpleMap() { - HashMap map = new HashMap<>(); + HashMap map = new HashMap<>(); try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), new MapCSC(map), singleConnectionPoolConfig.get())) { control.set("foo", "bar"); assertThat(map, Matchers.aMapWithSize(0)); diff --git a/src/test/java/redis/clients/jedis/JedisSentineledClientSideCacheTest.java b/src/test/java/redis/clients/jedis/JedisSentineledClientSideCacheTest.java index 5fa5a36f4b..de20a356f9 100644 --- a/src/test/java/redis/clients/jedis/JedisSentineledClientSideCacheTest.java +++ b/src/test/java/redis/clients/jedis/JedisSentineledClientSideCacheTest.java @@ -39,7 +39,7 @@ public void simple() { @Test public void simpleWithSimpleMap() { - HashMap map = new HashMap<>(); + HashMap map = new HashMap<>(); try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, masterClientConfig, new MapCSC(map), sentinels, sentinelClientConfig)) { jedis.set("foo", "bar"); assertThat(map, Matchers.aMapWithSize(0)); @@ -68,7 +68,7 @@ public void flushAll() { @Test public void flushAllWithSimpleMap() { - HashMap map = new HashMap<>(); + HashMap map = new HashMap<>(); try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, masterClientConfig, new MapCSC(map), sentinels, sentinelClientConfig)) { jedis.set("foo", "bar"); assertThat(map, Matchers.aMapWithSize(0)); diff --git a/src/test/java/redis/clients/jedis/util/MapCSC.java b/src/test/java/redis/clients/jedis/util/MapCSC.java index a9d1ef6f37..1db1a64810 100644 --- a/src/test/java/redis/clients/jedis/util/MapCSC.java +++ b/src/test/java/redis/clients/jedis/util/MapCSC.java @@ -1,39 +1,38 @@ package redis.clients.jedis.util; -import java.nio.ByteBuffer; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import redis.clients.jedis.ClientSideCache; public class MapCSC extends ClientSideCache { - private final Map cache; + private final Map cache; public MapCSC() { - this(new HashMap<>()); + this(new ConcurrentHashMap<>()); } - public MapCSC(Map map) { + public MapCSC(Map map) { this.cache = map; } @Override - public final void clear() { + public final void invalidateAll() { cache.clear(); } @Override - protected void remove(ByteBuffer key) { - cache.remove(key); + protected void invalidateAll(Iterable hashes) { + hashes.forEach(hash -> cache.remove(hash)); } @Override - protected void put(ByteBuffer key, Object value) { - cache.put(key, value); + protected void put(long hash, Object value) { + cache.put(hash, value); } @Override - protected Object get(ByteBuffer key) { - return cache.get(key); + protected Object get(long hash) { + return cache.get(hash); } } From 3827e7d7dc98480d28f0e1b67559d288a94bf31a Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Sun, 28 Jan 2024 16:08:10 +0600 Subject: [PATCH 05/16] send keys --- src/main/java/redis/clients/jedis/UnifiedJedis.java | 6 +++--- .../clients/jedis/JedisClusterClientSideCacheTest.java | 1 - .../redis/clients/jedis/JedisPooledClientSideCacheTest.java | 1 - .../clients/jedis/JedisSentineledClientSideCacheTest.java | 1 - 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/java/redis/clients/jedis/UnifiedJedis.java b/src/main/java/redis/clients/jedis/UnifiedJedis.java index c7936bfb62..3a2dec9d77 100644 --- a/src/main/java/redis/clients/jedis/UnifiedJedis.java +++ b/src/main/java/redis/clients/jedis/UnifiedJedis.java @@ -295,12 +295,12 @@ public void setBroadcastAndRoundRobinConfig(JedisBroadcastAndRoundRobinConfig co this.commandObjects.setBroadcastAndRoundRobinConfig(this.broadcastAndRoundRobinConfig); } - private T executeClientSideCacheCommand(CommandObject command) { + private T executeClientSideCacheCommand(CommandObject command, String... keys) { if (clientSideCache == null) { return executeCommand(command); } - return clientSideCache.getValue((cmd) -> executeCommand(cmd), command); + return clientSideCache.getValue((cmd) -> executeCommand(cmd), command, keys); } public String ping() { @@ -757,7 +757,7 @@ public String set(String key, String value, SetParams params) { @Override public String get(String key) { - return executeClientSideCacheCommand(commandObjects.get(key)); + return executeClientSideCacheCommand(commandObjects.get(key), key); } @Override diff --git a/src/test/java/redis/clients/jedis/JedisClusterClientSideCacheTest.java b/src/test/java/redis/clients/jedis/JedisClusterClientSideCacheTest.java index 2f11af837b..60ddf002d7 100644 --- a/src/test/java/redis/clients/jedis/JedisClusterClientSideCacheTest.java +++ b/src/test/java/redis/clients/jedis/JedisClusterClientSideCacheTest.java @@ -4,7 +4,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -import java.nio.ByteBuffer; import java.util.Arrays; import java.util.HashMap; import java.util.Set; diff --git a/src/test/java/redis/clients/jedis/JedisPooledClientSideCacheTest.java b/src/test/java/redis/clients/jedis/JedisPooledClientSideCacheTest.java index b5741454db..2e641e0f3a 100644 --- a/src/test/java/redis/clients/jedis/JedisPooledClientSideCacheTest.java +++ b/src/test/java/redis/clients/jedis/JedisPooledClientSideCacheTest.java @@ -4,7 +4,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -import java.nio.ByteBuffer; import java.util.HashMap; import java.util.function.Supplier; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; diff --git a/src/test/java/redis/clients/jedis/JedisSentineledClientSideCacheTest.java b/src/test/java/redis/clients/jedis/JedisSentineledClientSideCacheTest.java index de20a356f9..9e5f720933 100644 --- a/src/test/java/redis/clients/jedis/JedisSentineledClientSideCacheTest.java +++ b/src/test/java/redis/clients/jedis/JedisSentineledClientSideCacheTest.java @@ -4,7 +4,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -import java.nio.ByteBuffer; import java.util.Arrays; import java.util.HashMap; import java.util.Set; From e910168f2cbdef84b20abb18daa5056f607e5b52 Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Tue, 30 Jan 2024 16:20:30 +0600 Subject: [PATCH 06/16] todo comment for clean-up --- src/main/java/redis/clients/jedis/ClientSideCache.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/redis/clients/jedis/ClientSideCache.java b/src/main/java/redis/clients/jedis/ClientSideCache.java index 063087cff5..d21170afe3 100644 --- a/src/main/java/redis/clients/jedis/ClientSideCache.java +++ b/src/main/java/redis/clients/jedis/ClientSideCache.java @@ -14,7 +14,7 @@ public abstract class ClientSideCache { - private final Map> keyHashes; + private final Map> keyHashes; // TODO: clean-up private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); private final Lock readLock = rwl.readLock(); private final Lock writeLock = rwl.writeLock(); @@ -44,6 +44,7 @@ final void invalidateKeys(List list) { Set hashes = new HashSet<>(); list.forEach(key -> hashes.addAll(getHashes(key))); invalidateAll(hashes); + // TODO: clean-up keyHashes } private Set getHashes(Object key) { From 0ba4d7d3f0654c330f76140545062e31780bd8bb Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Tue, 30 Jan 2024 16:24:16 +0600 Subject: [PATCH 07/16] rename method to invalidate --- src/main/java/redis/clients/jedis/ClientSideCache.java | 2 +- src/main/java/redis/clients/jedis/Protocol.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/redis/clients/jedis/ClientSideCache.java b/src/main/java/redis/clients/jedis/ClientSideCache.java index d21170afe3..616fe7d7b5 100644 --- a/src/main/java/redis/clients/jedis/ClientSideCache.java +++ b/src/main/java/redis/clients/jedis/ClientSideCache.java @@ -35,7 +35,7 @@ protected ClientSideCache(Map> keyHashes) { protected abstract Object get(long hash); - final void invalidateKeys(List list) { + final void invalidate(List list) { if (list == null) { invalidateAll(); return; diff --git a/src/main/java/redis/clients/jedis/Protocol.java b/src/main/java/redis/clients/jedis/Protocol.java index 4af1261cd4..4bd82fec1e 100644 --- a/src/main/java/redis/clients/jedis/Protocol.java +++ b/src/main/java/redis/clients/jedis/Protocol.java @@ -248,7 +248,7 @@ private static void processPush(final RedisInputStream is, ClientSideCache cache //System.out.println("PUSH: " + SafeEncoder.encodeObject(list)); if (list.size() == 2 && list.get(0) instanceof byte[] && Arrays.equals(INVALIDATE_BYTES, (byte[]) list.get(0))) { - cache.invalidateKeys((List) list.get(1)); + cache.invalidate((List) list.get(1)); } } From e3a5900fa32f931373c02d421fa1e6f85762c0ce Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Wed, 31 Jan 2024 00:41:31 +0600 Subject: [PATCH 08/16] Client-side caching by hashing command arguments --- .../redis/clients/jedis/ClientSideCache.java | 17 ++-- .../redis/clients/jedis/util/CaffeineCSC.java | 12 +++ .../redis/clients/jedis/util/GuavaCSC.java | 82 +++++++++++++++---- .../java/redis/clients/jedis/util/MapCSC.java | 12 +++ 4 files changed, 97 insertions(+), 26 deletions(-) diff --git a/src/main/java/redis/clients/jedis/ClientSideCache.java b/src/main/java/redis/clients/jedis/ClientSideCache.java index 616fe7d7b5..2b15fb75df 100644 --- a/src/main/java/redis/clients/jedis/ClientSideCache.java +++ b/src/main/java/redis/clients/jedis/ClientSideCache.java @@ -23,18 +23,10 @@ protected ClientSideCache() { this.keyHashes = new ConcurrentHashMap<>(); } - protected ClientSideCache(Map> keyHashes) { - this.keyHashes = keyHashes; - } - public abstract void invalidateAll(); protected abstract void invalidateAll(Iterable hashes); - protected abstract void put(long hash, Object value); - - protected abstract Object get(long hash); - final void invalidate(List list) { if (list == null) { invalidateAll(); @@ -62,6 +54,10 @@ private Set getHashes(Object key) { } } + protected abstract void put(long hash, Object value); + + protected abstract Object get(long hash); + final T getValue(Function, T> loader, CommandObject command, String... keys) { final long hash = getHash(command); @@ -94,10 +90,7 @@ final T getValue(Function, T> loader, CommandObject comm return value; } - private long getHash(CommandObject command) { - // TODO: - return 0; - } + protected abstract long getHash(CommandObject command); private ByteBuffer makeKey(String key) { return makeKey(SafeEncoder.encode(key)); diff --git a/src/main/java/redis/clients/jedis/util/CaffeineCSC.java b/src/main/java/redis/clients/jedis/util/CaffeineCSC.java index c3364168c7..c12f7a475b 100644 --- a/src/main/java/redis/clients/jedis/util/CaffeineCSC.java +++ b/src/main/java/redis/clients/jedis/util/CaffeineCSC.java @@ -2,8 +2,11 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; +import java.util.Arrays; import java.util.concurrent.TimeUnit; import redis.clients.jedis.ClientSideCache; +import redis.clients.jedis.CommandObject; +import redis.clients.jedis.args.Rawable; public class CaffeineCSC extends ClientSideCache { @@ -47,4 +50,13 @@ protected void put(long hash, Object value) { protected Object get(long hash) { return cache.getIfPresent(hash); } + + @Override // TODO: + protected final long getHash(CommandObject command) { + long result = 1; + for (Rawable raw : command.getArguments()) { + result = 31 * result + Arrays.hashCode(raw.getRaw()); + } + return 31 * result + command.getBuilder().hashCode(); + } } diff --git a/src/main/java/redis/clients/jedis/util/GuavaCSC.java b/src/main/java/redis/clients/jedis/util/GuavaCSC.java index e51e0c25f8..282c03790e 100644 --- a/src/main/java/redis/clients/jedis/util/GuavaCSC.java +++ b/src/main/java/redis/clients/jedis/util/GuavaCSC.java @@ -2,30 +2,25 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hasher; +import java.time.Duration; import java.util.concurrent.TimeUnit; import redis.clients.jedis.ClientSideCache; +import redis.clients.jedis.CommandObject; public class GuavaCSC extends ClientSideCache { private static final int DEFAULT_MAXIMUM_SIZE = 10_000; + private static final int DEFAULT_EXPIRE_MINUTES = 5; + private static final HashFunction DEFAULT_FUNCTION = com.google.common.hash.Hashing.fingerprint2011(); private final Cache cache; + private final HashFunction function; - public GuavaCSC() { - this(DEFAULT_MAXIMUM_SIZE); - } - - public GuavaCSC(int maximumSize) { - this(CacheBuilder.newBuilder().maximumSize(maximumSize).build()); - } - - public GuavaCSC(int maximumSize, int ttlSeconds) { - this(CacheBuilder.newBuilder().maximumSize(maximumSize) - .expireAfterWrite(ttlSeconds, TimeUnit.SECONDS).build()); - } - - public GuavaCSC(Cache guavaCache) { + public GuavaCSC(Cache guavaCache, HashFunction hashFunction) { this.cache = guavaCache; + this.function = hashFunction; } @Override @@ -47,4 +42,63 @@ protected void put(long hash, Object value) { protected Object get(long hash) { return cache.getIfPresent(hash); } + + @Override + protected final long getHash(CommandObject command) { + Hasher hasher = function.newHasher(); + command.getArguments().forEach(raw -> hasher.putBytes(raw.getRaw())); + hasher.putInt(command.getBuilder().hashCode()); + return hasher.hash().asLong(); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private long maximumSize = DEFAULT_MAXIMUM_SIZE; + private Duration expireDuration = null; + private long expireTime = DEFAULT_EXPIRE_MINUTES; + private TimeUnit expireTimeUnit = TimeUnit.MINUTES; + + private HashFunction hashFunction = DEFAULT_FUNCTION; + + private Builder() { } + + public Builder maximumSize(int size) { + this.maximumSize = size; + return this; + } + + public Builder ttl(Duration duration) { + this.expireDuration = duration; + return this; + } + + public Builder ttl(long time, TimeUnit unit) { + this.expireTime = time; + this.expireTimeUnit = unit; + return this; + } + + public Builder hashFunction(HashFunction function) { + this.hashFunction = function; + return this; + } + + public GuavaCSC build() { + CacheBuilder cb = CacheBuilder.newBuilder(); + + cb.maximumSize(maximumSize); + + if (expireDuration != null) { + cb.expireAfterWrite(expireDuration); + } else { + cb.expireAfterWrite(expireTime, expireTimeUnit); + } + + return new GuavaCSC(cb.build(), hashFunction); + } + } } diff --git a/src/test/java/redis/clients/jedis/util/MapCSC.java b/src/test/java/redis/clients/jedis/util/MapCSC.java index 1db1a64810..0915fd9050 100644 --- a/src/test/java/redis/clients/jedis/util/MapCSC.java +++ b/src/test/java/redis/clients/jedis/util/MapCSC.java @@ -1,8 +1,11 @@ package redis.clients.jedis.util; +import java.util.Arrays; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import redis.clients.jedis.ClientSideCache; +import redis.clients.jedis.CommandObject; +import redis.clients.jedis.args.Rawable; public class MapCSC extends ClientSideCache { @@ -35,4 +38,13 @@ protected void put(long hash, Object value) { protected Object get(long hash) { return cache.get(hash); } + + @Override + protected final long getHash(CommandObject command) { + long result = 1; + for (Rawable raw : command.getArguments()) { + result = 31 * result + Arrays.hashCode(raw.getRaw()); + } + return 31 * result + command.getBuilder().hashCode(); + } } From d7720a1386b9ad633925cda32eddcf848f39bc72 Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Thu, 1 Feb 2024 00:47:01 +0600 Subject: [PATCH 09/16] Hash command arguments for CaffeineCSC using OpenHFT hashing --- pom.xml | 6 ++ .../redis/clients/jedis/util/CaffeineCSC.java | 71 ++++++++++++++----- .../redis/clients/jedis/util/GuavaCSC.java | 28 +++----- 3 files changed, 66 insertions(+), 39 deletions(-) diff --git a/pom.xml b/pom.xml index 1fcb758640..e6ff5bc70b 100644 --- a/pom.xml +++ b/pom.xml @@ -89,6 +89,12 @@ 2.9.3 true + + net.openhft + zero-allocation-hashing + 0.16 + true + diff --git a/src/main/java/redis/clients/jedis/util/CaffeineCSC.java b/src/main/java/redis/clients/jedis/util/CaffeineCSC.java index c12f7a475b..36f354bc4a 100644 --- a/src/main/java/redis/clients/jedis/util/CaffeineCSC.java +++ b/src/main/java/redis/clients/jedis/util/CaffeineCSC.java @@ -2,8 +2,8 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; -import java.util.Arrays; import java.util.concurrent.TimeUnit; +import net.openhft.hashing.LongHashFunction; import redis.clients.jedis.ClientSideCache; import redis.clients.jedis.CommandObject; import redis.clients.jedis.args.Rawable; @@ -11,24 +11,15 @@ public class CaffeineCSC extends ClientSideCache { private static final int DEFAULT_MAXIMUM_SIZE = 10_000; + private static final int DEFAULT_EXPIRE_SECONDS = 100; + private static final LongHashFunction DEFAULT_HASH_FUNCTION = LongHashFunction.xx3(); private final Cache cache; + private final LongHashFunction function; - public CaffeineCSC() { - this(DEFAULT_MAXIMUM_SIZE); - } - - public CaffeineCSC(int maximumSize) { - this(Caffeine.newBuilder().maximumSize(maximumSize).build()); - } - - public CaffeineCSC(int maximumSize, int ttlSeconds) { - this(Caffeine.newBuilder().maximumSize(maximumSize) - .expireAfterWrite(ttlSeconds, TimeUnit.SECONDS).build()); - } - - public CaffeineCSC(Cache caffeineCache) { + public CaffeineCSC(Cache caffeineCache, LongHashFunction hashFunction) { this.cache = caffeineCache; + this.function = hashFunction; } @Override @@ -51,12 +42,54 @@ protected Object get(long hash) { return cache.getIfPresent(hash); } - @Override // TODO: + @Override protected final long getHash(CommandObject command) { - long result = 1; + long[] nums = new long[command.getArguments().size() + 1]; + int idx = 0; for (Rawable raw : command.getArguments()) { - result = 31 * result + Arrays.hashCode(raw.getRaw()); + nums[idx++] = function.hashBytes(raw.getRaw()); + } + nums[idx] = function.hashInt(command.getBuilder().hashCode()); + return function.hashLongs(nums); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private long maximumSize = DEFAULT_MAXIMUM_SIZE; + private long expireTime = DEFAULT_EXPIRE_SECONDS; + private final TimeUnit expireTimeUnit = TimeUnit.SECONDS; + + private LongHashFunction hashFunction = DEFAULT_HASH_FUNCTION; + + private Builder() { } + + public Builder maximumSize(int size) { + this.maximumSize = size; + return this; + } + + public Builder ttl(int seconds) { + this.expireTime = seconds; + return this; + } + + public Builder hashFunction(LongHashFunction function) { + this.hashFunction = function; + return this; + } + + public CaffeineCSC build() { + Caffeine cb = Caffeine.newBuilder(); + + cb.maximumSize(maximumSize); + + cb.expireAfterWrite(expireTime, expireTimeUnit); + + return new CaffeineCSC(cb.build(), hashFunction); } - return 31 * result + command.getBuilder().hashCode(); } } diff --git a/src/main/java/redis/clients/jedis/util/GuavaCSC.java b/src/main/java/redis/clients/jedis/util/GuavaCSC.java index 282c03790e..643c62b9a9 100644 --- a/src/main/java/redis/clients/jedis/util/GuavaCSC.java +++ b/src/main/java/redis/clients/jedis/util/GuavaCSC.java @@ -4,7 +4,6 @@ import com.google.common.cache.CacheBuilder; import com.google.common.hash.HashFunction; import com.google.common.hash.Hasher; -import java.time.Duration; import java.util.concurrent.TimeUnit; import redis.clients.jedis.ClientSideCache; import redis.clients.jedis.CommandObject; @@ -12,8 +11,8 @@ public class GuavaCSC extends ClientSideCache { private static final int DEFAULT_MAXIMUM_SIZE = 10_000; - private static final int DEFAULT_EXPIRE_MINUTES = 5; - private static final HashFunction DEFAULT_FUNCTION = com.google.common.hash.Hashing.fingerprint2011(); + private static final int DEFAULT_EXPIRE_SECONDS = 100; + private static final HashFunction DEFAULT_HASH_FUNCTION = com.google.common.hash.Hashing.fingerprint2011(); private final Cache cache; private final HashFunction function; @@ -58,11 +57,10 @@ public static Builder builder() { public static class Builder { private long maximumSize = DEFAULT_MAXIMUM_SIZE; - private Duration expireDuration = null; - private long expireTime = DEFAULT_EXPIRE_MINUTES; - private TimeUnit expireTimeUnit = TimeUnit.MINUTES; + private long expireTime = DEFAULT_EXPIRE_SECONDS; + private final TimeUnit expireTimeUnit = TimeUnit.SECONDS; - private HashFunction hashFunction = DEFAULT_FUNCTION; + private HashFunction hashFunction = DEFAULT_HASH_FUNCTION; private Builder() { } @@ -71,14 +69,8 @@ public Builder maximumSize(int size) { return this; } - public Builder ttl(Duration duration) { - this.expireDuration = duration; - return this; - } - - public Builder ttl(long time, TimeUnit unit) { - this.expireTime = time; - this.expireTimeUnit = unit; + public Builder ttl(int seconds) { + this.expireTime = seconds; return this; } @@ -92,11 +84,7 @@ public GuavaCSC build() { cb.maximumSize(maximumSize); - if (expireDuration != null) { - cb.expireAfterWrite(expireDuration); - } else { - cb.expireAfterWrite(expireTime, expireTimeUnit); - } + cb.expireAfterWrite(expireTime, expireTimeUnit); return new GuavaCSC(cb.build(), hashFunction); } From d3acff22529e6469bb371cc909feaf3752df59e2 Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Thu, 1 Feb 2024 15:02:39 +0600 Subject: [PATCH 10/16] Clean-up keyHashes map --- .../redis/clients/jedis/ClientSideCache.java | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/main/java/redis/clients/jedis/ClientSideCache.java b/src/main/java/redis/clients/jedis/ClientSideCache.java index 2b15fb75df..1371a2d376 100644 --- a/src/main/java/redis/clients/jedis/ClientSideCache.java +++ b/src/main/java/redis/clients/jedis/ClientSideCache.java @@ -1,23 +1,19 @@ package redis.clients.jedis; import java.nio.ByteBuffer; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.Function; import redis.clients.jedis.util.SafeEncoder; public abstract class ClientSideCache { - private final Map> keyHashes; // TODO: clean-up - private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); - private final Lock readLock = rwl.readLock(); - private final Lock writeLock = rwl.writeLock(); + private final Map> keyHashes; + private final ReentrantLock writeLock = new ReentrantLock(); protected ClientSideCache() { this.keyHashes = new ConcurrentHashMap<>(); @@ -33,24 +29,25 @@ final void invalidate(List list) { return; } - Set hashes = new HashSet<>(); - list.forEach(key -> hashes.addAll(getHashes(key))); - invalidateAll(hashes); - // TODO: clean-up keyHashes + list.forEach(this::invalidate0); } - private Set getHashes(Object key) { + private void invalidate0(Object key) { if (!(key instanceof byte[])) { throw new AssertionError("" + key.getClass().getSimpleName() + " is not supported. Value: " + String.valueOf(key)); } final ByteBuffer mapKey = makeKey((byte[]) key); - readLock.lock(); - try { - Set hashes = keyHashes.get(mapKey); - return hashes != null ? hashes : Collections.emptySet(); - } finally { - readLock.unlock(); + + Set hashes = keyHashes.get(mapKey); + if (hashes != null) { + writeLock.lock(); + try { + invalidateAll(hashes); + keyHashes.remove(mapKey); + } finally { + writeLock.unlock(); + } } } From ec395b5eef4a8107a54e4e86f864acc963754430 Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Sun, 4 Feb 2024 17:09:07 +0600 Subject: [PATCH 11/16] Support Client-side caching through URI/URL --- .../java/redis/clients/jedis/JedisPooled.java | 4 +- .../redis/clients/jedis/UnifiedJedis.java | 8 +- .../clients/jedis/util/JedisURIHelper.java | 78 +++++++++++++++++-- 3 files changed, 81 insertions(+), 9 deletions(-) diff --git a/src/main/java/redis/clients/jedis/JedisPooled.java b/src/main/java/redis/clients/jedis/JedisPooled.java index 44e476d7b8..a5840c6530 100644 --- a/src/main/java/redis/clients/jedis/JedisPooled.java +++ b/src/main/java/redis/clients/jedis/JedisPooled.java @@ -26,7 +26,7 @@ public JedisPooled() { * @param url */ public JedisPooled(final String url) { - this(URI.create(url)); + super(url); } /** @@ -76,7 +76,7 @@ public JedisPooled(final HostAndPort hostAndPort, final JedisClientConfig client } public JedisPooled(final HostAndPort hostAndPort, final JedisClientConfig clientConfig, ClientSideCache csCache) { - super(new PooledConnectionProvider(hostAndPort, clientConfig, csCache), clientConfig.getRedisProtocol(), csCache); + super(hostAndPort, clientConfig, csCache); } public JedisPooled(PooledObjectFactory factory) { diff --git a/src/main/java/redis/clients/jedis/UnifiedJedis.java b/src/main/java/redis/clients/jedis/UnifiedJedis.java index 3a2dec9d77..1bdd7a7ca8 100644 --- a/src/main/java/redis/clients/jedis/UnifiedJedis.java +++ b/src/main/java/redis/clients/jedis/UnifiedJedis.java @@ -72,7 +72,7 @@ public UnifiedJedis(final URI uri) { this(JedisURIHelper.getHostAndPort(uri), DefaultJedisClientConfig.builder() .user(JedisURIHelper.getUser(uri)).password(JedisURIHelper.getPassword(uri)) .database(JedisURIHelper.getDBIndex(uri)).protocol(JedisURIHelper.getRedisProtocol(uri)) - .ssl(JedisURIHelper.isRedisSSLScheme(uri)).build()); + .ssl(JedisURIHelper.isRedisSSLScheme(uri)).build(), JedisURIHelper.getClientSideCache(uri)); } public UnifiedJedis(final URI uri, JedisClientConfig config) { @@ -85,13 +85,17 @@ public UnifiedJedis(final URI uri, JedisClientConfig config) { .protocol(JedisURIHelper.getRedisProtocol(uri)) .ssl(JedisURIHelper.isRedisSSLScheme(uri)).sslSocketFactory(config.getSslSocketFactory()) .sslParameters(config.getSslParameters()).hostnameVerifier(config.getHostnameVerifier()) - .build()); + .build(), JedisURIHelper.getClientSideCache(uri)); } public UnifiedJedis(HostAndPort hostAndPort, JedisClientConfig clientConfig) { this(new PooledConnectionProvider(hostAndPort, clientConfig), clientConfig.getRedisProtocol()); } + public UnifiedJedis(HostAndPort hostAndPort, JedisClientConfig clientConfig, ClientSideCache clientSideCache) { + this(new PooledConnectionProvider(hostAndPort, clientConfig, clientSideCache), clientConfig.getRedisProtocol(), clientSideCache); + } + public UnifiedJedis(ConnectionProvider provider) { this(new DefaultCommandExecutor(provider), provider); } diff --git a/src/main/java/redis/clients/jedis/util/JedisURIHelper.java b/src/main/java/redis/clients/jedis/util/JedisURIHelper.java index 6bbd1599a8..8f0a245af6 100644 --- a/src/main/java/redis/clients/jedis/util/JedisURIHelper.java +++ b/src/main/java/redis/clients/jedis/util/JedisURIHelper.java @@ -1,6 +1,7 @@ package redis.clients.jedis.util; import java.net.URI; +import redis.clients.jedis.ClientSideCache; import redis.clients.jedis.HostAndPort; import redis.clients.jedis.Protocol; import redis.clients.jedis.RedisProtocol; @@ -54,11 +55,11 @@ public static int getDBIndex(URI uri) { public static RedisProtocol getRedisProtocol(URI uri) { if (uri.getQuery() == null) return null; - String[] pairs = uri.getQuery().split("&"); - for (String pair : pairs) { - int idx = pair.indexOf("="); - if ("protocol".equals(pair.substring(0, idx))) { - String ver = pair.substring(idx + 1); + String[] params = uri.getQuery().split("&"); + for (String param : params) { + int idx = param.indexOf("="); + if ("protocol".equals(param.substring(0, idx))) { + String ver = param.substring(idx + 1); for (RedisProtocol proto : RedisProtocol.values()) { if (proto.version().equals(ver)) { return proto; @@ -70,6 +71,73 @@ public static RedisProtocol getRedisProtocol(URI uri) { return null; // null (default) when not defined } + private static final Integer ZERO_INTEGER = 0; + + public static ClientSideCache getClientSideCache(URI uri) { + if (uri.getQuery() == null) return null; + + boolean guava = false, caffeine = false; // cache_lib + Integer maxSize = null; // cache_max_size --> 0 = disbale + Integer ttl = null; // cache_ttl --> 0 = no ttl + // cache-max-idle + + String[] params = uri.getQuery().split("&"); + for (String param : params) { + int idx = param.indexOf("="); + String key = param.substring(0, idx); + String val = param.substring(idx + 1); + + switch (key) { + + case "cache_lib": + switch (val) { + case "guava": + guava = true; + break; + case "caffeine": + caffeine = true; + break; + default: + throw new IllegalArgumentException("Unsupported library " + val); + } + break; + + case "cache_max_size": + maxSize = Integer.parseInt(val); + break; + + case "ttl": + ttl = Integer.parseInt(val); + break; + } + } + + // special cases + if (ZERO_INTEGER.equals(maxSize)) { + return null; + } + if (ZERO_INTEGER.equals(ttl)) { + ttl = null; // below, only null will be checked + } + if (!guava && !caffeine) { + throw new IllegalArgumentException("The cache library (guava OR caffeine) must be selected."); + } + + if (guava) { + GuavaCSC.Builder guavaBuilder = GuavaCSC.builder(); + if (maxSize != null) guavaBuilder.maximumSize(maxSize); + if (ttl != null) guavaBuilder.ttl(ttl); + return guavaBuilder.build(); + } else if (caffeine) { + CaffeineCSC.Builder caffeineBuilder = CaffeineCSC.builder(); + if (maxSize != null) caffeineBuilder.maximumSize(maxSize); + if (ttl != null) caffeineBuilder.ttl(ttl); + return caffeineBuilder.build(); + } + + return null; // null (default) when not defined + } + public static boolean isValid(URI uri) { if (isEmpty(uri.getScheme()) || isEmpty(uri.getHost()) || uri.getPort() == -1) { return false; From cb40a985b6c3d265e3999e78b23e80d11e3b67ca Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Wed, 7 Feb 2024 18:46:17 +0600 Subject: [PATCH 12/16] check idx of '=' sign --- src/main/java/redis/clients/jedis/util/JedisURIHelper.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/redis/clients/jedis/util/JedisURIHelper.java b/src/main/java/redis/clients/jedis/util/JedisURIHelper.java index 8f0a245af6..b12cb0ea66 100644 --- a/src/main/java/redis/clients/jedis/util/JedisURIHelper.java +++ b/src/main/java/redis/clients/jedis/util/JedisURIHelper.java @@ -58,6 +58,7 @@ public static RedisProtocol getRedisProtocol(URI uri) { String[] params = uri.getQuery().split("&"); for (String param : params) { int idx = param.indexOf("="); + if (idx < 0) continue; if ("protocol".equals(param.substring(0, idx))) { String ver = param.substring(idx + 1); for (RedisProtocol proto : RedisProtocol.values()) { @@ -84,6 +85,8 @@ public static ClientSideCache getClientSideCache(URI uri) { String[] params = uri.getQuery().split("&"); for (String param : params) { int idx = param.indexOf("="); + if (idx < 0) continue; + String key = param.substring(0, idx); String val = param.substring(idx + 1); From 9aa8a130b703ffa36ad6b67bc7aaaaa236127762 Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Wed, 7 Feb 2024 18:47:17 +0600 Subject: [PATCH 13/16] nicer exception --- .../redis/clients/jedis/util/JedisURIHelper.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/java/redis/clients/jedis/util/JedisURIHelper.java b/src/main/java/redis/clients/jedis/util/JedisURIHelper.java index b12cb0ea66..44f294d3dc 100644 --- a/src/main/java/redis/clients/jedis/util/JedisURIHelper.java +++ b/src/main/java/redis/clients/jedis/util/JedisURIHelper.java @@ -5,6 +5,7 @@ import redis.clients.jedis.HostAndPort; import redis.clients.jedis.Protocol; import redis.clients.jedis.RedisProtocol; +import redis.clients.jedis.exceptions.JedisValidationException; public final class JedisURIHelper { @@ -106,11 +107,19 @@ public static ClientSideCache getClientSideCache(URI uri) { break; case "cache_max_size": - maxSize = Integer.parseInt(val); + try { + maxSize = Integer.parseInt(val); + } catch (NumberFormatException nfe) { + throw new JedisValidationException("Value of cache_max_size must be an integer.", nfe); + } break; case "ttl": - ttl = Integer.parseInt(val); + try { + ttl = Integer.parseInt(val); + } catch (NumberFormatException nfe) { + throw new JedisValidationException("Value of ttl must be an integer denoting seconds.", nfe); + } break; } } From be3b45b7dfb8c47ff4e8b121c15b95fb2a0b5782 Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Wed, 7 Feb 2024 18:49:13 +0600 Subject: [PATCH 14/16] edit/fix condition --- src/main/java/redis/clients/jedis/util/JedisURIHelper.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/redis/clients/jedis/util/JedisURIHelper.java b/src/main/java/redis/clients/jedis/util/JedisURIHelper.java index 44f294d3dc..e574c41227 100644 --- a/src/main/java/redis/clients/jedis/util/JedisURIHelper.java +++ b/src/main/java/redis/clients/jedis/util/JedisURIHelper.java @@ -128,12 +128,12 @@ public static ClientSideCache getClientSideCache(URI uri) { if (ZERO_INTEGER.equals(maxSize)) { return null; } + if (!guava && !caffeine && (maxSize != null || ttl != null)) { + throw new IllegalArgumentException("The cache library (guava OR caffeine) must be selected."); + } if (ZERO_INTEGER.equals(ttl)) { ttl = null; // below, only null will be checked } - if (!guava && !caffeine) { - throw new IllegalArgumentException("The cache library (guava OR caffeine) must be selected."); - } if (guava) { GuavaCSC.Builder guavaBuilder = GuavaCSC.builder(); From 1067fc3fa85c5e483e15c716da0da9fd6c138d1c Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Wed, 7 Feb 2024 18:53:47 +0600 Subject: [PATCH 15/16] rename param --- src/main/java/redis/clients/jedis/util/JedisURIHelper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/redis/clients/jedis/util/JedisURIHelper.java b/src/main/java/redis/clients/jedis/util/JedisURIHelper.java index e574c41227..65e4585bf0 100644 --- a/src/main/java/redis/clients/jedis/util/JedisURIHelper.java +++ b/src/main/java/redis/clients/jedis/util/JedisURIHelper.java @@ -114,11 +114,11 @@ public static ClientSideCache getClientSideCache(URI uri) { } break; - case "ttl": + case "cache_ttl": try { ttl = Integer.parseInt(val); } catch (NumberFormatException nfe) { - throw new JedisValidationException("Value of ttl must be an integer denoting seconds.", nfe); + throw new JedisValidationException("Value of cache_ttl must be an integer denoting seconds.", nfe); } break; } From aa1012b094b6ae7624311309ecf8f36e87a8a28b Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Thu, 15 Feb 2024 15:46:30 +0600 Subject: [PATCH 16/16] Throw IllegalArgumentException at all such cases --- src/main/java/redis/clients/jedis/util/JedisURIHelper.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/redis/clients/jedis/util/JedisURIHelper.java b/src/main/java/redis/clients/jedis/util/JedisURIHelper.java index 65e4585bf0..2d73cf04d4 100644 --- a/src/main/java/redis/clients/jedis/util/JedisURIHelper.java +++ b/src/main/java/redis/clients/jedis/util/JedisURIHelper.java @@ -5,7 +5,6 @@ import redis.clients.jedis.HostAndPort; import redis.clients.jedis.Protocol; import redis.clients.jedis.RedisProtocol; -import redis.clients.jedis.exceptions.JedisValidationException; public final class JedisURIHelper { @@ -110,7 +109,7 @@ public static ClientSideCache getClientSideCache(URI uri) { try { maxSize = Integer.parseInt(val); } catch (NumberFormatException nfe) { - throw new JedisValidationException("Value of cache_max_size must be an integer.", nfe); + throw new IllegalArgumentException("Value of cache_max_size must be an integer.", nfe); } break; @@ -118,7 +117,7 @@ public static ClientSideCache getClientSideCache(URI uri) { try { ttl = Integer.parseInt(val); } catch (NumberFormatException nfe) { - throw new JedisValidationException("Value of cache_ttl must be an integer denoting seconds.", nfe); + throw new IllegalArgumentException("Value of cache_ttl must be an integer denoting seconds.", nfe); } break; }