From 54426424a21070bba3b12454e48aa692efc3a9fa Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Fri, 27 Sep 2024 19:54:37 +0600 Subject: [PATCH] Support Server-assisted Client-side Caching (#3757) * Initial support for client-side caching (#3658) * Support for client-side caching - phase 2 (#3673) * Code re-use? * Stop forcing to read push notifications before checking cache and remove BCAST * Rename variable * Remove ensureFillSafe() * Refactor peeking and reading push notifications * Cleanup comments * Fix transaction failure tests using mock (#3683) Now we have to mock Protocol#read(RedisInputStream, ClientSideCache) instead of Protocol#read(RedisInputStream). * Support client-side caching from UnifiedJedis (#3691) * Support client side caching from UnifiedJedis * Support client side caching as a separate parameter * format imports * Support CSC in sentinel mode * undo change * Client-side caching by hashing command arguments (#3700) * Support TTL in client side caching (using Caffeine library) * Also Guava cache * format pom.xml * Client-side caching by command arguments TODO: Compute hash code. * send keys * todo comment for clean-up * rename method to invalidate * Client-side caching by hashing command arguments * Hash command arguments for CaffeineCSC using OpenHFT hashing * Clean-up keyHashes map * added javadoc * rename method * remove lock * descriptive name * descriptive names and fix * common default values in base class * Cover Redis commands for client side caching (#3702) * Support Client-side caching through URI/URL (#3703) * Support Client-side caching through URI/URL * check idx of '=' sign * nicer exception * edit/fix condition * rename param * Throw IllegalArgumentException at all such cases * Test GuavaCSC and CaffeineCSC (#3742) * Support white-list and black-list commands and keys (#3755) * Create csc package * Create csc.util package * Create a config interface for client-side caching * Default isCacheable * Config to WhiteList/BlackList commands and String keys * Create csc test package(s) * Test white-list/black-list commands and keys * Merge fix * Remove csc.util package * Fix javadoc links * Added ClientSideCacheable interface and removed ClientSideCacheConfig interface * Format imports * Re-create csc.util package * Rename to allow/deny instead of white/black * Introduce interface(s) for hashing CommandObject (#3743) * Client-side cache related naming changes (#3758) Changes: 1. CommandLongHashing is renamed to CommandLongHasher. 2. Expanded the names of GuavaCSC (GuavaClientSideCache) and CaffeineCSC (CaffeineClientSideCache). * Reformat clientSideCache variable names (#3761) * Format tabs in pom.xml * Use Experimental annotation * Fix client side cache tests (#3799) Due to https://github.com/redis/redis/pull/13167 * Fix JedisClusterClientSideCacheTest * Fix JedisSentineledClientSideCacheTest * Remove openhft hashing from source dependency (#3800) * Test different functionalities of client side cache (#3828) * Test JedisURIHelper#getClientSideCache(URI) (#3835) * Merge fix: after introducing EndpointConfig in #3836 * Tweak maximumSize test in CaffeineClientSideCacheTest * Little more tweak maximumSize test in CaffeineClientSideCacheTest * Fix incompatibilities with the latest RedisStack (#3855) * Fix tests - Skip Graph tests - Fix JSON RESP3 test * JSON.GET behaves identically on RESP2 and RESP3 * Revert "Fix incompatibilities with the latest RedisStack (#3855)" This reverts commit 6b9d3387c63398bfc26f3c043ecc7f38b15c2381. * [TEMPORARY] [TEST] Use redis-stack-server:7.4.0-rc1 image for testing * Support RediSearch DIALECT 5 (#3831) - [x] Avoid escaping at query time - [ ] Alias for tag fields (EXACT) - [x] Avoid repeating for numeral equality - [x] New dialect (5) * Support FLOAT16 and BFLOAT16 VecSim storage types (#3849) * Test: INTERSECTS and DIJOINT conditions support in GeoSearch (#3862) * Support IGNORE and other optional arguments for timeseries commands (#3860) * Re-implement TS.ADD command with optional arguments * Implement TS.INCRBY and TS.DECRBY commands with optional arguments * Support IGNORE argument for TS.[ CREATE | ALTER | ADD | INCRBY | DECRBY] commands --- * Cover optional arguments for timeseries commands - Re-implement TS.ADD command with optional arguments - Implement TS.INCRBY and TS.DECRBY commands with optional arguments * Introduce EncodingFormat enum for * Support IGNORE option and rename to TSIncrOrDecrByParams * Polish #3860: Separate params for TS.INCRBY and TS.DECRBY (#3863) * Support indexing of MISSING and EMPTY values (#3866) * Little tweak maximumSize test in CaffeineClientSideCacheTest * Inject ClientSideCacheable via set method (#3882) * Use CommandObject(s) as cache-key (#3875) and remove hashing of CommandObject(s). * #3886 merge fix * Revert "[TEMPORARY] [TEST] Use redis-stack-server:7.4.0-rc1 image for testing" This reverts commit 92c09f3aab79629b505743b5a85169590410555e. * More tweak maximumSize test in CaffeineClientSideCacheTest This reverts and modifies commit 3534996b897f543a3805bd547b4e21963aba3656. * Remove client side cache support through uri/url (#3892) This partially reverts #3703 and #3835 * Bump com.google.guava:guava from 33.0.0-jre to 33.2.1-jre (#3893) * Prepare client side caching - design 2 (#3889) * Separate CacheConnection * Introduce CacheKey and CacheEntry * Little tweak maximumSize test in CaffeineClientSideCacheTest * Remove resetting timeout; we'll PING instead * Refactor Client-Side Caching implementation (#3900) * adding a DataProvider to access connection from cache * resolve keys from commandarguments * clean up in unifiiedjedis and add csc test with ssl * - fix readtimeout exception with sockets for consuming invalidations pending in buffer - apply a default list of cacheable commands to DefaultClientSideCacheable - fix failing unit tests with cacheable / non-cacheable keys - remove formatting changes * - add serialization for cache instances - add unit test with UnifiedJedis - add benchmark for CSC execution - clean unused imports * - added 'Cache' interface and 'DefaultCache' implementation in regard to design doc - added 'EvictionPolicy' interface and LRU implementation - move cache object validation and cache control stuf from 'ClientSideCache' into 'CacheConnection' - make guava and caffeine caches experimental * - added SSLSocketWrapper and plug it to use 'available' - handle exceptions properly - fix some issues with unit tests * implementing thread safety * - fix eviction issue and add related test - fix consuming invalidation messages on a response read - introduce cachestats - fix potential issue with cacheKeysRelatedtoRedisKey cleanup - tests for sequential access, concurrent acces and maxsize * - renmae abstract cache class - add test case for returning new instance of cache object * - change order of execution in sequential acces test * - flush the cache on any disconnect - replace LRU policy references with EvictionPolicy interface - add some constructor overloads to enable custom eviction policies on cache * fix testcache * fix javadoc issue * - fix multithreaded eviction policy issue - update guava and caffeine implementations according to abstract cache * Jedis test plan coverage for CSC (#3918) * initial changes * cover tests for JedisPooled and functionality * fix javadoc * cover new tests for JedisCluster and JedisSentineled * Fix CSC allow-and-deny-list and rename Cacheable interface * Tag CommandArguments#getKeys() as Internal * cover lruEvictionTest * Address code reviews and more updates * fix format and more minor changes * format Connection * modify WeakReference usage * Use ExecutorService.shutdownNow() in tests (#3922) * Use ExecutorService.shutdownNow() * More ExecutorService.shutdownNow() and other changes * [minor change] Avoid creating same CacheKey twice * Support caching null values (#3939) * caching null results * add more assertion * Adding CacheConfig (#3919) * add cacheconfig * remove empty file * -modify constructors with cache as public - trim guava caffeine * remove cachetype * - add getCache to UnifiedJedis - add builder method to CacheConfig * add evictionpolicy to cacheconfig * - unifiedjedis constructor with cacheconfig - wrap IOException on protocol read error * fix merge issue --------- Co-authored-by: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> * Polish "Adding CacheConfig" Polish #3919 - address some pending change requests - Swap contructor placements - Fix grammar in exception message * Adding Cache class to CacheConfig (#3942) * adding cacheclass to cacheconfig * - add cachefactory test * - revert connection ctors to public - udpate some tests with UnifiedJedis.getCache - add ping to flaky tests * remove unnecessary anonymous types * change ctor access modifiers * fix test name * make cachefactory methods static * removing pings due to still flaky with inv messages * - drop CustomCache in tests and use TestCache - check null cacheable issue with defaultcache - support both ctors in custom cache classes regarding to value of cacheconfig.cacheable * remove unncessary maxsize * - remove inline anonymious * Server version check for CSC activation (#3954) * checking server version for CSC * fix format change * fix noauth hello exception in integration tests * fix version check * remove redundant check * remove unused imports * 'toString' for Version * rename to RedisVersion * moving RedisVersion package --------- Co-authored-by: Igor Malinovskiy Co-authored-by: atakavci <58048133+atakavci@users.noreply.github.com> --- pom.xml | 3 + .../redis/clients/jedis/CommandArguments.java | 27 + .../redis/clients/jedis/CommandObject.java | 38 ++ .../java/redis/clients/jedis/Connection.java | 126 ++-- .../clients/jedis/ConnectionFactory.java | 21 +- .../redis/clients/jedis/ConnectionPool.java | 13 + .../jedis/DefaultJedisSocketFactory.java | 2 + .../redis/clients/jedis/JedisCluster.java | 63 +- .../clients/jedis/JedisClusterInfoCache.java | 49 +- .../redis/clients/jedis/JedisFactory.java | 4 +- .../java/redis/clients/jedis/JedisPooled.java | 30 +- .../redis/clients/jedis/JedisSentineled.java | 26 + .../java/redis/clients/jedis/Protocol.java | 56 +- .../redis/clients/jedis/SSLSocketWrapper.java | 408 +++++++++++++ .../redis/clients/jedis/UnifiedJedis.java | 100 ++- .../clients/jedis/annots/Experimental.java | 2 +- .../redis/clients/jedis/args/Rawable.java | 6 + .../clients/jedis/args/RawableFactory.java | 11 +- .../clients/jedis/csc/AbstractCache.java | 232 +++++++ .../java/redis/clients/jedis/csc/Cache.java | 113 ++++ .../redis/clients/jedis/csc/CacheConfig.java | 65 ++ .../clients/jedis/csc/CacheConnection.java | 128 ++++ .../redis/clients/jedis/csc/CacheEntry.java | 56 ++ .../redis/clients/jedis/csc/CacheFactory.java | 63 ++ .../redis/clients/jedis/csc/CacheKey.java | 37 ++ .../redis/clients/jedis/csc/CacheStats.java | 89 +++ .../redis/clients/jedis/csc/Cacheable.java | 9 + .../redis/clients/jedis/csc/DefaultCache.java | 75 +++ .../clients/jedis/csc/DefaultCacheable.java | 98 +++ .../clients/jedis/csc/EvictionPolicy.java | 77 +++ .../redis/clients/jedis/csc/LRUEviction.java | 106 ++++ .../redis/clients/jedis/csc/RedisVersion.java | 41 ++ .../redis/clients/jedis/csc/package-info.java | 7 + .../util/AllowAndDenyListWithStringKeys.java | 48 ++ .../clients/jedis/csc/util/package-info.java | 7 + .../jedis/exceptions/JedisCacheException.java | 19 + .../redis/clients/jedis/mcf/package-info.java | 3 + .../providers/ClusterConnectionProvider.java | 27 +- .../providers/PooledConnectionProvider.java | 17 +- .../SentineledConnectionProvider.java | 58 +- .../clients/jedis/util/JedisURIHelper.java | 11 +- .../clients/jedis/util/RedisInputStream.java | 24 +- .../redis/clients/jedis/JedisClusterTest.java | 32 - .../clients/jedis/JedisClusterTestBase.java | 10 +- .../redis/clients/jedis/SSLJedisTest.java | 2 +- .../jedis/benchmark/CSCPooleadBenchmark.java | 79 +++ .../jedis/TransactionCommandsTest.java | 2 +- .../csc/AllowAndDenyListCacheableTest.java | 79 +++ .../csc/ClientSideCacheFunctionalityTest.java | 572 ++++++++++++++++++ .../jedis/csc/ClientSideCacheTestBase.java | 43 ++ .../csc/JedisClusterClientSideCacheTest.java | 41 ++ .../csc/JedisPooledClientSideCacheTest.java | 13 + .../JedisPooledClientSideCacheTestBase.java | 56 ++ .../JedisSentineledClientSideCacheTest.java | 36 ++ .../SSLJedisPooledClientSideCacheTest.java | 16 + .../redis/clients/jedis/csc/TestCache.java | 28 + .../UnifiedJedisClientSideCacheTestBase.java | 223 +++++++ .../redis/clients/jedis/csc/VersionTest.java | 30 + .../modules/search/SearchWithParamsTest.java | 4 +- 59 files changed, 3491 insertions(+), 170 deletions(-) create mode 100644 src/main/java/redis/clients/jedis/SSLSocketWrapper.java create mode 100644 src/main/java/redis/clients/jedis/csc/AbstractCache.java create mode 100644 src/main/java/redis/clients/jedis/csc/Cache.java create mode 100644 src/main/java/redis/clients/jedis/csc/CacheConfig.java create mode 100644 src/main/java/redis/clients/jedis/csc/CacheConnection.java create mode 100644 src/main/java/redis/clients/jedis/csc/CacheEntry.java create mode 100644 src/main/java/redis/clients/jedis/csc/CacheFactory.java create mode 100644 src/main/java/redis/clients/jedis/csc/CacheKey.java create mode 100644 src/main/java/redis/clients/jedis/csc/CacheStats.java create mode 100644 src/main/java/redis/clients/jedis/csc/Cacheable.java create mode 100644 src/main/java/redis/clients/jedis/csc/DefaultCache.java create mode 100644 src/main/java/redis/clients/jedis/csc/DefaultCacheable.java create mode 100644 src/main/java/redis/clients/jedis/csc/EvictionPolicy.java create mode 100644 src/main/java/redis/clients/jedis/csc/LRUEviction.java create mode 100644 src/main/java/redis/clients/jedis/csc/RedisVersion.java create mode 100644 src/main/java/redis/clients/jedis/csc/package-info.java create mode 100644 src/main/java/redis/clients/jedis/csc/util/AllowAndDenyListWithStringKeys.java create mode 100644 src/main/java/redis/clients/jedis/csc/util/package-info.java create mode 100644 src/main/java/redis/clients/jedis/exceptions/JedisCacheException.java create mode 100644 src/test/java/redis/clients/jedis/benchmark/CSCPooleadBenchmark.java create mode 100644 src/test/java/redis/clients/jedis/csc/AllowAndDenyListCacheableTest.java create mode 100644 src/test/java/redis/clients/jedis/csc/ClientSideCacheFunctionalityTest.java create mode 100644 src/test/java/redis/clients/jedis/csc/ClientSideCacheTestBase.java create mode 100644 src/test/java/redis/clients/jedis/csc/JedisClusterClientSideCacheTest.java create mode 100644 src/test/java/redis/clients/jedis/csc/JedisPooledClientSideCacheTest.java create mode 100644 src/test/java/redis/clients/jedis/csc/JedisPooledClientSideCacheTestBase.java create mode 100644 src/test/java/redis/clients/jedis/csc/JedisSentineledClientSideCacheTest.java create mode 100644 src/test/java/redis/clients/jedis/csc/SSLJedisPooledClientSideCacheTest.java create mode 100644 src/test/java/redis/clients/jedis/csc/TestCache.java create mode 100644 src/test/java/redis/clients/jedis/csc/UnifiedJedisClientSideCacheTestBase.java create mode 100644 src/test/java/redis/clients/jedis/csc/VersionTest.java diff --git a/pom.xml b/pom.xml index 548fe1ca23..2da7b4eb0a 100644 --- a/pom.xml +++ b/pom.xml @@ -75,6 +75,8 @@ 2.11.0 + + com.kohlschutter.junixsocket @@ -90,6 +92,7 @@ 1.20.0 test + junit diff --git a/src/main/java/redis/clients/jedis/CommandArguments.java b/src/main/java/redis/clients/jedis/CommandArguments.java index 763a60e026..da51c098e1 100644 --- a/src/main/java/redis/clients/jedis/CommandArguments.java +++ b/src/main/java/redis/clients/jedis/CommandArguments.java @@ -3,9 +3,12 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.Iterator; +import java.util.List; import redis.clients.jedis.annots.Experimental; +import redis.clients.jedis.annots.Internal; import redis.clients.jedis.args.Rawable; import redis.clients.jedis.args.RawableFactory; import redis.clients.jedis.commands.ProtocolCommand; @@ -17,6 +20,8 @@ public class CommandArguments implements Iterable { private CommandKeyArgumentPreProcessor keyPreProc = null; private final ArrayList args; + private List keys; + private boolean blocking; private CommandArguments() { @@ -26,6 +31,8 @@ private CommandArguments() { public CommandArguments(ProtocolCommand command) { args = new ArrayList<>(); args.add(command); + + keys = Collections.emptyList(); } public ProtocolCommand getCommand() { @@ -127,9 +134,24 @@ public CommandArguments key(Object key) { throw new IllegalArgumentException("\"" + key.toString() + "\" is not a valid argument."); } + addKeyInKeys(key); + return this; } + private void addKeyInKeys(Object key) { + if (keys.isEmpty()) { + keys = Collections.singletonList(key); + } else if (keys.size() == 1) { + List oldKeys = keys; + keys = new ArrayList(); + keys.addAll(oldKeys); + keys.add(key); + } else { + keys.add(key); + } + } + public final CommandArguments keys(Object... keys) { Arrays.stream(keys).forEach(this::key); return this; @@ -178,6 +200,11 @@ public Iterator iterator() { return args.iterator(); } + @Internal + public List getKeys() { + return keys; + } + public boolean isBlocking() { return blocking; } diff --git a/src/main/java/redis/clients/jedis/CommandObject.java b/src/main/java/redis/clients/jedis/CommandObject.java index b4931f2634..c44a0be7de 100644 --- a/src/main/java/redis/clients/jedis/CommandObject.java +++ b/src/main/java/redis/clients/jedis/CommandObject.java @@ -1,5 +1,8 @@ package redis.clients.jedis; +import java.util.Iterator; +import redis.clients.jedis.args.Rawable; + public class CommandObject { private final CommandArguments arguments; @@ -17,4 +20,39 @@ public CommandArguments getArguments() { public Builder getBuilder() { return builder; } + + @Override + public int hashCode() { + int hashCode = 1; + for (Rawable e : arguments) { + hashCode = 31 * hashCode + e.hashCode(); + } + hashCode = 31 * hashCode + builder.hashCode(); + return hashCode; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof CommandObject)) { + return false; + } + + Iterator e1 = arguments.iterator(); + Iterator e2 = ((CommandObject) o).arguments.iterator(); + while (e1.hasNext() && e2.hasNext()) { + Rawable o1 = e1.next(); + Rawable o2 = e2.next(); + if (!(o1 == null ? o2 == null : o1.equals(o2))) { + return false; + } + } + if (e1.hasNext() || e2.hasNext()) { + return false; + } + + return builder == ((CommandObject) o).builder; + } } diff --git a/src/main/java/redis/clients/jedis/Connection.java b/src/main/java/redis/clients/jedis/Connection.java index 9bffd71644..2860866c6e 100644 --- a/src/main/java/redis/clients/jedis/Connection.java +++ b/src/main/java/redis/clients/jedis/Connection.java @@ -12,6 +12,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.function.Supplier; import redis.clients.jedis.Protocol.Command; @@ -31,7 +32,7 @@ public class Connection implements Closeable { private ConnectionPool memberOf; - private RedisProtocol protocol; + protected RedisProtocol protocol; private final JedisSocketFactory socketFactory; private Socket socket; private RedisOutputStream outputStream; @@ -41,6 +42,8 @@ public class Connection implements Closeable { private boolean broken = false; private boolean strValActive; private String strVal; + protected String server; + protected String version; public Connection() { this(Protocol.DEFAULT_HOST, Protocol.DEFAULT_PORT); @@ -55,9 +58,7 @@ public Connection(final HostAndPort hostAndPort) { } public Connection(final HostAndPort hostAndPort, final JedisClientConfig clientConfig) { - this(new DefaultJedisSocketFactory(hostAndPort, clientConfig)); - this.infiniteSoTimeout = clientConfig.getBlockingSocketTimeoutMillis(); - initializeFromClientConfig(clientConfig); + this(new DefaultJedisSocketFactory(hostAndPort, clientConfig), clientConfig); } public Connection(final JedisSocketFactory socketFactory) { @@ -373,16 +374,40 @@ protected void flush() { } } + @Experimental + protected Object protocolRead(RedisInputStream is) { + return Protocol.read(is); + } + + @Experimental + protected void protocolReadPushes(RedisInputStream is) { + } + protected Object readProtocolWithCheckingBroken() { if (broken) { throw new JedisConnectionException("Attempting to read from a broken connection."); } try { - return Protocol.read(inputStream); -// Object read = Protocol.read(inputStream); -// System.out.println(redis.clients.jedis.util.SafeEncoder.encodeObject(read)); -// return read; + return protocolRead(inputStream); + } catch (JedisConnectionException exc) { + broken = true; + throw exc; + } + } + + protected void readPushesWithCheckingBroken() { + if (broken) { + throw new JedisConnectionException("Attempting to read from a broken connection."); + } + + try { + if (inputStream.available() > 0) { + protocolReadPushes(inputStream); + } + } catch (IOException e) { + broken = true; + throw new JedisConnectionException("Failed to check buffer on connection.", e); } catch (JedisConnectionException exc) { setBroken(); throw exc; @@ -404,6 +429,7 @@ public List getMany(final int count) { /** * Check if the client name libname, libver, characters are legal + * * @param info the name * @return Returns true if legal, false throws exception * @throws JedisException if characters illegal @@ -419,7 +445,7 @@ private static boolean validateClientInfo(String info) { return true; } - private void initializeFromClientConfig(final JedisClientConfig config) { + protected void initializeFromClientConfig(final JedisClientConfig config) { try { connect(); @@ -430,12 +456,12 @@ private void initializeFromClientConfig(final JedisClientConfig config) { final RedisCredentialsProvider redisCredentialsProvider = (RedisCredentialsProvider) credentialsProvider; try { redisCredentialsProvider.prepare(); - helloOrAuth(protocol, redisCredentialsProvider.get()); + helloAndAuth(protocol, redisCredentialsProvider.get()); } finally { redisCredentialsProvider.cleanUp(); } } else { - helloOrAuth(protocol, credentialsProvider != null ? credentialsProvider.get() + helloAndAuth(protocol, credentialsProvider != null ? credentialsProvider.get() : new DefaultRedisCredentials(config.getUser(), config.getPassword())); } @@ -447,7 +473,9 @@ private void initializeFromClientConfig(final JedisClientConfig config) { } ClientSetInfoConfig setInfoConfig = config.getClientSetInfoConfig(); - if (setInfoConfig == null) setInfoConfig = ClientSetInfoConfig.DEFAULT; + if (setInfoConfig == null) { + setInfoConfig = ClientSetInfoConfig.DEFAULT; + } if (!setInfoConfig.isDisabled()) { String libName = JedisMetaInfo.getArtifactId(); @@ -492,50 +520,56 @@ private void initializeFromClientConfig(final JedisClientConfig config) { } } - private void helloOrAuth(final RedisProtocol protocol, final RedisCredentials credentials) { - - if (credentials == null || credentials.getPassword() == null) { - if (protocol != null) { - sendCommand(Command.HELLO, encode(protocol.version())); - getOne(); + private void helloAndAuth(final RedisProtocol protocol, final RedisCredentials credentials) { + Map helloResult = null; + if (protocol != null && credentials != null && credentials.getUser() != null) { + byte[] rawPass = encodeToBytes(credentials.getPassword()); + try { + helloResult = hello(encode(protocol.version()), Keyword.AUTH.getRaw(), encode(credentials.getUser()), rawPass); + } finally { + Arrays.fill(rawPass, (byte) 0); // clear sensitive data } - return; + } else { + auth(credentials); + helloResult = protocol == null ? null : hello(encode(protocol.version())); + } + if (helloResult != null) { + server = (String) helloResult.get("server"); + version = (String) helloResult.get("version"); } - // Source: https://stackoverflow.com/a/9670279/4021802 - ByteBuffer passBuf = Protocol.CHARSET.encode(CharBuffer.wrap(credentials.getPassword())); - byte[] rawPass = Arrays.copyOfRange(passBuf.array(), passBuf.position(), passBuf.limit()); - Arrays.fill(passBuf.array(), (byte) 0); // clear sensitive data + // clearing 'char[] credentials.getPassword()' should be + // handled in RedisCredentialsProvider.cleanUp() + } + private void auth(RedisCredentials credentials) { + if (credentials == null || credentials.getPassword() == null) { + return; + } + byte[] rawPass = encodeToBytes(credentials.getPassword()); try { - /// actual HELLO or AUTH --> - if (protocol != null) { - if (credentials.getUser() != null) { - sendCommand(Command.HELLO, encode(protocol.version()), - Keyword.AUTH.getRaw(), encode(credentials.getUser()), rawPass); - getOne(); // Map - } else { - sendCommand(Command.AUTH, rawPass); - getStatusCodeReply(); // OK - sendCommand(Command.HELLO, encode(protocol.version())); - getOne(); // Map - } - } else { // protocol == null - if (credentials.getUser() != null) { - sendCommand(Command.AUTH, encode(credentials.getUser()), rawPass); - } else { - sendCommand(Command.AUTH, rawPass); - } - getStatusCodeReply(); // OK + if (credentials.getUser() == null) { + sendCommand(Command.AUTH, rawPass); + } else { + sendCommand(Command.AUTH, encode(credentials.getUser()), rawPass); } - /// <-- actual HELLO or AUTH } finally { - Arrays.fill(rawPass, (byte) 0); // clear sensitive data } + getStatusCodeReply(); + } - // clearing 'char[] credentials.getPassword()' should be - // handled in RedisCredentialsProvider.cleanUp() + protected Map hello(byte[]... args) { + sendCommand(Command.HELLO, args); + return BuilderFactory.ENCODED_OBJECT_MAP.build(getOne()); + } + + protected byte[] encodeToBytes(char[] chars) { + // Source: https://stackoverflow.com/a/9670279/4021802 + ByteBuffer passBuf = Protocol.CHARSET.encode(CharBuffer.wrap(chars)); + byte[] rawPass = Arrays.copyOfRange(passBuf.array(), passBuf.position(), passBuf.limit()); + Arrays.fill(passBuf.array(), (byte) 0); // clear sensitive data + return rawPass; } public String select(final int index) { diff --git a/src/main/java/redis/clients/jedis/ConnectionFactory.java b/src/main/java/redis/clients/jedis/ConnectionFactory.java index 3500a21172..cc53df56f0 100644 --- a/src/main/java/redis/clients/jedis/ConnectionFactory.java +++ b/src/main/java/redis/clients/jedis/ConnectionFactory.java @@ -1,12 +1,14 @@ package redis.clients.jedis; - import org.apache.commons.pool2.PooledObject; import org.apache.commons.pool2.PooledObjectFactory; import org.apache.commons.pool2.impl.DefaultPooledObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import redis.clients.jedis.annots.Experimental; +import redis.clients.jedis.csc.Cache; +import redis.clients.jedis.csc.CacheConnection; import redis.clients.jedis.exceptions.JedisException; /** @@ -17,8 +19,8 @@ public class ConnectionFactory implements PooledObjectFactory { private static final Logger logger = LoggerFactory.getLogger(ConnectionFactory.class); private final JedisSocketFactory jedisSocketFactory; - private final JedisClientConfig clientConfig; + private Cache clientSideCache = null; public ConnectionFactory(final HostAndPort hostAndPort) { this.clientConfig = DefaultJedisClientConfig.builder().build(); @@ -26,12 +28,19 @@ public ConnectionFactory(final HostAndPort hostAndPort) { } public ConnectionFactory(final HostAndPort hostAndPort, final JedisClientConfig clientConfig) { - this.clientConfig = DefaultJedisClientConfig.copyConfig(clientConfig); + this.clientConfig = clientConfig; + this.jedisSocketFactory = new DefaultJedisSocketFactory(hostAndPort, this.clientConfig); + } + + @Experimental + public ConnectionFactory(final HostAndPort hostAndPort, final JedisClientConfig clientConfig, Cache csCache) { + this.clientConfig = clientConfig; this.jedisSocketFactory = new DefaultJedisSocketFactory(hostAndPort, this.clientConfig); + this.clientSideCache = csCache; } public ConnectionFactory(final JedisSocketFactory jedisSocketFactory, final JedisClientConfig clientConfig) { - this.clientConfig = DefaultJedisClientConfig.copyConfig(clientConfig); + this.clientConfig = clientConfig; this.jedisSocketFactory = jedisSocketFactory; } @@ -54,9 +63,9 @@ public void destroyObject(PooledObject pooledConnection) throws Exce @Override public PooledObject makeObject() throws Exception { - Connection jedis = null; try { - jedis = new Connection(jedisSocketFactory, clientConfig); + Connection jedis = clientSideCache == null ? new Connection(jedisSocketFactory, clientConfig) + : new CacheConnection(jedisSocketFactory, clientConfig, clientSideCache); return new DefaultPooledObject<>(jedis); } catch (JedisException je) { logger.debug("Error while makeObject", je); diff --git a/src/main/java/redis/clients/jedis/ConnectionPool.java b/src/main/java/redis/clients/jedis/ConnectionPool.java index 5899b22260..40d4861f98 100644 --- a/src/main/java/redis/clients/jedis/ConnectionPool.java +++ b/src/main/java/redis/clients/jedis/ConnectionPool.java @@ -2,6 +2,8 @@ import org.apache.commons.pool2.PooledObjectFactory; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import redis.clients.jedis.annots.Experimental; +import redis.clients.jedis.csc.Cache; import redis.clients.jedis.util.Pool; public class ConnectionPool extends Pool { @@ -10,6 +12,11 @@ public ConnectionPool(HostAndPort hostAndPort, JedisClientConfig clientConfig) { this(new ConnectionFactory(hostAndPort, clientConfig)); } + @Experimental + public ConnectionPool(HostAndPort hostAndPort, JedisClientConfig clientConfig, Cache clientSideCache) { + this(new ConnectionFactory(hostAndPort, clientConfig, clientSideCache)); + } + public ConnectionPool(PooledObjectFactory factory) { super(factory); } @@ -19,6 +26,12 @@ public ConnectionPool(HostAndPort hostAndPort, JedisClientConfig clientConfig, this(new ConnectionFactory(hostAndPort, clientConfig), poolConfig); } + @Experimental + public ConnectionPool(HostAndPort hostAndPort, JedisClientConfig clientConfig, Cache clientSideCache, + GenericObjectPoolConfig poolConfig) { + this(new ConnectionFactory(hostAndPort, clientConfig, clientSideCache), poolConfig); + } + public ConnectionPool(PooledObjectFactory factory, GenericObjectPoolConfig poolConfig) { super(factory, poolConfig); diff --git a/src/main/java/redis/clients/jedis/DefaultJedisSocketFactory.java b/src/main/java/redis/clients/jedis/DefaultJedisSocketFactory.java index c9ef6646ba..0d41693d0f 100644 --- a/src/main/java/redis/clients/jedis/DefaultJedisSocketFactory.java +++ b/src/main/java/redis/clients/jedis/DefaultJedisSocketFactory.java @@ -94,11 +94,13 @@ public Socket createSocket() throws JedisConnectionException { if (null == _sslSocketFactory) { _sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); } + Socket plainSocket = socket; socket = _sslSocketFactory.createSocket(socket, _hostAndPort.getHost(), _hostAndPort.getPort(), true); if (null != sslParameters) { ((SSLSocket) socket).setSSLParameters(sslParameters); } + socket = new SSLSocketWrapper((SSLSocket) socket, plainSocket); if (null != hostnameVerifier && !hostnameVerifier.verify(_hostAndPort.getHost(), ((SSLSocket) socket).getSession())) { diff --git a/src/main/java/redis/clients/jedis/JedisCluster.java b/src/main/java/redis/clients/jedis/JedisCluster.java index 1ba62402d7..db8d17ee15 100644 --- a/src/main/java/redis/clients/jedis/JedisCluster.java +++ b/src/main/java/redis/clients/jedis/JedisCluster.java @@ -7,8 +7,12 @@ import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import redis.clients.jedis.annots.Experimental; import redis.clients.jedis.executors.ClusterCommandExecutor; import redis.clients.jedis.providers.ClusterConnectionProvider; +import redis.clients.jedis.csc.Cache; +import redis.clients.jedis.csc.CacheConfig; +import redis.clients.jedis.csc.CacheFactory; import redis.clients.jedis.util.JedisClusterCRC16; public class JedisCluster extends UnifiedJedis { @@ -252,6 +256,12 @@ public JedisCluster(Set clusterNodes, JedisClientConfig clientConfi Duration.ofMillis((long) clientConfig.getSocketTimeoutMillis() * maxAttempts), poolConfig); } + public JedisCluster(Set clusterNodes, JedisClientConfig clientConfig, int maxAttempts, + Duration maxTotalRetriesDuration, GenericObjectPoolConfig poolConfig) { + this(new ClusterConnectionProvider(clusterNodes, clientConfig, poolConfig), maxAttempts, maxTotalRetriesDuration, + clientConfig.getRedisProtocol()); + } + public JedisCluster(Set clusterNodes, JedisClientConfig clientConfig, GenericObjectPoolConfig poolConfig, Duration topologyRefreshPeriod, int maxAttempts, Duration maxTotalRetriesDuration) { @@ -259,12 +269,6 @@ public JedisCluster(Set clusterNodes, JedisClientConfig clientConfi maxAttempts, maxTotalRetriesDuration, clientConfig.getRedisProtocol()); } - public JedisCluster(Set clusterNodes, JedisClientConfig clientConfig, int maxAttempts, - Duration maxTotalRetriesDuration, GenericObjectPoolConfig poolConfig) { - this(new ClusterConnectionProvider(clusterNodes, clientConfig, poolConfig), maxAttempts, maxTotalRetriesDuration, - clientConfig.getRedisProtocol()); - } - // Uses a fetched connection to process protocol. Should be avoided if possible. public JedisCluster(ClusterConnectionProvider provider, int maxAttempts, Duration maxTotalRetriesDuration) { super(provider, maxAttempts, maxTotalRetriesDuration); @@ -275,6 +279,53 @@ private JedisCluster(ClusterConnectionProvider provider, int maxAttempts, Durati super(provider, maxAttempts, maxTotalRetriesDuration, protocol); } + @Experimental + public JedisCluster(Set hnp, JedisClientConfig jedisClientConfig, CacheConfig cacheConfig) { + this(hnp, jedisClientConfig, CacheFactory.getCache(cacheConfig)); + } + + @Experimental + public JedisCluster(Set clusterNodes, JedisClientConfig clientConfig, Cache clientSideCache) { + this(clusterNodes, clientConfig, clientSideCache, DEFAULT_MAX_ATTEMPTS, + Duration.ofMillis(DEFAULT_MAX_ATTEMPTS * clientConfig.getSocketTimeoutMillis())); + } + + @Experimental + public JedisCluster(Set clusterNodes, JedisClientConfig clientConfig, Cache clientSideCache, + int maxAttempts, Duration maxTotalRetriesDuration) { + this(new ClusterConnectionProvider(clusterNodes, clientConfig, clientSideCache), maxAttempts, + maxTotalRetriesDuration, clientConfig.getRedisProtocol(), clientSideCache); + } + + @Experimental + public JedisCluster(Set clusterNodes, JedisClientConfig clientConfig, Cache clientSideCache, + int maxAttempts, Duration maxTotalRetriesDuration, GenericObjectPoolConfig poolConfig) { + this(new ClusterConnectionProvider(clusterNodes, clientConfig, clientSideCache, poolConfig), + maxAttempts, maxTotalRetriesDuration, clientConfig.getRedisProtocol(), clientSideCache); + } + + @Experimental + public JedisCluster(Set clusterNodes, JedisClientConfig clientConfig, Cache clientSideCache, + GenericObjectPoolConfig poolConfig) { + this(new ClusterConnectionProvider(clusterNodes, clientConfig, clientSideCache, poolConfig), + DEFAULT_MAX_ATTEMPTS, Duration.ofMillis(DEFAULT_MAX_ATTEMPTS * clientConfig.getSocketTimeoutMillis()), + clientConfig.getRedisProtocol(), clientSideCache); + } + + @Experimental + public JedisCluster(Set clusterNodes, JedisClientConfig clientConfig, Cache clientSideCache, + GenericObjectPoolConfig poolConfig, Duration topologyRefreshPeriod, int maxAttempts, + Duration maxTotalRetriesDuration) { + this(new ClusterConnectionProvider(clusterNodes, clientConfig, clientSideCache, poolConfig, topologyRefreshPeriod), + maxAttempts, maxTotalRetriesDuration, clientConfig.getRedisProtocol(), clientSideCache); + } + + @Experimental + private JedisCluster(ClusterConnectionProvider provider, int maxAttempts, Duration maxTotalRetriesDuration, + RedisProtocol protocol, Cache clientSideCache) { + super(provider, maxAttempts, maxTotalRetriesDuration, protocol, clientSideCache); + } + /** * Returns all nodes that were configured to connect to in key-value pairs ({@link Map}).
* Key is the HOST:PORT and the value is the connection pool. diff --git a/src/main/java/redis/clients/jedis/JedisClusterInfoCache.java b/src/main/java/redis/clients/jedis/JedisClusterInfoCache.java index 2c4ea3b3c5..ec63c5206a 100644 --- a/src/main/java/redis/clients/jedis/JedisClusterInfoCache.java +++ b/src/main/java/redis/clients/jedis/JedisClusterInfoCache.java @@ -23,7 +23,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import redis.clients.jedis.annots.Experimental; import redis.clients.jedis.annots.Internal; +import redis.clients.jedis.csc.Cache; import redis.clients.jedis.exceptions.JedisClusterOperationException; import redis.clients.jedis.exceptions.JedisException; import redis.clients.jedis.util.SafeEncoder; @@ -47,6 +49,7 @@ public class JedisClusterInfoCache { private final GenericObjectPoolConfig poolConfig; private final JedisClientConfig clientConfig; + private final Cache clientSideCache; private final Set startNodes; private static final int MASTER_NODE_INDEX = 2; @@ -66,19 +69,39 @@ public void run() { } public JedisClusterInfoCache(final JedisClientConfig clientConfig, final Set startNodes) { - this(clientConfig, null, startNodes); + this(clientConfig, null, null, startNodes); + } + + @Experimental + public JedisClusterInfoCache(final JedisClientConfig clientConfig, Cache clientSideCache, + final Set startNodes) { + this(clientConfig, clientSideCache, null, startNodes); } public JedisClusterInfoCache(final JedisClientConfig clientConfig, final GenericObjectPoolConfig poolConfig, final Set startNodes) { - this(clientConfig, poolConfig, startNodes, null); + this(clientConfig, null, poolConfig, startNodes); + } + + @Experimental + public JedisClusterInfoCache(final JedisClientConfig clientConfig, Cache clientSideCache, + final GenericObjectPoolConfig poolConfig, final Set startNodes) { + this(clientConfig, clientSideCache, poolConfig, startNodes, null); } public JedisClusterInfoCache(final JedisClientConfig clientConfig, final GenericObjectPoolConfig poolConfig, final Set startNodes, final Duration topologyRefreshPeriod) { + this(clientConfig, null, poolConfig, startNodes, topologyRefreshPeriod); + } + + @Experimental + public JedisClusterInfoCache(final JedisClientConfig clientConfig, Cache clientSideCache, + final GenericObjectPoolConfig poolConfig, final Set startNodes, + final Duration topologyRefreshPeriod) { this.poolConfig = poolConfig; this.clientConfig = clientConfig; + this.clientSideCache = clientSideCache; this.startNodes = startNodes; if (topologyRefreshPeriod != null) { logger.info("Cluster topology refresh start, period: {}, startNodes: {}", topologyRefreshPeriod, startNodes); @@ -221,6 +244,9 @@ private void discoverClusterSlots(Connection jedis) { try { Arrays.fill(slots, null); Arrays.fill(slotNodes, null); + if (clientSideCache != null) { + clientSideCache.flush(); + } Set hostAndPortKeys = new HashSet<>(); for (Object slotInfoObj : slotsInfo) { @@ -284,8 +310,7 @@ public ConnectionPool setupNodeIfNotExist(final HostAndPort node) { ConnectionPool existingPool = nodes.get(nodeKey); if (existingPool != null) return existingPool; - ConnectionPool nodePool = poolConfig == null ? new ConnectionPool(node, clientConfig) - : new ConnectionPool(node, clientConfig, poolConfig); + ConnectionPool nodePool = createNodePool(node); nodes.put(nodeKey, nodePool); return nodePool; } finally { @@ -293,6 +318,22 @@ public ConnectionPool setupNodeIfNotExist(final HostAndPort node) { } } + private ConnectionPool createNodePool(HostAndPort node) { + if (poolConfig == null) { + if (clientSideCache == null) { + return new ConnectionPool(node, clientConfig); + } else { + return new ConnectionPool(node, clientConfig, clientSideCache); + } + } else { + if (clientSideCache == null) { + return new ConnectionPool(node, clientConfig, poolConfig); + } else { + return new ConnectionPool(node, clientConfig, clientSideCache, poolConfig); + } + } + } + public void assignSlotToNode(int slot, HostAndPort targetNode) { w.lock(); try { diff --git a/src/main/java/redis/clients/jedis/JedisFactory.java b/src/main/java/redis/clients/jedis/JedisFactory.java index 0e07ccc286..0ff5bebe1c 100644 --- a/src/main/java/redis/clients/jedis/JedisFactory.java +++ b/src/main/java/redis/clients/jedis/JedisFactory.java @@ -66,7 +66,7 @@ protected JedisFactory(final String host, final int port, final int connectionTi } protected JedisFactory(final HostAndPort hostAndPort, final JedisClientConfig clientConfig) { - this.clientConfig = DefaultJedisClientConfig.copyConfig(clientConfig); + this.clientConfig = clientConfig; this.jedisSocketFactory = new DefaultJedisSocketFactory(hostAndPort, this.clientConfig); } @@ -83,7 +83,7 @@ protected JedisFactory(final String host, final int port, final int connectionTi } protected JedisFactory(final JedisSocketFactory jedisSocketFactory, final JedisClientConfig clientConfig) { - this.clientConfig = DefaultJedisClientConfig.copyConfig(clientConfig); + this.clientConfig = clientConfig; this.jedisSocketFactory = jedisSocketFactory; } diff --git a/src/main/java/redis/clients/jedis/JedisPooled.java b/src/main/java/redis/clients/jedis/JedisPooled.java index c6d022e094..c3429319e7 100644 --- a/src/main/java/redis/clients/jedis/JedisPooled.java +++ b/src/main/java/redis/clients/jedis/JedisPooled.java @@ -7,7 +7,10 @@ import org.apache.commons.pool2.PooledObjectFactory; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; - +import redis.clients.jedis.annots.Experimental; +import redis.clients.jedis.csc.Cache; +import redis.clients.jedis.csc.CacheConfig; +import redis.clients.jedis.csc.CacheFactory; import redis.clients.jedis.providers.PooledConnectionProvider; import redis.clients.jedis.util.JedisURIHelper; import redis.clients.jedis.util.Pool; @@ -27,7 +30,7 @@ public JedisPooled() { * @param url */ public JedisPooled(final String url) { - this(URI.create(url)); + super(url); } /** @@ -76,6 +79,16 @@ public JedisPooled(final HostAndPort hostAndPort, final JedisClientConfig client super(hostAndPort, clientConfig); } + @Experimental + public JedisPooled(final HostAndPort hostAndPort, final JedisClientConfig clientConfig, CacheConfig cacheConfig) { + this(hostAndPort, clientConfig, CacheFactory.getCache(cacheConfig)); + } + + @Experimental + public JedisPooled(final HostAndPort hostAndPort, final JedisClientConfig clientConfig, Cache clientSideCache) { + super(hostAndPort, clientConfig, clientSideCache); + } + public JedisPooled(PooledObjectFactory factory) { this(new PooledConnectionProvider(factory)); } @@ -376,6 +389,19 @@ public JedisPooled(final HostAndPort hostAndPort, final JedisClientConfig client super(new PooledConnectionProvider(hostAndPort, clientConfig, poolConfig), clientConfig.getRedisProtocol()); } + @Experimental + public JedisPooled(final HostAndPort hostAndPort, final JedisClientConfig clientConfig, CacheConfig cacheConfig, + final GenericObjectPoolConfig poolConfig) { + this(hostAndPort, clientConfig, CacheFactory.getCache(cacheConfig), poolConfig); + } + + @Experimental + public JedisPooled(final HostAndPort hostAndPort, final JedisClientConfig clientConfig, Cache clientSideCache, + final GenericObjectPoolConfig poolConfig) { + super(new PooledConnectionProvider(hostAndPort, clientConfig, clientSideCache, poolConfig), + clientConfig.getRedisProtocol(), clientSideCache); + } + public JedisPooled(final GenericObjectPoolConfig poolConfig, final JedisSocketFactory jedisSocketFactory, final JedisClientConfig clientConfig) { super(new PooledConnectionProvider(new ConnectionFactory(jedisSocketFactory, clientConfig), poolConfig), diff --git a/src/main/java/redis/clients/jedis/JedisSentineled.java b/src/main/java/redis/clients/jedis/JedisSentineled.java index 0ea0221c1a..26f208a03b 100644 --- a/src/main/java/redis/clients/jedis/JedisSentineled.java +++ b/src/main/java/redis/clients/jedis/JedisSentineled.java @@ -2,6 +2,10 @@ import java.util.Set; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import redis.clients.jedis.annots.Experimental; +import redis.clients.jedis.csc.Cache; +import redis.clients.jedis.csc.CacheConfig; +import redis.clients.jedis.csc.CacheFactory; import redis.clients.jedis.providers.SentineledConnectionProvider; public class JedisSentineled extends UnifiedJedis { @@ -12,6 +16,20 @@ public JedisSentineled(String masterName, final JedisClientConfig masterClientCo masterClientConfig.getRedisProtocol()); } + @Experimental + public JedisSentineled(String masterName, final JedisClientConfig masterClientConfig, CacheConfig cacheConfig, + Set sentinels, final JedisClientConfig sentinelClientConfig) { + this(masterName, masterClientConfig, CacheFactory.getCache(cacheConfig), + sentinels, sentinelClientConfig); + } + + @Experimental + public JedisSentineled(String masterName, final JedisClientConfig masterClientConfig, Cache clientSideCache, + Set sentinels, final JedisClientConfig sentinelClientConfig) { + super(new SentineledConnectionProvider(masterName, masterClientConfig, clientSideCache, + sentinels, sentinelClientConfig), masterClientConfig.getRedisProtocol(), clientSideCache); + } + public JedisSentineled(String masterName, final JedisClientConfig masterClientConfig, final GenericObjectPoolConfig poolConfig, Set sentinels, final JedisClientConfig sentinelClientConfig) { @@ -19,6 +37,14 @@ public JedisSentineled(String masterName, final JedisClientConfig masterClientCo masterClientConfig.getRedisProtocol()); } + @Experimental + public JedisSentineled(String masterName, final JedisClientConfig masterClientConfig, Cache clientSideCache, + final GenericObjectPoolConfig poolConfig, + Set sentinels, final JedisClientConfig sentinelClientConfig) { + super(new SentineledConnectionProvider(masterName, masterClientConfig, clientSideCache, poolConfig, + sentinels, sentinelClientConfig), masterClientConfig.getRedisProtocol(), clientSideCache); + } + public JedisSentineled(SentineledConnectionProvider sentineledConnectionProvider) { super(sentineledConnectionProvider); } diff --git a/src/main/java/redis/clients/jedis/Protocol.java b/src/main/java/redis/clients/jedis/Protocol.java index d9c9872e1c..cd6e41581f 100644 --- a/src/main/java/redis/clients/jedis/Protocol.java +++ b/src/main/java/redis/clients/jedis/Protocol.java @@ -4,13 +4,16 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; +import redis.clients.jedis.annots.Experimental; import redis.clients.jedis.exceptions.*; import redis.clients.jedis.args.Rawable; import redis.clients.jedis.commands.ProtocolCommand; +import redis.clients.jedis.csc.Cache; import redis.clients.jedis.util.KeyValue; import redis.clients.jedis.util.RedisInputStream; import redis.clients.jedis.util.RedisOutputStream; @@ -61,6 +64,8 @@ public final class Protocol { private static final String WRONGPASS_PREFIX = "WRONGPASS"; private static final String NOPERM_PREFIX = "NOPERM"; + private static final byte[] INVALIDATE_BYTES = SafeEncoder.encode("invalidate"); + private Protocol() { throw new InstantiationError("Must not instantiate this class"); } @@ -87,13 +92,9 @@ private static void processError(final RedisInputStream is) { // Maybe Read only first 5 bytes instead? if (message.startsWith(MOVED_PREFIX)) { String[] movedInfo = parseTargetHostAndSlot(message); -// throw new JedisMovedDataException(message, new HostAndPort(movedInfo[1], -// Integer.parseInt(movedInfo[2])), Integer.parseInt(movedInfo[0])); throw new JedisMovedDataException(message, HostAndPort.from(movedInfo[1]), Integer.parseInt(movedInfo[0])); } else if (message.startsWith(ASK_PREFIX)) { String[] askInfo = parseTargetHostAndSlot(message); -// throw new JedisAskDataException(message, new HostAndPort(askInfo[1], -// Integer.parseInt(askInfo[2])), Integer.parseInt(askInfo[0])); throw new JedisAskDataException(message, HostAndPort.from(askInfo[1]), Integer.parseInt(askInfo[0])); } else if (message.startsWith(CLUSTERDOWN_PREFIX)) { throw new JedisClusterException(message); @@ -118,15 +119,6 @@ public static String readErrorLineIfPossible(RedisInputStream is) { return is.readLine(); } -// private static String[] parseTargetHostAndSlot(String clusterRedirectResponse) { -// String[] response = new String[3]; -// String[] messageInfo = clusterRedirectResponse.split(" "); -// String[] targetHostAndPort = HostAndPort.extractParts(messageInfo[2]); -// response[0] = messageInfo[1]; -// response[1] = targetHostAndPort[0]; -// response[2] = targetHostAndPort[1]; -// return response; -// } private static String[] parseTargetHostAndSlot(String clusterRedirectResponse) { String[] response = new String[2]; String[] messageInfo = clusterRedirectResponse.split(" "); @@ -137,7 +129,7 @@ private static String[] parseTargetHostAndSlot(String clusterRedirectResponse) { private static Object process(final RedisInputStream is) { final byte b = is.readByte(); - //System.out.println((char) b); + // System.out.println("BYTE: " + (char) b); switch (b) { case PLUS_BYTE: return is.readLineBytes(); @@ -196,7 +188,8 @@ private static byte[] processBulkReply(final RedisInputStream is) { private static List processMultiBulkReply(final RedisInputStream is) { final int num = is.readIntCrLf(); - if (num == -1) return null; + if (num == -1) + return null; final List ret = new ArrayList<>(num); for (int i = 0; i < num; i++) { try { @@ -228,6 +221,39 @@ public static Object read(final RedisInputStream is) { return process(is); } + @Experimental + public static Object read(final RedisInputStream is, final Cache cache) { + readPushes(is, cache, false); + return process(is); + } + + @Experimental + public static void readPushes(final RedisInputStream is, final Cache cache, boolean onlyPendingBuffer) { + if (onlyPendingBuffer) { + try { + while (is.available() > 0 && is.peek(GREATER_THAN_BYTE)) { + is.readByte(); + processPush(is, cache); + } + } catch (IOException e) { + throw new JedisConnectionException("Failed to read pending buffer for push messages!", e); + } + } else { + while (is.peek(GREATER_THAN_BYTE)) { + is.readByte(); + processPush(is, cache); + } + } + } + + private static void processPush(final RedisInputStream is, Cache cache) { + List list = processMultiBulkReply(is); + if (list.size() == 2 && list.get(0) instanceof byte[] + && Arrays.equals(INVALIDATE_BYTES, (byte[]) list.get(0))) { + cache.deleteByRedisKeys((List) list.get(1)); + } + } + public static final byte[] toByteArray(final boolean value) { return value ? BYTES_TRUE : BYTES_FALSE; } diff --git a/src/main/java/redis/clients/jedis/SSLSocketWrapper.java b/src/main/java/redis/clients/jedis/SSLSocketWrapper.java new file mode 100644 index 0000000000..a2b9e5b74c --- /dev/null +++ b/src/main/java/redis/clients/jedis/SSLSocketWrapper.java @@ -0,0 +1,408 @@ +package redis.clients.jedis; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketException; +import java.util.function.BiFunction; +import java.util.List; +import javax.net.ssl.HandshakeCompletedListener; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; + +public class SSLSocketWrapper extends SSLSocket { + + SSLSocket actual; + Socket underlying; + InputStream wrapper; + + private class InputStreamWrapper extends InputStream { + private InputStream actual; + private InputStream underlying; + + public InputStreamWrapper(InputStream actual, InputStream underlying) { + this.actual = actual; + this.underlying = underlying; + } + + @Override + public int read() throws IOException { + return actual.read(); + } + + @Override + public int read(byte[] b) throws IOException { + return actual.read(b); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return actual.read(b, off, len); + } + + @Override + public long skip(long n) throws IOException { + return actual.skip(n); + } + + @Override + public int available() throws IOException { + return underlying.available(); + } + + @Override + public void close() throws IOException { + actual.close(); + } + + @Override + public synchronized void mark(int readlimit) { + actual.mark(readlimit); + } + + @Override + public synchronized void reset() throws IOException { + actual.reset(); + } + + @Override + public boolean markSupported() { + return actual.markSupported(); + } + } + + public SSLSocketWrapper(SSLSocket actual, Socket underlying) throws IOException { + this.actual = actual; + this.underlying = underlying; + this.wrapper = new InputStreamWrapper(actual.getInputStream(), underlying.getInputStream()); + } + + @Override + public void connect(SocketAddress endpoint) throws IOException { + actual.connect(endpoint); + } + + @Override + public void connect(SocketAddress endpoint, int timeout) throws IOException { + actual.connect(endpoint, timeout); + } + + @Override + public void bind(SocketAddress bindpoint) throws IOException { + actual.bind(bindpoint); + } + + @Override + public InetAddress getInetAddress() { + return actual.getInetAddress(); + } + + @Override + public InetAddress getLocalAddress() { + return actual.getLocalAddress(); + } + + @Override + public int getPort() { + return actual.getPort(); + } + + @Override + public int getLocalPort() { + return actual.getLocalPort(); + } + + @Override + public SocketAddress getRemoteSocketAddress() { + return actual.getRemoteSocketAddress(); + } + + @Override + public SocketAddress getLocalSocketAddress() { + return actual.getLocalSocketAddress(); + } + + @Override + public void setTcpNoDelay(boolean on) throws SocketException { + actual.setTcpNoDelay(on); + } + + @Override + public boolean getTcpNoDelay() throws SocketException { + return actual.getTcpNoDelay(); + } + + @Override + public void setSoLinger(boolean on, int linger) throws SocketException { + actual.setSoLinger(on, linger); + } + + @Override + public int getSoLinger() throws SocketException { + return actual.getSoLinger(); + } + + @Override + public void sendUrgentData(int data) throws IOException { + actual.sendUrgentData(data); + } + + @Override + public void setOOBInline(boolean on) throws SocketException { + actual.setOOBInline(on); + } + + @Override + public boolean getOOBInline() throws SocketException { + return actual.getOOBInline(); + } + + @Override + public synchronized void setSoTimeout(int timeout) throws SocketException { + actual.setSoTimeout(timeout); + } + + @Override + public synchronized int getSoTimeout() throws SocketException { + return actual.getSoTimeout(); + } + + @Override + public synchronized void setSendBufferSize(int size) throws SocketException { + actual.setSendBufferSize(size); + } + + @Override + public synchronized int getSendBufferSize() throws SocketException { + return actual.getSendBufferSize(); + } + + @Override + public synchronized void setReceiveBufferSize(int size) throws SocketException { + actual.setReceiveBufferSize(size); + } + + @Override + public synchronized int getReceiveBufferSize() throws SocketException { + return actual.getReceiveBufferSize(); + } + + @Override + public void setKeepAlive(boolean on) throws SocketException { + actual.setKeepAlive(on); + } + + @Override + public boolean getKeepAlive() throws SocketException { + return actual.getKeepAlive(); + } + + @Override + public void setTrafficClass(int tc) throws SocketException { + actual.setTrafficClass(tc); + } + + @Override + public int getTrafficClass() throws SocketException { + return actual.getTrafficClass(); + } + + @Override + public void setReuseAddress(boolean on) throws SocketException { + actual.setReuseAddress(on); + } + + @Override + public boolean getReuseAddress() throws SocketException { + return actual.getReuseAddress(); + } + + @Override + public synchronized void close() throws IOException { + actual.close(); + } + + @Override + public void shutdownInput() throws IOException { + actual.shutdownInput(); + } + + @Override + public void shutdownOutput() throws IOException { + actual.shutdownOutput(); + } + + @Override + public String toString() { + return actual.toString(); + } + + @Override + public boolean isConnected() { + return actual.isConnected(); + } + + @Override + public boolean isBound() { + return actual.isBound(); + } + + @Override + public boolean isClosed() { + return actual.isClosed(); + } + + @Override + public boolean isInputShutdown() { + return actual.isInputShutdown(); + } + + @Override + public boolean isOutputShutdown() { + return actual.isOutputShutdown(); + } + + @Override + public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) { + actual.setPerformancePreferences(connectionTime, latency, bandwidth); + } + + @Override + public InputStream getInputStream() throws IOException { + return wrapper; + } + + @Override + public OutputStream getOutputStream() throws IOException { + return actual.getOutputStream(); + } + + @Override + public String[] getSupportedCipherSuites() { + return actual.getSupportedCipherSuites(); + } + + @Override + public String[] getEnabledCipherSuites() { + return actual.getEnabledCipherSuites(); + } + + @Override + public void setEnabledCipherSuites(String[] var1) { + actual.setEnabledCipherSuites(var1); + } + + @Override + public String[] getSupportedProtocols() { + return actual.getSupportedProtocols(); + } + + @Override + public String[] getEnabledProtocols() { + return actual.getEnabledProtocols(); + } + + @Override + public void setEnabledProtocols(String[] var1) { + actual.setEnabledProtocols(var1); + } + + @Override + public SSLSession getSession() { + return actual.getSession(); + } + + @Override + public SSLSession getHandshakeSession() { + return actual.getHandshakeSession(); + } + + @Override + public void addHandshakeCompletedListener(HandshakeCompletedListener var1) { + actual.addHandshakeCompletedListener(var1); + } + + @Override + public void removeHandshakeCompletedListener(HandshakeCompletedListener var1) { + actual.removeHandshakeCompletedListener(var1); + } + + @Override + public void startHandshake() throws IOException { + actual.startHandshake(); + } + + @Override + public void setUseClientMode(boolean var1) { + actual.setUseClientMode(var1); + } + + @Override + public boolean getUseClientMode() { + return actual.getUseClientMode(); + } + + @Override + public void setNeedClientAuth(boolean var1) { + actual.setNeedClientAuth(var1); + } + + @Override + public boolean getNeedClientAuth() { + return actual.getNeedClientAuth(); + } + + @Override + public void setWantClientAuth(boolean var1) { + actual.setWantClientAuth(var1); + } + + @Override + public boolean getWantClientAuth() { + return actual.getWantClientAuth(); + } + + @Override + public void setEnableSessionCreation(boolean var1) { + actual.setEnableSessionCreation(var1); + } + + @Override + public boolean getEnableSessionCreation() { + return actual.getEnableSessionCreation(); + } + + @Override + public SSLParameters getSSLParameters() { + return actual.getSSLParameters(); + } + + @Override + public void setSSLParameters(SSLParameters var1) { + actual.setSSLParameters(var1); + } + + @Override + public String getApplicationProtocol() { + return actual.getApplicationProtocol(); + } + + @Override + public String getHandshakeApplicationProtocol() { + return actual.getHandshakeApplicationProtocol(); + } + + @Override + public void setHandshakeApplicationProtocolSelector(BiFunction, String> var1) { + actual.setHandshakeApplicationProtocolSelector(var1); + } + + @Override + public BiFunction, String> getHandshakeApplicationProtocolSelector() { + return actual.getHandshakeApplicationProtocolSelector(); + } +} diff --git a/src/main/java/redis/clients/jedis/UnifiedJedis.java b/src/main/java/redis/clients/jedis/UnifiedJedis.java index 50d18351d7..db5a52c231 100644 --- a/src/main/java/redis/clients/jedis/UnifiedJedis.java +++ b/src/main/java/redis/clients/jedis/UnifiedJedis.java @@ -19,6 +19,10 @@ import redis.clients.jedis.commands.SampleBinaryKeyedCommands; import redis.clients.jedis.commands.SampleKeyedCommands; import redis.clients.jedis.commands.RedisModuleCommands; +import redis.clients.jedis.csc.Cache; +import redis.clients.jedis.csc.CacheConfig; +import redis.clients.jedis.csc.CacheConnection; +import redis.clients.jedis.csc.CacheFactory; import redis.clients.jedis.exceptions.JedisException; import redis.clients.jedis.executors.*; import redis.clients.jedis.gears.TFunctionListParams; @@ -50,12 +54,14 @@ public class UnifiedJedis implements JedisCommands, JedisBinaryCommands, SampleKeyedCommands, SampleBinaryKeyedCommands, RedisModuleCommands, AutoCloseable { + @Deprecated protected RedisProtocol protocol = null; protected final ConnectionProvider provider; protected final CommandExecutor executor; protected final CommandObjects commandObjects; private final GraphCommandObjects graphCommandObjects; private JedisBroadcastAndRoundRobinConfig broadcastAndRoundRobinConfig = null; + private final Cache cache; public UnifiedJedis() { this(new HostAndPort(Protocol.DEFAULT_HOST, Protocol.DEFAULT_PORT)); @@ -85,14 +91,23 @@ public UnifiedJedis(final URI uri, JedisClientConfig config) { .database(JedisURIHelper.getDBIndex(uri)).clientName(config.getClientName()) .protocol(JedisURIHelper.getRedisProtocol(uri)) .ssl(JedisURIHelper.isRedisSSLScheme(uri)).sslSocketFactory(config.getSslSocketFactory()) - .sslParameters(config.getSslParameters()).hostnameVerifier(config.getHostnameVerifier()) - .build()); + .sslParameters(config.getSslParameters()).hostnameVerifier(config.getHostnameVerifier()).build()); } public UnifiedJedis(HostAndPort hostAndPort, JedisClientConfig clientConfig) { this(new PooledConnectionProvider(hostAndPort, clientConfig), clientConfig.getRedisProtocol()); } + @Experimental + public UnifiedJedis(HostAndPort hostAndPort, JedisClientConfig clientConfig, CacheConfig cacheConfig) { + this(hostAndPort, clientConfig, CacheFactory.getCache(cacheConfig)); + } + + @Experimental + public UnifiedJedis(HostAndPort hostAndPort, JedisClientConfig clientConfig, Cache cache) { + this(new PooledConnectionProvider(hostAndPort, clientConfig, cache), clientConfig.getRedisProtocol(), cache); + } + public UnifiedJedis(ConnectionProvider provider) { this(new DefaultCommandExecutor(provider), provider); } @@ -101,6 +116,11 @@ protected UnifiedJedis(ConnectionProvider provider, RedisProtocol protocol) { this(new DefaultCommandExecutor(provider), provider, new CommandObjects(), protocol); } + @Experimental + protected UnifiedJedis(ConnectionProvider provider, RedisProtocol protocol, Cache cache) { + this(new DefaultCommandExecutor(provider), provider, new CommandObjects(), protocol, cache); + } + /** * The constructor to directly use a custom {@link JedisSocketFactory}. *

@@ -132,13 +152,21 @@ public UnifiedJedis(Connection connection) { this.executor = new SimpleCommandExecutor(connection); this.commandObjects = new CommandObjects(); RedisProtocol proto = connection.getRedisProtocol(); - if (proto != null) this.commandObjects.setProtocol(proto); + if (proto != null) { + this.commandObjects.setProtocol(proto); + } this.graphCommandObjects = new GraphCommandObjects(this); + if (connection instanceof CacheConnection) { + this.cache = ((CacheConnection) connection).getCache(); + } else { + this.cache = null; + } } @Deprecated public UnifiedJedis(Set jedisClusterNodes, JedisClientConfig clientConfig, int maxAttempts) { - this(jedisClusterNodes, clientConfig, maxAttempts, Duration.ofMillis(maxAttempts * clientConfig.getSocketTimeoutMillis())); + this(jedisClusterNodes, clientConfig, maxAttempts, + Duration.ofMillis(maxAttempts * clientConfig.getSocketTimeoutMillis())); } @Deprecated @@ -167,6 +195,13 @@ protected UnifiedJedis(ClusterConnectionProvider provider, int maxAttempts, Dura new ClusterCommandObjects(), protocol); } + @Experimental + protected UnifiedJedis(ClusterConnectionProvider provider, int maxAttempts, Duration maxTotalRetriesDuration, + RedisProtocol protocol, Cache cache) { + this(new ClusterCommandExecutor(provider, maxAttempts, maxTotalRetriesDuration), provider, + new ClusterCommandObjects(), protocol, cache); + } + /** * @deprecated Sharding/Sharded feature will be removed in next major release. */ @@ -180,7 +215,8 @@ public UnifiedJedis(ShardedConnectionProvider provider) { */ @Deprecated public UnifiedJedis(ShardedConnectionProvider provider, Pattern tagPattern) { - this(new DefaultCommandExecutor(provider), provider, new ShardedCommandObjects(provider.getHashingAlgo(), tagPattern)); + this(new DefaultCommandExecutor(provider), provider, + new ShardedCommandObjects(provider.getHashingAlgo(), tagPattern)); } public UnifiedJedis(ConnectionProvider provider, int maxAttempts, Duration maxTotalRetriesDuration) { @@ -216,19 +252,34 @@ private UnifiedJedis(CommandExecutor executor, ConnectionProvider provider) { // Uses a fetched connection to process protocol. Should be avoided if possible. @VisibleForTesting public UnifiedJedis(CommandExecutor executor, ConnectionProvider provider, CommandObjects commandObjects) { - this(executor, provider, commandObjects, null); + this(executor, provider, commandObjects, null, null); if (this.provider != null) { try (Connection conn = this.provider.getConnection()) { if (conn != null) { RedisProtocol proto = conn.getRedisProtocol(); - if (proto != null) this.commandObjects.setProtocol(proto); + if (proto != null) { + this.commandObjects.setProtocol(proto); + } } - } catch (JedisException je) { } + } catch (JedisException je) { + } } } + @Experimental private UnifiedJedis(CommandExecutor executor, ConnectionProvider provider, CommandObjects commandObjects, RedisProtocol protocol) { + this(executor, provider, commandObjects, protocol, (Cache) null); + } + + @Experimental + private UnifiedJedis(CommandExecutor executor, ConnectionProvider provider, CommandObjects commandObjects, + RedisProtocol protocol, Cache cache) { + + if (cache != null && protocol != RedisProtocol.RESP3) { + throw new IllegalArgumentException("Client-side caching is only supported with RESP3."); + } + this.provider = provider; this.executor = executor; @@ -239,6 +290,7 @@ private UnifiedJedis(CommandExecutor executor, ConnectionProvider provider, Comm this.graphCommandObjects = new GraphCommandObjects(this); this.graphCommandObjects.setBaseCommandArgumentsCreator((comm) -> this.commandObjects.commandArguments(comm)); + this.cache = cache; } @Override @@ -265,7 +317,8 @@ private T checkAndBroadcastCommand(CommandObject commandObject) { if (broadcastAndRoundRobinConfig == null) { } else if (commandObject.getArguments().getCommand() instanceof SearchProtocol.SearchCommand - && broadcastAndRoundRobinConfig.getRediSearchModeInCluster() == JedisBroadcastAndRoundRobinConfig.RediSearchMode.LIGHT) { + && broadcastAndRoundRobinConfig + .getRediSearchModeInCluster() == JedisBroadcastAndRoundRobinConfig.RediSearchMode.LIGHT) { broadcast = false; } @@ -277,6 +330,10 @@ public void setBroadcastAndRoundRobinConfig(JedisBroadcastAndRoundRobinConfig co this.commandObjects.setBroadcastAndRoundRobinConfig(this.broadcastAndRoundRobinConfig); } + public Cache getCache() { + return cache; + } + public String ping() { return checkAndBroadcastCommand(commandObjects.ping()); } @@ -3204,14 +3261,12 @@ public Map> xreadAsMap(XReadParams xReadParams, Map>> xreadGroup(String groupName, String consumer, - XReadGroupParams xReadGroupParams, Map streams) { + public List>> xreadGroup(String groupName, String consumer, XReadGroupParams xReadGroupParams, Map streams) { return executeCommand(commandObjects.xreadGroup(groupName, consumer, xReadGroupParams, streams)); } @Override - public Map> xreadGroupAsMap(String groupName, String consumer, - XReadGroupParams xReadGroupParams, Map streams) { + public Map> xreadGroupAsMap(String groupName, String consumer, XReadGroupParams xReadGroupParams, Map streams) { return executeCommand(commandObjects.xreadGroupAsMap(groupName, consumer, xReadGroupParams, streams)); } @@ -3677,7 +3732,7 @@ public List scriptExists(List sha1s) { @Override public Boolean scriptExists(String sha1, String sampleKey) { - return scriptExists(sampleKey, new String[]{sha1}).get(0); + return scriptExists(sampleKey, new String[] { sha1 }).get(0); } @Override @@ -3687,7 +3742,7 @@ public List scriptExists(String sampleKey, String... sha1s) { @Override public Boolean scriptExists(byte[] sha1, byte[] sampleKey) { - return scriptExists(sampleKey, new byte[][]{sha1}).get(0); + return scriptExists(sampleKey, new byte[][] { sha1 }).get(0); } @Override @@ -3852,6 +3907,7 @@ public SearchResult ftSearch(String indexName, String query, FTSearchParams para /** * {@link FTSearchParams#limit(int, int)} will be ignored. + * * @param batchSize batch size * @param indexName index name * @param query query @@ -3983,7 +4039,8 @@ public Map> ftSpellCheck(String index, String query) } @Override - public Map> ftSpellCheck(String index, String query, FTSpellCheckParams spellCheckParams) { + public Map> ftSpellCheck(String index, String query, + FTSpellCheckParams spellCheckParams) { return executeCommand(commandObjects.ftSpellCheck(index, query, spellCheckParams)); } @@ -4575,7 +4632,8 @@ public String tsCreateRule(String sourceKey, String destKey, AggregationType agg @Override public String tsCreateRule(String sourceKey, String destKey, AggregationType aggregationType, long bucketDuration, long alignTimestamp) { - return executeCommand(commandObjects.tsCreateRule(sourceKey, destKey, aggregationType, bucketDuration, alignTimestamp)); + return executeCommand( + commandObjects.tsCreateRule(sourceKey, destKey, aggregationType, bucketDuration, alignTimestamp)); } @Override @@ -4590,7 +4648,7 @@ public List tsQueryIndex(String... filters) { @Override public TSInfo tsInfo(String key) { - return executor.executeCommand(commandObjects.tsInfo(key)); + return executeCommand(commandObjects.tsInfo(key)); } @Override @@ -5074,7 +5132,8 @@ public Object sendCommand(byte[] sampleKey, ProtocolCommand cmd, byte[]... args) } public Object sendBlockingCommand(byte[] sampleKey, ProtocolCommand cmd, byte[]... args) { - return executeCommand(commandObjects.commandArguments(cmd).addObjects((Object[]) args).blocking().processKey(sampleKey)); + return executeCommand( + commandObjects.commandArguments(cmd).addObjects((Object[]) args).blocking().processKey(sampleKey)); } public Object sendCommand(String sampleKey, ProtocolCommand cmd, String... args) { @@ -5082,7 +5141,8 @@ public Object sendCommand(String sampleKey, ProtocolCommand cmd, String... args) } public Object sendBlockingCommand(String sampleKey, ProtocolCommand cmd, String... args) { - return executeCommand(commandObjects.commandArguments(cmd).addObjects((Object[]) args).blocking().processKey(sampleKey)); + return executeCommand( + commandObjects.commandArguments(cmd).addObjects((Object[]) args).blocking().processKey(sampleKey)); } public Object executeCommand(CommandArguments args) { diff --git a/src/main/java/redis/clients/jedis/annots/Experimental.java b/src/main/java/redis/clients/jedis/annots/Experimental.java index e0c642e630..0d17084085 100644 --- a/src/main/java/redis/clients/jedis/annots/Experimental.java +++ b/src/main/java/redis/clients/jedis/annots/Experimental.java @@ -13,5 +13,5 @@ * If a type is marked with this annotation, all its members are considered experimental. */ @Documented -@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR}) +@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR}) public @interface Experimental { } diff --git a/src/main/java/redis/clients/jedis/args/Rawable.java b/src/main/java/redis/clients/jedis/args/Rawable.java index 7515386861..be266f58aa 100644 --- a/src/main/java/redis/clients/jedis/args/Rawable.java +++ b/src/main/java/redis/clients/jedis/args/Rawable.java @@ -10,4 +10,10 @@ public interface Rawable { * @return binary */ byte[] getRaw(); + + @Override + int hashCode(); + + @Override + boolean equals(Object o); } diff --git a/src/main/java/redis/clients/jedis/args/RawableFactory.java b/src/main/java/redis/clients/jedis/args/RawableFactory.java index 813ddd021b..4a2ec782a7 100644 --- a/src/main/java/redis/clients/jedis/args/RawableFactory.java +++ b/src/main/java/redis/clients/jedis/args/RawableFactory.java @@ -96,17 +96,12 @@ public int hashCode() { /** * A {@link Rawable} wrapping a {@link String}. */ - public static class RawString implements Rawable { + public static class RawString extends Raw { - private final byte[] raw; + // TODO: private final String str; ^ implements Rawable public RawString(String str) { - this.raw = SafeEncoder.encode(str); - } - - @Override - public byte[] getRaw() { - return raw; + super(SafeEncoder.encode(str)); } } diff --git a/src/main/java/redis/clients/jedis/csc/AbstractCache.java b/src/main/java/redis/clients/jedis/csc/AbstractCache.java new file mode 100644 index 0000000000..84b4d2ef81 --- /dev/null +++ b/src/main/java/redis/clients/jedis/csc/AbstractCache.java @@ -0,0 +1,232 @@ +package redis.clients.jedis.csc; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +import redis.clients.jedis.annots.Experimental; +import redis.clients.jedis.util.SafeEncoder; + +/** + * The class to manage the client-side caching. User can provide an of implementation of this class + * to the client object. + */ +@Experimental +public abstract class AbstractCache implements Cache { + + private Cacheable cacheable; + private final Map>> redisKeysToCacheKeys = new ConcurrentHashMap<>(); + private final int maximumSize; + private ReentrantLock lock = new ReentrantLock(); + private volatile CacheStats stats = new CacheStats(); + + protected AbstractCache(int maximumSize) { + this(maximumSize, DefaultCacheable.INSTANCE); + } + + protected AbstractCache(int maximumSize, Cacheable cacheable) { + this.maximumSize = maximumSize; + this.cacheable = cacheable; + } + + // Cache interface methods + + @Override + public int getMaxSize() { + return maximumSize; + } + + @Override + public abstract int getSize(); + + @Override + public abstract Collection getCacheEntries(); + + @Override + public CacheEntry get(CacheKey cacheKey) { + CacheEntry entry = getFromStore(cacheKey); + if (entry != null) { + getEvictionPolicy().touch(cacheKey); + } + return entry; + } + + @Override + public CacheEntry set(CacheKey cacheKey, CacheEntry entry) { + lock.lock(); + try { + entry = putIntoStore(cacheKey, entry); + EvictionPolicy policy = getEvictionPolicy(); + policy.touch(cacheKey); + CacheKey evictedKey = policy.evictNext(); + if (evictedKey != null) { + delete(evictedKey); + stats.evict(); + } + for (Object redisKey : cacheKey.getRedisKeys()) { + ByteBuffer mapKey = makeKeyForRedisKeysToCacheKeys(redisKey); + if (redisKeysToCacheKeys.containsKey(mapKey)) { + redisKeysToCacheKeys.get(mapKey).add(cacheKey); + } else { + Set> set = ConcurrentHashMap.newKeySet(); + set.add(cacheKey); + redisKeysToCacheKeys.put(mapKey, set); + } + } + stats.load(); + return entry; + } finally { + lock.unlock(); + } + } + + @Override + public boolean delete(CacheKey cacheKey) { + lock.lock(); + try { + boolean removed = removeFromStore(cacheKey); + getEvictionPolicy().reset(cacheKey); + + // removing it from redisKeysToCacheKeys as well + // TODO: considering not doing it, what is the impact of not doing it ?? + for (Object redisKey : cacheKey.getRedisKeys()) { + ByteBuffer mapKey = makeKeyForRedisKeysToCacheKeys(redisKey); + Set> cacheKeysRelatedtoRedisKey = redisKeysToCacheKeys.get(mapKey); + if (cacheKeysRelatedtoRedisKey != null) { + cacheKeysRelatedtoRedisKey.remove(cacheKey); + } + } + return removed; + } finally { + lock.unlock(); + } + } + + @Override + public List delete(List cacheKeys) { + lock.lock(); + try { + return cacheKeys.stream().map(this::delete).collect(Collectors.toList()); + } finally { + lock.unlock(); + } + } + + @Override + public List deleteByRedisKey(Object key) { + lock.lock(); + try { + final ByteBuffer mapKey = makeKeyForRedisKeysToCacheKeys(key); + + Set> commands = redisKeysToCacheKeys.get(mapKey); + List cacheKeys = new ArrayList<>(); + if (commands != null) { + cacheKeys.addAll(commands.stream().filter(this::removeFromStore).collect(Collectors.toList())); + stats.invalidationByServer(cacheKeys.size()); + redisKeysToCacheKeys.remove(mapKey); + } + stats.invalidationMessages(); + return cacheKeys; + } finally { + lock.unlock(); + } + } + + @Override + public List deleteByRedisKeys(List keys) { + if (keys == null) { + flush(); + return null; + } + lock.lock(); + try { + return ((List) keys).stream() + .map(this::deleteByRedisKey).flatMap(List::stream).collect(Collectors.toList()); + } finally { + lock.unlock(); + } + } + + @Override + public int flush() { + lock.lock(); + try { + int result = this.getSize(); + clearStore(); + redisKeysToCacheKeys.clear(); + getEvictionPolicy().resetAll(); + getStats().flush(); + return result; + } finally { + lock.unlock(); + } + } + + @Override + public boolean isCacheable(CacheKey cacheKey) { + return cacheable.isCacheable(cacheKey.getRedisCommand(), cacheKey.getRedisKeys()); + } + + @Override + public boolean hasCacheKey(CacheKey cacheKey) { + return containsKeyInStore(cacheKey); + } + + @Override + public abstract EvictionPolicy getEvictionPolicy(); + + @Override + public CacheStats getStats() { + return stats; + } + + @Override + public CacheStats getAndResetStats() { + CacheStats result = stats; + stats = new CacheStats(); + return result; + } + + @Override + public boolean compatibilityMode() { + return false; + } + // End of Cache interface methods + + // abstract methods to be implemented by the concrete classes + protected abstract CacheEntry getFromStore(CacheKey cacheKey); + + protected abstract CacheEntry putIntoStore(CacheKey cacheKey, CacheEntry entry); + + protected abstract boolean removeFromStore(CacheKey cacheKey); + + // protected abstract Collection remove(Set> commands); + + protected abstract void clearStore(); + + protected abstract boolean containsKeyInStore(CacheKey cacheKey); + + // End of abstract methods to be implemented by the concrete classes + + private ByteBuffer makeKeyForRedisKeysToCacheKeys(Object key) { + if (key instanceof byte[]) { + return makeKeyForRedisKeysToCacheKeys((byte[]) key); + } else if (key instanceof String) { + return makeKeyForRedisKeysToCacheKeys(SafeEncoder.encode((String) key)); + } else { + throw new IllegalArgumentException(key.getClass().getSimpleName() + " is not supported." + + " Value: \"" + String.valueOf(key) + "\"."); + } + } + + private static ByteBuffer makeKeyForRedisKeysToCacheKeys(byte[] b) { + return ByteBuffer.wrap(b); + } + +} diff --git a/src/main/java/redis/clients/jedis/csc/Cache.java b/src/main/java/redis/clients/jedis/csc/Cache.java new file mode 100644 index 0000000000..0bf4592b59 --- /dev/null +++ b/src/main/java/redis/clients/jedis/csc/Cache.java @@ -0,0 +1,113 @@ +package redis.clients.jedis.csc; + +import java.util.Collection; +import java.util.List; + +/** + * The cache that is used by a connection + */ +public interface Cache { + + /** + * @return The size of the cache + */ + int getMaxSize(); + + /** + * @return The current size of the cache + */ + int getSize(); + + /** + * @return All the entries within the cache + */ + Collection getCacheEntries(); + + /** + * Fetches a value from the cache + * + * @param cacheKey The key within the cache + * @return The entry within the cache + */ + CacheEntry get(CacheKey cacheKey); + + /** + * Puts a value into the cache + * + * @param cacheKey The key by which the value can be accessed within the cache + * @param value The value to be put into the cache + * @return The cache entry + */ + CacheEntry set(CacheKey cacheKey, CacheEntry value); + + /** + * Delete an entry by cache key + * @param cacheKey The cache key of the entry in the cache + * @return True if the entry could be deleted, false if the entry wasn't found. + */ + boolean delete(CacheKey cacheKey); + + /** + * Delete entries by cache key from the cache + * + * @param cacheKeys The cache keys of the entries that should be deleted + * @return True for every entry that could be deleted. False if the entry was not there. + */ + List delete(List cacheKeys); + + /** + * Delete an entry by the Redis key from the cache + * + * @param key The Redis key as binary + * @return True if the entry could be deleted. False if the entry was not there. + */ + List deleteByRedisKey(Object key); + + /** + * Delete entries by the Redis key from the cache + * + * @param keys The Redis keys as binaries + * @return True for every entry that could be deleted. False if the entry was not there. + */ + List deleteByRedisKeys(List keys); + + /** + * Flushes the entire cache + * + * @return Return the number of entries that were flushed + */ + int flush(); + + /** + * @param cacheKey The key of the cache entry + * @return True if the entry is cachable, false otherwise + */ + boolean isCacheable(CacheKey cacheKey); + + /** + * + * @param cacheKey The key of the cache entry + * @return True if the cache already contains the key + */ + boolean hasCacheKey(CacheKey cacheKey); + + /** + * @return The eviction policy that is used by the cache + */ + EvictionPolicy getEvictionPolicy(); + + /** + * @return The statistics of the cache + */ + CacheStats getStats(); + + /** + * @return The statistics of the cache + */ + CacheStats getAndResetStats(); + + /** + * @return The compatibility of cache against different Redis versions + */ + boolean compatibilityMode(); +} diff --git a/src/main/java/redis/clients/jedis/csc/CacheConfig.java b/src/main/java/redis/clients/jedis/csc/CacheConfig.java new file mode 100644 index 0000000000..ab907dfbde --- /dev/null +++ b/src/main/java/redis/clients/jedis/csc/CacheConfig.java @@ -0,0 +1,65 @@ +package redis.clients.jedis.csc; + +public class CacheConfig { + + private int maxSize; + private Cacheable cacheable; + private EvictionPolicy evictionPolicy; + private Class cacheClass; + + public int getMaxSize() { + return maxSize; + } + + public Cacheable getCacheable() { + return cacheable; + } + + public EvictionPolicy getEvictionPolicy() { + return evictionPolicy; + } + + public Class getCacheClass() { + return cacheClass; + } + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private final int DEFAULT_MAX_SIZE = 10000; + private int maxSize = DEFAULT_MAX_SIZE; + private Cacheable cacheable = DefaultCacheable.INSTANCE; + private EvictionPolicy evictionPolicy; + private Class cacheClass; + + public Builder maxSize(int maxSize) { + this.maxSize = maxSize; + return this; + } + + public Builder evictionPolicy(EvictionPolicy policy) { + this.evictionPolicy = policy; + return this; + } + + public Builder cacheable(Cacheable cacheable) { + this.cacheable = cacheable; + return this; + } + + public Builder cacheClass(Class cacheClass) { + this.cacheClass = cacheClass; + return this; + } + + public CacheConfig build() { + CacheConfig cacheConfig = new CacheConfig(); + cacheConfig.maxSize = this.maxSize; + cacheConfig.cacheable = this.cacheable; + cacheConfig.evictionPolicy = this.evictionPolicy; + cacheConfig.cacheClass = this.cacheClass; + return cacheConfig; + } + } +} \ No newline at end of file diff --git a/src/main/java/redis/clients/jedis/csc/CacheConnection.java b/src/main/java/redis/clients/jedis/csc/CacheConnection.java new file mode 100644 index 0000000000..f157d95a94 --- /dev/null +++ b/src/main/java/redis/clients/jedis/csc/CacheConnection.java @@ -0,0 +1,128 @@ +package redis.clients.jedis.csc; + +import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; + +import redis.clients.jedis.CommandObject; +import redis.clients.jedis.Connection; +import redis.clients.jedis.JedisClientConfig; +import redis.clients.jedis.JedisSocketFactory; +import redis.clients.jedis.Protocol; +import redis.clients.jedis.RedisProtocol; +import redis.clients.jedis.exceptions.JedisException; +import redis.clients.jedis.util.RedisInputStream; + +public class CacheConnection extends Connection { + + private final Cache cache; + private ReentrantLock lock; + private static final String REDIS = "redis"; + private static final String MIN_REDIS_VERSION = "7.4"; + + public CacheConnection(final JedisSocketFactory socketFactory, JedisClientConfig clientConfig, Cache cache) { + super(socketFactory, clientConfig); + + if (protocol != RedisProtocol.RESP3) { + throw new JedisException("Client side caching is only supported with RESP3."); + } + if (!cache.compatibilityMode()) { + RedisVersion current = new RedisVersion(version); + RedisVersion required = new RedisVersion(MIN_REDIS_VERSION); + if (!REDIS.equals(server) || current.compareTo(required) < 0) { + throw new JedisException(String.format("Client side caching is only supported with 'Redis %s' or later.", MIN_REDIS_VERSION)); + } + } + this.cache = Objects.requireNonNull(cache); + initializeClientSideCache(); + } + + @Override + protected void initializeFromClientConfig(JedisClientConfig config) { + lock = new ReentrantLock(); + super.initializeFromClientConfig(config); + } + + @Override + protected Object protocolRead(RedisInputStream inputStream) { + lock.lock(); + try { + return Protocol.read(inputStream, cache); + } finally { + lock.unlock(); + } + } + + @Override + protected void protocolReadPushes(RedisInputStream inputStream) { + if (lock.tryLock()) { + try { + Protocol.readPushes(inputStream, cache, true); + } finally { + lock.unlock(); + } + } + } + + @Override + public void disconnect() { + super.disconnect(); + cache.flush(); + } + + @Override + public T executeCommand(final CommandObject commandObject) { + final CacheKey cacheKey = new CacheKey(commandObject); + if (!cache.isCacheable(cacheKey)) { + cache.getStats().nonCacheable(); + return super.executeCommand(commandObject); + } + + CacheEntry cacheEntry = cache.get(cacheKey); + if (cacheEntry != null) { // (probable) CACHE HIT !! + cacheEntry = validateEntry(cacheEntry); + if (cacheEntry != null) { + // CACHE HIT confirmed !!! + cache.getStats().hit(); + return cacheEntry.getValue(); + } + } + + // CACHE MISS !! + cache.getStats().miss(); + T value = super.executeCommand(commandObject); + cacheEntry = new CacheEntry<>(cacheKey, value, this); + cache.set(cacheKey, cacheEntry); + // this line actually provides a deep copy of cached object instance + value = cacheEntry.getValue(); + return value; + } + + public Cache getCache() { + return cache; + } + + private void initializeClientSideCache() { + sendCommand(Protocol.Command.CLIENT, "TRACKING", "ON"); + String reply = getStatusCodeReply(); + if (!"OK".equals(reply)) { + throw new JedisException("Could not enable client tracking. Reply: " + reply); + } + } + + private CacheEntry validateEntry(CacheEntry cacheEntry) { + CacheConnection cacheOwner = cacheEntry.getConnection(); + if (cacheOwner == null || cacheOwner.isBroken() || !cacheOwner.isConnected()) { + cache.delete(cacheEntry.getCacheKey()); + return null; + } else { + try { + cacheOwner.readPushesWithCheckingBroken(); + } catch (JedisException e) { + cache.delete(cacheEntry.getCacheKey()); + return null; + } + + return cache.get(cacheEntry.getCacheKey()); + } + } +} diff --git a/src/main/java/redis/clients/jedis/csc/CacheEntry.java b/src/main/java/redis/clients/jedis/csc/CacheEntry.java new file mode 100644 index 0000000000..36c308db8d --- /dev/null +++ b/src/main/java/redis/clients/jedis/csc/CacheEntry.java @@ -0,0 +1,56 @@ +package redis.clients.jedis.csc; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.ref.WeakReference; + +import redis.clients.jedis.exceptions.JedisCacheException; + +public class CacheEntry { + + private final CacheKey cacheKey; + private final WeakReference connection; + private final byte[] bytes; + + public CacheEntry(CacheKey cacheKey, T value, CacheConnection connection) { + this.cacheKey = cacheKey; + this.connection = new WeakReference<>(connection); + this.bytes = toBytes(value); + } + + public CacheKey getCacheKey() { + return cacheKey; + } + + public T getValue() { + return toObject(bytes); + } + + public CacheConnection getConnection() { + return connection.get(); + } + + private static byte[] toBytes(Object object) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(object); + oos.flush(); + oos.close(); + return baos.toByteArray(); + } catch (IOException e) { + throw new JedisCacheException("Failed to serialize object", e); + } + } + + private T toObject(byte[] data) { + try (ByteArrayInputStream bais = new ByteArrayInputStream(data); + ObjectInputStream ois = new ObjectInputStream(bais)) { + return (T) ois.readObject(); + } catch (IOException | ClassNotFoundException e) { + throw new JedisCacheException("Failed to deserialize object", e); + } + } +} diff --git a/src/main/java/redis/clients/jedis/csc/CacheFactory.java b/src/main/java/redis/clients/jedis/csc/CacheFactory.java new file mode 100644 index 0000000000..0286783dfc --- /dev/null +++ b/src/main/java/redis/clients/jedis/csc/CacheFactory.java @@ -0,0 +1,63 @@ +package redis.clients.jedis.csc; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; + +import redis.clients.jedis.exceptions.JedisCacheException; + +public final class CacheFactory { + + public static Cache getCache(CacheConfig config) { + if (config.getCacheClass() == null) { + if (config.getCacheable() == null) { + throw new JedisCacheException("Cacheable is required to create the default cache!"); + } + return new DefaultCache(config.getMaxSize(), config.getCacheable(), getEvictionPolicy(config)); + } + return instantiateCustomCache(config); + } + + private static Cache instantiateCustomCache(CacheConfig config) { + try { + if (config.getCacheable() != null) { + Constructor ctorWithCacheable = findConstructorWithCacheable(config.getCacheClass()); + if (ctorWithCacheable != null) { + return (Cache) ctorWithCacheable.newInstance(config.getMaxSize(), getEvictionPolicy(config), config.getCacheable()); + } + } + Constructor ctor = getConstructor(config.getCacheClass()); + return (Cache) ctor.newInstance(config.getMaxSize(), getEvictionPolicy(config)); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException + | SecurityException e) { + throw new JedisCacheException("Failed to insantiate custom cache type!", e); + } + } + + private static Constructor findConstructorWithCacheable(Class customCacheType) { + return Arrays.stream(customCacheType.getConstructors()) + .filter(ctor -> Arrays.equals(ctor.getParameterTypes(), new Class[] { int.class, EvictionPolicy.class, Cacheable.class })) + .findFirst().orElse(null); + } + + private static Constructor getConstructor(Class customCacheType) { + try { + return customCacheType.getConstructor(int.class, EvictionPolicy.class); + } catch (NoSuchMethodException e) { + String className = customCacheType.getName(); + throw new JedisCacheException(String.format( + "Failed to find compatible constructor for custom cache type! Provide one of these;" + // give hints about the compatible constructors + + "\n - %s(int maxSize, EvictionPolicy evictionPolicy)\n - %s(int maxSize, EvictionPolicy evictionPolicy, Cacheable cacheable)", + className, className), e); + } + } + + private static EvictionPolicy getEvictionPolicy(CacheConfig config) { + if (config.getEvictionPolicy() == null) { + // It will be default to LRUEviction, until we have other eviction implementations + return new LRUEviction(config.getMaxSize()); + } + return config.getEvictionPolicy(); + } +} \ No newline at end of file diff --git a/src/main/java/redis/clients/jedis/csc/CacheKey.java b/src/main/java/redis/clients/jedis/csc/CacheKey.java new file mode 100644 index 0000000000..dedd88374e --- /dev/null +++ b/src/main/java/redis/clients/jedis/csc/CacheKey.java @@ -0,0 +1,37 @@ +package redis.clients.jedis.csc; + +import java.util.List; +import java.util.Objects; + +import redis.clients.jedis.CommandObject; +import redis.clients.jedis.commands.ProtocolCommand; + +public class CacheKey { + + private final CommandObject command; + + public CacheKey(CommandObject command) { + this.command = Objects.requireNonNull(command); + } + + @Override + public int hashCode() { + return command.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + final CacheKey other = (CacheKey) obj; + return Objects.equals(this.command, other.command); + } + + public List getRedisKeys() { + return command.getArguments().getKeys(); + } + + public ProtocolCommand getRedisCommand() { + return command.getArguments().getCommand(); + } +} diff --git a/src/main/java/redis/clients/jedis/csc/CacheStats.java b/src/main/java/redis/clients/jedis/csc/CacheStats.java new file mode 100644 index 0000000000..e689ea0d77 --- /dev/null +++ b/src/main/java/redis/clients/jedis/csc/CacheStats.java @@ -0,0 +1,89 @@ +package redis.clients.jedis.csc; + +import java.util.concurrent.atomic.AtomicLong; + +public class CacheStats { + + private AtomicLong hits = new AtomicLong(0); + private AtomicLong misses = new AtomicLong(0); + private AtomicLong loads = new AtomicLong(0); + private AtomicLong evicts = new AtomicLong(0); + private AtomicLong nonCacheable = new AtomicLong(0); + private AtomicLong flush = new AtomicLong(0); + private AtomicLong invalidationsByServer = new AtomicLong(0); + private AtomicLong invalidationMessages = new AtomicLong(0); + + protected void hit() { + hits.incrementAndGet(); + } + + protected void miss() { + misses.incrementAndGet(); + } + + protected void load() { + loads.incrementAndGet(); + } + + protected void evict() { + evicts.incrementAndGet(); + } + + protected void nonCacheable() { + nonCacheable.incrementAndGet(); + } + + protected void flush() { + flush.incrementAndGet(); + } + + protected void invalidationByServer(int size) { + invalidationsByServer.addAndGet(size); + } + + protected void invalidationMessages() { + invalidationMessages.incrementAndGet(); + } + + public long getHitCount() { + return hits.get(); + } + + public long getMissCount() { + return misses.get(); + } + + public long getLoadCount() { + return loads.get(); + } + + public long getEvictCount() { + return evicts.get(); + } + + public long getNonCacheableCount() { + return nonCacheable.get(); + } + + public long getFlushCount() { + return flush.get(); + } + + public long getInvalidationCount() { + return invalidationsByServer.get(); + } + + public String toString() { + return "CacheStats{" + + "hits=" + hits + + ", misses=" + misses + + ", loads=" + loads + + ", evicts=" + evicts + + ", nonCacheable=" + nonCacheable + + ", flush=" + flush + + ", invalidationsByServer=" + invalidationsByServer + + ", invalidationMessages=" + invalidationMessages + + '}'; + } + +} diff --git a/src/main/java/redis/clients/jedis/csc/Cacheable.java b/src/main/java/redis/clients/jedis/csc/Cacheable.java new file mode 100644 index 0000000000..908b004cbb --- /dev/null +++ b/src/main/java/redis/clients/jedis/csc/Cacheable.java @@ -0,0 +1,9 @@ +package redis.clients.jedis.csc; + +import java.util.List; +import redis.clients.jedis.commands.ProtocolCommand; + +public interface Cacheable { + + boolean isCacheable(ProtocolCommand command, List keys); +} diff --git a/src/main/java/redis/clients/jedis/csc/DefaultCache.java b/src/main/java/redis/clients/jedis/csc/DefaultCache.java new file mode 100644 index 0000000000..5577cc0758 --- /dev/null +++ b/src/main/java/redis/clients/jedis/csc/DefaultCache.java @@ -0,0 +1,75 @@ +package redis.clients.jedis.csc; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public class DefaultCache extends AbstractCache { + + protected final Map cache; + private final EvictionPolicy evictionPolicy; + + protected DefaultCache(int maximumSize) { + this(maximumSize, new HashMap()); + } + + protected DefaultCache(int maximumSize, Map map) { + this(maximumSize, map, DefaultCacheable.INSTANCE, new LRUEviction(maximumSize)); + } + + protected DefaultCache(int maximumSize, Cacheable cacheable) { + this(maximumSize, new HashMap(), cacheable, new LRUEviction(maximumSize)); + } + + protected DefaultCache(int maximumSize, Cacheable cacheable, EvictionPolicy evictionPolicy) { + this(maximumSize, new HashMap(), cacheable, evictionPolicy); + } + + protected DefaultCache(int maximumSize, Map map, Cacheable cacheable, EvictionPolicy evictionPolicy) { + super(maximumSize, cacheable); + this.cache = map; + this.evictionPolicy = evictionPolicy; + this.evictionPolicy.setCache(this); + } + + @Override + public int getSize() { + return cache.size(); + } + + @Override + public Collection getCacheEntries() { + return cache.values(); + } + + @Override + public EvictionPolicy getEvictionPolicy() { + return this.evictionPolicy; + } + + @Override + public CacheEntry getFromStore(CacheKey key) { + return cache.get(key); + } + + @Override + public CacheEntry putIntoStore(CacheKey key, CacheEntry entry) { + return cache.put(key, entry); + } + + @Override + public boolean removeFromStore(CacheKey key) { + return cache.remove(key) != null; + } + + @Override + protected final void clearStore() { + cache.clear(); + } + + @Override + protected boolean containsKeyInStore(CacheKey cacheKey) { + return cache.containsKey(cacheKey); + } + +} \ No newline at end of file diff --git a/src/main/java/redis/clients/jedis/csc/DefaultCacheable.java b/src/main/java/redis/clients/jedis/csc/DefaultCacheable.java new file mode 100644 index 0000000000..47f9ca0ccc --- /dev/null +++ b/src/main/java/redis/clients/jedis/csc/DefaultCacheable.java @@ -0,0 +1,98 @@ +package redis.clients.jedis.csc; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import redis.clients.jedis.Protocol.Command; +import redis.clients.jedis.commands.ProtocolCommand; +import redis.clients.jedis.json.JsonProtocol.JsonCommand; +import redis.clients.jedis.timeseries.TimeSeriesProtocol.TimeSeriesCommand; + +public class DefaultCacheable implements Cacheable { + + public static final DefaultCacheable INSTANCE = new DefaultCacheable(); + + private static final Set DEFAULT_CACHEABLE_COMMANDS = new HashSet() { + { + add(Command.BITCOUNT); + add(Command.BITFIELD_RO); + add(Command.BITPOS); + add(Command.EXISTS); + add(Command.GEODIST); + add(Command.GEOHASH); + add(Command.GEOPOS); + add(Command.GEORADIUSBYMEMBER_RO); + add(Command.GEORADIUS_RO); + add(Command.GEOSEARCH); + add(Command.GET); + add(Command.GETBIT); + add(Command.GETRANGE); + add(Command.HEXISTS); + add(Command.HGET); + add(Command.HGETALL); + add(Command.HKEYS); + add(Command.HLEN); + add(Command.HMGET); + add(Command.HSTRLEN); + add(Command.HVALS); + add(JsonCommand.ARRINDEX); + add(JsonCommand.ARRLEN); + add(JsonCommand.GET); + add(JsonCommand.MGET); + add(JsonCommand.OBJKEYS); + add(JsonCommand.OBJLEN); + add(JsonCommand.STRLEN); + add(JsonCommand.TYPE); + add(Command.LCS); + add(Command.LINDEX); + add(Command.LLEN); + add(Command.LPOS); + add(Command.LRANGE); + add(Command.MGET); + add(Command.SCARD); + add(Command.SDIFF); + add(Command.SINTER); + add(Command.SISMEMBER); + add(Command.SMEMBERS); + add(Command.SMISMEMBER); + add(Command.STRLEN); + add(Command.SUBSTR); + add(Command.SUNION); + add(TimeSeriesCommand.GET); + add(TimeSeriesCommand.INFO); + add(TimeSeriesCommand.RANGE); + add(TimeSeriesCommand.REVRANGE); + add(Command.TYPE); + add(Command.XLEN); + add(Command.XPENDING); + add(Command.XRANGE); + add(Command.XREVRANGE); + add(Command.ZCARD); + add(Command.ZCOUNT); + add(Command.ZLEXCOUNT); + add(Command.ZMSCORE); + add(Command.ZRANGE); + add(Command.ZRANGEBYLEX); + add(Command.ZRANGEBYSCORE); + add(Command.ZRANK); + add(Command.ZREVRANGE); + add(Command.ZREVRANGEBYLEX); + add(Command.ZREVRANGEBYSCORE); + add(Command.ZREVRANK); + add(Command.ZSCORE); + } + }; + + public DefaultCacheable() { + } + + public static boolean isDefaultCacheableCommand(ProtocolCommand command) { + return DEFAULT_CACHEABLE_COMMANDS.contains(command); + } + + @Override + public boolean isCacheable(ProtocolCommand command, List keys) { + return isDefaultCacheableCommand(command); + } +} diff --git a/src/main/java/redis/clients/jedis/csc/EvictionPolicy.java b/src/main/java/redis/clients/jedis/csc/EvictionPolicy.java new file mode 100644 index 0000000000..217b04263e --- /dev/null +++ b/src/main/java/redis/clients/jedis/csc/EvictionPolicy.java @@ -0,0 +1,77 @@ +package redis.clients.jedis.csc; + +import java.util.List; + +/** + * Describes the properties and functionality of an eviction policy + *

+ * One policy instance belongs to exactly one cache instance + */ +public interface EvictionPolicy { + + /** + * Types of eviction policies + * + * AGE - based on the time of access, e.g., LRU + * FREQ - based on the frequency of access, e.g., LFU + * HYBR - AGE + FREQ, e.g., CLOCK + * MISC - Anythin that isn't time based, frequency based or a combination of the two, e.g., FIFO + */ + enum EvictionType { + AGE, FREQ, HYBR, MISC + } + + /** + * @return The cache that is associated to this policy instance + */ + Cache getCache(); + + /** + * Sets the cache that is associated to this policy instance + * @param cache The cache instance + */ + void setCache(Cache cache); + + /** + * @return The type of policy + */ + EvictionType getType(); + + /** + * @return The name of the policy + */ + String getName(); + + /** + * Evict the next element from the cache + * This one should provide O(1) complexity + * @return The key of the entry that was evicted + */ + CacheKey evictNext(); + + /** + * + * @param n The number of entries to evict + * @return The list of keys of evicted entries + */ + List evictMany(int n); + + /** + * Indicates that a cache key was touched + * This one should provide O(1) complexity + * @param cacheKey The key within the cache + */ + void touch(CacheKey cacheKey); + + /** + * Resets the state that the eviction policy maintains about the cache key + * @param cacheKey + */ + boolean reset(CacheKey cacheKey); + + /** + * Resets the entire state of the eviction data + * @return True if the reset could be performed successfully + */ + int resetAll(); +} diff --git a/src/main/java/redis/clients/jedis/csc/LRUEviction.java b/src/main/java/redis/clients/jedis/csc/LRUEviction.java new file mode 100644 index 0000000000..b75c7338ba --- /dev/null +++ b/src/main/java/redis/clients/jedis/csc/LRUEviction.java @@ -0,0 +1,106 @@ +package redis.clients.jedis.csc; + +import java.util.*; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Simple L(east) R(ecently) U(sed) eviction policy + * ATTENTION: this class is not thread safe + */ +public class LRUEviction implements EvictionPolicy { + + // For future reference, in case there is a need to make it thread safe, + // the LinkedHashMap can be wrapped in a Collections.synchronizedMap + + /** + * The cache that is associated to that policy instance + */ + protected Cache cache; + protected LinkedHashMap accessTimes; + + protected ArrayDeque pendingEvictions = new ArrayDeque(); + + protected ConcurrentLinkedQueue msg = new ConcurrentLinkedQueue(); + + private int initialCapacity; + + /** + * Constructor that gets the cache passed + * + * @param initialCapacity + */ + public LRUEviction(int initialCapacity) { + this.initialCapacity = initialCapacity; + } + + @Override + public void setCache(Cache cache) { + this.cache = cache; + this.accessTimes = new LinkedHashMap(initialCapacity, 1f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + boolean evictionRequired = cache.getSize() > cache.getMaxSize() + || accessTimes.size() > cache.getMaxSize(); + // here the cache check is only for performance gain; we are trying to avoid the sequence add + poll + hasCacheKey + // and prefer to check it in cache once in early stage. + // if there is nothing to remove in actual cache as of now, stop worrying about it. + if (evictionRequired && cache.hasCacheKey(eldest.getKey())) { + pendingEvictions.addLast(eldest.getKey()); + + } + return evictionRequired; + } + }; + } + + @Override + public Cache getCache() { + return this.cache; + } + + @Override + public EvictionType getType() { + return EvictionType.AGE; + } + + @Override + public String getName() { + return "Simple L(east) R(ecently) U(sed)"; + } + + @Override + public synchronized CacheKey evictNext() { + CacheKey cacheKey = pendingEvictions.pollFirst(); + while (cacheKey != null && !cache.hasCacheKey(cacheKey)) { + cacheKey = pendingEvictions.pollFirst(); + } + return cacheKey; + } + + @Override + public synchronized List evictMany(int n) { + List result = new ArrayList<>(); + for (int i = 0; i < n; i++) { + result.add(this.evictNext()); + } + return result; + } + + @Override + public synchronized void touch(CacheKey cacheKey) { + this.accessTimes.put(cacheKey, new Date().getTime()); + } + + @Override + public synchronized boolean reset(CacheKey cacheKey) { + return this.accessTimes.remove(cacheKey) != null; + } + + @Override + public synchronized int resetAll() { + int result = this.accessTimes.size(); + accessTimes.clear(); + return result; + } + +} diff --git a/src/main/java/redis/clients/jedis/csc/RedisVersion.java b/src/main/java/redis/clients/jedis/csc/RedisVersion.java new file mode 100644 index 0000000000..2daf6393c7 --- /dev/null +++ b/src/main/java/redis/clients/jedis/csc/RedisVersion.java @@ -0,0 +1,41 @@ +package redis.clients.jedis.csc; + +import java.util.Arrays; + +class RedisVersion implements Comparable { + + private String version; + private Integer[] numbers; + + public RedisVersion(String version) { + if (version == null) throw new IllegalArgumentException("Version can not be null"); + this.version = version; + this.numbers = Arrays.stream(version.split("\\.")).map(n -> Integer.parseInt(n)).toArray(Integer[]::new); + } + + @Override + public int compareTo(RedisVersion other) { + int max = Math.max(this.numbers.length, other.numbers.length); + for (int i = 0; i < max; i++) { + int thisNumber = this.numbers.length > i ? this.numbers[i]:0; + int otherNumber = other.numbers.length > i ? other.numbers[i]:0; + if (thisNumber < otherNumber) return -1; + if (thisNumber > otherNumber) return 1; + } + return 0; + } + + @Override + public String toString() { + return this.version; + } + + @Override + public boolean equals(Object that) { + if (this == that) return true; + if (that == null) return false; + if (this.getClass() != that.getClass()) return false; + return this.compareTo((RedisVersion) that) == 0; + } + +} diff --git a/src/main/java/redis/clients/jedis/csc/package-info.java b/src/main/java/redis/clients/jedis/csc/package-info.java new file mode 100644 index 0000000000..d74aee56cd --- /dev/null +++ b/src/main/java/redis/clients/jedis/csc/package-info.java @@ -0,0 +1,7 @@ +/** + * This package contains the classes and interfaces related to Server-assisted Client-side Caching. + */ +@Experimental +package redis.clients.jedis.csc; + +import redis.clients.jedis.annots.Experimental; \ No newline at end of file diff --git a/src/main/java/redis/clients/jedis/csc/util/AllowAndDenyListWithStringKeys.java b/src/main/java/redis/clients/jedis/csc/util/AllowAndDenyListWithStringKeys.java new file mode 100644 index 0000000000..25fd89cff1 --- /dev/null +++ b/src/main/java/redis/clients/jedis/csc/util/AllowAndDenyListWithStringKeys.java @@ -0,0 +1,48 @@ +package redis.clients.jedis.csc.util; + +import java.util.List; +import java.util.Set; +import redis.clients.jedis.commands.ProtocolCommand; +import redis.clients.jedis.csc.DefaultCacheable; +import redis.clients.jedis.csc.Cacheable; + +public class AllowAndDenyListWithStringKeys implements Cacheable { + + private final Set allowCommands; + private final Set denyCommands; + + private final Set allowKeys; + private final Set denyKeys; + + public AllowAndDenyListWithStringKeys(Set allowCommands, Set denyCommands, + Set allowKeys, Set denyKeys) { + this.allowCommands = allowCommands; + this.denyCommands = denyCommands; + this.allowKeys = allowKeys; + this.denyKeys = denyKeys; + } + + @Override + public boolean isCacheable(ProtocolCommand command, List keys) { + if (allowCommands != null && !allowCommands.contains(command)) { + return false; + } + if (denyCommands != null && denyCommands.contains(command)) { + return false; + } + + for (Object key : keys) { + if (!(key instanceof String)) { + return false; + } + if (allowKeys != null && !allowKeys.contains((String) key)) { + return false; + } + if (denyKeys != null && denyKeys.contains((String) key)) { + return false; + } + } + + return DefaultCacheable.isDefaultCacheableCommand(command); + } +} diff --git a/src/main/java/redis/clients/jedis/csc/util/package-info.java b/src/main/java/redis/clients/jedis/csc/util/package-info.java new file mode 100644 index 0000000000..abd1e73b9e --- /dev/null +++ b/src/main/java/redis/clients/jedis/csc/util/package-info.java @@ -0,0 +1,7 @@ +/** + * This package contains the helper classes related to Server-assisted Client-side Caching. + */ +@Experimental +package redis.clients.jedis.csc.util; + +import redis.clients.jedis.annots.Experimental; \ No newline at end of file diff --git a/src/main/java/redis/clients/jedis/exceptions/JedisCacheException.java b/src/main/java/redis/clients/jedis/exceptions/JedisCacheException.java new file mode 100644 index 0000000000..94a745e1bf --- /dev/null +++ b/src/main/java/redis/clients/jedis/exceptions/JedisCacheException.java @@ -0,0 +1,19 @@ +package redis.clients.jedis.exceptions; + +public class JedisCacheException extends JedisException { + + private static final long serialVersionUID = 3878126572474819403L; + + public JedisCacheException(String message) { + super(message); + } + + public JedisCacheException(Throwable cause) { + super(cause); + } + + public JedisCacheException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/redis/clients/jedis/mcf/package-info.java b/src/main/java/redis/clients/jedis/mcf/package-info.java index 6b89d9c77b..60b1f9c123 100644 --- a/src/main/java/redis/clients/jedis/mcf/package-info.java +++ b/src/main/java/redis/clients/jedis/mcf/package-info.java @@ -1,4 +1,7 @@ /** * This package contains the classes that are related to Active-Active cluster(s) and Multi-Cluster failover. */ +@Experimental package redis.clients.jedis.mcf; + +import redis.clients.jedis.annots.Experimental; \ No newline at end of file diff --git a/src/main/java/redis/clients/jedis/providers/ClusterConnectionProvider.java b/src/main/java/redis/clients/jedis/providers/ClusterConnectionProvider.java index 925645e169..d7e2f9fee7 100644 --- a/src/main/java/redis/clients/jedis/providers/ClusterConnectionProvider.java +++ b/src/main/java/redis/clients/jedis/providers/ClusterConnectionProvider.java @@ -17,6 +17,8 @@ import redis.clients.jedis.Connection; import redis.clients.jedis.ConnectionPool; import redis.clients.jedis.JedisClusterInfoCache; +import redis.clients.jedis.annots.Experimental; +import redis.clients.jedis.csc.Cache; import redis.clients.jedis.exceptions.JedisClusterOperationException; import redis.clients.jedis.exceptions.JedisException; @@ -31,18 +33,38 @@ public ClusterConnectionProvider(Set clusterNodes, JedisClientConfi initializeSlotsCache(clusterNodes, clientConfig); } + @Experimental + public ClusterConnectionProvider(Set clusterNodes, JedisClientConfig clientConfig, Cache clientSideCache) { + this.cache = new JedisClusterInfoCache(clientConfig, clientSideCache, clusterNodes); + initializeSlotsCache(clusterNodes, clientConfig); + } + public ClusterConnectionProvider(Set clusterNodes, JedisClientConfig clientConfig, GenericObjectPoolConfig poolConfig) { this.cache = new JedisClusterInfoCache(clientConfig, poolConfig, clusterNodes); initializeSlotsCache(clusterNodes, clientConfig); } + @Experimental + public ClusterConnectionProvider(Set clusterNodes, JedisClientConfig clientConfig, Cache clientSideCache, + GenericObjectPoolConfig poolConfig) { + this.cache = new JedisClusterInfoCache(clientConfig, clientSideCache, poolConfig, clusterNodes); + initializeSlotsCache(clusterNodes, clientConfig); + } + public ClusterConnectionProvider(Set clusterNodes, JedisClientConfig clientConfig, GenericObjectPoolConfig poolConfig, Duration topologyRefreshPeriod) { this.cache = new JedisClusterInfoCache(clientConfig, poolConfig, clusterNodes, topologyRefreshPeriod); initializeSlotsCache(clusterNodes, clientConfig); } + @Experimental + public ClusterConnectionProvider(Set clusterNodes, JedisClientConfig clientConfig, Cache clientSideCache, + GenericObjectPoolConfig poolConfig, Duration topologyRefreshPeriod) { + this.cache = new JedisClusterInfoCache(clientConfig, clientSideCache, poolConfig, clusterNodes, topologyRefreshPeriod); + initializeSlotsCache(clusterNodes, clientConfig); + } + private void initializeSlotsCache(Set startNodes, JedisClientConfig clientConfig) { if (startNodes.isEmpty()) { throw new JedisClusterOperationException("No nodes to initialize cluster slots cache."); @@ -111,9 +133,8 @@ public Connection getReplicaConnection(CommandArguments args) { @Override public Connection getConnection() { - // In antirez's redis-rb-cluster implementation, getRandomConnection always - // return valid connection (able to ping-pong) or exception if all - // connections are invalid + // In antirez's redis-rb-cluster implementation, getRandomConnection always return + // valid connection (able to ping-pong) or exception if all connections are invalid List pools = cache.getShuffledNodesPool(); diff --git a/src/main/java/redis/clients/jedis/providers/PooledConnectionProvider.java b/src/main/java/redis/clients/jedis/providers/PooledConnectionProvider.java index f7b90e2953..ddbd768f9b 100644 --- a/src/main/java/redis/clients/jedis/providers/PooledConnectionProvider.java +++ b/src/main/java/redis/clients/jedis/providers/PooledConnectionProvider.java @@ -11,6 +11,8 @@ import redis.clients.jedis.ConnectionPool; import redis.clients.jedis.HostAndPort; import redis.clients.jedis.JedisClientConfig; +import redis.clients.jedis.annots.Experimental; +import redis.clients.jedis.csc.Cache; import redis.clients.jedis.util.Pool; public class PooledConnectionProvider implements ConnectionProvider { @@ -28,9 +30,22 @@ public PooledConnectionProvider(HostAndPort hostAndPort, JedisClientConfig clien this.connectionMapKey = hostAndPort; } + @Experimental + public PooledConnectionProvider(HostAndPort hostAndPort, JedisClientConfig clientConfig, Cache clientSideCache) { + this(new ConnectionPool(hostAndPort, clientConfig, clientSideCache)); + this.connectionMapKey = hostAndPort; + } + public PooledConnectionProvider(HostAndPort hostAndPort, JedisClientConfig clientConfig, GenericObjectPoolConfig poolConfig) { - this(new ConnectionFactory(hostAndPort, clientConfig), poolConfig); + this(new ConnectionPool(hostAndPort, clientConfig, poolConfig)); + this.connectionMapKey = hostAndPort; + } + + @Experimental + public PooledConnectionProvider(HostAndPort hostAndPort, JedisClientConfig clientConfig, Cache clientSideCache, + GenericObjectPoolConfig poolConfig) { + this(new ConnectionPool(hostAndPort, clientConfig, clientSideCache, poolConfig)); this.connectionMapKey = hostAndPort; } diff --git a/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java b/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java index f2f0746460..dedf34fb69 100644 --- a/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java +++ b/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java @@ -19,6 +19,8 @@ import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisClientConfig; import redis.clients.jedis.JedisPubSub; +import redis.clients.jedis.annots.Experimental; +import redis.clients.jedis.csc.Cache; import redis.clients.jedis.exceptions.JedisConnectionException; import redis.clients.jedis.exceptions.JedisException; import redis.clients.jedis.util.IOUtils; @@ -37,6 +39,8 @@ public class SentineledConnectionProvider implements ConnectionProvider { private final JedisClientConfig masterClientConfig; + private final Cache clientSideCache; + private final GenericObjectPoolConfig masterPoolConfig; protected final Collection sentinelListeners = new ArrayList<>(); @@ -49,7 +53,13 @@ public class SentineledConnectionProvider implements ConnectionProvider { public SentineledConnectionProvider(String masterName, final JedisClientConfig masterClientConfig, Set sentinels, final JedisClientConfig sentinelClientConfig) { - this(masterName, masterClientConfig, /*poolConfig*/ null, sentinels, sentinelClientConfig); + this(masterName, masterClientConfig, null, null, sentinels, sentinelClientConfig); + } + + @Experimental + public SentineledConnectionProvider(String masterName, final JedisClientConfig masterClientConfig, + Cache clientSideCache, Set sentinels, final JedisClientConfig sentinelClientConfig) { + this(masterName, masterClientConfig, clientSideCache, null, sentinels, sentinelClientConfig); } public SentineledConnectionProvider(String masterName, final JedisClientConfig masterClientConfig, @@ -59,13 +69,30 @@ public SentineledConnectionProvider(String masterName, final JedisClientConfig m DEFAULT_SUBSCRIBE_RETRY_WAIT_TIME_MILLIS); } + @Experimental + public SentineledConnectionProvider(String masterName, final JedisClientConfig masterClientConfig, + Cache clientSideCache, final GenericObjectPoolConfig poolConfig, + Set sentinels, final JedisClientConfig sentinelClientConfig) { + this(masterName, masterClientConfig, clientSideCache, poolConfig, sentinels, sentinelClientConfig, + DEFAULT_SUBSCRIBE_RETRY_WAIT_TIME_MILLIS); + } + public SentineledConnectionProvider(String masterName, final JedisClientConfig masterClientConfig, final GenericObjectPoolConfig poolConfig, Set sentinels, final JedisClientConfig sentinelClientConfig, final long subscribeRetryWaitTimeMillis) { + this(masterName, masterClientConfig, null, poolConfig, sentinels, sentinelClientConfig, subscribeRetryWaitTimeMillis); + } + + @Experimental + public SentineledConnectionProvider(String masterName, final JedisClientConfig masterClientConfig, + Cache clientSideCache, final GenericObjectPoolConfig poolConfig, + Set sentinels, final JedisClientConfig sentinelClientConfig, + final long subscribeRetryWaitTimeMillis) { this.masterName = masterName; this.masterClientConfig = masterClientConfig; + this.clientSideCache = clientSideCache; this.masterPoolConfig = poolConfig; this.sentinelClientConfig = sentinelClientConfig; @@ -98,18 +125,19 @@ public HostAndPort getCurrentMaster() { private void initMaster(HostAndPort master) { initPoolLock.lock(); - + try { if (!master.equals(currentMaster)) { currentMaster = master; - ConnectionPool newPool = masterPoolConfig != null - ? new ConnectionPool(currentMaster, masterClientConfig, masterPoolConfig) - : new ConnectionPool(currentMaster, masterClientConfig); + ConnectionPool newPool = createNodePool(currentMaster); ConnectionPool existingPool = pool; pool = newPool; LOG.info("Created connection pool to master at {}.", master); + if (clientSideCache != null) { + clientSideCache.flush(); + } if (existingPool != null) { // although we clear the pool, we still have to check the returned object in getResource, @@ -123,6 +151,22 @@ private void initMaster(HostAndPort master) { } } + private ConnectionPool createNodePool(HostAndPort master) { + if (masterPoolConfig == null) { + if (clientSideCache == null) { + return new ConnectionPool(master, masterClientConfig); + } else { + return new ConnectionPool(master, masterClientConfig, clientSideCache); + } + } else { + if (clientSideCache == null) { + return new ConnectionPool(master, masterClientConfig, masterPoolConfig); + } else { + return new ConnectionPool(master, masterClientConfig, clientSideCache, masterPoolConfig); + } + } + } + private HostAndPort initSentinels(Set sentinels) { HostAndPort master = null; @@ -239,8 +283,8 @@ public void onMessage(String channel, String message) { initMaster(toHostAndPort(switchMasterMsg[3], switchMasterMsg[4])); } else { LOG.debug( - "Ignoring message on +switch-master for master {}. Our master is {}.", - switchMasterMsg[0], masterName); + "Ignoring message on +switch-master for master {}. Our master is {}.", + switchMasterMsg[0], masterName); } } else { diff --git a/src/main/java/redis/clients/jedis/util/JedisURIHelper.java b/src/main/java/redis/clients/jedis/util/JedisURIHelper.java index 6bbd1599a8..a565d38048 100644 --- a/src/main/java/redis/clients/jedis/util/JedisURIHelper.java +++ b/src/main/java/redis/clients/jedis/util/JedisURIHelper.java @@ -54,11 +54,12 @@ 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 (idx < 0) continue; + 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; diff --git a/src/main/java/redis/clients/jedis/util/RedisInputStream.java b/src/main/java/redis/clients/jedis/util/RedisInputStream.java index f7e320f164..5baf1b3225 100644 --- a/src/main/java/redis/clients/jedis/util/RedisInputStream.java +++ b/src/main/java/redis/clients/jedis/util/RedisInputStream.java @@ -15,6 +15,7 @@ import java.io.InputStream; import java.math.BigInteger; +import redis.clients.jedis.annots.Experimental; import redis.clients.jedis.exceptions.JedisConnectionException; /** @@ -44,6 +45,12 @@ public RedisInputStream(InputStream in) { this(in, INPUT_BUFFER_SIZE); } + @Experimental + public boolean peek(byte b) throws JedisConnectionException { + ensureFill(); // in current design, at least one reply is expected. so ensureFillSafe() is not necessary. + return buf[count] == b; + } + public byte readByte() throws JedisConnectionException { ensureFill(); return buf[count++]; @@ -177,9 +184,12 @@ public boolean readBooleanCrLf() { ensureCrLf(); switch (b) { - case 't': return true; - case 'f': return false; - default: throw new JedisConnectionException("Unexpected character!"); + case 't': + return true; + case 'f': + return false; + default: + throw new JedisConnectionException("Unexpected character!"); } } @@ -253,4 +263,12 @@ private void ensureFill() throws JedisConnectionException { } } } + + @Override + public int available() throws IOException { + int availableInBuf = limit - count; + int availableInSocket = this.in.available(); + return (availableInBuf > availableInSocket) ? availableInBuf : availableInSocket; + } + } diff --git a/src/test/java/redis/clients/jedis/JedisClusterTest.java b/src/test/java/redis/clients/jedis/JedisClusterTest.java index 6ebec4e73f..f3dfe630e5 100644 --- a/src/test/java/redis/clients/jedis/JedisClusterTest.java +++ b/src/test/java/redis/clients/jedis/JedisClusterTest.java @@ -108,16 +108,6 @@ public void testSetClientName() { try (JedisCluster jc = new JedisCluster(jedisClusterNode, DEFAULT_TIMEOUT, DEFAULT_TIMEOUT, DEFAULT_REDIRECTIONS, "cluster", clientName, DEFAULT_POOL_CONFIG)) { -// Map clusterNodes = jc.getClusterNodes(); -// Collection values = clusterNodes.values(); -// for (JedisPool jedisPool : values) { -// Jedis jedis = jedisPool.getResource(); -// try { -// assertEquals(clientName, jedis.clientGetname()); -// } finally { -// jedis.close(); -// } -// } for (Pool pool : jc.getClusterNodes().values()) { try (Jedis jedis = new Jedis(pool.getResource())) { assertEquals(clientName, jedis.clientGetname()); @@ -133,11 +123,6 @@ public void testSetClientNameWithConfig() { try (JedisCluster jc = new JedisCluster(Collections.singleton(hp), DefaultJedisClientConfig.builder().password("cluster").clientName(clientName).build(), DEFAULT_REDIRECTIONS, DEFAULT_POOL_CONFIG)) { -// jc.getClusterNodes().values().forEach(jedisPool -> { -// try (Jedis jedis = jedisPool.getResource()) { -// assertEquals(clientName, jedis.clientGetname()); -// } -// }); jc.getClusterNodes().values().forEach(pool -> { try (Jedis jedis = new Jedis(pool.getResource())) { assertEquals(clientName, jedis.clientGetname()); @@ -513,7 +498,6 @@ public void testStableSlotWhenMigratingNodeOrImportingNodeIsNotSpecified() } } -// @Test(expected = JedisExhaustedPoolException.class) @Test(expected = JedisException.class) public void testIfPoolConfigAppliesToClusterPools() { GenericObjectPoolConfig config = new GenericObjectPoolConfig<>(); @@ -560,12 +544,6 @@ public void testJedisClusterTimeout() { try (JedisCluster jc = new JedisCluster(jedisClusterNode, 4000, 4000, DEFAULT_REDIRECTIONS, "cluster", DEFAULT_POOL_CONFIG)) { -// for (JedisPool pool : jc.getClusterNodes().values()) { -// Jedis jedis = pool.getResource(); -// assertEquals(4000, jedis.getClient().getConnectionTimeout()); -// assertEquals(4000, jedis.getClient().getSoTimeout()); -// jedis.close(); -// } for (Pool pool : jc.getClusterNodes().values()) { try (Connection conn = pool.getResource()) { assertEquals(4000, conn.getSoTimeout()); @@ -582,10 +560,6 @@ public void testJedisClusterTimeoutWithConfig() { DEFAULT_REDIRECTIONS, DEFAULT_POOL_CONFIG)) { jc.getClusterNodes().values().forEach(pool -> { -// try (Jedis jedis = pool.getResource()) { -// assertEquals(4000, jedis.getClient().getConnectionTimeout()); -// assertEquals(4000, jedis.getClient().getSoTimeout()); -// } try (Connection conn = pool.getResource()) { assertEquals(4000, conn.getSoTimeout()); } @@ -633,10 +607,6 @@ public void testReturnConnectionOnJedisConnectionException() throws InterruptedE try (JedisCluster jc = new JedisCluster(jedisClusterNode, DEFAULT_TIMEOUT, DEFAULT_TIMEOUT, DEFAULT_REDIRECTIONS, "cluster", config)) { -// try (Jedis j = jc.getClusterNodes().get("127.0.0.1:7380").getResource()) { -// ClientKillerUtil.tagClient(j, "DEAD"); -// ClientKillerUtil.killClient(j, "DEAD"); -// } try (Connection c = jc.getClusterNodes().get("127.0.0.1:7380").getResource()) { Jedis j = new Jedis(c); ClientKillerUtil.tagClient(j, "DEAD"); @@ -674,7 +644,6 @@ public void testLocalhostNodeNotAddedWhen127Present() { try (JedisCluster jc = new JedisCluster(jedisClusterNode, DEFAULT_TIMEOUT, DEFAULT_TIMEOUT, DEFAULT_REDIRECTIONS, "cluster", config)) { -// Map clusterNodes = jc.getClusterNodes(); Map clusterNodes = jc.getClusterNodes(); assertEquals(3, clusterNodes.size()); assertFalse(clusterNodes.containsKey(JedisClusterInfoCache.getNodeKey(localhost))); @@ -691,7 +660,6 @@ public void testInvalidStartNodeNotAdded() { config.setMaxTotal(1); try (JedisCluster jc = new JedisCluster(jedisClusterNode, DEFAULT_TIMEOUT, DEFAULT_TIMEOUT, DEFAULT_REDIRECTIONS, "cluster", config)) { -// Map clusterNodes = jc.getClusterNodes(); Map clusterNodes = jc.getClusterNodes(); assertEquals(3, clusterNodes.size()); assertFalse(clusterNodes.containsKey(JedisClusterInfoCache.getNodeKey(invalidHost))); diff --git a/src/test/java/redis/clients/jedis/JedisClusterTestBase.java b/src/test/java/redis/clients/jedis/JedisClusterTestBase.java index 0746c2d37c..bb6656a812 100644 --- a/src/test/java/redis/clients/jedis/JedisClusterTestBase.java +++ b/src/test/java/redis/clients/jedis/JedisClusterTestBase.java @@ -15,11 +15,11 @@ public abstract class JedisClusterTestBase { protected static Jedis node4; protected static Jedis nodeSlave2; - protected HostAndPort nodeInfo1 = HostAndPorts.getClusterServers().get(0); - protected HostAndPort nodeInfo2 = HostAndPorts.getClusterServers().get(1); - protected HostAndPort nodeInfo3 = HostAndPorts.getClusterServers().get(2); - protected HostAndPort nodeInfo4 = HostAndPorts.getClusterServers().get(3); - protected HostAndPort nodeInfoSlave2 = HostAndPorts.getClusterServers().get(4); + protected static HostAndPort nodeInfo1 = HostAndPorts.getClusterServers().get(0); + protected static HostAndPort nodeInfo2 = HostAndPorts.getClusterServers().get(1); + protected static HostAndPort nodeInfo3 = HostAndPorts.getClusterServers().get(2); + protected static HostAndPort nodeInfo4 = HostAndPorts.getClusterServers().get(3); + protected static HostAndPort nodeInfoSlave2 = HostAndPorts.getClusterServers().get(4); protected static final String LOCAL_IP = "127.0.0.1"; diff --git a/src/test/java/redis/clients/jedis/SSLJedisTest.java b/src/test/java/redis/clients/jedis/SSLJedisTest.java index d3a5853f42..4ef4f969bb 100644 --- a/src/test/java/redis/clients/jedis/SSLJedisTest.java +++ b/src/test/java/redis/clients/jedis/SSLJedisTest.java @@ -32,7 +32,7 @@ public static void prepare() { setupTrustStore(); } - static void setupTrustStore() { + public static void setupTrustStore() { setJvmTrustStore("src/test/resources/truststore.jceks", "jceks"); } diff --git a/src/test/java/redis/clients/jedis/benchmark/CSCPooleadBenchmark.java b/src/test/java/redis/clients/jedis/benchmark/CSCPooleadBenchmark.java new file mode 100644 index 0000000000..8ee0580011 --- /dev/null +++ b/src/test/java/redis/clients/jedis/benchmark/CSCPooleadBenchmark.java @@ -0,0 +1,79 @@ +package redis.clients.jedis.benchmark; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import redis.clients.jedis.*; +import redis.clients.jedis.csc.Cache; +import redis.clients.jedis.csc.TestCache; + +public class CSCPooleadBenchmark { + + private static EndpointConfig endpoint = HostAndPorts.getRedisEndpoint("standalone0"); + private static final int TOTAL_OPERATIONS = 1000000; + private static final int NUMBER_OF_THREADS = 50; + + public static void main(String[] args) throws Exception { + + try (Jedis j = new Jedis(endpoint.getHost(), endpoint.getPort())) { + j.auth(endpoint.getPassword()); + j.flushAll(); + j.disconnect(); + } + + int totalRounds = 50; + long withoutCache = 0; + long withCache = 0; + + for (int i = 0; i < totalRounds; i++) { + withoutCache += runBenchmark(null); + withCache += runBenchmark(new TestCache()); + } + for (int i = 0; i < totalRounds; i++) { + } + System.out.println(String.format("after %d rounds withoutCache: %d ms, withCache: %d ms", totalRounds, + withoutCache, withCache)); + System.out.println("execution time ratio: " + (double) withCache / withoutCache); + } + + private static long runBenchmark(Cache cache) throws Exception { + long start = System.currentTimeMillis(); + withPool(cache); + long elapsed = System.currentTimeMillis() - start; + System.out.println(String.format("%s round elapsed: %d ms", cache == null ? "no cache" : "cached", elapsed)); + return elapsed; + } + + private static void withPool(Cache cache) throws Exception { + JedisClientConfig config = DefaultJedisClientConfig.builder().protocol(RedisProtocol.RESP3) + .password(endpoint.getPassword()).build(); + List tds = new ArrayList<>(); + final AtomicInteger ind = new AtomicInteger(); + try (JedisPooled jedis = new JedisPooled(endpoint.getHostAndPort(), config, cache)) { + for (int i = 0; i < NUMBER_OF_THREADS; i++) { + Thread hj = new Thread(new Runnable() { + @Override + public void run() { + for (int i = 0; (i = ind.getAndIncrement()) < TOTAL_OPERATIONS;) { + try { + final String key = "foo" + i; + jedis.set(key, key); + jedis.get(key); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + } + } + }); + tds.add(hj); + hj.start(); + } + + for (Thread t : tds) { + t.join(); + } + } + } +} diff --git a/src/test/java/redis/clients/jedis/commands/jedis/TransactionCommandsTest.java b/src/test/java/redis/clients/jedis/commands/jedis/TransactionCommandsTest.java index f7ab13df25..f6bedc1741 100644 --- a/src/test/java/redis/clients/jedis/commands/jedis/TransactionCommandsTest.java +++ b/src/test/java/redis/clients/jedis/commands/jedis/TransactionCommandsTest.java @@ -1,8 +1,8 @@ package redis.clients.jedis.commands.jedis; import static org.junit.Assert.*; - import static org.mockito.ArgumentMatchers.any; + import static redis.clients.jedis.Protocol.Command.INCR; import static redis.clients.jedis.Protocol.Command.GET; import static redis.clients.jedis.Protocol.Command.SET; diff --git a/src/test/java/redis/clients/jedis/csc/AllowAndDenyListCacheableTest.java b/src/test/java/redis/clients/jedis/csc/AllowAndDenyListCacheableTest.java new file mode 100644 index 0000000000..a0b7d68381 --- /dev/null +++ b/src/test/java/redis/clients/jedis/csc/AllowAndDenyListCacheableTest.java @@ -0,0 +1,79 @@ +package redis.clients.jedis.csc; + +import static java.util.Collections.singleton; +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import redis.clients.jedis.JedisPooled; +import redis.clients.jedis.Protocol; +import redis.clients.jedis.csc.util.AllowAndDenyListWithStringKeys; + +public class AllowAndDenyListCacheableTest extends ClientSideCacheTestBase { + + private static CacheConfig createConfig(Cacheable cacheable) { + return CacheConfig.builder().cacheable(cacheable).cacheClass(TestCache.class).build(); + } + + @Test + public void none() { + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), + createConfig(new AllowAndDenyListWithStringKeys(null, null, null, null)), singleConnectionPoolConfig.get())) { + Cache cache = jedis.getCache(); + control.set("foo", "bar"); + assertEquals(0, cache.getSize()); + assertEquals("bar", jedis.get("foo")); + assertEquals(1, cache.getSize()); + } + } + + @Test + public void whiteListCommand() { + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), + createConfig(new AllowAndDenyListWithStringKeys(singleton(Protocol.Command.GET), null, null, null)), + singleConnectionPoolConfig.get())) { + Cache cache = jedis.getCache(); + control.set("foo", "bar"); + assertEquals(0, cache.getSize()); + assertEquals("bar", jedis.get("foo")); + assertEquals(1, cache.getSize()); + } + } + + @Test + public void blackListCommand() { + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), + createConfig(new AllowAndDenyListWithStringKeys(null, singleton(Protocol.Command.GET), null, null)), + singleConnectionPoolConfig.get())) { + Cache cache = jedis.getCache(); + control.set("foo", "bar"); + assertEquals(0, cache.getSize()); + assertEquals("bar", jedis.get("foo")); + assertEquals(0, cache.getSize()); + } + } + + @Test + public void whiteListKey() { + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), + createConfig(new AllowAndDenyListWithStringKeys(null, null, singleton("foo"), null)), singleConnectionPoolConfig.get())) { + control.set("foo", "bar"); + Cache cache = jedis.getCache(); + assertEquals(0, cache.getSize()); + assertEquals("bar", jedis.get("foo")); + assertEquals(1, cache.getSize()); + } + } + + @Test + public void blackListKey() { + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), + createConfig(new AllowAndDenyListWithStringKeys(null, null, null, singleton("foo"))), singleConnectionPoolConfig.get())) { + Cache cache = jedis.getCache(); + control.set("foo", "bar"); + assertEquals(0, cache.getSize()); + assertEquals("bar", jedis.get("foo")); + assertEquals(0, cache.getSize()); + } + } +} diff --git a/src/test/java/redis/clients/jedis/csc/ClientSideCacheFunctionalityTest.java b/src/test/java/redis/clients/jedis/csc/ClientSideCacheFunctionalityTest.java new file mode 100644 index 0000000000..d2032e5d23 --- /dev/null +++ b/src/test/java/redis/clients/jedis/csc/ClientSideCacheFunctionalityTest.java @@ -0,0 +1,572 @@ +package redis.clients.jedis.csc; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +import org.hamcrest.Matchers; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import redis.clients.jedis.CommandObjects; +import redis.clients.jedis.JedisPooled; +import redis.clients.jedis.UnifiedJedis; + +public class ClientSideCacheFunctionalityTest extends ClientSideCacheTestBase { + + @Test // T.5.1 + public void flushAllTest() { + final int count = 100; + for (int i = 0; i < count; i++) { + control.set("k" + i, "v" + i); + } + + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), CacheConfig.builder().build())) { + Cache cache = jedis.getCache(); + for (int i = 0; i < count; i++) { + jedis.get("k" + i); + } + + assertEquals(count, cache.getSize()); + cache.flush(); + assertEquals(0, cache.getSize()); + } + } + + @Test // T.4.1 + public void lruEvictionTest() { + final int count = 100; + final int extra = 10; + + // Add 100 + 10 keys to Redis + for (int i = 0; i < count + extra; i++) { + control.set("key:" + i, "value" + i); + } + + Map map = new LinkedHashMap<>(count); + Cache cache = new DefaultCache(count, map); + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), cache)) { + + // Retrieve the 100 keys in the same order + for (int i = 0; i < count; i++) { + jedis.get("key:" + i); + } + assertThat(map, aMapWithSize(count)); + + List earlierKeys = new ArrayList<>(map.keySet()).subList(0, extra); + // earlier keys in map + earlierKeys.forEach(cacheKey -> assertThat(map, Matchers.hasKey(cacheKey))); + + // Retrieve the 10 extra keys + for (int i = count; i < count + extra; i++) { + jedis.get("key:" + i); + } + + // earlier keys NOT in map + earlierKeys.forEach(cacheKey -> assertThat(map, Matchers.not(Matchers.hasKey(cacheKey)))); + assertThat(map, aMapWithSize(count)); + } + } + + @Test // T.5.2 + public void deleteByKeyUsingMGetTest() { + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), CacheConfig.builder().build())) { + Cache clientSideCache = jedis.getCache(); + + jedis.set("1", "one"); + jedis.set("2", "two"); + + assertEquals(Arrays.asList("one", "two"), jedis.mget("1", "2")); + assertEquals(1, clientSideCache.getSize()); + + assertThat(clientSideCache.deleteByRedisKey("1"), hasSize(1)); + assertEquals(0, clientSideCache.getSize()); + } + } + + @Test // T.5.2 + public void deleteByKeyTest() { + final int count = 100; + for (int i = 0; i < count; i++) { + control.set("k" + i, "v" + i); + } + + // By using LinkedHashMap, we can get the hashes (map keys) at the same order of the actual keys. + LinkedHashMap map = new LinkedHashMap<>(); + Cache clientSideCache = new TestCache(map); + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), clientSideCache)) { + for (int i = 0; i < count; i++) { + jedis.get("k" + i); + } + assertThat(map, aMapWithSize(count)); + + ArrayList cacheKeys = new ArrayList<>(map.keySet()); + for (int i = 0; i < count; i++) { + String key = "k" + i; + CacheKey cacheKey = cacheKeys.get(i); + assertTrue(map.containsKey(cacheKey)); + assertThat(clientSideCache.deleteByRedisKey(key), hasSize(1)); + assertFalse(map.containsKey(cacheKey)); + assertThat(map, aMapWithSize(count - i - 1)); + } + assertThat(map, aMapWithSize(0)); + } + } + + @Test // T.5.2 + public void deleteByKeysTest() { + final int count = 100; + final int delete = 10; + + for (int i = 0; i < count; i++) { + control.set("k" + i, "v" + i); + } + + // By using LinkedHashMap, we can get the hashes (map keys) at the same order of the actual keys. + LinkedHashMap map = new LinkedHashMap<>(); + Cache clientSideCache = new TestCache(map); + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), clientSideCache)) { + for (int i = 0; i < count; i++) { + jedis.get("k" + i); + } + assertThat(map, aMapWithSize(count)); + + List keysToDelete = new ArrayList<>(delete); + for (int i = 0; i < delete; i++) { + String key = "k" + i; + keysToDelete.add(key); + } + assertThat(clientSideCache.deleteByRedisKeys(keysToDelete), hasSize(delete)); + assertThat(map, aMapWithSize(count - delete)); + } + } + + @Test // T.5.3 + public void deleteByEntryTest() { + final int count = 100; + for (int i = 0; i < count; i++) { + control.set("k" + i, "v" + i); + } + + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), CacheConfig.builder().build())) { + Cache cache = jedis.getCache(); + for (int i = 0; i < count; i++) { + jedis.get("k" + i); + } + assertEquals(count, cache.getSize()); + + List cacheKeys = new ArrayList<>(cache.getCacheEntries()); + for (int i = 0; i < count; i++) { + CacheKey cacheKey = cacheKeys.get(i).getCacheKey(); + assertTrue(cache.delete(cacheKey)); + assertFalse(cache.hasCacheKey(cacheKey)); + assertEquals(count - i - 1, cache.getSize()); + } + } + } + + @Test // T.5.3 + public void deleteByEntriesTest() { + final int count = 100; + final int delete = 10; + for (int i = 0; i < count; i++) { + control.set("k" + i, "v" + i); + } + + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), CacheConfig.builder().build())) { + Cache cache = jedis.getCache(); + for (int i = 0; i < count; i++) { + jedis.get("k" + i); + } + assertEquals(count, cache.getSize()); + + List cacheKeysToDelete = new ArrayList<>(cache.getCacheEntries()).subList(0, delete).stream().map(e -> e.getCacheKey()) + .collect(Collectors.toList()); + List isDeleted = cache.delete(cacheKeysToDelete); + assertThat(isDeleted, hasSize(delete)); + isDeleted.forEach(Assert::assertTrue); + assertEquals(count - delete, cache.getSize()); + } + } + + @Test + public void multiKeyOperation() { + control.set("k1", "v1"); + control.set("k2", "v2"); + + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), CacheConfig.builder().build())) { + jedis.mget("k1", "k2"); + assertEquals(1, jedis.getCache().getSize()); + } + } + + @Test + public void maximumSizeExact() { + control.set("k1", "v1"); + control.set("k2", "v2"); + + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), CacheConfig.builder().maxSize(1).build())) { + Cache cache = jedis.getCache(); + assertEquals(0, cache.getSize()); + jedis.get("k1"); + assertEquals(1, cache.getSize()); + assertEquals(0, cache.getStats().getEvictCount()); + jedis.get("k2"); + assertEquals(1, cache.getSize()); + assertEquals(1, cache.getStats().getEvictCount()); + } + } + + @Test + public void testInvalidationWithUnifiedJedis() { + Cache cache = new TestCache(); + Cache mock = Mockito.spy(cache); + UnifiedJedis client = new UnifiedJedis(hnp, clientConfig.get(), mock); + UnifiedJedis controlClient = new UnifiedJedis(hnp, clientConfig.get()); + + try { + // "foo" is cached + client.set("foo", "bar"); + client.get("foo"); // read from the server + Assert.assertEquals("bar", client.get("foo")); // cache hit + + // Using another connection + controlClient.set("foo", "bar2"); + Assert.assertEquals("bar2", controlClient.get("foo")); + + //invalidating the cache and read it back from server + Assert.assertEquals("bar2", client.get("foo")); + + Mockito.verify(mock, Mockito.times(1)).deleteByRedisKeys(Mockito.anyList()); + Mockito.verify(mock, Mockito.times(2)).set(Mockito.any(CacheKey.class), Mockito.any(CacheEntry.class)); + } finally { + client.close(); + controlClient.close(); + } + } + + @Test + public void differentInstanceOnEachCacheHit() { + + // fill the cache for maxSize + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), CacheConfig.builder().build())) { + Cache cache = jedis.getCache(); + jedis.sadd("foo", "a"); + jedis.sadd("foo", "b"); + + Set expected = new HashSet<>(); + expected.add("a"); + expected.add("b"); + + Set members1 = jedis.smembers("foo"); + Set members2 = jedis.smembers("foo"); + + Set fromMap = (Set) cache.get(new CacheKey<>(new CommandObjects().smembers("foo"))).getValue(); + assertEquals(expected, members1); + assertEquals(expected, members2); + assertEquals(expected, fromMap); + assertTrue(members1 != members2); + assertTrue(members1 != fromMap); + } + } + + @Test + public void testSequentialAccess() throws InterruptedException { + int threadCount = 10; + int iterations = 10000; + + control.set("foo", "0"); + + ReentrantLock lock = new ReentrantLock(true); + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + + CacheConfig cacheConfig = CacheConfig.builder().maxSize(1000).build(); + try (JedisPooled jedis = new JedisPooled(endpoint.getHostAndPort(), clientConfig.get(), cacheConfig)) { + // Submit multiple threads to perform concurrent operations + CountDownLatch latch = new CountDownLatch(threadCount); + for (int i = 0; i < threadCount; i++) { + executorService.submit(() -> { + try { + for (int j = 0; j < iterations; j++) { + lock.lock(); + try { + // Simulate continious get and update operations and consume invalidation events meanwhile + assertEquals(control.get("foo"), jedis.get("foo")); + Integer value = new Integer(jedis.get("foo")); + assertEquals("OK", jedis.set("foo", (++value).toString())); + } finally { + lock.unlock(); + } + } + } finally { + latch.countDown(); + } + }); + } + + // wait for all threads to complete + latch.await(); + } + + executorService.shutdownNow(); + + // Verify the final value of "foo" in Redis + String finalValue = control.get("foo"); + assertEquals(threadCount * iterations, Integer.parseInt(finalValue)); + } + + @Test + public void testConcurrentAccessWithStats() throws InterruptedException { + int threadCount = 10; + int iterations = 10000; + + control.set("foo", "0"); + + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + + // Create the shared mock instance of cache + try (JedisPooled jedis = new JedisPooled(endpoint.getHostAndPort(), clientConfig.get(), CacheConfig.builder().build())) { + Cache cache = jedis.getCache(); + // Submit multiple threads to perform concurrent operations + CountDownLatch latch = new CountDownLatch(threadCount); + for (int i = 0; i < threadCount; i++) { + executorService.submit(() -> { + try { + for (int j = 0; j < iterations; j++) { + // Simulate continious get and update operations and consume invalidation events meanwhile + Integer value = new Integer(jedis.get("foo")) + 1; + assertEquals("OK", jedis.set("foo", value.toString())); + } + } finally { + latch.countDown(); + } + }); + } + + // wait for all threads to complete + latch.await(); + + executorService.shutdownNow(); + + CacheStats stats = cache.getStats(); + assertEquals(threadCount * iterations, stats.getMissCount() + stats.getHitCount()); + assertEquals(stats.getMissCount(), stats.getLoadCount()); + } + } + + @Test + public void testMaxSize() throws InterruptedException { + int threadCount = 10; + int iterations = 11000; + int maxSize = 1000; + + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + + try (JedisPooled jedis = new JedisPooled(endpoint.getHostAndPort(), clientConfig.get(), CacheConfig.builder().maxSize(maxSize).build())) { + Cache testCache = jedis.getCache(); + // Submit multiple threads to perform concurrent operations + CountDownLatch latch = new CountDownLatch(threadCount); + for (int i = 0; i < threadCount; i++) { + executorService.submit(() -> { + try { + for (int j = 0; j < iterations; j++) { + // Simulate continious get and update operations and consume invalidation events meanwhile + assertEquals("OK", jedis.set("foo" + j, "foo" + j)); + jedis.get("foo" + j); + } + } finally { + latch.countDown(); + } + }); + } + + // wait for all threads to complete + latch.await(); + + executorService.shutdownNow(); + + CacheStats stats = testCache.getStats(); + + assertEquals(threadCount * iterations, stats.getMissCount() + stats.getHitCount()); + assertEquals(stats.getMissCount(), stats.getLoadCount()); + assertEquals(threadCount * iterations, stats.getNonCacheableCount()); + assertTrue(maxSize >= testCache.getSize()); + } + } + + @Test + public void testEvictionPolicy() throws InterruptedException { + int maxSize = 100; + int expectedEvictions = 20; + int touchOffset = 10; + + // fill the cache for maxSize + try (JedisPooled jedis = new JedisPooled(endpoint.getHostAndPort(), clientConfig.get(), + CacheConfig.builder().maxSize(maxSize).build())) { + Cache cache = jedis.getCache(); + for (int i = 0; i < maxSize; i++) { + jedis.set("foo" + i, "bar" + i); + assertEquals("bar" + i, jedis.get("foo" + i)); + } + + // touch a set of keys to prevent from eviction from index 10 to 29 + for (int i = touchOffset; i < touchOffset + expectedEvictions; i++) { + assertEquals("bar" + i, jedis.get("foo" + i)); + } + + // add more keys to trigger eviction, adding from 100 to 119 + for (int i = maxSize; i < maxSize + expectedEvictions; i++) { + jedis.set("foo" + i, "bar" + i); + assertEquals("bar" + i, jedis.get("foo" + i)); + } + + // check touched keys not evicted + for (int i = touchOffset; i < touchOffset + expectedEvictions; i++) { + assertTrue(cache.hasCacheKey(new CacheKey(new CommandObjects().get("foo" + i)))); + } + + // check expected evictions are done till the offset + for (int i = 0; i < touchOffset; i++) { + assertTrue(!cache.hasCacheKey(new CacheKey(new CommandObjects().get("foo" + i)))); + } + + /// check expected evictions are done after the touched keys + for (int i = touchOffset + expectedEvictions; i < (2 * expectedEvictions); i++) { + assertTrue(!cache.hasCacheKey(new CacheKey(new CommandObjects().get("foo" + i)))); + } + + assertEquals(maxSize, cache.getSize()); + } + } + + @Test + public void testEvictionPolicyMultithreaded() throws InterruptedException { + int NUMBER_OF_THREADS = 100; + int TOTAL_OPERATIONS = 1000000; + int NUMBER_OF_DISTINCT_KEYS = 53; + int MAX_SIZE = 20; + List exceptions = new ArrayList<>(); + + List tds = new ArrayList<>(); + final AtomicInteger ind = new AtomicInteger(); + try (JedisPooled jedis = new JedisPooled(endpoint.getHostAndPort(), clientConfig.get(), + CacheConfig.builder().maxSize(MAX_SIZE).build())) { + Cache cache = jedis.getCache(); + for (int i = 0; i < NUMBER_OF_THREADS; i++) { + Thread hj = new Thread(new Runnable() { + @Override + public void run() { + for (int i = 0; (i = ind.getAndIncrement()) < TOTAL_OPERATIONS;) { + try { + final String key = "foo" + i % NUMBER_OF_DISTINCT_KEYS; + if (i < NUMBER_OF_DISTINCT_KEYS) { + jedis.set(key, key); + } + jedis.get(key); + } catch (Exception e) { + exceptions.add(e); + throw e; + } + } + } + }); + tds.add(hj); + hj.start(); + } + + for (Thread t : tds) { + t.join(); + } + + assertEquals(MAX_SIZE, cache.getSize()); + assertEquals(0, exceptions.size()); + } + } + + @Test + public void testNullValue() throws InterruptedException { + int MAX_SIZE = 20; + String nonExisting = "non-existing-key"; + control.del(nonExisting); + + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), CacheConfig.builder().maxSize(MAX_SIZE).build())) { + Cache cache = jedis.getCache(); + CacheStats stats = cache.getStats(); + + String val = jedis.get(nonExisting); + assertNull(val); + assertEquals(1, cache.getSize()); + assertEquals(0, stats.getHitCount()); + assertEquals(1, stats.getMissCount()); + + val = jedis.get(nonExisting); + assertNull(val); + assertEquals(1, cache.getSize()); + assertNull(cache.getCacheEntries().iterator().next().getValue()); + assertEquals(1, stats.getHitCount()); + assertEquals(1, stats.getMissCount()); + + control.set(nonExisting, "bar"); + val = jedis.get(nonExisting); + assertEquals("bar", val); + assertEquals(1, cache.getSize()); + assertEquals("bar", cache.getCacheEntries().iterator().next().getValue()); + assertEquals(1, stats.getHitCount()); + assertEquals(2, stats.getMissCount()); + } + } + + @Test + public void testCacheFactory() throws InterruptedException { + // this checks the instantiation with parameters (int, EvictionPolicy, Cacheable) + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), CacheConfig.builder().cacheClass(TestCache.class).build())) { + Cache cache = jedis.getCache(); + CacheStats stats = cache.getStats(); + + String val = jedis.get("foo"); + val = jedis.get("foo"); + assertNull(val); + assertEquals(1, cache.getSize()); + assertNull(cache.getCacheEntries().iterator().next().getValue()); + assertEquals(1, stats.getHitCount()); + assertEquals(1, stats.getMissCount()); + } + + // this checks the instantiation with parameters (int, EvictionPolicy) + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), + CacheConfig.builder().cacheClass(TestCache.class).cacheable(null).build())) { + Cache cache = jedis.getCache(); + CacheStats stats = cache.getStats(); + + String val = jedis.get("foo"); + val = jedis.get("foo"); + assertNull(val); + assertEquals(1, cache.getSize()); + assertNull(cache.getCacheEntries().iterator().next().getValue()); + assertEquals(1, stats.getHitCount()); + assertEquals(1, stats.getMissCount()); + } + } +} diff --git a/src/test/java/redis/clients/jedis/csc/ClientSideCacheTestBase.java b/src/test/java/redis/clients/jedis/csc/ClientSideCacheTestBase.java new file mode 100644 index 0000000000..db53b085be --- /dev/null +++ b/src/test/java/redis/clients/jedis/csc/ClientSideCacheTestBase.java @@ -0,0 +1,43 @@ +package redis.clients.jedis.csc; + +import java.util.function.Supplier; +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import org.junit.After; +import org.junit.Before; + +import redis.clients.jedis.Connection; +import redis.clients.jedis.ConnectionPoolConfig; +import redis.clients.jedis.EndpointConfig; +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.HostAndPorts; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisClientConfig; + +abstract class ClientSideCacheTestBase { + + protected static final EndpointConfig endpoint = HostAndPorts.getRedisEndpoint("standalone1"); + + protected static final HostAndPort hnp = endpoint.getHostAndPort(); + + protected Jedis control; + + @Before + public void setUp() throws Exception { + control = new Jedis(hnp, endpoint.getClientConfigBuilder().build()); + control.flushAll(); + } + + @After + public void tearDown() throws Exception { + control.close(); + } + + protected static final Supplier clientConfig = () -> endpoint.getClientConfigBuilder().resp3().build(); + + protected static final Supplier> singleConnectionPoolConfig = () -> { + ConnectionPoolConfig poolConfig = new ConnectionPoolConfig(); + poolConfig.setMaxTotal(1); + return poolConfig; + }; + +} diff --git a/src/test/java/redis/clients/jedis/csc/JedisClusterClientSideCacheTest.java b/src/test/java/redis/clients/jedis/csc/JedisClusterClientSideCacheTest.java new file mode 100644 index 0000000000..89114d154f --- /dev/null +++ b/src/test/java/redis/clients/jedis/csc/JedisClusterClientSideCacheTest.java @@ -0,0 +1,41 @@ +package redis.clients.jedis.csc; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.Supplier; + +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; + +import redis.clients.jedis.Connection; +import redis.clients.jedis.ConnectionPoolConfig; +import redis.clients.jedis.DefaultJedisClientConfig; +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.HostAndPorts; +import redis.clients.jedis.JedisClientConfig; +import redis.clients.jedis.JedisCluster; + +public class JedisClusterClientSideCacheTest extends UnifiedJedisClientSideCacheTestBase { + + private static final Set hnp = new HashSet<>(HostAndPorts.getStableClusterServers()); + + private static final Supplier clientConfig + = () -> DefaultJedisClientConfig.builder().resp3().password("cluster").build(); + + private static final Supplier> singleConnectionPoolConfig + = () -> { + ConnectionPoolConfig poolConfig = new ConnectionPoolConfig(); + poolConfig.setMaxTotal(1); + return poolConfig; + }; + + @Override + protected JedisCluster createRegularJedis() { + return new JedisCluster(hnp, clientConfig.get()); + } + + @Override + protected JedisCluster createCachedJedis(CacheConfig cacheConfig) { + return new JedisCluster(hnp, clientConfig.get(), cacheConfig); + } + +} diff --git a/src/test/java/redis/clients/jedis/csc/JedisPooledClientSideCacheTest.java b/src/test/java/redis/clients/jedis/csc/JedisPooledClientSideCacheTest.java new file mode 100644 index 0000000000..d7b2bd4989 --- /dev/null +++ b/src/test/java/redis/clients/jedis/csc/JedisPooledClientSideCacheTest.java @@ -0,0 +1,13 @@ +package redis.clients.jedis.csc; + +import org.junit.BeforeClass; +import redis.clients.jedis.HostAndPorts; + +public class JedisPooledClientSideCacheTest extends JedisPooledClientSideCacheTestBase { + + @BeforeClass + public static void prepare() { + endpoint = HostAndPorts.getRedisEndpoint("standalone1"); + } + +} diff --git a/src/test/java/redis/clients/jedis/csc/JedisPooledClientSideCacheTestBase.java b/src/test/java/redis/clients/jedis/csc/JedisPooledClientSideCacheTestBase.java new file mode 100644 index 0000000000..133efcb3fc --- /dev/null +++ b/src/test/java/redis/clients/jedis/csc/JedisPooledClientSideCacheTestBase.java @@ -0,0 +1,56 @@ +package redis.clients.jedis.csc; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import redis.clients.jedis.EndpointConfig; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPooled; +import redis.clients.jedis.args.ClientType; +import redis.clients.jedis.exceptions.JedisConnectionException; +import redis.clients.jedis.params.ClientKillParams; + +public abstract class JedisPooledClientSideCacheTestBase extends UnifiedJedisClientSideCacheTestBase { + + protected static EndpointConfig endpoint; + + @Override + protected JedisPooled createRegularJedis() { + return new JedisPooled(endpoint.getHostAndPort(), endpoint.getClientConfigBuilder().build()); + } + + @Override + protected JedisPooled createCachedJedis(CacheConfig cacheConfig) { + return new JedisPooled(endpoint.getHostAndPort(), endpoint.getClientConfigBuilder().resp3().build(), cacheConfig); + } + + @Test + public void clearIfOneDiesTest() { + try (JedisPooled jedis = createCachedJedis(CacheConfig.builder().build())) { + Cache cache = jedis.getCache(); + // Create 100 keys + for (int i = 0; i < 100; i++) { + jedis.set("key" + i, "value" + i); + } + assertEquals(0, cache.getSize()); + + // Get 100 keys into the cache + for (int i = 0; i < 100; i++) { + jedis.get("key" + i); + } + assertEquals(100, cache.getSize()); + + try (Jedis killer = new Jedis(endpoint.getHostAndPort(), endpoint.getClientConfigBuilder().build())) { + killer.clientKill(ClientKillParams.clientKillParams().type(ClientType.NORMAL).skipMe(ClientKillParams.SkipMe.YES)); + } + + try { + jedis.get("foo"); + } catch (JedisConnectionException jce) { + // expected + } + assertEquals(0, cache.getSize()); + } + } +} diff --git a/src/test/java/redis/clients/jedis/csc/JedisSentineledClientSideCacheTest.java b/src/test/java/redis/clients/jedis/csc/JedisSentineledClientSideCacheTest.java new file mode 100644 index 0000000000..82da0b14af --- /dev/null +++ b/src/test/java/redis/clients/jedis/csc/JedisSentineledClientSideCacheTest.java @@ -0,0 +1,36 @@ +package redis.clients.jedis.csc; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import redis.clients.jedis.DefaultJedisClientConfig; +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.HostAndPorts; +import redis.clients.jedis.JedisClientConfig; +import redis.clients.jedis.JedisSentineled; + +public class JedisSentineledClientSideCacheTest extends UnifiedJedisClientSideCacheTestBase { + + private static final String MASTER_NAME = "mymaster"; + + protected static final HostAndPort sentinel1 = HostAndPorts.getSentinelServers().get(1); + protected static final HostAndPort sentinel2 = HostAndPorts.getSentinelServers().get(3); + + private static final Set sentinels = new HashSet<>(Arrays.asList(sentinel1, sentinel2)); + + private static final JedisClientConfig masterClientConfig = DefaultJedisClientConfig.builder().resp3().password("foobared").build(); + + private static final JedisClientConfig sentinelClientConfig = DefaultJedisClientConfig.builder().resp3().build(); + + @Override + protected JedisSentineled createRegularJedis() { + return new JedisSentineled(MASTER_NAME, masterClientConfig, sentinels, sentinelClientConfig); + } + + @Override + protected JedisSentineled createCachedJedis(CacheConfig cacheConfig) { + return new JedisSentineled(MASTER_NAME, masterClientConfig, cacheConfig, sentinels, sentinelClientConfig); + } + +} diff --git a/src/test/java/redis/clients/jedis/csc/SSLJedisPooledClientSideCacheTest.java b/src/test/java/redis/clients/jedis/csc/SSLJedisPooledClientSideCacheTest.java new file mode 100644 index 0000000000..b8df3910cd --- /dev/null +++ b/src/test/java/redis/clients/jedis/csc/SSLJedisPooledClientSideCacheTest.java @@ -0,0 +1,16 @@ +package redis.clients.jedis.csc; + +import org.junit.BeforeClass; +import redis.clients.jedis.HostAndPorts; +import redis.clients.jedis.SSLJedisTest; + +public class SSLJedisPooledClientSideCacheTest extends JedisPooledClientSideCacheTestBase { + + @BeforeClass + public static void prepare() { + SSLJedisTest.setupTrustStore(); + + endpoint = HostAndPorts.getRedisEndpoint("standalone0-tls"); + } + +} diff --git a/src/test/java/redis/clients/jedis/csc/TestCache.java b/src/test/java/redis/clients/jedis/csc/TestCache.java new file mode 100644 index 0000000000..0c9db2dbba --- /dev/null +++ b/src/test/java/redis/clients/jedis/csc/TestCache.java @@ -0,0 +1,28 @@ +package redis.clients.jedis.csc; + +import java.util.HashMap; +import java.util.Map; + +public class TestCache extends DefaultCache { + + public TestCache() { + this(new HashMap()); + } + + public TestCache(Map map) { + super(10000, map); + } + + public TestCache(Map map, Cacheable cacheable) { + super(10000, map, cacheable, new LRUEviction(10000)); + } + + public TestCache(int maximumSize, EvictionPolicy evictionPolicy ) { + super(maximumSize, new HashMap(), DefaultCacheable.INSTANCE, evictionPolicy); + } + + public TestCache(int maximumSize, EvictionPolicy evictionPolicy, Cacheable cacheable ) { + super(maximumSize, new HashMap(), cacheable, evictionPolicy); + } + +} diff --git a/src/test/java/redis/clients/jedis/csc/UnifiedJedisClientSideCacheTestBase.java b/src/test/java/redis/clients/jedis/csc/UnifiedJedisClientSideCacheTestBase.java new file mode 100644 index 0000000000..388113307b --- /dev/null +++ b/src/test/java/redis/clients/jedis/csc/UnifiedJedisClientSideCacheTestBase.java @@ -0,0 +1,223 @@ +package redis.clients.jedis.csc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; + +import java.util.Arrays; +import java.util.List; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import redis.clients.jedis.UnifiedJedis; + +public abstract class UnifiedJedisClientSideCacheTestBase { + + protected UnifiedJedis control; + + protected abstract UnifiedJedis createRegularJedis(); + + protected abstract UnifiedJedis createCachedJedis(CacheConfig cacheConfig); + + @Before + public void setUp() throws Exception { + control = createRegularJedis(); + control.flushAll(); + } + + @After + public void tearDown() throws Exception { + control.close(); + } + + @Test + public void simple() { + CacheConfig cacheConfig = CacheConfig.builder().maxSize(1000).build(); + try (UnifiedJedis jedis = createCachedJedis(cacheConfig)) { + control.set("foo", "bar"); + assertEquals("bar", jedis.get("foo")); + control.del("foo"); + assertNull(jedis.get("foo")); + } + } + + @Test + public void simpleWithSimpleMap() { + try (UnifiedJedis jedis = createCachedJedis(CacheConfig.builder().build())) { + Cache cache = jedis.getCache(); + control.set("foo", "bar"); + assertEquals(0, cache.getSize()); + assertEquals("bar", jedis.get("foo")); + assertEquals(1, cache.getSize()); + control.del("foo"); + assertEquals(1, cache.getSize()); + assertNull(jedis.get("foo")); + assertEquals(1, cache.getSize()); + assertNull(jedis.get("foo")); + assertEquals(1, cache.getSize()); + } + } + + @Test + public void flushAll() { + CacheConfig cacheConfig = CacheConfig.builder().maxSize(1000).build(); + try (UnifiedJedis jedis = createCachedJedis(cacheConfig)) { + control.set("foo", "bar"); + assertEquals("bar", jedis.get("foo")); + control.flushAll(); + assertNull(jedis.get("foo")); + } + } + + @Test + public void flushAllWithSimpleMap() { + try (UnifiedJedis jedis = createCachedJedis(CacheConfig.builder().build())) { + Cache cache = jedis.getCache(); + control.set("foo", "bar"); + assertEquals(0, cache.getSize()); + assertEquals("bar", jedis.get("foo")); + assertEquals(1, cache.getSize()); + control.flushAll(); + assertEquals(1, cache.getSize()); + assertNull(jedis.get("foo")); + assertEquals(1, cache.getSize()); + assertNull(jedis.get("foo")); + assertEquals(1, cache.getSize()); + } + } + + @Test + public void cacheNotEmptyTest() { + try (UnifiedJedis jedis = createCachedJedis(CacheConfig.builder().build())) { + Cache cache = jedis.getCache(); + control.set("foo", "bar"); + assertEquals(0, cache.getSize()); + assertEquals("bar", jedis.get("foo")); + assertEquals(1, cache.getSize()); + } + } + + @Test + public void cacheUsedTest() { + try (UnifiedJedis jedis = createCachedJedis(CacheConfig.builder().build())) { + Cache cache = jedis.getCache(); + + control.set("foo", "bar"); + + assertEquals(0, cache.getStats().getMissCount()); + assertEquals(0, cache.getStats().getHitCount()); + + assertEquals("bar", jedis.get("foo")); + assertEquals(1, cache.getStats().getMissCount()); + assertEquals(0, cache.getStats().getHitCount()); + + assertEquals("bar", jedis.get("foo")); + assertEquals(1, cache.getStats().getMissCount()); + assertEquals(1, cache.getStats().getHitCount()); + } + } + + @Test + public void immutableCacheEntriesTest() { + try (UnifiedJedis jedis = createCachedJedis(CacheConfig.builder().build())) { + jedis.set("{csc}a", "AA"); + jedis.set("{csc}b", "BB"); + jedis.set("{csc}c", "CC"); + + List expected = Arrays.asList("AA", "BB", "CC"); + + List reply1 = jedis.mget("{csc}a", "{csc}b", "{csc}c"); + List reply2 = jedis.mget("{csc}a", "{csc}b", "{csc}c"); + + assertEquals(expected, reply1); + assertEquals(expected, reply2); + assertEquals(reply1, reply2); + assertNotSame(reply1, reply2); + } + } + + @Test + public void invalidationTest() { + try (UnifiedJedis jedis = createCachedJedis(CacheConfig.builder().build())) { + Cache cache = jedis.getCache(); + jedis.set("{csc}1", "one"); + jedis.set("{csc}2", "two"); + jedis.set("{csc}3", "three"); + + assertEquals(0, cache.getSize()); + assertEquals(0, cache.getStats().getInvalidationCount()); + + List reply1 = jedis.mget("{csc}1", "{csc}2", "{csc}3"); + assertEquals(Arrays.asList("one", "two", "three"), reply1); + assertEquals(1, cache.getSize()); + assertEquals(0, cache.getStats().getInvalidationCount()); + + jedis.set("{csc}1", "new-one"); + List reply2 = jedis.mget("{csc}1", "{csc}2", "{csc}3"); + assertEquals(Arrays.asList("new-one", "two", "three"), reply2); + + assertEquals(1, cache.getSize()); + assertEquals(1, cache.getStats().getInvalidationCount()); + } + } + + @Test + public void getNumEntriesTest() { + try (UnifiedJedis jedis = createCachedJedis(CacheConfig.builder().build())) { + Cache cache = jedis.getCache(); + + // Create 100 keys + for (int i = 0; i < 100; i++) { + jedis.set("key" + i, "value" + i); + } + assertEquals(0, cache.getSize()); + + // Get 100 keys into the cache + for (int i = 0; i < 100; i++) { + jedis.get("key" + i); + } + assertEquals(100, cache.getSize()); + } + } + + @Test + public void invalidationOnCacheHitTest() { + try (UnifiedJedis jedis = createCachedJedis(CacheConfig.builder().build())) { + Cache cache = jedis.getCache(); + // Create 100 keys + for (int i = 0; i < 100; i++) { + jedis.set("key" + i, "value" + i); + } + assertEquals(0, cache.getSize()); + + // Get 100 keys into the cache + for (int i = 0; i < 100; i++) { + jedis.get("key" + i); + } + assertEquals(100, cache.getSize()); + + assertEquals(100, cache.getStats().getLoadCount()); + assertEquals(0, cache.getStats().getInvalidationCount()); + + // Change 50 of the 100 keys + for (int i = 1; i < 100; i += 2) { + jedis.set("key" + i, "val" + i); + } + + assertEquals(100, cache.getStats().getLoadCount()); + // invalidation count is anything between 0 and 50 + + // Get the 100 keys again + for (int i = 0; i < 100; i++) { + jedis.get("key" + i); + } + assertEquals(100, cache.getSize()); + + assertEquals(150, cache.getStats().getLoadCount()); + assertEquals(50, cache.getStats().getInvalidationCount()); + } + } + +} diff --git a/src/test/java/redis/clients/jedis/csc/VersionTest.java b/src/test/java/redis/clients/jedis/csc/VersionTest.java new file mode 100644 index 0000000000..b154e88a05 --- /dev/null +++ b/src/test/java/redis/clients/jedis/csc/VersionTest.java @@ -0,0 +1,30 @@ +package redis.clients.jedis.csc; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class VersionTest { + + @Test + public void compareSameVersions() { + RedisVersion a = new RedisVersion("5.2.4"); + RedisVersion b = new RedisVersion("5.2.4"); + assertEquals(a, b); + + RedisVersion c = new RedisVersion("5.2.0.0"); + RedisVersion d = new RedisVersion("5.2"); + assertEquals(a, b); + } + + @Test + public void compareDifferentVersions() { + RedisVersion a = new RedisVersion("5.2.4"); + RedisVersion b = new RedisVersion("5.1.4"); + assertEquals(1, a.compareTo(b)); + + RedisVersion c = new RedisVersion("5.2.4"); + RedisVersion d = new RedisVersion("5.2.5"); + assertEquals(-1, c.compareTo(d)); + } +} diff --git a/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java b/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java index 9f190a06ae..03d4dc62dd 100644 --- a/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java +++ b/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java @@ -1223,7 +1223,7 @@ public void vectorFieldParams() { public void float16StorageType() { assertOK(client.ftCreate(index, VectorField.builder().fieldName("v") - .algorithm(VectorField.VectorAlgorithm.HNSW) + .algorithm(VectorAlgorithm.HNSW) .addAttribute("TYPE", "FLOAT16") .addAttribute("DIM", 4) .addAttribute("DISTANCE_METRIC", "L2") @@ -1234,7 +1234,7 @@ public void float16StorageType() { public void bfloat16StorageType() { assertOK(client.ftCreate(index, VectorField.builder().fieldName("v") - .algorithm(VectorField.VectorAlgorithm.HNSW) + .algorithm(VectorAlgorithm.HNSW) .addAttribute("TYPE", "BFLOAT16") .addAttribute("DIM", 4) .addAttribute("DISTANCE_METRIC", "L2")