Skip to content

Commit 5442642

Browse files
sazzad16uglideatakavci
authored
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 redis/redis#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 6b9d338. * [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 <COMPRESSED|UNCOMPRESSED> * 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 92c09f3. * More tweak maximumSize test in CaffeineClientSideCacheTest This reverts and modifies commit 3534996. * 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 <[email protected]> * 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 <[email protected]> Co-authored-by: atakavci <[email protected]>
1 parent d22df4b commit 5442642

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+3491
-170
lines changed

Diff for: pom.xml

+3
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@
7575
<version>2.11.0</version>
7676
</dependency>
7777

78+
<!-- Optional dependencies -->
79+
7880
<!-- UNIX socket connection support -->
7981
<dependency>
8082
<groupId>com.kohlschutter.junixsocket</groupId>
@@ -90,6 +92,7 @@
9092
<version>1.20.0</version>
9193
<scope>test</scope>
9294
</dependency>
95+
9396
<!-- test -->
9497
<dependency>
9598
<groupId>junit</groupId>

Diff for: src/main/java/redis/clients/jedis/CommandArguments.java

+27
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
import java.util.ArrayList;
44
import java.util.Arrays;
55
import java.util.Collection;
6+
import java.util.Collections;
67
import java.util.Iterator;
8+
import java.util.List;
79

810
import redis.clients.jedis.annots.Experimental;
11+
import redis.clients.jedis.annots.Internal;
912
import redis.clients.jedis.args.Rawable;
1013
import redis.clients.jedis.args.RawableFactory;
1114
import redis.clients.jedis.commands.ProtocolCommand;
@@ -17,6 +20,8 @@ public class CommandArguments implements Iterable<Rawable> {
1720
private CommandKeyArgumentPreProcessor keyPreProc = null;
1821
private final ArrayList<Rawable> args;
1922

23+
private List<Object> keys;
24+
2025
private boolean blocking;
2126

2227
private CommandArguments() {
@@ -26,6 +31,8 @@ private CommandArguments() {
2631
public CommandArguments(ProtocolCommand command) {
2732
args = new ArrayList<>();
2833
args.add(command);
34+
35+
keys = Collections.emptyList();
2936
}
3037

3138
public ProtocolCommand getCommand() {
@@ -127,9 +134,24 @@ public CommandArguments key(Object key) {
127134
throw new IllegalArgumentException("\"" + key.toString() + "\" is not a valid argument.");
128135
}
129136

137+
addKeyInKeys(key);
138+
130139
return this;
131140
}
132141

142+
private void addKeyInKeys(Object key) {
143+
if (keys.isEmpty()) {
144+
keys = Collections.singletonList(key);
145+
} else if (keys.size() == 1) {
146+
List oldKeys = keys;
147+
keys = new ArrayList();
148+
keys.addAll(oldKeys);
149+
keys.add(key);
150+
} else {
151+
keys.add(key);
152+
}
153+
}
154+
133155
public final CommandArguments keys(Object... keys) {
134156
Arrays.stream(keys).forEach(this::key);
135157
return this;
@@ -178,6 +200,11 @@ public Iterator<Rawable> iterator() {
178200
return args.iterator();
179201
}
180202

203+
@Internal
204+
public List<Object> getKeys() {
205+
return keys;
206+
}
207+
181208
public boolean isBlocking() {
182209
return blocking;
183210
}

Diff for: src/main/java/redis/clients/jedis/CommandObject.java

+38
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package redis.clients.jedis;
22

3+
import java.util.Iterator;
4+
import redis.clients.jedis.args.Rawable;
5+
36
public class CommandObject<T> {
47

58
private final CommandArguments arguments;
@@ -17,4 +20,39 @@ public CommandArguments getArguments() {
1720
public Builder<T> getBuilder() {
1821
return builder;
1922
}
23+
24+
@Override
25+
public int hashCode() {
26+
int hashCode = 1;
27+
for (Rawable e : arguments) {
28+
hashCode = 31 * hashCode + e.hashCode();
29+
}
30+
hashCode = 31 * hashCode + builder.hashCode();
31+
return hashCode;
32+
}
33+
34+
@Override
35+
public boolean equals(Object o) {
36+
if (o == this) {
37+
return true;
38+
}
39+
if (!(o instanceof CommandObject)) {
40+
return false;
41+
}
42+
43+
Iterator<Rawable> e1 = arguments.iterator();
44+
Iterator<Rawable> e2 = ((CommandObject) o).arguments.iterator();
45+
while (e1.hasNext() && e2.hasNext()) {
46+
Rawable o1 = e1.next();
47+
Rawable o2 = e2.next();
48+
if (!(o1 == null ? o2 == null : o1.equals(o2))) {
49+
return false;
50+
}
51+
}
52+
if (e1.hasNext() || e2.hasNext()) {
53+
return false;
54+
}
55+
56+
return builder == ((CommandObject) o).builder;
57+
}
2058
}

Diff for: src/main/java/redis/clients/jedis/Connection.java

+80-46
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.util.ArrayList;
1313
import java.util.Arrays;
1414
import java.util.List;
15+
import java.util.Map;
1516
import java.util.function.Supplier;
1617

1718
import redis.clients.jedis.Protocol.Command;
@@ -31,7 +32,7 @@
3132
public class Connection implements Closeable {
3233

3334
private ConnectionPool memberOf;
34-
private RedisProtocol protocol;
35+
protected RedisProtocol protocol;
3536
private final JedisSocketFactory socketFactory;
3637
private Socket socket;
3738
private RedisOutputStream outputStream;
@@ -41,6 +42,8 @@ public class Connection implements Closeable {
4142
private boolean broken = false;
4243
private boolean strValActive;
4344
private String strVal;
45+
protected String server;
46+
protected String version;
4447

4548
public Connection() {
4649
this(Protocol.DEFAULT_HOST, Protocol.DEFAULT_PORT);
@@ -55,9 +58,7 @@ public Connection(final HostAndPort hostAndPort) {
5558
}
5659

5760
public Connection(final HostAndPort hostAndPort, final JedisClientConfig clientConfig) {
58-
this(new DefaultJedisSocketFactory(hostAndPort, clientConfig));
59-
this.infiniteSoTimeout = clientConfig.getBlockingSocketTimeoutMillis();
60-
initializeFromClientConfig(clientConfig);
61+
this(new DefaultJedisSocketFactory(hostAndPort, clientConfig), clientConfig);
6162
}
6263

6364
public Connection(final JedisSocketFactory socketFactory) {
@@ -373,16 +374,40 @@ protected void flush() {
373374
}
374375
}
375376

377+
@Experimental
378+
protected Object protocolRead(RedisInputStream is) {
379+
return Protocol.read(is);
380+
}
381+
382+
@Experimental
383+
protected void protocolReadPushes(RedisInputStream is) {
384+
}
385+
376386
protected Object readProtocolWithCheckingBroken() {
377387
if (broken) {
378388
throw new JedisConnectionException("Attempting to read from a broken connection.");
379389
}
380390

381391
try {
382-
return Protocol.read(inputStream);
383-
// Object read = Protocol.read(inputStream);
384-
// System.out.println(redis.clients.jedis.util.SafeEncoder.encodeObject(read));
385-
// return read;
392+
return protocolRead(inputStream);
393+
} catch (JedisConnectionException exc) {
394+
broken = true;
395+
throw exc;
396+
}
397+
}
398+
399+
protected void readPushesWithCheckingBroken() {
400+
if (broken) {
401+
throw new JedisConnectionException("Attempting to read from a broken connection.");
402+
}
403+
404+
try {
405+
if (inputStream.available() > 0) {
406+
protocolReadPushes(inputStream);
407+
}
408+
} catch (IOException e) {
409+
broken = true;
410+
throw new JedisConnectionException("Failed to check buffer on connection.", e);
386411
} catch (JedisConnectionException exc) {
387412
setBroken();
388413
throw exc;
@@ -404,6 +429,7 @@ public List<Object> getMany(final int count) {
404429

405430
/**
406431
* Check if the client name libname, libver, characters are legal
432+
*
407433
* @param info the name
408434
* @return Returns true if legal, false throws exception
409435
* @throws JedisException if characters illegal
@@ -419,7 +445,7 @@ private static boolean validateClientInfo(String info) {
419445
return true;
420446
}
421447

422-
private void initializeFromClientConfig(final JedisClientConfig config) {
448+
protected void initializeFromClientConfig(final JedisClientConfig config) {
423449
try {
424450
connect();
425451

@@ -430,12 +456,12 @@ private void initializeFromClientConfig(final JedisClientConfig config) {
430456
final RedisCredentialsProvider redisCredentialsProvider = (RedisCredentialsProvider) credentialsProvider;
431457
try {
432458
redisCredentialsProvider.prepare();
433-
helloOrAuth(protocol, redisCredentialsProvider.get());
459+
helloAndAuth(protocol, redisCredentialsProvider.get());
434460
} finally {
435461
redisCredentialsProvider.cleanUp();
436462
}
437463
} else {
438-
helloOrAuth(protocol, credentialsProvider != null ? credentialsProvider.get()
464+
helloAndAuth(protocol, credentialsProvider != null ? credentialsProvider.get()
439465
: new DefaultRedisCredentials(config.getUser(), config.getPassword()));
440466
}
441467

@@ -447,7 +473,9 @@ private void initializeFromClientConfig(final JedisClientConfig config) {
447473
}
448474

449475
ClientSetInfoConfig setInfoConfig = config.getClientSetInfoConfig();
450-
if (setInfoConfig == null) setInfoConfig = ClientSetInfoConfig.DEFAULT;
476+
if (setInfoConfig == null) {
477+
setInfoConfig = ClientSetInfoConfig.DEFAULT;
478+
}
451479

452480
if (!setInfoConfig.isDisabled()) {
453481
String libName = JedisMetaInfo.getArtifactId();
@@ -492,50 +520,56 @@ private void initializeFromClientConfig(final JedisClientConfig config) {
492520
}
493521
}
494522

495-
private void helloOrAuth(final RedisProtocol protocol, final RedisCredentials credentials) {
496-
497-
if (credentials == null || credentials.getPassword() == null) {
498-
if (protocol != null) {
499-
sendCommand(Command.HELLO, encode(protocol.version()));
500-
getOne();
523+
private void helloAndAuth(final RedisProtocol protocol, final RedisCredentials credentials) {
524+
Map<String, Object> helloResult = null;
525+
if (protocol != null && credentials != null && credentials.getUser() != null) {
526+
byte[] rawPass = encodeToBytes(credentials.getPassword());
527+
try {
528+
helloResult = hello(encode(protocol.version()), Keyword.AUTH.getRaw(), encode(credentials.getUser()), rawPass);
529+
} finally {
530+
Arrays.fill(rawPass, (byte) 0); // clear sensitive data
501531
}
502-
return;
532+
} else {
533+
auth(credentials);
534+
helloResult = protocol == null ? null : hello(encode(protocol.version()));
535+
}
536+
if (helloResult != null) {
537+
server = (String) helloResult.get("server");
538+
version = (String) helloResult.get("version");
503539
}
504540

505-
// Source: https://stackoverflow.com/a/9670279/4021802
506-
ByteBuffer passBuf = Protocol.CHARSET.encode(CharBuffer.wrap(credentials.getPassword()));
507-
byte[] rawPass = Arrays.copyOfRange(passBuf.array(), passBuf.position(), passBuf.limit());
508-
Arrays.fill(passBuf.array(), (byte) 0); // clear sensitive data
541+
// clearing 'char[] credentials.getPassword()' should be
542+
// handled in RedisCredentialsProvider.cleanUp()
543+
}
509544

545+
private void auth(RedisCredentials credentials) {
546+
if (credentials == null || credentials.getPassword() == null) {
547+
return;
548+
}
549+
byte[] rawPass = encodeToBytes(credentials.getPassword());
510550
try {
511-
/// actual HELLO or AUTH -->
512-
if (protocol != null) {
513-
if (credentials.getUser() != null) {
514-
sendCommand(Command.HELLO, encode(protocol.version()),
515-
Keyword.AUTH.getRaw(), encode(credentials.getUser()), rawPass);
516-
getOne(); // Map
517-
} else {
518-
sendCommand(Command.AUTH, rawPass);
519-
getStatusCodeReply(); // OK
520-
sendCommand(Command.HELLO, encode(protocol.version()));
521-
getOne(); // Map
522-
}
523-
} else { // protocol == null
524-
if (credentials.getUser() != null) {
525-
sendCommand(Command.AUTH, encode(credentials.getUser()), rawPass);
526-
} else {
527-
sendCommand(Command.AUTH, rawPass);
528-
}
529-
getStatusCodeReply(); // OK
551+
if (credentials.getUser() == null) {
552+
sendCommand(Command.AUTH, rawPass);
553+
} else {
554+
sendCommand(Command.AUTH, encode(credentials.getUser()), rawPass);
530555
}
531-
/// <-- actual HELLO or AUTH
532556
} finally {
533-
534557
Arrays.fill(rawPass, (byte) 0); // clear sensitive data
535558
}
559+
getStatusCodeReply();
560+
}
536561

537-
// clearing 'char[] credentials.getPassword()' should be
538-
// handled in RedisCredentialsProvider.cleanUp()
562+
protected Map<String, Object> hello(byte[]... args) {
563+
sendCommand(Command.HELLO, args);
564+
return BuilderFactory.ENCODED_OBJECT_MAP.build(getOne());
565+
}
566+
567+
protected byte[] encodeToBytes(char[] chars) {
568+
// Source: https://stackoverflow.com/a/9670279/4021802
569+
ByteBuffer passBuf = Protocol.CHARSET.encode(CharBuffer.wrap(chars));
570+
byte[] rawPass = Arrays.copyOfRange(passBuf.array(), passBuf.position(), passBuf.limit());
571+
Arrays.fill(passBuf.array(), (byte) 0); // clear sensitive data
572+
return rawPass;
539573
}
540574

541575
public String select(final int index) {

0 commit comments

Comments
 (0)