From 780fbc74c6b114953ea31492c92c1955bab93ce7 Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Wed, 3 Jul 2024 15:29:02 +0600 Subject: [PATCH 01/63] [TEMPORARY] [TEST] Disable FT.PROFILE tests (#3881) Disable FT.PROFILE tests --- .../jedis/modules/search/AggregationTest.java | 1 + .../jedis/modules/search/SearchTest.java | 55 ++++++++++--------- .../modules/search/SearchWithParamsTest.java | 7 +++ 3 files changed, 37 insertions(+), 26 deletions(-) diff --git a/src/test/java/redis/clients/jedis/modules/search/AggregationTest.java b/src/test/java/redis/clients/jedis/modules/search/AggregationTest.java index cefdfbe5d34..98e811472ed 100644 --- a/src/test/java/redis/clients/jedis/modules/search/AggregationTest.java +++ b/src/test/java/redis/clients/jedis/modules/search/AggregationTest.java @@ -134,6 +134,7 @@ public void testAggregations2() { assertEquals("10", rows.get(1).get("sum")); } + @org.junit.Ignore @Test public void testAggregations2Profile() { Schema sc = new Schema(); diff --git a/src/test/java/redis/clients/jedis/modules/search/SearchTest.java b/src/test/java/redis/clients/jedis/modules/search/SearchTest.java index 1bc6cb345db..27f72483dbc 100644 --- a/src/test/java/redis/clients/jedis/modules/search/SearchTest.java +++ b/src/test/java/redis/clients/jedis/modules/search/SearchTest.java @@ -1140,6 +1140,7 @@ public void testDialectsWithFTExplain() throws Exception { assertTrue("Should contain '{K=10 nearest vector'", client.ftExplain(index, query).contains("{K=10 nearest vector")); } + @org.junit.Ignore @Test public void searchProfile() { Schema sc = new Schema().addTextField("t1", 1.0).addTextField("t2", 1.0); @@ -1200,19 +1201,20 @@ public void testHNSWVVectorSimilarity() { assertEquals("a", doc1.getId()); assertEquals("0", doc1.get("__v_score")); - // profile - Map.Entry> reply - = client.ftProfileSearch(index, FTProfileParams.profileParams(), query); - doc1 = reply.getKey().getDocuments().get(0); - assertEquals("a", doc1.getId()); - assertEquals("0", doc1.get("__v_score")); - if (protocol != RedisProtocol.RESP3) { - assertEquals("VECTOR", ((Map) reply.getValue().get("Iterators profile")).get("Type")); - } else { - assertEquals(Arrays.asList("VECTOR"), - ((List>) reply.getValue().get("Iterators profile")).stream() - .map(map -> map.get("Type")).collect(Collectors.toList())); - } +// // @org.junit.Ignore +// // profile +// Map.Entry> reply +// = client.ftProfileSearch(index, FTProfileParams.profileParams(), query); +// doc1 = reply.getKey().getDocuments().get(0); +// assertEquals("a", doc1.getId()); +// assertEquals("0", doc1.get("__v_score")); +// if (protocol != RedisProtocol.RESP3) { +// assertEquals("VECTOR", ((Map) reply.getValue().get("Iterators profile")).get("Type")); +// } else { +// assertEquals(Arrays.asList("VECTOR"), +// ((List>) reply.getValue().get("Iterators profile")).stream() +// .map(map -> map.get("Type")).collect(Collectors.toList())); +// } } @Test @@ -1238,19 +1240,20 @@ public void testFlatVectorSimilarity() { assertEquals("a", doc1.getId()); assertEquals("0", doc1.get("__v_score")); - // profile - Map.Entry> reply - = client.ftProfileSearch(index, FTProfileParams.profileParams(), query); - doc1 = reply.getKey().getDocuments().get(0); - assertEquals("a", doc1.getId()); - assertEquals("0", doc1.get("__v_score")); - if (protocol != RedisProtocol.RESP3) { - assertEquals("VECTOR", ((Map) reply.getValue().get("Iterators profile")).get("Type")); - } else { - assertEquals(Arrays.asList("VECTOR"), - ((List>) reply.getValue().get("Iterators profile")).stream() - .map(map -> map.get("Type")).collect(Collectors.toList())); - } +// // @org.junit.Ignore +// // profile +// Map.Entry> reply +// = client.ftProfileSearch(index, FTProfileParams.profileParams(), query); +// doc1 = reply.getKey().getDocuments().get(0); +// assertEquals("a", doc1.getId()); +// assertEquals("0", doc1.get("__v_score")); +// if (protocol != RedisProtocol.RESP3) { +// assertEquals("VECTOR", ((Map) reply.getValue().get("Iterators profile")).get("Type")); +// } else { +// assertEquals(Arrays.asList("VECTOR"), +// ((List>) reply.getValue().get("Iterators profile")).stream() +// .map(map -> map.get("Type")).collect(Collectors.toList())); +// } } @Test 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 897da8eece9..8550e5c64a1 100644 --- a/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java +++ b/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java @@ -9,6 +9,7 @@ import java.util.stream.Collectors; import org.hamcrest.Matchers; import org.junit.BeforeClass; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -1056,6 +1057,7 @@ public void testFlatVectorSimilarity() { assertEquals("0", doc1.get("__v_score")); } + @Ignore @Test public void searchProfile() { assertOK(client.ftCreate(index, TextField.of("t1"), TextField.of("t2"))); @@ -1092,6 +1094,7 @@ public void searchProfile() { .map(map -> map.get("Type")).collect(Collectors.toList())); } + @Ignore @Test public void vectorSearchProfile() { assertOK(client.ftCreate(index, VectorField.builder().fieldName("v") @@ -1131,6 +1134,7 @@ public void vectorSearchProfile() { assertEquals("Sorter", resultProcessorsProfile.get(2).get("Type")); } + @Ignore @Test public void maxPrefixExpansionSearchProfile() { final String configParam = "MAXPREFIXEXPANSIONS"; @@ -1158,6 +1162,7 @@ public void maxPrefixExpansionSearchProfile() { } } + @Ignore @Test public void noContentSearchProfile() { assertOK(client.ftCreate(index, TextField.of("t"))); @@ -1185,6 +1190,7 @@ public void noContentSearchProfile() { } } + @Ignore @Test public void deepReplySearchProfile() { assertOK(client.ftCreate(index, TextField.of("t"))); @@ -1226,6 +1232,7 @@ private void deepReplySearchProfile_assertProfile(Map attr, } } + @Ignore @Test public void limitedSearchProfile() { assertOK(client.ftCreate(index, TextField.of("t"))); From 7c0e57d222dbfefdaa4847cd2a1e19494dab4afd Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Fri, 14 Jun 2024 10:16:47 +0600 Subject: [PATCH 02/63] 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 --- .../redis/clients/jedis/CommandObjects.java | 20 ++- .../redis/clients/jedis/PipeliningBase.java | 15 ++ .../redis/clients/jedis/UnifiedJedis.java | 15 ++ .../jedis/timeseries/EncodingFormat.java | 24 ++++ .../timeseries/RedisTimeSeriesCommands.java | 55 +++++++- .../RedisTimeSeriesPipelineCommands.java | 7 + .../clients/jedis/timeseries/TSAddParams.java | 128 +++++++++++++++++ .../jedis/timeseries/TSAlterParams.java | 28 ++++ .../jedis/timeseries/TSCreateParams.java | 39 ++++-- .../timeseries/TSIncrOrDecrByParams.java | 132 ++++++++++++++++++ .../jedis/timeseries/TimeSeriesProtocol.java | 1 + .../PipeliningBaseTimeSeriesCommandsTest.java | 47 +++++-- .../UnifiedJedisTimeSeriesCommandsTest.java | 71 ++++++++-- .../modules/timeseries/TimeSeriesTest.java | 55 ++++++++ 14 files changed, 600 insertions(+), 37 deletions(-) create mode 100644 src/main/java/redis/clients/jedis/timeseries/EncodingFormat.java create mode 100644 src/main/java/redis/clients/jedis/timeseries/TSAddParams.java create mode 100644 src/main/java/redis/clients/jedis/timeseries/TSIncrOrDecrByParams.java diff --git a/src/main/java/redis/clients/jedis/CommandObjects.java b/src/main/java/redis/clients/jedis/CommandObjects.java index 7226a014a79..421b81c4e2d 100644 --- a/src/main/java/redis/clients/jedis/CommandObjects.java +++ b/src/main/java/redis/clients/jedis/CommandObjects.java @@ -3946,9 +3946,15 @@ public final CommandObject tsAdd(String key, long timestamp, double value) return new CommandObject<>(commandArguments(TimeSeriesCommand.ADD).key(key).add(timestamp).add(value), BuilderFactory.LONG); } + @Deprecated public final CommandObject tsAdd(String key, long timestamp, double value, TSCreateParams createParams) { - return new CommandObject<>(commandArguments(TimeSeriesCommand.ADD).key(key) - .add(timestamp).add(value).addParams(createParams), BuilderFactory.LONG); + return new CommandObject<>(commandArguments(TimeSeriesCommand.ADD).key(key).add(timestamp).add(value) + .addParams(createParams), BuilderFactory.LONG); + } + + public final CommandObject tsAdd(String key, long timestamp, double value, TSAddParams addParams) { + return new CommandObject<>(commandArguments(TimeSeriesCommand.ADD).key(key).add(timestamp).add(value) + .addParams(addParams), BuilderFactory.LONG); } public final CommandObject> tsMAdd(Map.Entry... entries) { @@ -3968,6 +3974,11 @@ public final CommandObject tsIncrBy(String key, double value, long timesta .add(TimeSeriesKeyword.TIMESTAMP).add(timestamp), BuilderFactory.LONG); } + public final CommandObject tsIncrBy(String key, double addend, TSIncrOrDecrByParams incrByParams) { + return new CommandObject<>(commandArguments(TimeSeriesCommand.INCRBY).key(key).add(addend) + .addParams(incrByParams), BuilderFactory.LONG); + } + public final CommandObject tsDecrBy(String key, double value) { return new CommandObject<>(commandArguments(TimeSeriesCommand.DECRBY).key(key).add(value), BuilderFactory.LONG); } @@ -3977,6 +3988,11 @@ public final CommandObject tsDecrBy(String key, double value, long timesta .add(TimeSeriesKeyword.TIMESTAMP).add(timestamp), BuilderFactory.LONG); } + public final CommandObject tsDecrBy(String key, double subtrahend, TSIncrOrDecrByParams decrByParams) { + return new CommandObject<>(commandArguments(TimeSeriesCommand.DECRBY).key(key).add(subtrahend) + .addParams(decrByParams), BuilderFactory.LONG); + } + public final CommandObject> tsRange(String key, long fromTimestamp, long toTimestamp) { return new CommandObject<>(commandArguments(TimeSeriesCommand.RANGE).key(key) .add(fromTimestamp).add(toTimestamp), TimeSeriesBuilderFactory.TIMESERIES_ELEMENT_LIST); diff --git a/src/main/java/redis/clients/jedis/PipeliningBase.java b/src/main/java/redis/clients/jedis/PipeliningBase.java index 928126a7047..9967a2e6940 100644 --- a/src/main/java/redis/clients/jedis/PipeliningBase.java +++ b/src/main/java/redis/clients/jedis/PipeliningBase.java @@ -3948,6 +3948,11 @@ public Response tsAdd(String key, long timestamp, double value, TSCreatePa return appendCommand(commandObjects.tsAdd(key, timestamp, value, createParams)); } + @Override + public Response tsAdd(String key, long timestamp, double value, TSAddParams addParams) { + return appendCommand(commandObjects.tsAdd(key, timestamp, value, addParams)); + } + @Override public Response> tsMAdd(Map.Entry... entries) { return appendCommand(commandObjects.tsMAdd(entries)); @@ -3963,6 +3968,11 @@ public Response tsIncrBy(String key, double value, long timestamp) { return appendCommand(commandObjects.tsIncrBy(key, value, timestamp)); } + @Override + public Response tsIncrBy(String key, double addend, TSIncrOrDecrByParams incrByParams) { + return appendCommand(commandObjects.tsIncrBy(key, addend, incrByParams)); + } + @Override public Response tsDecrBy(String key, double value) { return appendCommand(commandObjects.tsDecrBy(key, value)); @@ -3973,6 +3983,11 @@ public Response tsDecrBy(String key, double value, long timestamp) { return appendCommand(commandObjects.tsDecrBy(key, value, timestamp)); } + @Override + public Response tsDecrBy(String key, double subtrahend, TSIncrOrDecrByParams decrByParams) { + return appendCommand(commandObjects.tsDecrBy(key, subtrahend, decrByParams)); + } + @Override public Response> tsRange(String key, long fromTimestamp, long toTimestamp) { return appendCommand(commandObjects.tsRange(key, fromTimestamp, toTimestamp)); diff --git a/src/main/java/redis/clients/jedis/UnifiedJedis.java b/src/main/java/redis/clients/jedis/UnifiedJedis.java index 2d6e77fcf0a..87ba0d8a142 100644 --- a/src/main/java/redis/clients/jedis/UnifiedJedis.java +++ b/src/main/java/redis/clients/jedis/UnifiedJedis.java @@ -4473,6 +4473,11 @@ public long tsAdd(String key, long timestamp, double value, TSCreateParams creat return executeCommand(commandObjects.tsAdd(key, timestamp, value, createParams)); } + @Override + public long tsAdd(String key, long timestamp, double value, TSAddParams addParams) { + return executeCommand(commandObjects.tsAdd(key, timestamp, value, addParams)); + } + @Override public List tsMAdd(Map.Entry... entries) { return executeCommand(commandObjects.tsMAdd(entries)); @@ -4488,6 +4493,11 @@ public long tsIncrBy(String key, double value, long timestamp) { return executeCommand(commandObjects.tsIncrBy(key, value, timestamp)); } + @Override + public long tsIncrBy(String key, double addend, TSIncrOrDecrByParams incrByParams) { + return executeCommand(commandObjects.tsIncrBy(key, addend, incrByParams)); + } + @Override public long tsDecrBy(String key, double value) { return executeCommand(commandObjects.tsDecrBy(key, value)); @@ -4498,6 +4508,11 @@ public long tsDecrBy(String key, double value, long timestamp) { return executeCommand(commandObjects.tsDecrBy(key, value, timestamp)); } + @Override + public long tsDecrBy(String key, double subtrahend, TSIncrOrDecrByParams decrByParams) { + return executeCommand(commandObjects.tsDecrBy(key, subtrahend, decrByParams)); + } + @Override public List tsRange(String key, long fromTimestamp, long toTimestamp) { return executeCommand(commandObjects.tsRange(key, fromTimestamp, toTimestamp)); diff --git a/src/main/java/redis/clients/jedis/timeseries/EncodingFormat.java b/src/main/java/redis/clients/jedis/timeseries/EncodingFormat.java new file mode 100644 index 00000000000..5130d7da251 --- /dev/null +++ b/src/main/java/redis/clients/jedis/timeseries/EncodingFormat.java @@ -0,0 +1,24 @@ +package redis.clients.jedis.timeseries; + +import redis.clients.jedis.args.Rawable; +import redis.clients.jedis.util.SafeEncoder; + +/** + * Specifies the series samples encoding format. + */ +public enum EncodingFormat implements Rawable { + + COMPRESSED, + UNCOMPRESSED; + + private final byte[] raw; + + private EncodingFormat() { + raw = SafeEncoder.encode(name()); + } + + @Override + public byte[] getRaw() { + return raw; + } +} diff --git a/src/main/java/redis/clients/jedis/timeseries/RedisTimeSeriesCommands.java b/src/main/java/redis/clients/jedis/timeseries/RedisTimeSeriesCommands.java index c002b94c08e..67c1b26fcf8 100644 --- a/src/main/java/redis/clients/jedis/timeseries/RedisTimeSeriesCommands.java +++ b/src/main/java/redis/clients/jedis/timeseries/RedisTimeSeriesCommands.java @@ -59,16 +59,33 @@ public interface RedisTimeSeriesCommands { long tsAdd(String key, long timestamp, double value); /** - * {@code TS.ADD key timestamp value [RETENTION retentionTime] [ENCODING [COMPRESSED|UNCOMPRESSED]] [CHUNK_SIZE size] [ON_DUPLICATE policy] [LABELS label value..]} - * * @param key * @param timestamp * @param value * @param createParams * @return timestamp + * @deprecated Use {@link RedisTimeSeriesCommands#tsAdd(java.lang.String, long, double, redis.clients.jedis.timeseries.TSAddParams)}. */ + @Deprecated long tsAdd(String key, long timestamp, double value, TSCreateParams createParams); + /** + * {@code TS.ADD key timestamp value + * [RETENTION retentionTime] + * [ENCODING ] + * [CHUNK_SIZE size] + * [DUPLICATE_POLICY policy] + * [ON_DUPLICATE policy_ovr] + * [LABELS label value..]} + * + * @param key + * @param timestamp + * @param value + * @param addParams + * @return timestamp + */ + long tsAdd(String key, long timestamp, double value, TSAddParams addParams); + /** * {@code TS.MADD key timestamp value [key timestamp value ...]} * @@ -81,10 +98,44 @@ public interface RedisTimeSeriesCommands { long tsIncrBy(String key, double value, long timestamp); + /** + * {@code TS.INCRBY key addend + * [TIMESTAMP timestamp] + * [RETENTION retentionPeriod] + * [ENCODING ] + * [CHUNK_SIZE size] + * [DUPLICATE_POLICY policy] + * [IGNORE ignoreMaxTimediff ignoreMaxValDiff] + * [LABELS [label value ...]]} + * + * @param key + * @param addend + * @param incrByParams + * @return timestamp + */ + long tsIncrBy(String key, double addend, TSIncrOrDecrByParams incrByParams); + long tsDecrBy(String key, double value); long tsDecrBy(String key, double value, long timestamp); + /** + * {@code TS.DECRBY key subtrahend + * [TIMESTAMP timestamp] + * [RETENTION retentionPeriod] + * [ENCODING ] + * [CHUNK_SIZE size] + * [DUPLICATE_POLICY policy] + * [IGNORE ignoreMaxTimediff ignoreMaxValDiff] + * [LABELS [label value ...]]} + * + * @param key + * @param subtrahend + * @param decrByParams + * @return timestamp + */ + long tsDecrBy(String key, double subtrahend, TSIncrOrDecrByParams decrByParams); + /** * {@code TS.RANGE key fromTimestamp toTimestamp} * diff --git a/src/main/java/redis/clients/jedis/timeseries/RedisTimeSeriesPipelineCommands.java b/src/main/java/redis/clients/jedis/timeseries/RedisTimeSeriesPipelineCommands.java index 288b3f195e9..b3304716ddd 100644 --- a/src/main/java/redis/clients/jedis/timeseries/RedisTimeSeriesPipelineCommands.java +++ b/src/main/java/redis/clients/jedis/timeseries/RedisTimeSeriesPipelineCommands.java @@ -18,18 +18,25 @@ public interface RedisTimeSeriesPipelineCommands { Response tsAdd(String key, long timestamp, double value); + @Deprecated Response tsAdd(String key, long timestamp, double value, TSCreateParams createParams); + Response tsAdd(String key, long timestamp, double value, TSAddParams addParams); + Response> tsMAdd(Map.Entry... entries); Response tsIncrBy(String key, double value); Response tsIncrBy(String key, double value, long timestamp); + Response tsIncrBy(String key, double addend, TSIncrOrDecrByParams incrByParams); + Response tsDecrBy(String key, double value); Response tsDecrBy(String key, double value, long timestamp); + Response tsDecrBy(String key, double subtrahend, TSIncrOrDecrByParams decrByParams); + Response> tsRange(String key, long fromTimestamp, long toTimestamp); Response> tsRange(String key, TSRangeParams rangeParams); diff --git a/src/main/java/redis/clients/jedis/timeseries/TSAddParams.java b/src/main/java/redis/clients/jedis/timeseries/TSAddParams.java new file mode 100644 index 00000000000..0a9713cefb8 --- /dev/null +++ b/src/main/java/redis/clients/jedis/timeseries/TSAddParams.java @@ -0,0 +1,128 @@ +package redis.clients.jedis.timeseries; + +import static redis.clients.jedis.Protocol.toByteArray; +import static redis.clients.jedis.timeseries.TimeSeriesProtocol.TimeSeriesKeyword.*; + +import java.util.LinkedHashMap; +import java.util.Map; +import redis.clients.jedis.CommandArguments; +import redis.clients.jedis.params.IParams; + +/** + * Represents optional arguments of TS.ADD command. + */ +public class TSAddParams implements IParams { + + private Long retentionPeriod; + private EncodingFormat encoding; + private Long chunkSize; + private DuplicatePolicy duplicatePolicy; + private DuplicatePolicy onDuplicate; + + private boolean ignore; + private long ignoreMaxTimediff; + private double ignoreMaxValDiff; + + private Map labels; + + public TSAddParams() { + } + + public static TSAddParams addParams() { + return new TSAddParams(); + } + + public TSAddParams retention(long retentionPeriod) { + this.retentionPeriod = retentionPeriod; + return this; + } + + public TSAddParams encoding(EncodingFormat encoding) { + this.encoding = encoding; + return this; + } + + public TSAddParams chunkSize(long chunkSize) { + this.chunkSize = chunkSize; + return this; + } + + public TSAddParams duplicatePolicy(DuplicatePolicy duplicatePolicy) { + this.duplicatePolicy = duplicatePolicy; + return this; + } + + public TSAddParams onDuplicate(DuplicatePolicy onDuplicate) { + this.onDuplicate = onDuplicate; + return this; + } + + public TSAddParams ignore(long maxTimediff, double maxValDiff) { + this.ignore = true; + this.ignoreMaxTimediff = maxTimediff; + this.ignoreMaxValDiff = maxValDiff; + return this; + } + + /** + * Set label-value pairs + * + * @param labels label-value pairs + * @return the object itself + */ + public TSAddParams labels(Map labels) { + this.labels = labels; + return this; + } + + /** + * Add label-value pair. Multiple pairs can be added through chaining. + * @param label + * @param value + * @return the object itself + */ + public TSAddParams label(String label, String value) { + if (this.labels == null) { + this.labels = new LinkedHashMap<>(); + } + this.labels.put(label, value); + return this; + } + + @Override + public void addParams(CommandArguments args) { + + if (retentionPeriod != null) { + args.add(RETENTION).add(toByteArray(retentionPeriod)); + } + + if (encoding != null) { + args.add(ENCODING).add(encoding); + } + + if (chunkSize != null) { + args.add(CHUNK_SIZE).add(toByteArray(chunkSize)); + } + + if (duplicatePolicy != null) { + args.add(DUPLICATE_POLICY).add(duplicatePolicy); + } + + if (duplicatePolicy != null) { + args.add(DUPLICATE_POLICY).add(duplicatePolicy); + } + + if (onDuplicate != null) { + args.add(ON_DUPLICATE).add(onDuplicate); + } + + if (ignore) { + args.add(IGNORE).add(ignoreMaxTimediff).add(ignoreMaxValDiff); + } + + if (labels != null) { + args.add(LABELS); + labels.entrySet().forEach((entry) -> args.add(entry.getKey()).add(entry.getValue())); + } + } +} diff --git a/src/main/java/redis/clients/jedis/timeseries/TSAlterParams.java b/src/main/java/redis/clients/jedis/timeseries/TSAlterParams.java index 4576a1b6b75..50ba9723acc 100644 --- a/src/main/java/redis/clients/jedis/timeseries/TSAlterParams.java +++ b/src/main/java/redis/clients/jedis/timeseries/TSAlterParams.java @@ -17,6 +17,11 @@ public class TSAlterParams implements IParams { private Long retentionPeriod; private Long chunkSize; private DuplicatePolicy duplicatePolicy; + + private boolean ignore; + private long ignoreMaxTimediff; + private double ignoreMaxValDiff; + private Map labels; public TSAlterParams() { @@ -41,11 +46,30 @@ public TSAlterParams duplicatePolicy(DuplicatePolicy duplicatePolicy) { return this; } + public TSAlterParams ignore(long maxTimediff, double maxValDiff) { + this.ignore = true; + this.ignoreMaxTimediff = maxTimediff; + this.ignoreMaxValDiff = maxValDiff; + return this; + } + + /** + * Set label-value pairs + * + * @param labels label-value pairs + * @return the object itself + */ public TSAlterParams labels(Map labels) { this.labels = labels; return this; } + /** + * Add label-value pair. Multiple pairs can be added through chaining. + * @param label + * @param value + * @return the object itself + */ public TSAlterParams label(String label, String value) { if (this.labels == null) { this.labels = new LinkedHashMap<>(); @@ -73,6 +97,10 @@ public void addParams(CommandArguments args) { args.add(DUPLICATE_POLICY).add(duplicatePolicy); } + if (ignore) { + args.add(IGNORE).add(ignoreMaxTimediff).add(ignoreMaxValDiff); + } + if (labels != null) { args.add(LABELS); labels.entrySet().forEach((entry) -> args.add(entry.getKey()).add(entry.getValue())); diff --git a/src/main/java/redis/clients/jedis/timeseries/TSCreateParams.java b/src/main/java/redis/clients/jedis/timeseries/TSCreateParams.java index ca07de1f01f..0611383d4d2 100644 --- a/src/main/java/redis/clients/jedis/timeseries/TSCreateParams.java +++ b/src/main/java/redis/clients/jedis/timeseries/TSCreateParams.java @@ -14,10 +14,14 @@ public class TSCreateParams implements IParams { private Long retentionPeriod; - private boolean uncompressed; - private boolean compressed; + private EncodingFormat encoding; private Long chunkSize; private DuplicatePolicy duplicatePolicy; + + private boolean ignore; + private long ignoreMaxTimediff; + private double ignoreMaxValDiff; + private Map labels; public TSCreateParams() { @@ -32,13 +36,18 @@ public TSCreateParams retention(long retentionPeriod) { return this; } + // TODO: deprecate public TSCreateParams uncompressed() { - this.uncompressed = true; - return this; + return encoding(EncodingFormat.UNCOMPRESSED); } + // TODO: deprecate public TSCreateParams compressed() { - this.compressed = true; + return encoding(EncodingFormat.COMPRESSED); + } + + public TSCreateParams encoding(EncodingFormat encoding) { + this.encoding = encoding; return this; } @@ -52,6 +61,13 @@ public TSCreateParams duplicatePolicy(DuplicatePolicy duplicatePolicy) { return this; } + public TSCreateParams ignore(long maxTimediff, double maxValDiff) { + this.ignore = true; + this.ignoreMaxTimediff = maxTimediff; + this.ignoreMaxValDiff = maxValDiff; + return this; + } + /** * Set label-value pairs * @@ -65,6 +81,9 @@ public TSCreateParams labels(Map labels) { /** * Add label-value pair. Multiple pairs can be added through chaining. + * @param label + * @param value + * @return the object itself */ public TSCreateParams label(String label, String value) { if (this.labels == null) { @@ -81,10 +100,8 @@ public void addParams(CommandArguments args) { args.add(RETENTION).add(toByteArray(retentionPeriod)); } - if (uncompressed) { - args.add(ENCODING).add(UNCOMPRESSED); - } else if (compressed) { - args.add(ENCODING).add(COMPRESSED); + if (encoding != null) { + args.add(ENCODING).add(encoding); } if (chunkSize != null) { @@ -95,6 +112,10 @@ public void addParams(CommandArguments args) { args.add(DUPLICATE_POLICY).add(duplicatePolicy); } + if (ignore) { + args.add(IGNORE).add(ignoreMaxTimediff).add(ignoreMaxValDiff); + } + if (labels != null) { args.add(LABELS); labels.entrySet().forEach((entry) -> args.add(entry.getKey()).add(entry.getValue())); diff --git a/src/main/java/redis/clients/jedis/timeseries/TSIncrOrDecrByParams.java b/src/main/java/redis/clients/jedis/timeseries/TSIncrOrDecrByParams.java new file mode 100644 index 00000000000..fde848fb5a8 --- /dev/null +++ b/src/main/java/redis/clients/jedis/timeseries/TSIncrOrDecrByParams.java @@ -0,0 +1,132 @@ +package redis.clients.jedis.timeseries; + +import static redis.clients.jedis.Protocol.toByteArray; +import static redis.clients.jedis.timeseries.TimeSeriesProtocol.TimeSeriesKeyword.*; + +import java.util.LinkedHashMap; +import java.util.Map; +import redis.clients.jedis.CommandArguments; +import redis.clients.jedis.params.IParams; + +/** + * Represents optional arguments of TS.INCRBY or TS.DECRBY commands. + */ +public class TSIncrOrDecrByParams implements IParams { + + private Long timestamp; + private Long retentionPeriod; + private EncodingFormat encoding; + private Long chunkSize; + private DuplicatePolicy duplicatePolicy; + + private boolean ignore; + private long ignoreMaxTimediff; + private double ignoreMaxValDiff; + + private Map labels; + + public TSIncrOrDecrByParams() { + } + + public static TSIncrOrDecrByParams params() { + return new TSIncrOrDecrByParams(); + } + + public static TSIncrOrDecrByParams incrByParams() { + return new TSIncrOrDecrByParams(); + } + + public static TSIncrOrDecrByParams decrByParams() { + return new TSIncrOrDecrByParams(); + } + + public TSIncrOrDecrByParams timestamp(long timestamp) { + this.timestamp = timestamp; + return this; + } + + public TSIncrOrDecrByParams retention(long retentionPeriod) { + this.retentionPeriod = retentionPeriod; + return this; + } + + public TSIncrOrDecrByParams encoding(EncodingFormat encoding) { + this.encoding = encoding; + return this; + } + + public TSIncrOrDecrByParams chunkSize(long chunkSize) { + this.chunkSize = chunkSize; + return this; + } + + public TSIncrOrDecrByParams duplicatePolicy(DuplicatePolicy duplicatePolicy) { + this.duplicatePolicy = duplicatePolicy; + return this; + } + + public TSIncrOrDecrByParams ignore(long maxTimediff, double maxValDiff) { + this.ignore = true; + this.ignoreMaxTimediff = maxTimediff; + this.ignoreMaxValDiff = maxValDiff; + return this; + } + + /** + * Set label-value pairs + * + * @param labels label-value pairs + * @return the object itself + */ + public TSIncrOrDecrByParams labels(Map labels) { + this.labels = labels; + return this; + } + + /** + * Add label-value pair. Multiple pairs can be added through chaining. + * @param label + * @param value + * @return the object itself + */ + public TSIncrOrDecrByParams label(String label, String value) { + if (this.labels == null) { + this.labels = new LinkedHashMap<>(); + } + this.labels.put(label, value); + return this; + } + + @Override + public void addParams(CommandArguments args) { + + if (timestamp != null) { + args.add(TIMESTAMP).add(timestamp); + } + + if (retentionPeriod != null) { + args.add(RETENTION).add(toByteArray(retentionPeriod)); + } + + if (encoding != null) { + args.add(ENCODING).add(encoding); + } + + if (chunkSize != null) { + args.add(CHUNK_SIZE).add(toByteArray(chunkSize)); + } + + if (duplicatePolicy != null) { + args.add(DUPLICATE_POLICY).add(duplicatePolicy); + } + + if (ignore) { + args.add(IGNORE).add(ignoreMaxTimediff).add(ignoreMaxValDiff); + } + + if (labels != null) { + args.add(LABELS); + labels.entrySet().forEach((entry) -> args.add(entry.getKey()).add(entry.getValue())); + } + } +} diff --git a/src/main/java/redis/clients/jedis/timeseries/TimeSeriesProtocol.java b/src/main/java/redis/clients/jedis/timeseries/TimeSeriesProtocol.java index 2476979f0d0..384a4549218 100644 --- a/src/main/java/redis/clients/jedis/timeseries/TimeSeriesProtocol.java +++ b/src/main/java/redis/clients/jedis/timeseries/TimeSeriesProtocol.java @@ -57,6 +57,7 @@ public enum TimeSeriesKeyword implements Rawable { UNCOMPRESSED, CHUNK_SIZE, DUPLICATE_POLICY, + IGNORE, ON_DUPLICATE, ALIGN, FILTER_BY_TS, diff --git a/src/test/java/redis/clients/jedis/mocked/pipeline/PipeliningBaseTimeSeriesCommandsTest.java b/src/test/java/redis/clients/jedis/mocked/pipeline/PipeliningBaseTimeSeriesCommandsTest.java index 44e653c0116..b8cfb85dc8b 100644 --- a/src/test/java/redis/clients/jedis/mocked/pipeline/PipeliningBaseTimeSeriesCommandsTest.java +++ b/src/test/java/redis/clients/jedis/mocked/pipeline/PipeliningBaseTimeSeriesCommandsTest.java @@ -3,6 +3,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.AbstractMap; @@ -11,17 +12,7 @@ import org.junit.Test; import redis.clients.jedis.Response; -import redis.clients.jedis.timeseries.AggregationType; -import redis.clients.jedis.timeseries.TSAlterParams; -import redis.clients.jedis.timeseries.TSCreateParams; -import redis.clients.jedis.timeseries.TSElement; -import redis.clients.jedis.timeseries.TSGetParams; -import redis.clients.jedis.timeseries.TSInfo; -import redis.clients.jedis.timeseries.TSMGetElement; -import redis.clients.jedis.timeseries.TSMGetParams; -import redis.clients.jedis.timeseries.TSMRangeElements; -import redis.clients.jedis.timeseries.TSMRangeParams; -import redis.clients.jedis.timeseries.TSRangeParams; +import redis.clients.jedis.timeseries.*; public class PipeliningBaseTimeSeriesCommandsTest extends PipeliningBaseMockedTestBase { @@ -57,6 +48,18 @@ public void testTsAddWithTimestampAndParams() { assertThat(response, is(predefinedResponse)); } + @Test + public void testTsAddWithParams() { + TSAddParams addParams = mock(TSAddParams.class); + + when(commandObjects.tsAdd("myTimeSeries", 1000L, 42.0, addParams)).thenReturn(longCommandObject); + + Response response = pipeliningBase.tsAdd("myTimeSeries", 1000L, 42.0, addParams); + + assertThat(commands, contains(longCommandObject)); + assertThat(response, is(predefinedResponse)); + } + @Test public void testTsAlter() { TSAlterParams alterParams = TSAlterParams.alterParams(); @@ -138,6 +141,17 @@ public void testTsDecrByWithTimestamp() { assertThat(response, is(predefinedResponse)); } + @Test + public void testTsDecrByWithParams() { + TSIncrOrDecrByParams DecrByParams = mock(TSIncrOrDecrByParams.class); + when(commandObjects.tsDecrBy("myTimeSeries", 1.0, DecrByParams)).thenReturn(longCommandObject); + + Response response = pipeliningBase.tsDecrBy("myTimeSeries", 1.0, DecrByParams); + + assertThat(commands, contains(longCommandObject)); + assertThat(response, is(predefinedResponse)); + } + @Test public void testTsDel() { when(commandObjects.tsDel("myTimeSeries", 1000L, 2000L)).thenReturn(longCommandObject); @@ -200,6 +214,17 @@ public void testTsIncrByWithTimestamp() { assertThat(response, is(predefinedResponse)); } + @Test + public void testTsIncrByWithParams() { + TSIncrOrDecrByParams incrByParams = mock(TSIncrOrDecrByParams.class); + when(commandObjects.tsIncrBy("myTimeSeries", 1.0, incrByParams)).thenReturn(longCommandObject); + + Response response = pipeliningBase.tsIncrBy("myTimeSeries", 1.0, incrByParams); + + assertThat(commands, contains(longCommandObject)); + assertThat(response, is(predefinedResponse)); + } + @Test public void testTsInfo() { when(commandObjects.tsInfo("myTimeSeries")).thenReturn(tsInfoCommandObject); diff --git a/src/test/java/redis/clients/jedis/mocked/unified/UnifiedJedisTimeSeriesCommandsTest.java b/src/test/java/redis/clients/jedis/mocked/unified/UnifiedJedisTimeSeriesCommandsTest.java index d9e06ce77cb..53c673da49d 100644 --- a/src/test/java/redis/clients/jedis/mocked/unified/UnifiedJedisTimeSeriesCommandsTest.java +++ b/src/test/java/redis/clients/jedis/mocked/unified/UnifiedJedisTimeSeriesCommandsTest.java @@ -15,17 +15,7 @@ import java.util.Map; import org.junit.Test; -import redis.clients.jedis.timeseries.AggregationType; -import redis.clients.jedis.timeseries.TSAlterParams; -import redis.clients.jedis.timeseries.TSCreateParams; -import redis.clients.jedis.timeseries.TSElement; -import redis.clients.jedis.timeseries.TSGetParams; -import redis.clients.jedis.timeseries.TSInfo; -import redis.clients.jedis.timeseries.TSMGetElement; -import redis.clients.jedis.timeseries.TSMGetParams; -import redis.clients.jedis.timeseries.TSMRangeElements; -import redis.clients.jedis.timeseries.TSMRangeParams; -import redis.clients.jedis.timeseries.TSRangeParams; +import redis.clients.jedis.timeseries.*; public class UnifiedJedisTimeSeriesCommandsTest extends UnifiedJedisMockedTestBase { @@ -83,6 +73,25 @@ public void testTsAddWithTimestampAndParams() { verify(commandObjects).tsAdd(key, timestamp, value, createParams); } + @Test + public void testTsAddWithParams() { + String key = "testKey"; + long timestamp = 1582605077000L; + double value = 123.45; + TSAddParams createParams = mock(TSAddParams.class); + long expectedResponse = timestamp; // Timestamp of the added value + + when(commandObjects.tsAdd(key, timestamp, value, createParams)).thenReturn(longCommandObject); + when(commandExecutor.executeCommand(longCommandObject)).thenReturn(expectedResponse); + + long result = jedis.tsAdd(key, timestamp, value, createParams); + + assertEquals(expectedResponse, result); + + verify(commandExecutor).executeCommand(longCommandObject); + verify(commandObjects).tsAdd(key, timestamp, value, createParams); + } + @Test public void testTsAlter() { String key = "testKey"; @@ -194,7 +203,7 @@ public void testTsDecrByWithTimestamp() { String key = "testKey"; double value = 1.5; long timestamp = 1582605077000L; - long expectedResponse = -1L; // Assuming the decrement results in a total of -1 + long expectedResponse = 5L; when(commandObjects.tsDecrBy(key, value, timestamp)).thenReturn(longCommandObject); when(commandExecutor.executeCommand(longCommandObject)).thenReturn(expectedResponse); @@ -207,6 +216,24 @@ public void testTsDecrByWithTimestamp() { verify(commandObjects).tsDecrBy(key, value, timestamp); } + @Test + public void testTsDecrByWithParams() { + String key = "testKey"; + double value = 1.5; + TSIncrOrDecrByParams decrByParams = mock(TSIncrOrDecrByParams.class); + long expectedResponse = 5L; + + when(commandObjects.tsDecrBy(key, value, decrByParams)).thenReturn(longCommandObject); + when(commandExecutor.executeCommand(longCommandObject)).thenReturn(expectedResponse); + + long result = jedis.tsDecrBy(key, value, decrByParams); + + assertEquals(expectedResponse, result); + + verify(commandExecutor).executeCommand(longCommandObject); + verify(commandObjects).tsDecrBy(key, value, decrByParams); + } + @Test public void testTsDel() { String key = "testKey"; @@ -297,7 +324,7 @@ public void testTsIncrByWithTimestamp() { String key = "testKey"; double value = 2.5; long timestamp = 1582605077000L; - long expectedResponse = 5L; // Assuming the increment results in a total of 5 + long expectedResponse = 5L; when(commandObjects.tsIncrBy(key, value, timestamp)).thenReturn(longCommandObject); when(commandExecutor.executeCommand(longCommandObject)).thenReturn(expectedResponse); @@ -310,6 +337,24 @@ public void testTsIncrByWithTimestamp() { verify(commandObjects).tsIncrBy(key, value, timestamp); } + @Test + public void testTsIncrByWithParams() { + String key = "testKey"; + double value = 2.5; + TSIncrOrDecrByParams incrByParams = mock(TSIncrOrDecrByParams.class); + long expectedResponse = 5L; + + when(commandObjects.tsIncrBy(key, value, incrByParams)).thenReturn(longCommandObject); + when(commandExecutor.executeCommand(longCommandObject)).thenReturn(expectedResponse); + + long result = jedis.tsIncrBy(key, value, incrByParams); + + assertEquals(expectedResponse, result); + + verify(commandExecutor).executeCommand(longCommandObject); + verify(commandObjects).tsIncrBy(key, value, incrByParams); + } + @Test public void testTsInfo() { String key = "testKey"; diff --git a/src/test/java/redis/clients/jedis/modules/timeseries/TimeSeriesTest.java b/src/test/java/redis/clients/jedis/modules/timeseries/TimeSeriesTest.java index fe0f7d1604a..dd0688f080f 100644 --- a/src/test/java/redis/clients/jedis/modules/timeseries/TimeSeriesTest.java +++ b/src/test/java/redis/clients/jedis/modules/timeseries/TimeSeriesTest.java @@ -122,6 +122,23 @@ public void testAlter() { assertEquals("v33", info.getLabel("l3")); } + @Test + public void createAndAlterParams() { + Map labels = new HashMap<>(); + labels.put("l1", "v1"); + labels.put("l2", "v2"); + + assertEquals("OK", client.tsCreate("ts-params", + TSCreateParams.createParams().retention(60000).encoding(EncodingFormat.UNCOMPRESSED).chunkSize(4096) + .duplicatePolicy(DuplicatePolicy.BLOCK).ignore(50, 12.5).labels(labels))); + + labels.put("l1", "v11"); + labels.remove("l2"); + labels.put("l3", "v33"); + assertEquals("OK", client.tsAlter("ts-params", TSAlterParams.alterParams().retention(15000).chunkSize(8192) + .duplicatePolicy(DuplicatePolicy.SUM).ignore(50, 12.5).labels(labels))); + } + @Test public void testRule() { assertEquals("OK", client.tsCreate("source")); @@ -147,6 +164,21 @@ public void testRule() { } } + @Test + public void addParams() { + Map labels = new HashMap<>(); + labels.put("l1", "v1"); + labels.put("l2", "v2"); + + assertEquals(1000L, client.tsAdd("add1", 1000L, 1.1, + TSAddParams.addParams().retention(10000).encoding(EncodingFormat.UNCOMPRESSED).chunkSize(1000) + .duplicatePolicy(DuplicatePolicy.FIRST).onDuplicate(DuplicatePolicy.LAST).ignore(50, 12.5).labels(labels))); + + assertEquals(1000L, client.tsAdd("add2", 1000L, 1.1, + TSAddParams.addParams().retention(10000).encoding(EncodingFormat.COMPRESSED).chunkSize(1000) + .duplicatePolicy(DuplicatePolicy.MIN).onDuplicate(DuplicatePolicy.MAX).ignore(50, 12.5).labels(labels))); + } + @Test public void testAdd() { Map labels = new HashMap<>(); @@ -414,6 +446,29 @@ public void testIncrByDecrBy() throws InterruptedException { client.tsDecrBy("seriesIncDec", 33); } + @Test + public void incrByDecrByParams() { + Map labels = new HashMap<>(); + labels.put("l1", "v1"); + labels.put("l2", "v2"); + + assertEquals(1000L, client.tsIncrBy("incr1", 1.1, + TSIncrOrDecrByParams.incrByParams().timestamp(1000).retention(10000).encoding(EncodingFormat.UNCOMPRESSED) + .chunkSize(1000).duplicatePolicy(DuplicatePolicy.FIRST).ignore(50, 12.5).labels(labels))); + + assertEquals(1000L, client.tsIncrBy("incr2", 1.1, + TSIncrOrDecrByParams.incrByParams().timestamp(1000).retention(10000).encoding(EncodingFormat.COMPRESSED) + .chunkSize(1000).duplicatePolicy(DuplicatePolicy.MIN).ignore(50, 12.5).labels(labels))); + + assertEquals(1000L, client.tsDecrBy("decr1", 1.1, + TSIncrOrDecrByParams.decrByParams().timestamp(1000).retention(10000).encoding(EncodingFormat.COMPRESSED) + .chunkSize(1000).duplicatePolicy(DuplicatePolicy.LAST).ignore(50, 12.5).labels(labels))); + + assertEquals(1000L, client.tsDecrBy("decr2", 1.1, + TSIncrOrDecrByParams.decrByParams().timestamp(1000).retention(10000).encoding(EncodingFormat.UNCOMPRESSED) + .chunkSize(1000).duplicatePolicy(DuplicatePolicy.MAX).ignore(50, 12.5).labels(labels))); + } + @Test public void align() { client.tsAdd("align", 1, 10d); From beb39a1f3504d7e92d7143ece681d5bd7b86a99b Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Fri, 14 Jun 2024 19:59:25 +0600 Subject: [PATCH 03/63] Polish #3860: Separate params for TS.INCRBY and TS.DECRBY (#3863) --- .../redis/clients/jedis/CommandObjects.java | 4 +- .../redis/clients/jedis/PipeliningBase.java | 4 +- .../redis/clients/jedis/UnifiedJedis.java | 4 +- .../timeseries/RedisTimeSeriesCommands.java | 4 +- .../RedisTimeSeriesPipelineCommands.java | 4 +- ...DecrByParams.java => TSArithByParams.java} | 48 +++++++------------ .../jedis/timeseries/TSDecrByParams.java | 14 ++++++ .../jedis/timeseries/TSIncrByParams.java | 14 ++++++ .../PipeliningBaseTimeSeriesCommandsTest.java | 8 ++-- .../UnifiedJedisTimeSeriesCommandsTest.java | 4 +- .../modules/timeseries/TimeSeriesTest.java | 8 ++-- 11 files changed, 66 insertions(+), 50 deletions(-) rename src/main/java/redis/clients/jedis/timeseries/{TSIncrOrDecrByParams.java => TSArithByParams.java} (67%) create mode 100644 src/main/java/redis/clients/jedis/timeseries/TSDecrByParams.java create mode 100644 src/main/java/redis/clients/jedis/timeseries/TSIncrByParams.java diff --git a/src/main/java/redis/clients/jedis/CommandObjects.java b/src/main/java/redis/clients/jedis/CommandObjects.java index 421b81c4e2d..cbf0e197239 100644 --- a/src/main/java/redis/clients/jedis/CommandObjects.java +++ b/src/main/java/redis/clients/jedis/CommandObjects.java @@ -3974,7 +3974,7 @@ public final CommandObject tsIncrBy(String key, double value, long timesta .add(TimeSeriesKeyword.TIMESTAMP).add(timestamp), BuilderFactory.LONG); } - public final CommandObject tsIncrBy(String key, double addend, TSIncrOrDecrByParams incrByParams) { + public final CommandObject tsIncrBy(String key, double addend, TSIncrByParams incrByParams) { return new CommandObject<>(commandArguments(TimeSeriesCommand.INCRBY).key(key).add(addend) .addParams(incrByParams), BuilderFactory.LONG); } @@ -3988,7 +3988,7 @@ public final CommandObject tsDecrBy(String key, double value, long timesta .add(TimeSeriesKeyword.TIMESTAMP).add(timestamp), BuilderFactory.LONG); } - public final CommandObject tsDecrBy(String key, double subtrahend, TSIncrOrDecrByParams decrByParams) { + public final CommandObject tsDecrBy(String key, double subtrahend, TSDecrByParams decrByParams) { return new CommandObject<>(commandArguments(TimeSeriesCommand.DECRBY).key(key).add(subtrahend) .addParams(decrByParams), BuilderFactory.LONG); } diff --git a/src/main/java/redis/clients/jedis/PipeliningBase.java b/src/main/java/redis/clients/jedis/PipeliningBase.java index 9967a2e6940..ffe1c2a31c7 100644 --- a/src/main/java/redis/clients/jedis/PipeliningBase.java +++ b/src/main/java/redis/clients/jedis/PipeliningBase.java @@ -3969,7 +3969,7 @@ public Response tsIncrBy(String key, double value, long timestamp) { } @Override - public Response tsIncrBy(String key, double addend, TSIncrOrDecrByParams incrByParams) { + public Response tsIncrBy(String key, double addend, TSIncrByParams incrByParams) { return appendCommand(commandObjects.tsIncrBy(key, addend, incrByParams)); } @@ -3984,7 +3984,7 @@ public Response tsDecrBy(String key, double value, long timestamp) { } @Override - public Response tsDecrBy(String key, double subtrahend, TSIncrOrDecrByParams decrByParams) { + public Response tsDecrBy(String key, double subtrahend, TSDecrByParams decrByParams) { return appendCommand(commandObjects.tsDecrBy(key, subtrahend, decrByParams)); } diff --git a/src/main/java/redis/clients/jedis/UnifiedJedis.java b/src/main/java/redis/clients/jedis/UnifiedJedis.java index 87ba0d8a142..398b1302ba2 100644 --- a/src/main/java/redis/clients/jedis/UnifiedJedis.java +++ b/src/main/java/redis/clients/jedis/UnifiedJedis.java @@ -4494,7 +4494,7 @@ public long tsIncrBy(String key, double value, long timestamp) { } @Override - public long tsIncrBy(String key, double addend, TSIncrOrDecrByParams incrByParams) { + public long tsIncrBy(String key, double addend, TSIncrByParams incrByParams) { return executeCommand(commandObjects.tsIncrBy(key, addend, incrByParams)); } @@ -4509,7 +4509,7 @@ public long tsDecrBy(String key, double value, long timestamp) { } @Override - public long tsDecrBy(String key, double subtrahend, TSIncrOrDecrByParams decrByParams) { + public long tsDecrBy(String key, double subtrahend, TSDecrByParams decrByParams) { return executeCommand(commandObjects.tsDecrBy(key, subtrahend, decrByParams)); } diff --git a/src/main/java/redis/clients/jedis/timeseries/RedisTimeSeriesCommands.java b/src/main/java/redis/clients/jedis/timeseries/RedisTimeSeriesCommands.java index 67c1b26fcf8..513027c4cf2 100644 --- a/src/main/java/redis/clients/jedis/timeseries/RedisTimeSeriesCommands.java +++ b/src/main/java/redis/clients/jedis/timeseries/RedisTimeSeriesCommands.java @@ -113,7 +113,7 @@ public interface RedisTimeSeriesCommands { * @param incrByParams * @return timestamp */ - long tsIncrBy(String key, double addend, TSIncrOrDecrByParams incrByParams); + long tsIncrBy(String key, double addend, TSIncrByParams incrByParams); long tsDecrBy(String key, double value); @@ -134,7 +134,7 @@ public interface RedisTimeSeriesCommands { * @param decrByParams * @return timestamp */ - long tsDecrBy(String key, double subtrahend, TSIncrOrDecrByParams decrByParams); + long tsDecrBy(String key, double subtrahend, TSDecrByParams decrByParams); /** * {@code TS.RANGE key fromTimestamp toTimestamp} diff --git a/src/main/java/redis/clients/jedis/timeseries/RedisTimeSeriesPipelineCommands.java b/src/main/java/redis/clients/jedis/timeseries/RedisTimeSeriesPipelineCommands.java index b3304716ddd..71b6e4c8816 100644 --- a/src/main/java/redis/clients/jedis/timeseries/RedisTimeSeriesPipelineCommands.java +++ b/src/main/java/redis/clients/jedis/timeseries/RedisTimeSeriesPipelineCommands.java @@ -29,13 +29,13 @@ public interface RedisTimeSeriesPipelineCommands { Response tsIncrBy(String key, double value, long timestamp); - Response tsIncrBy(String key, double addend, TSIncrOrDecrByParams incrByParams); + Response tsIncrBy(String key, double addend, TSIncrByParams incrByParams); Response tsDecrBy(String key, double value); Response tsDecrBy(String key, double value, long timestamp); - Response tsDecrBy(String key, double subtrahend, TSIncrOrDecrByParams decrByParams); + Response tsDecrBy(String key, double subtrahend, TSDecrByParams decrByParams); Response> tsRange(String key, long fromTimestamp, long toTimestamp); diff --git a/src/main/java/redis/clients/jedis/timeseries/TSIncrOrDecrByParams.java b/src/main/java/redis/clients/jedis/timeseries/TSArithByParams.java similarity index 67% rename from src/main/java/redis/clients/jedis/timeseries/TSIncrOrDecrByParams.java rename to src/main/java/redis/clients/jedis/timeseries/TSArithByParams.java index fde848fb5a8..1bc3df1c55b 100644 --- a/src/main/java/redis/clients/jedis/timeseries/TSIncrOrDecrByParams.java +++ b/src/main/java/redis/clients/jedis/timeseries/TSArithByParams.java @@ -11,7 +11,7 @@ /** * Represents optional arguments of TS.INCRBY or TS.DECRBY commands. */ -public class TSIncrOrDecrByParams implements IParams { +class TSArithByParams> implements IParams { private Long timestamp; private Long retentionPeriod; @@ -25,51 +25,39 @@ public class TSIncrOrDecrByParams implements IParams { private Map labels; - public TSIncrOrDecrByParams() { + TSArithByParams() { } - public static TSIncrOrDecrByParams params() { - return new TSIncrOrDecrByParams(); - } - - public static TSIncrOrDecrByParams incrByParams() { - return new TSIncrOrDecrByParams(); - } - - public static TSIncrOrDecrByParams decrByParams() { - return new TSIncrOrDecrByParams(); - } - - public TSIncrOrDecrByParams timestamp(long timestamp) { + public T timestamp(long timestamp) { this.timestamp = timestamp; - return this; + return (T) this; } - public TSIncrOrDecrByParams retention(long retentionPeriod) { + public T retention(long retentionPeriod) { this.retentionPeriod = retentionPeriod; - return this; + return (T) this; } - public TSIncrOrDecrByParams encoding(EncodingFormat encoding) { + public T encoding(EncodingFormat encoding) { this.encoding = encoding; - return this; + return (T) this; } - public TSIncrOrDecrByParams chunkSize(long chunkSize) { + public T chunkSize(long chunkSize) { this.chunkSize = chunkSize; - return this; + return (T) this; } - public TSIncrOrDecrByParams duplicatePolicy(DuplicatePolicy duplicatePolicy) { + public T duplicatePolicy(DuplicatePolicy duplicatePolicy) { this.duplicatePolicy = duplicatePolicy; - return this; + return (T) this; } - public TSIncrOrDecrByParams ignore(long maxTimediff, double maxValDiff) { + public T ignore(long maxTimediff, double maxValDiff) { this.ignore = true; this.ignoreMaxTimediff = maxTimediff; this.ignoreMaxValDiff = maxValDiff; - return this; + return (T) this; } /** @@ -78,9 +66,9 @@ public TSIncrOrDecrByParams ignore(long maxTimediff, double maxValDiff) { * @param labels label-value pairs * @return the object itself */ - public TSIncrOrDecrByParams labels(Map labels) { + public T labels(Map labels) { this.labels = labels; - return this; + return (T) this; } /** @@ -89,12 +77,12 @@ public TSIncrOrDecrByParams labels(Map labels) { * @param value * @return the object itself */ - public TSIncrOrDecrByParams label(String label, String value) { + public T label(String label, String value) { if (this.labels == null) { this.labels = new LinkedHashMap<>(); } this.labels.put(label, value); - return this; + return (T) this; } @Override diff --git a/src/main/java/redis/clients/jedis/timeseries/TSDecrByParams.java b/src/main/java/redis/clients/jedis/timeseries/TSDecrByParams.java new file mode 100644 index 00000000000..afb776ad6bc --- /dev/null +++ b/src/main/java/redis/clients/jedis/timeseries/TSDecrByParams.java @@ -0,0 +1,14 @@ +package redis.clients.jedis.timeseries; + +/** + * Represents optional arguments of TS.DECRBY command. + */ +public class TSDecrByParams extends TSArithByParams { + + public TSDecrByParams() { + } + + public static TSDecrByParams decrByParams() { + return new TSDecrByParams(); + } +} diff --git a/src/main/java/redis/clients/jedis/timeseries/TSIncrByParams.java b/src/main/java/redis/clients/jedis/timeseries/TSIncrByParams.java new file mode 100644 index 00000000000..db76a7ae265 --- /dev/null +++ b/src/main/java/redis/clients/jedis/timeseries/TSIncrByParams.java @@ -0,0 +1,14 @@ +package redis.clients.jedis.timeseries; + +/** + * Represents optional arguments of TS.INCRBY command. + */ +public class TSIncrByParams extends TSArithByParams { + + public TSIncrByParams() { + } + + public static TSIncrByParams incrByParams() { + return new TSIncrByParams(); + } +} diff --git a/src/test/java/redis/clients/jedis/mocked/pipeline/PipeliningBaseTimeSeriesCommandsTest.java b/src/test/java/redis/clients/jedis/mocked/pipeline/PipeliningBaseTimeSeriesCommandsTest.java index b8cfb85dc8b..671fc83ef9f 100644 --- a/src/test/java/redis/clients/jedis/mocked/pipeline/PipeliningBaseTimeSeriesCommandsTest.java +++ b/src/test/java/redis/clients/jedis/mocked/pipeline/PipeliningBaseTimeSeriesCommandsTest.java @@ -143,10 +143,10 @@ public void testTsDecrByWithTimestamp() { @Test public void testTsDecrByWithParams() { - TSIncrOrDecrByParams DecrByParams = mock(TSIncrOrDecrByParams.class); - when(commandObjects.tsDecrBy("myTimeSeries", 1.0, DecrByParams)).thenReturn(longCommandObject); + TSDecrByParams decrByParams = mock(TSDecrByParams.class); + when(commandObjects.tsDecrBy("myTimeSeries", 1.0, decrByParams)).thenReturn(longCommandObject); - Response response = pipeliningBase.tsDecrBy("myTimeSeries", 1.0, DecrByParams); + Response response = pipeliningBase.tsDecrBy("myTimeSeries", 1.0, decrByParams); assertThat(commands, contains(longCommandObject)); assertThat(response, is(predefinedResponse)); @@ -216,7 +216,7 @@ public void testTsIncrByWithTimestamp() { @Test public void testTsIncrByWithParams() { - TSIncrOrDecrByParams incrByParams = mock(TSIncrOrDecrByParams.class); + TSIncrByParams incrByParams = mock(TSIncrByParams.class); when(commandObjects.tsIncrBy("myTimeSeries", 1.0, incrByParams)).thenReturn(longCommandObject); Response response = pipeliningBase.tsIncrBy("myTimeSeries", 1.0, incrByParams); diff --git a/src/test/java/redis/clients/jedis/mocked/unified/UnifiedJedisTimeSeriesCommandsTest.java b/src/test/java/redis/clients/jedis/mocked/unified/UnifiedJedisTimeSeriesCommandsTest.java index 53c673da49d..bfc17620ea1 100644 --- a/src/test/java/redis/clients/jedis/mocked/unified/UnifiedJedisTimeSeriesCommandsTest.java +++ b/src/test/java/redis/clients/jedis/mocked/unified/UnifiedJedisTimeSeriesCommandsTest.java @@ -220,7 +220,7 @@ public void testTsDecrByWithTimestamp() { public void testTsDecrByWithParams() { String key = "testKey"; double value = 1.5; - TSIncrOrDecrByParams decrByParams = mock(TSIncrOrDecrByParams.class); + TSDecrByParams decrByParams = mock(TSDecrByParams.class); long expectedResponse = 5L; when(commandObjects.tsDecrBy(key, value, decrByParams)).thenReturn(longCommandObject); @@ -341,7 +341,7 @@ public void testTsIncrByWithTimestamp() { public void testTsIncrByWithParams() { String key = "testKey"; double value = 2.5; - TSIncrOrDecrByParams incrByParams = mock(TSIncrOrDecrByParams.class); + TSIncrByParams incrByParams = mock(TSIncrByParams.class); long expectedResponse = 5L; when(commandObjects.tsIncrBy(key, value, incrByParams)).thenReturn(longCommandObject); diff --git a/src/test/java/redis/clients/jedis/modules/timeseries/TimeSeriesTest.java b/src/test/java/redis/clients/jedis/modules/timeseries/TimeSeriesTest.java index dd0688f080f..723e914d473 100644 --- a/src/test/java/redis/clients/jedis/modules/timeseries/TimeSeriesTest.java +++ b/src/test/java/redis/clients/jedis/modules/timeseries/TimeSeriesTest.java @@ -453,19 +453,19 @@ public void incrByDecrByParams() { labels.put("l2", "v2"); assertEquals(1000L, client.tsIncrBy("incr1", 1.1, - TSIncrOrDecrByParams.incrByParams().timestamp(1000).retention(10000).encoding(EncodingFormat.UNCOMPRESSED) + TSIncrByParams.incrByParams().timestamp(1000).retention(10000).encoding(EncodingFormat.UNCOMPRESSED) .chunkSize(1000).duplicatePolicy(DuplicatePolicy.FIRST).ignore(50, 12.5).labels(labels))); assertEquals(1000L, client.tsIncrBy("incr2", 1.1, - TSIncrOrDecrByParams.incrByParams().timestamp(1000).retention(10000).encoding(EncodingFormat.COMPRESSED) + TSIncrByParams.incrByParams().timestamp(1000).retention(10000).encoding(EncodingFormat.COMPRESSED) .chunkSize(1000).duplicatePolicy(DuplicatePolicy.MIN).ignore(50, 12.5).labels(labels))); assertEquals(1000L, client.tsDecrBy("decr1", 1.1, - TSIncrOrDecrByParams.decrByParams().timestamp(1000).retention(10000).encoding(EncodingFormat.COMPRESSED) + TSDecrByParams.decrByParams().timestamp(1000).retention(10000).encoding(EncodingFormat.COMPRESSED) .chunkSize(1000).duplicatePolicy(DuplicatePolicy.LAST).ignore(50, 12.5).labels(labels))); assertEquals(1000L, client.tsDecrBy("decr2", 1.1, - TSIncrOrDecrByParams.decrByParams().timestamp(1000).retention(10000).encoding(EncodingFormat.UNCOMPRESSED) + TSDecrByParams.decrByParams().timestamp(1000).retention(10000).encoding(EncodingFormat.UNCOMPRESSED) .chunkSize(1000).duplicatePolicy(DuplicatePolicy.MAX).ignore(50, 12.5).labels(labels))); } From ec0e4d05da3e9b01dc6ede84be5ff13d200a5a24 Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Sat, 15 Jun 2024 20:54:54 +0600 Subject: [PATCH 04/63] Support indexing of MISSING and EMPTY values (#3866) --- .../clients/jedis/search/FTSearchParams.java | 2 +- .../redis/clients/jedis/search/Query.java | 2 +- .../redis/clients/jedis/search/Schema.java | 2 +- .../clients/jedis/search/SearchProtocol.java | 2 +- .../jedis/search/aggr/AggregationBuilder.java | 4 +- .../jedis/search/querybuilder/QueryNode.java | 6 +- .../jedis/search/schemafields/GeoField.java | 33 ++++- .../search/schemafields/GeoShapeField.java | 23 +++- .../search/schemafields/NumericField.java | 14 +- .../jedis/search/schemafields/TagField.java | 78 +++++++---- .../jedis/search/schemafields/TextField.java | 85 +++++++----- .../search/schemafields/VectorField.java | 15 ++- .../modules/search/SearchWithParamsTest.java | 126 ++++++++++++++++-- 13 files changed, 307 insertions(+), 85 deletions(-) diff --git a/src/main/java/redis/clients/jedis/search/FTSearchParams.java b/src/main/java/redis/clients/jedis/search/FTSearchParams.java index d2ca5d6d946..2d4c6e00576 100644 --- a/src/main/java/redis/clients/jedis/search/FTSearchParams.java +++ b/src/main/java/redis/clients/jedis/search/FTSearchParams.java @@ -143,7 +143,7 @@ public void addParams(CommandArguments args) { } if (params != null && !params.isEmpty()) { - args.add(PARAMS).add(params.size() * 2); + args.add(PARAMS).add(params.size() << 1); params.entrySet().forEach(entry -> args.add(entry.getKey()).add(entry.getValue())); } diff --git a/src/main/java/redis/clients/jedis/search/Query.java b/src/main/java/redis/clients/jedis/search/Query.java index 66cba96acf5..66de3ce074c 100644 --- a/src/main/java/redis/clients/jedis/search/Query.java +++ b/src/main/java/redis/clients/jedis/search/Query.java @@ -292,7 +292,7 @@ public void addParams(CommandArguments args) { if (_params != null && _params.size() > 0) { args.add(SearchKeyword.PARAMS.getRaw()); - args.add(_params.size() * 2); + args.add(_params.size() << 1); for (Map.Entry entry : _params.entrySet()) { args.add(entry.getKey()); args.add(entry.getValue()); diff --git a/src/main/java/redis/clients/jedis/search/Schema.java b/src/main/java/redis/clients/jedis/search/Schema.java index 17f01cc9de2..1403aab5566 100644 --- a/src/main/java/redis/clients/jedis/search/Schema.java +++ b/src/main/java/redis/clients/jedis/search/Schema.java @@ -365,7 +365,7 @@ public VectorField(String name, VectorAlgo algorithm, Map attrib @Override public void addTypeArgs(CommandArguments args) { args.add(algorithm); - args.add(attributes.size() * 2); + args.add(attributes.size() << 1); for (Map.Entry entry : attributes.entrySet()) { args.add(entry.getKey()); args.add(entry.getValue()); diff --git a/src/main/java/redis/clients/jedis/search/SearchProtocol.java b/src/main/java/redis/clients/jedis/search/SearchProtocol.java index 7f2ad482fb7..64482f4fbb2 100644 --- a/src/main/java/redis/clients/jedis/search/SearchProtocol.java +++ b/src/main/java/redis/clients/jedis/search/SearchProtocol.java @@ -56,7 +56,7 @@ public enum SearchKeyword implements Rawable { LANGUAGE_FIELD, SCORE, SCORE_FIELD, SCORER, PARAMS, AS, DIALECT, SLOP, TIMEOUT, INORDER, EXPANDER, MAXTEXTFIELDS, SKIPINITIALSCAN, WITHSUFFIXTRIE, NOSTEM, NOINDEX, PHONETIC, WEIGHT, CASESENSITIVE, LOAD, APPLY, GROUPBY, MAXIDLE, WITHCURSOR, DISTANCE, TERMS, INCLUDE, EXCLUDE, - SEARCH, AGGREGATE, QUERY, LIMITED, COUNT, REDUCE; + SEARCH, AGGREGATE, QUERY, LIMITED, COUNT, REDUCE, INDEXMISSING, INDEXEMPTY; private final byte[] raw; diff --git a/src/main/java/redis/clients/jedis/search/aggr/AggregationBuilder.java b/src/main/java/redis/clients/jedis/search/aggr/AggregationBuilder.java index eb8e039d023..7c8b5473392 100644 --- a/src/main/java/redis/clients/jedis/search/aggr/AggregationBuilder.java +++ b/src/main/java/redis/clients/jedis/search/aggr/AggregationBuilder.java @@ -66,7 +66,7 @@ public AggregationBuilder limit(int count) { public AggregationBuilder sortBy(SortedField... fields) { aggrArgs.add(SearchKeyword.SORTBY); - aggrArgs.add(Integer.toString(fields.length * 2)); + aggrArgs.add(fields.length << 1); for (SortedField field : fields) { aggrArgs.add(field.getField()); aggrArgs.add(field.getOrder()); @@ -172,7 +172,7 @@ public AggregationBuilder timeout(long timeout) { public AggregationBuilder params(Map params) { aggrArgs.add(SearchKeyword.PARAMS); - aggrArgs.add(params.size() * 2); + aggrArgs.add(params.size() << 1); params.forEach((k, v) -> { aggrArgs.add(k); aggrArgs.add(v); diff --git a/src/main/java/redis/clients/jedis/search/querybuilder/QueryNode.java b/src/main/java/redis/clients/jedis/search/querybuilder/QueryNode.java index bc64374a5af..f5759b553de 100644 --- a/src/main/java/redis/clients/jedis/search/querybuilder/QueryNode.java +++ b/src/main/java/redis/clients/jedis/search/querybuilder/QueryNode.java @@ -61,11 +61,11 @@ public QueryNode add(Node... nodes) { protected boolean shouldParenthesize(Parenthesize mode) { if (mode == Parenthesize.ALWAYS) { return true; - } - if (mode == Parenthesize.NEVER) { + } else if (mode == Parenthesize.NEVER) { return false; + } else { + return children.size() > 1; } - return children.size() > 1; } @Override diff --git a/src/main/java/redis/clients/jedis/search/schemafields/GeoField.java b/src/main/java/redis/clients/jedis/search/schemafields/GeoField.java index 7ea421ab4f9..c5878f21b83 100644 --- a/src/main/java/redis/clients/jedis/search/schemafields/GeoField.java +++ b/src/main/java/redis/clients/jedis/search/schemafields/GeoField.java @@ -1,12 +1,16 @@ package redis.clients.jedis.search.schemafields; -import static redis.clients.jedis.search.SearchProtocol.SearchKeyword.GEO; +import static redis.clients.jedis.search.SearchProtocol.SearchKeyword.*; import redis.clients.jedis.CommandArguments; import redis.clients.jedis.search.FieldName; public class GeoField extends SchemaField { + private boolean indexMissing; + private boolean sortable; + private boolean noIndex; + public GeoField(String fieldName) { super(fieldName); } @@ -29,9 +33,36 @@ public GeoField as(String attribute) { return this; } + public GeoField indexMissing() { + this.indexMissing = true; + return this; + } + + public GeoField sortable() { + this.sortable = true; + return this; + } + + public GeoField noIndex() { + this.noIndex = true; + return this; + } + @Override public void addParams(CommandArguments args) { args.addParams(fieldName); args.add(GEO); + + if (indexMissing) { + args.add(INDEXMISSING); + } + + if (sortable) { + args.add(SORTABLE); + } + + if (noIndex) { + args.add(NOINDEX); + } } } diff --git a/src/main/java/redis/clients/jedis/search/schemafields/GeoShapeField.java b/src/main/java/redis/clients/jedis/search/schemafields/GeoShapeField.java index dd3b45e59ee..fedfed1297c 100644 --- a/src/main/java/redis/clients/jedis/search/schemafields/GeoShapeField.java +++ b/src/main/java/redis/clients/jedis/search/schemafields/GeoShapeField.java @@ -1,6 +1,6 @@ package redis.clients.jedis.search.schemafields; -import static redis.clients.jedis.search.SearchProtocol.SearchKeyword.GEOSHAPE; +import static redis.clients.jedis.search.SearchProtocol.SearchKeyword.*; import redis.clients.jedis.CommandArguments; import redis.clients.jedis.search.FieldName; @@ -22,6 +22,9 @@ public enum CoordinateSystem { private final CoordinateSystem system; + private boolean indexMissing; + private boolean noIndex; + public GeoShapeField(String fieldName, CoordinateSystem system) { super(fieldName); this.system = system; @@ -42,8 +45,26 @@ public GeoShapeField as(String attribute) { return this; } + public GeoShapeField indexMissing() { + this.indexMissing = true; + return this; + } + + public GeoShapeField noIndex() { + this.noIndex = true; + return this; + } + @Override public void addParams(CommandArguments args) { args.addParams(fieldName).add(GEOSHAPE).add(system); + + if (indexMissing) { + args.add(INDEXMISSING); + } + + if (noIndex) { + args.add(NOINDEX); + } } } diff --git a/src/main/java/redis/clients/jedis/search/schemafields/NumericField.java b/src/main/java/redis/clients/jedis/search/schemafields/NumericField.java index e1e39ef724f..244cc42640b 100644 --- a/src/main/java/redis/clients/jedis/search/schemafields/NumericField.java +++ b/src/main/java/redis/clients/jedis/search/schemafields/NumericField.java @@ -1,14 +1,13 @@ package redis.clients.jedis.search.schemafields; -import static redis.clients.jedis.search.SearchProtocol.SearchKeyword.NOINDEX; -import static redis.clients.jedis.search.SearchProtocol.SearchKeyword.NUMERIC; -import static redis.clients.jedis.search.SearchProtocol.SearchKeyword.SORTABLE; +import static redis.clients.jedis.search.SearchProtocol.SearchKeyword.*; import redis.clients.jedis.CommandArguments; import redis.clients.jedis.search.FieldName; public class NumericField extends SchemaField { + private boolean indexMissing; private boolean sortable; private boolean noIndex; @@ -34,6 +33,11 @@ public NumericField as(String attribute) { return this; } + public NumericField indexMissing() { + this.indexMissing = true; + return this; + } + /** * Sorts the results by the value of this field. */ @@ -55,6 +59,10 @@ public void addParams(CommandArguments args) { args.addParams(fieldName); args.add(NUMERIC); + if (indexMissing) { + args.add(INDEXMISSING); + } + if (sortable) { args.add(SORTABLE); } diff --git a/src/main/java/redis/clients/jedis/search/schemafields/TagField.java b/src/main/java/redis/clients/jedis/search/schemafields/TagField.java index 407c4dbddc4..451b12aad5a 100644 --- a/src/main/java/redis/clients/jedis/search/schemafields/TagField.java +++ b/src/main/java/redis/clients/jedis/search/schemafields/TagField.java @@ -8,12 +8,14 @@ public class TagField extends SchemaField { - private boolean sortable; - private boolean sortableUNF; - private boolean noIndex; + private boolean indexMissing; + private boolean indexEmpty; private byte[] separator; private boolean caseSensitive; private boolean withSuffixTrie; + private boolean sortable; + private boolean sortableUNF; + private boolean noIndex; public TagField(String fieldName) { super(fieldName); @@ -37,39 +39,19 @@ public TagField as(String attribute) { return this; } - /** - * Sorts the results by the value of this field. - */ - public TagField sortable() { - this.sortable = true; - return this; - } - - /** - * Sorts the results by the value of this field without normalization. - */ - public TagField sortableUNF() { - this.sortableUNF = true; + public TagField indexMissing() { + this.indexMissing = true; return this; } - /** - * @see TextField#sortableUNF() - */ - public TagField sortableUnNormalizedForm() { - return sortableUNF(); - } - - /** - * Avoid indexing. - */ - public TagField noIndex() { - this.noIndex = true; + public TagField indexEmpty() { + this.indexEmpty = true; return this; } /** * Indicates how the text contained in the attribute is to be split into individual tags. + * @param separator */ public TagField separator(char separator) { if (separator < 128) { @@ -97,11 +79,51 @@ public TagField withSuffixTrie() { return this; } + /** + * Sorts the results by the value of this field. + */ + public TagField sortable() { + this.sortable = true; + return this; + } + + /** + * Sorts the results by the value of this field without normalization. + */ + public TagField sortableUNF() { + this.sortableUNF = true; + return this; + } + + /** + * @deprecated Use {@code TagField#sortableUNF()}. + * @see TagField#sortableUNF() + */ + @Deprecated + public TagField sortableUnNormalizedForm() { + return sortableUNF(); + } + + /** + * Avoid indexing. + */ + public TagField noIndex() { + this.noIndex = true; + return this; + } + @Override public void addParams(CommandArguments args) { args.addParams(fieldName); args.add(TAG); + if (indexMissing) { + args.add(INDEXMISSING); + } + if (indexEmpty) { + args.add(INDEXEMPTY); + } + if (separator != null) { args.add(SEPARATOR).add(separator); } diff --git a/src/main/java/redis/clients/jedis/search/schemafields/TextField.java b/src/main/java/redis/clients/jedis/search/schemafields/TextField.java index 573cae90a3c..293104a1895 100644 --- a/src/main/java/redis/clients/jedis/search/schemafields/TextField.java +++ b/src/main/java/redis/clients/jedis/search/schemafields/TextField.java @@ -7,13 +7,15 @@ public class TextField extends SchemaField { - private boolean sortable; - private boolean sortableUNF; + private boolean indexMissing; + private boolean indexEmpty; + private Double weight; private boolean noStem; - private boolean noIndex; private String phoneticMatcher; - private Double weight; private boolean withSuffixTrie; + private boolean sortable; + private boolean sortableUNF; + private boolean noIndex; public TextField(String fieldName) { super(fieldName); @@ -37,27 +39,24 @@ public TextField as(String attribute) { return this; } - /** - * Sorts the results by the value of this field. - */ - public TextField sortable() { - this.sortable = true; + public TextField indexMissing() { + this.indexMissing = true; return this; } - /** - * Sorts the results by the value of this field without normalization. - */ - public TextField sortableUNF() { - this.sortableUNF = true; + public TextField indexEmpty() { + this.indexEmpty = true; return this; } /** - * @see TextField#sortableUNF() + * Declares the importance of this attribute when calculating result accuracy. This is a + * multiplication factor. + * @param weight */ - public TextField sortableUnNormalizedForm() { - return sortableUNF(); + public TextField weight(double weight) { + this.weight = weight; + return this; } /** @@ -69,36 +68,53 @@ public TextField noStem() { } /** - * Avoid indexing. + * Perform phonetic matching. + * @param matcher */ - public TextField noIndex() { - this.noIndex = true; + public TextField phonetic(String matcher) { + this.phoneticMatcher = matcher; return this; } /** - * Perform phonetic matching. + * Keeps a suffix trie with all terms which match the suffix. It is used to optimize + * contains and suffix queries. */ - public TextField phonetic(String matcher) { - this.phoneticMatcher = matcher; + public TextField withSuffixTrie() { + this.withSuffixTrie = true; return this; } /** - * Declares the importance of this attribute when calculating result accuracy. This is a - * multiplication factor. + * Sorts the results by the value of this field. */ - public TextField weight(double weight) { - this.weight = weight; + public TextField sortable() { + this.sortable = true; return this; } /** - * Keeps a suffix trie with all terms which match the suffix. It is used to optimize - * contains and suffix queries. + * Sorts the results by the value of this field without normalization. */ - public TextField withSuffixTrie() { - this.withSuffixTrie = true; + public TextField sortableUNF() { + this.sortableUNF = true; + return this; + } + + /** + * @deprecated Use {@code TextField#sortableUNF()}. + * @see TextField#sortableUNF() + */ + @Deprecated + public TextField sortableUnNormalizedForm() { + return sortableUNF(); + } + + /** + * Avoid indexing. + */ + public TextField noIndex() { + this.noIndex = true; return this; } @@ -107,6 +123,13 @@ public void addParams(CommandArguments args) { args.addParams(fieldName); args.add(TEXT); + if (indexMissing) { + args.add(INDEXMISSING); + } + if (indexEmpty) { + args.add(INDEXEMPTY); + } + if (weight != null) { args.add(WEIGHT).add(weight); } diff --git a/src/main/java/redis/clients/jedis/search/schemafields/VectorField.java b/src/main/java/redis/clients/jedis/search/schemafields/VectorField.java index 02287a5be35..f550f66e77c 100644 --- a/src/main/java/redis/clients/jedis/search/schemafields/VectorField.java +++ b/src/main/java/redis/clients/jedis/search/schemafields/VectorField.java @@ -1,5 +1,6 @@ package redis.clients.jedis.search.schemafields; +import static redis.clients.jedis.search.SearchProtocol.SearchKeyword.INDEXMISSING; import static redis.clients.jedis.search.SearchProtocol.SearchKeyword.VECTOR; import java.util.LinkedHashMap; @@ -17,6 +18,9 @@ public enum VectorAlgorithm { private final VectorAlgorithm algorithm; private final Map attributes; + private boolean indexMissing; + // private boolean noIndex; // throws Field `NOINDEX` does not have a type + public VectorField(String fieldName, VectorAlgorithm algorithm, Map attributes) { super(fieldName); this.algorithm = algorithm; @@ -35,14 +39,23 @@ public VectorField as(String attribute) { return this; } + public VectorField indexMissing() { + this.indexMissing = true; + return this; + } + @Override public void addParams(CommandArguments args) { args.addParams(fieldName); args.add(VECTOR); args.add(algorithm); - args.add(attributes.size() * 2); + args.add(attributes.size() << 1); attributes.forEach((name, value) -> args.add(name).add(value)); + + if (indexMissing) { + args.add(INDEXMISSING); + } } public static Builder builder() { 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 8550e5c64a1..f8e282af42d 100644 --- a/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java +++ b/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java @@ -29,6 +29,7 @@ import redis.clients.jedis.json.Path; import redis.clients.jedis.search.*; import redis.clients.jedis.search.schemafields.*; +import redis.clients.jedis.search.schemafields.GeoShapeField.CoordinateSystem; import redis.clients.jedis.search.schemafields.VectorField.VectorAlgorithm; import redis.clients.jedis.modules.RedisModuleCommandsTestBase; @@ -195,19 +196,19 @@ public void search() { addDocument(String.format("doc%d", i), fields); } - SearchResult res = client.ftSearch(index, "hello world", + SearchResult result = client.ftSearch(index, "hello world", FTSearchParams.searchParams().limit(0, 5).withScores()); - assertEquals(100, res.getTotalResults()); - assertEquals(5, res.getDocuments().size()); - for (Document d : res.getDocuments()) { + assertEquals(100, result.getTotalResults()); + assertEquals(5, result.getDocuments().size()); + for (Document d : result.getDocuments()) { assertTrue(d.getId().startsWith("doc")); assertTrue(d.getScore() < 100); } client.del("doc0"); - res = client.ftSearch(index, "hello world"); - assertEquals(99, res.getTotalResults()); + result = client.ftSearch(index, "hello world"); + assertEquals(99, result.getTotalResults()); assertEquals("OK", client.ftDropIndex(index)); try { @@ -217,6 +218,57 @@ public void search() { } } + @Test + public void textFieldParams() { + assertOK(client.ftCreate("testindex", TextField.of("title").indexMissing().indexEmpty() + .weight(2.5).noStem().phonetic("dm:en").withSuffixTrie().sortable())); + + assertOK(client.ftCreate("testunfindex", TextField.of("title").indexMissing().indexEmpty() + .weight(2.5).noStem().phonetic("dm:en").withSuffixTrie().sortableUNF())); + + assertOK(client.ftCreate("testnoindex", TextField.of("title").sortable().noIndex())); + + assertOK(client.ftCreate("testunfnoindex", TextField.of("title").sortableUNF().noIndex())); + } + + @Test + public void searchTextFieldsCondition() { + assertOK(client.ftCreate(index, FTCreateParams.createParams(), TextField.of("title"), + TextField.of("body").indexMissing().indexEmpty())); + + Map regular = new HashMap<>(); + regular.put("title", "hello world"); + regular.put("body", "lorem ipsum"); + client.hset("regular-doc", regular); + + Map empty = new HashMap<>(); + empty.put("title", "hello world"); + empty.put("body", ""); + client.hset("empty-doc", empty); + + Map missing = new HashMap<>(); + missing.put("title", "hello world"); + client.hset("missing-doc", missing); + + SearchResult result = client.ftSearch(index, "", FTSearchParams.searchParams().dialect(2)); + assertEquals(0, result.getTotalResults()); + assertEquals(0, result.getDocuments().size()); + + result = client.ftSearch(index, "*", FTSearchParams.searchParams().dialect(2)); + assertEquals(3, result.getTotalResults()); + assertEquals(3, result.getDocuments().size()); + + result = client.ftSearch(index, "@body:''", FTSearchParams.searchParams().dialect(2)); + assertEquals(1, result.getTotalResults()); + assertEquals(1, result.getDocuments().size()); + assertEquals("empty-doc", result.getDocuments().get(0).getId()); + + result = client.ftSearch(index, "ismissing(@body)", FTSearchParams.searchParams().dialect(2)); + assertEquals(1, result.getTotalResults()); + assertEquals(1, result.getDocuments().size()); + assertEquals("missing-doc", result.getDocuments().get(0).getId()); + } + @Test public void numericFilter() { assertOK(client.ftCreate(index, TextField.of("title"), NumericField.of("price"))); @@ -269,7 +321,15 @@ public void numericFilter() { .filter("price", Double.NEGATIVE_INFINITY, 10)); assertEquals(11, res.getTotalResults()); assertEquals(10, res.getDocuments().size()); + } + + @Test + public void numericFieldParams() { + assertOK(client.ftCreate("testindex", TextField.of("title"), + NumericField.of("price").as("px").indexMissing().sortable())); + assertOK(client.ftCreate("testnoindex", TextField.of("title"), + NumericField.of("price").as("px").sortable().noIndex())); } @Test @@ -350,12 +410,19 @@ public void geoFilterAndGeoCoordinateObject() { assertEquals(2, res.getTotalResults()); } + @Test + public void geoFieldParams() { + assertOK(client.ftCreate("testindex", TextField.of("title"), GeoField.of("location").as("loc").indexMissing().sortable())); + + assertOK(client.ftCreate("testnoindex", TextField.of("title"), GeoField.of("location").as("loc").sortable().noIndex())); + } + @Test public void geoShapeFilterSpherical() throws ParseException { final WKTReader reader = new WKTReader(); final GeometryFactory factory = new GeometryFactory(); - assertOK(client.ftCreate(index, GeoShapeField.of("geom", GeoShapeField.CoordinateSystem.SPHERICAL))); + assertOK(client.ftCreate(index, GeoShapeField.of("geom", CoordinateSystem.SPHERICAL))); // polygon type final Polygon small = factory.createPolygon(new Coordinate[]{new Coordinate(34.9001, 29.7001), @@ -404,7 +471,7 @@ public void geoShapeFilterFlat() throws ParseException { final WKTReader reader = new WKTReader(); final GeometryFactory factory = new GeometryFactory(); - assertOK(client.ftCreate(index, GeoShapeField.of("geom", GeoShapeField.CoordinateSystem.FLAT))); + assertOK(client.ftCreate(index, GeoShapeField.of("geom", CoordinateSystem.FLAT))); // polygon type final Polygon small = factory.createPolygon(new Coordinate[]{new Coordinate(1, 1), @@ -444,6 +511,13 @@ public void geoShapeFilterFlat() throws ParseException { assertEquals(2, res.getDocuments().size()); } + @Test + public void geoShapeFieldParams() { + assertOK(client.ftCreate("testindex", GeoShapeField.of("geometry", CoordinateSystem.SPHERICAL).as("geom").indexMissing())); + + assertOK(client.ftCreate("testnoindex", GeoShapeField.of("geometry", CoordinateSystem.SPHERICAL).as("geom").noIndex())); + } + @Test public void testQueryFlags() { assertOK(client.ftCreate(index, TextField.of("title"))); @@ -841,6 +915,23 @@ public void caseSensitiveTagField() { assertEquals(1, client.ftSearch(index, "hello").getTotalResults()); } + @Test + public void tagFieldParams() { + assertOK(client.ftCreate("testindex", TextField.of("title"), + TagField.of("category").as("cat").indexMissing().indexEmpty() + .separator(',').caseSensitive().withSuffixTrie().sortable())); + + assertOK(client.ftCreate("testunfindex", TextField.of("title"), + TagField.of("category").as("cat").indexMissing().indexEmpty() + .separator(',').caseSensitive().withSuffixTrie().sortableUNF())); + + assertOK(client.ftCreate("testnoindex", TextField.of("title"), + TagField.of("category").as("cat").sortable().noIndex())); + + assertOK(client.ftCreate("testunfnoindex", TextField.of("title"), + TagField.of("category").as("cat").sortableUNF().noIndex())); + } + @Test public void testReturnFields() { assertOK(client.ftCreate(index, TextField.of("field1"), TextField.of("field2"))); @@ -1008,14 +1099,14 @@ public void inOrder() { } @Test - public void testHNSWVVectorSimilarity() { + public void testHNSWVectorSimilarity() { Map attr = new HashMap<>(); attr.put("TYPE", "FLOAT32"); attr.put("DIM", 2); attr.put("DISTANCE_METRIC", "L2"); assertOK(client.ftCreate(index, VectorField.builder().fieldName("v") - .algorithm(VectorField.VectorAlgorithm.HNSW).attributes(attr).build())); + .algorithm(VectorAlgorithm.HNSW).attributes(attr).build())); client.hset("a", "v", "aaaaaaaa"); client.hset("b", "v", "aaaabaaa"); @@ -1035,7 +1126,7 @@ public void testHNSWVVectorSimilarity() { public void testFlatVectorSimilarity() { assertOK(client.ftCreate(index, VectorField.builder().fieldName("v") - .algorithm(VectorField.VectorAlgorithm.FLAT) + .algorithm(VectorAlgorithm.FLAT) .addAttribute("TYPE", "FLOAT32") .addAttribute("DIM", 2) .addAttribute("DISTANCE_METRIC", "L2") @@ -1057,6 +1148,19 @@ public void testFlatVectorSimilarity() { assertEquals("0", doc1.get("__v_score")); } + @Test + public void vectorFieldParams() { + Map attr = new HashMap<>(); + attr.put("TYPE", "FLOAT32"); + attr.put("DIM", 2); + attr.put("DISTANCE_METRIC", "L2"); + + assertOK(client.ftCreate("testindex", new VectorField("vector", VectorAlgorithm.HNSW, attr).as("vec").indexMissing())); + + // assertOK(client.ftCreate("testnoindex", new VectorField("vector", VectorAlgorithm.HNSW, attr).as("vec").noIndex())); + // throws Field `NOINDEX` does not have a type + } + @Ignore @Test public void searchProfile() { From 856804a8015fc74bb13b3af04e7c7ac065d90110 Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Thu, 13 Jun 2024 18:28:13 +0600 Subject: [PATCH 05/63] Test: INTERSECTS and DIJOINT conditions support in GeoSearch (#3862) --- .../GeoShapeFieldsUsageInRediSearch.java | 29 ++++--- .../modules/search/SearchWithParamsTest.java | 80 +++++++++++-------- 2 files changed, 64 insertions(+), 45 deletions(-) diff --git a/src/test/java/redis/clients/jedis/examples/GeoShapeFieldsUsageInRediSearch.java b/src/test/java/redis/clients/jedis/examples/GeoShapeFieldsUsageInRediSearch.java index fd309def50d..ea0570265d1 100644 --- a/src/test/java/redis/clients/jedis/examples/GeoShapeFieldsUsageInRediSearch.java +++ b/src/test/java/redis/clients/jedis/examples/GeoShapeFieldsUsageInRediSearch.java @@ -1,6 +1,5 @@ package redis.clients.jedis.examples; -import java.util.Collections; import org.junit.Assert; import org.locationtech.jts.geom.Coordinate; @@ -14,16 +13,21 @@ import redis.clients.jedis.JedisPooled; import redis.clients.jedis.UnifiedJedis; import redis.clients.jedis.search.FTSearchParams; -import redis.clients.jedis.search.RediSearchUtil; import redis.clients.jedis.search.SearchResult; import redis.clients.jedis.search.schemafields.GeoShapeField; /** * As of RediSearch 2.8.4, advanced GEO querying with GEOSHAPE fields is supported. + *

+ * Notes: + *

    + *
  • As of RediSearch 2.8.4, only POLYGON and POINT objects are supported.
  • + *
  • As of RediSearch 2.8.4, only WITHIN and CONTAINS conditions are supported.
  • + *
  • As of RedisStack 7.4.0, support for INTERSECTS and DISJOINT conditions are added.
  • + *
* - * Any object/library producing a - * well-known - * text (WKT) in {@code toString()} method can be used. + * Any object/library producing a + * well-known text (WKT) in {@code toString()} method can be used. * * This example uses the JTS library. *
@@ -65,7 +69,8 @@ public static void main(String[] args) {
     );
 
     // client.hset("small", RediSearchUtil.toStringMap(Collections.singletonMap("geometry", small))); // setting data
-    client.hset("small", "geometry", small.toString()); // simplified setting data
+    // client.hset("small", "geometry", small.toString()); // simplified setting data
+    client.hsetObject("small", "geometry", small); // more simplified setting data
 
     final Polygon large = factory.createPolygon(
         new Coordinate[]{new Coordinate(34.9001, 29.7001),
@@ -74,7 +79,8 @@ public static void main(String[] args) {
     );
 
     // client.hset("large", RediSearchUtil.toStringMap(Collections.singletonMap("geometry", large))); // setting data
-    client.hset("large", "geometry", large.toString()); // simplified setting data
+    // client.hset("large", "geometry", large.toString()); // simplified setting data
+    client.hsetObject("large", "geometry", large); // more simplified setting data
 
     // searching
     final Polygon within = factory.createPolygon(
@@ -84,11 +90,10 @@ public static void main(String[] args) {
     );
 
     SearchResult res = client.ftSearch("geometry-index",
-        "@geometry:[within $poly]", // querying 'within' condition.
-                                    // RediSearch also supports 'contains' condition.
+        "@geometry:[within $poly]",     // query string
         FTSearchParams.searchParams()
             .addParam("poly", within)
-            .dialect(3) // DIALECT '3' is required for this query
+            .dialect(3)                 // DIALECT '3' is required for this query
     ); 
     Assert.assertEquals(1, res.getTotalResults());
     Assert.assertEquals(1, res.getDocuments().size());
@@ -98,10 +103,8 @@ public static void main(String[] args) {
       final WKTReader reader = new WKTReader();
       Geometry object = reader.read(res.getDocuments().get(0).getString("geometry"));
       Assert.assertEquals(small, object);
-    } catch (ParseException ex) {
+    } catch (ParseException ex) { // WKTReader#read throws ParseException
       ex.printStackTrace(System.err);
     }
   }
-
-  // Note: As of RediSearch 2.8.4, only POLYGON and POINT objects are supported.
 }
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 f8e282af42d..e8029a42fd4 100644
--- a/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java
+++ b/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java
@@ -428,42 +428,42 @@ public void geoShapeFilterSpherical() throws ParseException {
     final Polygon small = factory.createPolygon(new Coordinate[]{new Coordinate(34.9001, 29.7001),
         new Coordinate(34.9001, 29.7100), new Coordinate(34.9100, 29.7100),
         new Coordinate(34.9100, 29.7001), new Coordinate(34.9001, 29.7001)});
-    client.hset("small", "geom", small.toString());
+    client.hsetObject("small", "geom", small);
 
     final Polygon large = factory.createPolygon(new Coordinate[]{new Coordinate(34.9001, 29.7001),
         new Coordinate(34.9001, 29.7200), new Coordinate(34.9200, 29.7200),
         new Coordinate(34.9200, 29.7001), new Coordinate(34.9001, 29.7001)});
-    client.hset("large", "geom", large.toString());
+    client.hsetObject("large", "geom", large);
 
     // within condition
     final Polygon within = factory.createPolygon(new Coordinate[]{new Coordinate(34.9000, 29.7000),
         new Coordinate(34.9000, 29.7150), new Coordinate(34.9150, 29.7150),
         new Coordinate(34.9150, 29.7000), new Coordinate(34.9000, 29.7000)});
 
-    SearchResult res = client.ftSearch(index, "@geom:[within $poly]",
+    SearchResult result = client.ftSearch(index, "@geom:[within $poly]",
         FTSearchParams.searchParams().addParam("poly", within).dialect(3));
-    assertEquals(1, res.getTotalResults());
-    assertEquals(1, res.getDocuments().size());
-    assertEquals(small, reader.read(res.getDocuments().get(0).getString("geom")));
+    assertEquals(1, result.getTotalResults());
+    assertEquals(1, result.getDocuments().size());
+    assertEquals(small, reader.read(result.getDocuments().get(0).getString("geom")));
 
     // contains condition
     final Polygon contains = factory.createPolygon(new Coordinate[]{new Coordinate(34.9002, 29.7002),
         new Coordinate(34.9002, 29.7050), new Coordinate(34.9050, 29.7050),
         new Coordinate(34.9050, 29.7002), new Coordinate(34.9002, 29.7002)});
 
-    res = client.ftSearch(index, "@geom:[contains $poly]",
+    result = client.ftSearch(index, "@geom:[contains $poly]",
         FTSearchParams.searchParams().addParam("poly", contains).dialect(3));
-    assertEquals(2, res.getTotalResults());
-    assertEquals(2, res.getDocuments().size());
+    assertEquals(2, result.getTotalResults());
+    assertEquals(2, result.getDocuments().size());
 
     // point type
     final Point point = factory.createPoint(new Coordinate(34.9010, 29.7010));
     client.hset("point", "geom", point.toString());
 
-    res = client.ftSearch(index, "@geom:[within $poly]",
+    result = client.ftSearch(index, "@geom:[within $poly]",
         FTSearchParams.searchParams().addParam("poly", within).dialect(3));
-    assertEquals(2, res.getTotalResults());
-    assertEquals(2, res.getDocuments().size());
+    assertEquals(2, result.getTotalResults());
+    assertEquals(2, result.getDocuments().size());
   }
 
   @Test
@@ -474,41 +474,57 @@ public void geoShapeFilterFlat() throws ParseException {
     assertOK(client.ftCreate(index, GeoShapeField.of("geom", CoordinateSystem.FLAT)));
 
     // polygon type
-    final Polygon small = factory.createPolygon(new Coordinate[]{new Coordinate(1, 1),
-        new Coordinate(1, 100), new Coordinate(100, 100), new Coordinate(100, 1), new Coordinate(1, 1)});
-    client.hset("small", "geom", small.toString());
+    final Polygon small = factory.createPolygon(new Coordinate[]{new Coordinate(20, 20),
+        new Coordinate(20, 100), new Coordinate(100, 100), new Coordinate(100, 20), new Coordinate(20, 20)});
+    client.hsetObject("small", "geom", small);
 
-    final Polygon large = factory.createPolygon(new Coordinate[]{new Coordinate(1, 1),
-        new Coordinate(1, 200), new Coordinate(200, 200), new Coordinate(200, 1), new Coordinate(1, 1)});
-    client.hset("large", "geom", large.toString());
+    final Polygon large = factory.createPolygon(new Coordinate[]{new Coordinate(10, 10),
+        new Coordinate(10, 200), new Coordinate(200, 200), new Coordinate(200, 10), new Coordinate(10, 10)});
+    client.hsetObject("large", "geom", large);
 
     // within condition
     final Polygon within = factory.createPolygon(new Coordinate[]{new Coordinate(0, 0),
         new Coordinate(0, 150), new Coordinate(150, 150), new Coordinate(150, 0), new Coordinate(0, 0)});
 
-    SearchResult res = client.ftSearch(index, "@geom:[within $poly]",
+    SearchResult result = client.ftSearch(index, "@geom:[within $poly]",
         FTSearchParams.searchParams().addParam("poly", within).dialect(3));
-    assertEquals(1, res.getTotalResults());
-    assertEquals(1, res.getDocuments().size());
-    assertEquals(small, reader.read(res.getDocuments().get(0).getString("geom")));
+    assertEquals(1, result.getTotalResults());
+    assertEquals(1, result.getDocuments().size());
+    assertEquals(small, reader.read(result.getDocuments().get(0).getString("geom")));
 
     // contains condition
-    final Polygon contains = factory.createPolygon(new Coordinate[]{new Coordinate(2, 2),
-        new Coordinate(2, 50), new Coordinate(50, 50), new Coordinate(50, 2), new Coordinate(2, 2)});
+    final Polygon contains = factory.createPolygon(new Coordinate[]{new Coordinate(25, 25),
+        new Coordinate(25, 50), new Coordinate(50, 50), new Coordinate(50, 25), new Coordinate(25, 25)});
 
-    res = client.ftSearch(index, "@geom:[contains $poly]",
+    result = client.ftSearch(index, "@geom:[contains $poly]",
         FTSearchParams.searchParams().addParam("poly", contains).dialect(3));
-    assertEquals(2, res.getTotalResults());
-    assertEquals(2, res.getDocuments().size());
+    assertEquals(2, result.getTotalResults());
+    assertEquals(2, result.getDocuments().size());
+
+    // intersects and disjoint
+    final Polygon disjointersect = factory.createPolygon(new Coordinate[]{new Coordinate(150, 150),
+        new Coordinate(150, 250), new Coordinate(250, 250), new Coordinate(250, 150), new Coordinate(150, 150)});
+
+    result = client.ftSearch(index, "@geom:[intersects $poly]",
+        FTSearchParams.searchParams().addParam("poly", disjointersect).dialect(3));
+    assertEquals(1, result.getTotalResults());
+    assertEquals(1, result.getDocuments().size());
+    assertEquals(large, reader.read(result.getDocuments().get(0).getString("geom")));
+
+    result = client.ftSearch(index, "@geom:[disjoint $poly]",
+        FTSearchParams.searchParams().addParam("poly", disjointersect).dialect(3));
+    assertEquals(1, result.getTotalResults());
+    assertEquals(1, result.getDocuments().size());
+    assertEquals(small, reader.read(result.getDocuments().get(0).getString("geom")));
 
     // point type
-    final Point point = factory.createPoint(new Coordinate(10, 10));
-    client.hset("point", "geom", point.toString());
+    final Point point = factory.createPoint(new Coordinate(30, 30));
+    client.hsetObject("point", "geom", point);
 
-    res = client.ftSearch(index, "@geom:[within $poly]",
+    result = client.ftSearch(index, "@geom:[within $poly]",
         FTSearchParams.searchParams().addParam("poly", within).dialect(3));
-    assertEquals(2, res.getTotalResults());
-    assertEquals(2, res.getDocuments().size());
+    assertEquals(2, result.getTotalResults());
+    assertEquals(2, result.getDocuments().size());
   }
 
   @Test

From 5277ed0072d222a504aa509a1d3d51e8c6a7d3b9 Mon Sep 17 00:00:00 2001
From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com>
Date: Thu, 13 Jun 2024 18:24:55 +0600
Subject: [PATCH 06/63] Support FLOAT16 and BFLOAT16 VecSim storage types
 (#3849)

---
 .../modules/search/SearchWithParamsTest.java  | 22 +++++++++++++++++++
 1 file changed, 22 insertions(+)

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 e8029a42fd4..2a60dec21f2 100644
--- a/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java
+++ b/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java
@@ -1177,6 +1177,28 @@ public void vectorFieldParams() {
     // throws Field `NOINDEX` does not have a type
   }
 
+  @Test
+  public void float16StorageType() {
+    assertOK(client.ftCreate(index,
+        VectorField.builder().fieldName("v")
+            .algorithm(VectorField.VectorAlgorithm.HNSW)
+            .addAttribute("TYPE", "FLOAT16")
+            .addAttribute("DIM", 4)
+            .addAttribute("DISTANCE_METRIC", "L2")
+            .build()));
+  }
+
+  @Test
+  public void bfloat16StorageType() {
+    assertOK(client.ftCreate(index,
+        VectorField.builder().fieldName("v")
+            .algorithm(VectorField.VectorAlgorithm.HNSW)
+            .addAttribute("TYPE", "BFLOAT16")
+            .addAttribute("DIM", 4)
+            .addAttribute("DISTANCE_METRIC", "L2")
+            .build()));
+  }
+
   @Ignore
   @Test
   public void searchProfile() {

From b2bee0e53db809a32dd579597ee7f45b9b253fb1 Mon Sep 17 00:00:00 2001
From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com>
Date: Thu, 13 Jun 2024 18:23:24 +0600
Subject: [PATCH 07/63] 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)
---
 .../clients/jedis/search/querybuilder/Values.java |  4 ++++
 .../clients/jedis/modules/search/SearchTest.java  | 13 +++++++++++++
 .../modules/search/SearchWithParamsTest.java      | 15 +++++++++++++++
 3 files changed, 32 insertions(+)

diff --git a/src/main/java/redis/clients/jedis/search/querybuilder/Values.java b/src/main/java/redis/clients/jedis/search/querybuilder/Values.java
index 67256f2359c..7b4971be191 100644
--- a/src/main/java/redis/clients/jedis/search/querybuilder/Values.java
+++ b/src/main/java/redis/clients/jedis/search/querybuilder/Values.java
@@ -41,10 +41,14 @@ public static RangeValue between(int from, int to) {
     return new LongRangeValue(from, to);
   }
 
+  // TODO: change to simpler [d] available since RedisStack 7.4.0-rc1;
+  // currently kept for backward compatibility
   public static RangeValue eq(double d) {
     return new DoubleRangeValue(d, d);
   }
 
+  // TODO: change to simpler [i] available since RedisStack 7.4.0-rc1;
+  // currently kept for backward compatibility
   public static RangeValue eq(int i) {
     return new LongRangeValue(i, i);
   }
diff --git a/src/test/java/redis/clients/jedis/modules/search/SearchTest.java b/src/test/java/redis/clients/jedis/modules/search/SearchTest.java
index 27f72483dbc..6fc9bcadf7a 100644
--- a/src/test/java/redis/clients/jedis/modules/search/SearchTest.java
+++ b/src/test/java/redis/clients/jedis/modules/search/SearchTest.java
@@ -412,6 +412,9 @@ public void testQueryParams() {
 
     Query query =  new Query("@numval:[$min $max]").addParam("min", 1).addParam("max", 2).dialect(2);
     assertEquals(2, client.ftSearch(index, query).getTotalResults());
+
+    query =  new Query("@numval:[$eq]").addParam("eq", 2).dialect(5);
+    assertEquals(1, client.ftSearch(index, query).getTotalResults());
   }
 
   @Test
@@ -532,6 +535,14 @@ public void testJsonWithAlias() {
     res = client.ftSearch(index, new Query("@num:[0 10]"));
     assertEquals(1, res.getTotalResults());
     assertEquals("king:2", res.getDocuments().get(0).getId());
+
+    res = client.ftSearch(index, new Query("@num:[42 42]"));
+    assertEquals(1, res.getTotalResults());
+    assertEquals("king:1", res.getDocuments().get(0).getId());
+
+    res = client.ftSearch(index, new Query("@num:[42]").dialect(5));
+    assertEquals(1, res.getTotalResults());
+    assertEquals("king:1", res.getDocuments().get(0).getId());
   }
 
   @Test
@@ -773,6 +784,7 @@ public void getTagField() {
     assertEquals(1, client.ftSearch(index, new Query("@category:{yellow}")).getTotalResults());
     assertEquals(0, client.ftSearch(index, new Query("@category:{purple}")).getTotalResults());
     assertEquals(1, client.ftSearch(index, new Query("@category:{orange\\;purple}")).getTotalResults());
+    assertEquals(1, client.ftSearch(index, new Query("@category:{orange;purple}").dialect(5)).getTotalResults());
     assertEquals(4, client.ftSearch(index, new Query("hello")).getTotalResults());
 
     assertEquals(new HashSet<>(Arrays.asList("red", "blue", "green", "yellow", "orange;purple")),
@@ -814,6 +826,7 @@ public void testGetTagFieldWithNonDefaultSeparator() {
     assertEquals(1, client.ftSearch(index, new Query("hello @category:{yellow}")).getTotalResults());
     assertEquals(0, client.ftSearch(index, new Query("@category:{purple}")).getTotalResults());
     assertEquals(1, client.ftSearch(index, new Query("@category:{orange\\,purple}")).getTotalResults());
+    assertEquals(1, client.ftSearch(index, new Query("@category:{orange,purple}").dialect(5)).getTotalResults());
     assertEquals(4, client.ftSearch(index, new Query("hello")).getTotalResults());
 
     assertEquals(new HashSet<>(Arrays.asList("red", "blue", "green", "yellow", "orange,purple")),
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 2a60dec21f2..3cd6bca1c64 100644
--- a/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java
+++ b/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java
@@ -608,6 +608,9 @@ public void testQueryParams() {
     assertEquals(2, client.ftSearch(index, "@numval:[$min $max]",
         FTSearchParams.searchParams().params(paramValues)
             .dialect(2)).getTotalResults());
+
+    assertEquals(1, client.ftSearch(index, "@numval:[$eq]",
+        FTSearchParams.searchParams().addParam("eq", 2).dialect(5)).getTotalResults());
   }
 
   @Test
@@ -665,6 +668,14 @@ public void testJsonWithAlias() {
     res = client.ftSearch(index, "@num:[0 10]");
     assertEquals(1, res.getTotalResults());
     assertEquals("king:2", res.getDocuments().get(0).getId());
+
+    res = client.ftSearch(index, "@num:[42 42]", FTSearchParams.searchParams());
+    assertEquals(1, res.getTotalResults());
+    assertEquals("king:1", res.getDocuments().get(0).getId());
+
+    res = client.ftSearch(index, "@num:[42]", FTSearchParams.searchParams().dialect(5));
+    assertEquals(1, res.getTotalResults());
+    assertEquals("king:1", res.getDocuments().get(0).getId());
   }
 
   @Test
@@ -868,6 +879,8 @@ public void getTagField() {
     assertEquals(1, client.ftSearch(index, "@category:{yellow}").getTotalResults());
     assertEquals(0, client.ftSearch(index, "@category:{purple}").getTotalResults());
     assertEquals(1, client.ftSearch(index, "@category:{orange\\;purple}").getTotalResults());
+    assertEquals(1, client.ftSearch(index, "@category:{orange;purple}",
+        FTSearchParams.searchParams().dialect(5)).getTotalResults());
     assertEquals(4, client.ftSearch(index, "hello").getTotalResults());
 
     assertEquals(new HashSet<>(Arrays.asList("red", "blue", "green", "yellow", "orange;purple")),
@@ -907,6 +920,8 @@ public void testGetTagFieldWithNonDefaultSeparator() {
     assertEquals(1, client.ftSearch(index, "hello @category:{yellow}").getTotalResults());
     assertEquals(0, client.ftSearch(index, "@category:{purple}").getTotalResults());
     assertEquals(1, client.ftSearch(index, "@category:{orange\\,purple}").getTotalResults());
+    assertEquals(1, client.ftSearch(index, "@category:{orange,purple}",
+        FTSearchParams.searchParams().dialect(5)).getTotalResults());
     assertEquals(4, client.ftSearch(index, "hello").getTotalResults());
 
     assertEquals(new HashSet<>(Arrays.asList("red", "blue", "green", "yellow", "orange,purple")),

From b47b70f58f97495bac761be593be6402f462ab26 Mon Sep 17 00:00:00 2001
From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com>
Date: Thu, 11 Jul 2024 13:01:36 +0600
Subject: [PATCH 08/63] Remove RediSearch DIALECT 5 support (#3886)

This is partial revert of #3831.

Some changes are still available without dialect 5. For example:
* Avoid repeating for numeral equality
---
 .../redis/clients/jedis/modules/search/SearchTest.java    | 6 ++----
 .../jedis/modules/search/SearchWithParamsTest.java        | 8 ++------
 2 files changed, 4 insertions(+), 10 deletions(-)

diff --git a/src/test/java/redis/clients/jedis/modules/search/SearchTest.java b/src/test/java/redis/clients/jedis/modules/search/SearchTest.java
index 6fc9bcadf7a..0776eabcd46 100644
--- a/src/test/java/redis/clients/jedis/modules/search/SearchTest.java
+++ b/src/test/java/redis/clients/jedis/modules/search/SearchTest.java
@@ -413,7 +413,7 @@ public void testQueryParams() {
     Query query =  new Query("@numval:[$min $max]").addParam("min", 1).addParam("max", 2).dialect(2);
     assertEquals(2, client.ftSearch(index, query).getTotalResults());
 
-    query =  new Query("@numval:[$eq]").addParam("eq", 2).dialect(5);
+    query =  new Query("@numval:[$eq]").addParam("eq", 2).dialect(4);
     assertEquals(1, client.ftSearch(index, query).getTotalResults());
   }
 
@@ -540,7 +540,7 @@ public void testJsonWithAlias() {
     assertEquals(1, res.getTotalResults());
     assertEquals("king:1", res.getDocuments().get(0).getId());
 
-    res = client.ftSearch(index, new Query("@num:[42]").dialect(5));
+    res = client.ftSearch(index, new Query("@num:[42]").dialect(4));
     assertEquals(1, res.getTotalResults());
     assertEquals("king:1", res.getDocuments().get(0).getId());
   }
@@ -784,7 +784,6 @@ public void getTagField() {
     assertEquals(1, client.ftSearch(index, new Query("@category:{yellow}")).getTotalResults());
     assertEquals(0, client.ftSearch(index, new Query("@category:{purple}")).getTotalResults());
     assertEquals(1, client.ftSearch(index, new Query("@category:{orange\\;purple}")).getTotalResults());
-    assertEquals(1, client.ftSearch(index, new Query("@category:{orange;purple}").dialect(5)).getTotalResults());
     assertEquals(4, client.ftSearch(index, new Query("hello")).getTotalResults());
 
     assertEquals(new HashSet<>(Arrays.asList("red", "blue", "green", "yellow", "orange;purple")),
@@ -826,7 +825,6 @@ public void testGetTagFieldWithNonDefaultSeparator() {
     assertEquals(1, client.ftSearch(index, new Query("hello @category:{yellow}")).getTotalResults());
     assertEquals(0, client.ftSearch(index, new Query("@category:{purple}")).getTotalResults());
     assertEquals(1, client.ftSearch(index, new Query("@category:{orange\\,purple}")).getTotalResults());
-    assertEquals(1, client.ftSearch(index, new Query("@category:{orange,purple}").dialect(5)).getTotalResults());
     assertEquals(4, client.ftSearch(index, new Query("hello")).getTotalResults());
 
     assertEquals(new HashSet<>(Arrays.asList("red", "blue", "green", "yellow", "orange,purple")),
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 3cd6bca1c64..f4c584d5075 100644
--- a/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java
+++ b/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java
@@ -610,7 +610,7 @@ public void testQueryParams() {
             .dialect(2)).getTotalResults());
 
     assertEquals(1, client.ftSearch(index, "@numval:[$eq]",
-        FTSearchParams.searchParams().addParam("eq", 2).dialect(5)).getTotalResults());
+        FTSearchParams.searchParams().addParam("eq", 2).dialect(4)).getTotalResults());
   }
 
   @Test
@@ -673,7 +673,7 @@ public void testJsonWithAlias() {
     assertEquals(1, res.getTotalResults());
     assertEquals("king:1", res.getDocuments().get(0).getId());
 
-    res = client.ftSearch(index, "@num:[42]", FTSearchParams.searchParams().dialect(5));
+    res = client.ftSearch(index, "@num:[42]", FTSearchParams.searchParams().dialect(4));
     assertEquals(1, res.getTotalResults());
     assertEquals("king:1", res.getDocuments().get(0).getId());
   }
@@ -879,8 +879,6 @@ public void getTagField() {
     assertEquals(1, client.ftSearch(index, "@category:{yellow}").getTotalResults());
     assertEquals(0, client.ftSearch(index, "@category:{purple}").getTotalResults());
     assertEquals(1, client.ftSearch(index, "@category:{orange\\;purple}").getTotalResults());
-    assertEquals(1, client.ftSearch(index, "@category:{orange;purple}",
-        FTSearchParams.searchParams().dialect(5)).getTotalResults());
     assertEquals(4, client.ftSearch(index, "hello").getTotalResults());
 
     assertEquals(new HashSet<>(Arrays.asList("red", "blue", "green", "yellow", "orange;purple")),
@@ -920,8 +918,6 @@ public void testGetTagFieldWithNonDefaultSeparator() {
     assertEquals(1, client.ftSearch(index, "hello @category:{yellow}").getTotalResults());
     assertEquals(0, client.ftSearch(index, "@category:{purple}").getTotalResults());
     assertEquals(1, client.ftSearch(index, "@category:{orange\\,purple}").getTotalResults());
-    assertEquals(1, client.ftSearch(index, "@category:{orange,purple}",
-        FTSearchParams.searchParams().dialect(5)).getTotalResults());
     assertEquals(4, client.ftSearch(index, "hello").getTotalResults());
 
     assertEquals(new HashSet<>(Arrays.asList("red", "blue", "green", "yellow", "orange,purple")),

From 59eaf342161b7c61478a2c1bf8807b39eb552ccc Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 15 Jul 2024 13:58:37 +0600
Subject: [PATCH 09/63] Bump com.google.code.gson:gson from 2.10.1 to 2.11.0
 (#3842)

Bumps [com.google.code.gson:gson](https://github.com/google/gson) from 2.10.1 to 2.11.0.
- [Release notes](https://github.com/google/gson/releases)
- [Changelog](https://github.com/google/gson/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google/gson/compare/gson-parent-2.10.1...gson-parent-2.11.0)

---
updated-dependencies:
- dependency-name: com.google.code.gson:gson
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 1ae12c300cc..00c30d37e1e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -72,7 +72,7 @@
 		
 			com.google.code.gson
 			gson
-			2.10.1
+			2.11.0
 		
 
 		

From bc94c53edd9c2d874d46e6aef584586506c0cd03 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 15 Jul 2024 14:39:17 +0600
Subject: [PATCH 10/63] Bump org.sonatype.plugins:nexus-staging-maven-plugin
 from 1.6.13 to 1.7.0 (#3850)

Bump org.sonatype.plugins:nexus-staging-maven-plugin

Bumps org.sonatype.plugins:nexus-staging-maven-plugin from 1.6.13 to 1.7.0.

---
updated-dependencies:
- dependency-name: org.sonatype.plugins:nexus-staging-maven-plugin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 00c30d37e1e..d20da04a3ea 100644
--- a/pom.xml
+++ b/pom.xml
@@ -254,7 +254,7 @@
 			
 				org.sonatype.plugins
 				nexus-staging-maven-plugin
-				1.6.13
+				1.7.0
 				true
 				
 					ossrh

From 337d8cae9e957756fe207ea4f5040080cfec8067 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 15 Jul 2024 14:39:30 +0600
Subject: [PATCH 11/63] Bump org.apache.maven.plugins:maven-javadoc-plugin from
 3.6.3 to 3.7.0 (#3851)

Bumps [org.apache.maven.plugins:maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) from 3.6.3 to 3.7.0.
- [Release notes](https://github.com/apache/maven-javadoc-plugin/releases)
- [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.6.3...maven-javadoc-plugin-3.7.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-javadoc-plugin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index d20da04a3ea..cee45b01e9d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -231,7 +231,7 @@
 			
 			
 				maven-javadoc-plugin
-				3.6.3
+				3.7.0
 				
 					8
 					false

From b90fd0b5be3308b5fa68a27e025e9630146ded86 Mon Sep 17 00:00:00 2001
From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com>
Date: Tue, 16 Jul 2024 11:48:37 +0100
Subject: [PATCH 12/63] Added bitfield code samples (#3894)

Co-authored-by: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com>
---
 .../io/redis/examples/BitfieldExample.java    | 52 +++++++++++++++++++
 1 file changed, 52 insertions(+)
 create mode 100644 src/test/java/io/redis/examples/BitfieldExample.java

diff --git a/src/test/java/io/redis/examples/BitfieldExample.java b/src/test/java/io/redis/examples/BitfieldExample.java
new file mode 100644
index 00000000000..88f700c4c1b
--- /dev/null
+++ b/src/test/java/io/redis/examples/BitfieldExample.java
@@ -0,0 +1,52 @@
+// EXAMPLE: bitfield_tutorial
+// REMOVE_START
+package io.redis.examples;
+
+import org.junit.Assert;
+import org.junit.Test;
+import java.util.List;
+// REMOVE_END
+
+// HIDE_START
+import redis.clients.jedis.UnifiedJedis;
+// HIDE_END
+
+// HIDE_START
+public class BitfieldExample {
+    @Test
+    public void run() {
+        UnifiedJedis jedis = new UnifiedJedis("redis://localhost:6379");
+// HIDE_END
+
+        //REMOVE_START
+        // Clear any keys here before using them in tests.
+        jedis.del("bike:1:stats");
+        //REMOVE_END
+
+        // STEP_START bf
+        List res1 = jedis.bitfield("bike:1:stats", "SET", "u32", "#0", "1000");
+        System.out.println(res1);   // >>> [0]
+
+        List res2 = jedis.bitfield("bike:1:stats", "INCRBY", "u32", "#0", "-50", "INCRBY", "u32", "#1", "1");
+        System.out.println(res2);   // >>> [950, 1]
+
+        List res3 = jedis.bitfield("bike:1:stats", "INCRBY", "u32", "#0", "500", "INCRBY", "u32", "#1", "1");
+        System.out.println(res3);   // >>> [1450, 2]
+
+        List res4 = jedis.bitfield("bike:1:stats", "GET", "u32", "#0", "GET", "u32", "#1");
+        System.out.println(res4);   // >>> [1450, 2]
+        // STEP_END
+
+        // Tests for 'bf' step.
+        // REMOVE_START
+        Assert.assertEquals("[0]", res1.toString());
+        Assert.assertEquals("[950, 1]", res2.toString());
+        Assert.assertEquals("[1450, 2]", res3.toString());
+        Assert.assertEquals("[1450, 2]", res4.toString());
+        // REMOVE_END
+
+// HIDE_START
+        jedis.close();
+    }
+}
+// HIDE_END

From ba98da74bf0cb74f467ea3765339ef438e86bfe8 Mon Sep 17 00:00:00 2001
From: zyfx595701088 <50442211+zyfx595701088@users.noreply.github.com>
Date: Tue, 16 Jul 2024 19:14:36 +0800
Subject: [PATCH 13/63] Extract messages of unsupported exception as constants
 (#3887)

Co-authored-by: zhangshuai 
Co-authored-by: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com>
---
 .../jedis/mcf/MultiClusterTransaction.java    | 22 ++++++++++---------
 1 file changed, 12 insertions(+), 10 deletions(-)

diff --git a/src/main/java/redis/clients/jedis/mcf/MultiClusterTransaction.java b/src/main/java/redis/clients/jedis/mcf/MultiClusterTransaction.java
index f39cdf36b69..7356f9ba79e 100644
--- a/src/main/java/redis/clients/jedis/mcf/MultiClusterTransaction.java
+++ b/src/main/java/redis/clients/jedis/mcf/MultiClusterTransaction.java
@@ -27,6 +27,8 @@
 public class MultiClusterTransaction extends TransactionBase {
 
   private static final Builder NO_OP_BUILDER = BuilderFactory.RAW_OBJECT;
+  
+  private static final String GRAPH_COMMANDS_NOT_SUPPORTED_MESSAGE = "Graph commands are not supported.";
 
   private final CircuitBreakerFailoverConnectionProvider failoverProvider;
   private final AtomicInteger extraCommandCount = new AtomicInteger();
@@ -213,52 +215,52 @@ public final String discard() {
   // RedisGraph commands
   @Override
   public Response graphQuery(String name, String query) {
-    throw new UnsupportedOperationException("Graph commands are not supported.");
+    throw new UnsupportedOperationException(GRAPH_COMMANDS_NOT_SUPPORTED_MESSAGE);
   }
 
   @Override
   public Response graphReadonlyQuery(String name, String query) {
-    throw new UnsupportedOperationException("Graph commands are not supported.");
+    throw new UnsupportedOperationException(GRAPH_COMMANDS_NOT_SUPPORTED_MESSAGE);
   }
 
   @Override
   public Response graphQuery(String name, String query, long timeout) {
-    throw new UnsupportedOperationException("Graph commands are not supported.");
+    throw new UnsupportedOperationException(GRAPH_COMMANDS_NOT_SUPPORTED_MESSAGE);
   }
 
   @Override
   public Response graphReadonlyQuery(String name, String query, long timeout) {
-    throw new UnsupportedOperationException("Graph commands are not supported.");
+    throw new UnsupportedOperationException(GRAPH_COMMANDS_NOT_SUPPORTED_MESSAGE);
   }
 
   @Override
   public Response graphQuery(String name, String query, Map params) {
-    throw new UnsupportedOperationException("Graph commands are not supported.");
+    throw new UnsupportedOperationException(GRAPH_COMMANDS_NOT_SUPPORTED_MESSAGE);
   }
 
   @Override
   public Response graphReadonlyQuery(String name, String query, Map params) {
-    throw new UnsupportedOperationException("Graph commands are not supported.");
+    throw new UnsupportedOperationException(GRAPH_COMMANDS_NOT_SUPPORTED_MESSAGE);
   }
 
   @Override
   public Response graphQuery(String name, String query, Map params, long timeout) {
-    throw new UnsupportedOperationException("Graph commands are not supported.");
+    throw new UnsupportedOperationException(GRAPH_COMMANDS_NOT_SUPPORTED_MESSAGE);
   }
 
   @Override
   public Response graphReadonlyQuery(String name, String query, Map params, long timeout) {
-    throw new UnsupportedOperationException("Graph commands are not supported.");
+    throw new UnsupportedOperationException(GRAPH_COMMANDS_NOT_SUPPORTED_MESSAGE);
   }
 
   @Override
   public Response graphDelete(String name) {
-    throw new UnsupportedOperationException("Graph commands are not supported.");
+    throw new UnsupportedOperationException(GRAPH_COMMANDS_NOT_SUPPORTED_MESSAGE);
   }
 
   @Override
   public Response> graphProfile(String graphName, String query) {
-    throw new UnsupportedOperationException("Graph commands are not supported.");
+    throw new UnsupportedOperationException(GRAPH_COMMANDS_NOT_SUPPORTED_MESSAGE);
   }
   // RedisGraph commands
 }

From e113b9ce3b1a3f51c2407cd237a849227434cf98 Mon Sep 17 00:00:00 2001
From: Ivan Babanin <1204061+babanin@users.noreply.github.com>
Date: Tue, 16 Jul 2024 13:41:25 +0200
Subject: [PATCH 14/63] Replace `synchronized` with `j.u.c.l.ReentrantLock` for
 Loom (#3480)

Co-authored-by: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com>
---
 .../redis/clients/jedis/CommandObjects.java   |  9 +++-
 .../clients/jedis/JedisSentinelPool.java      | 10 +++-
 .../jedis/graph/GraphCommandObjects.java      | 16 +++++--
 .../jedis/mcf/CircuitBreakerFailoverBase.java | 46 +++++++++++--------
 .../MultiClusterPooledConnectionProvider.java | 19 ++++++--
 .../SentineledConnectionProvider.java         | 10 +++-
 6 files changed, 77 insertions(+), 33 deletions(-)

diff --git a/src/main/java/redis/clients/jedis/CommandObjects.java b/src/main/java/redis/clients/jedis/CommandObjects.java
index cbf0e197239..5c6af322dc3 100644
--- a/src/main/java/redis/clients/jedis/CommandObjects.java
+++ b/src/main/java/redis/clients/jedis/CommandObjects.java
@@ -5,6 +5,8 @@
 
 import java.util.*;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import org.json.JSONArray;
@@ -50,6 +52,7 @@ protected RedisProtocol getProtocol() {
     return protocol;
   }
 
+  private Lock mapperLock = new ReentrantLock(true);    
   private volatile JsonObjectMapper jsonObjectMapper;
   private final AtomicInteger searchDialect = new AtomicInteger(0);
 
@@ -4435,11 +4438,15 @@ public final CommandObject tFunctionCallAsync(String library, String fun
   private JsonObjectMapper getJsonObjectMapper() {
     JsonObjectMapper localRef = this.jsonObjectMapper;
     if (Objects.isNull(localRef)) {
-      synchronized (this) {
+      mapperLock.lock();
+
+      try {
         localRef = this.jsonObjectMapper;
         if (Objects.isNull(localRef)) {
           this.jsonObjectMapper = localRef = new DefaultGsonObjectMapper();
         }
+      } finally {
+        mapperLock.unlock();
       }
     }
     return localRef;
diff --git a/src/main/java/redis/clients/jedis/JedisSentinelPool.java b/src/main/java/redis/clients/jedis/JedisSentinelPool.java
index 586750540c6..f6a9ea705d2 100644
--- a/src/main/java/redis/clients/jedis/JedisSentinelPool.java
+++ b/src/main/java/redis/clients/jedis/JedisSentinelPool.java
@@ -6,6 +6,8 @@
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 import java.util.stream.Collectors;
 
 import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
@@ -28,7 +30,7 @@ public class JedisSentinelPool extends Pool {
 
   private volatile HostAndPort currentHostMaster;
   
-  private final Object initPoolLock = new Object();
+  private final Lock initPoolLock = new ReentrantLock(true);
 
   public JedisSentinelPool(String masterName, Set sentinels,
       final JedisClientConfig masterClientConfig, final JedisClientConfig sentinelClientConfig) {
@@ -213,7 +215,9 @@ public HostAndPort getCurrentHostMaster() {
   }
 
   private void initMaster(HostAndPort master) {
-    synchronized (initPoolLock) {
+    initPoolLock.lock();
+    
+    try {
       if (!master.equals(currentHostMaster)) {
         currentHostMaster = master;
         factory.setHostAndPort(currentHostMaster);
@@ -223,6 +227,8 @@ private void initMaster(HostAndPort master) {
 
         LOG.info("Created JedisSentinelPool to master at {}", master);
       }
+    } finally {
+      initPoolLock.unlock();
     }
   }
 
diff --git a/src/main/java/redis/clients/jedis/graph/GraphCommandObjects.java b/src/main/java/redis/clients/jedis/graph/GraphCommandObjects.java
index a9a49c90810..7496e6e9286 100644
--- a/src/main/java/redis/clients/jedis/graph/GraphCommandObjects.java
+++ b/src/main/java/redis/clients/jedis/graph/GraphCommandObjects.java
@@ -9,6 +9,8 @@
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 import java.util.function.Function;
 
 import redis.clients.jedis.Builder;
@@ -106,9 +108,7 @@ private Builder getBuilder(String graphName) {
   }
 
   private void createBuilder(String graphName) {
-    synchronized (builders) {
-      builders.putIfAbsent(graphName, new ResultSetBuilder(new GraphCacheImpl(graphName)));
-    }
+    builders.computeIfAbsent(graphName, graphNameKey -> new ResultSetBuilder(new GraphCacheImpl(graphNameKey)));
   }
 
   private class GraphCacheImpl implements GraphCache {
@@ -144,6 +144,8 @@ private class GraphCacheList {
     private final String name;
     private final String query;
     private final List data = new CopyOnWriteArrayList<>();
+    
+    private final Lock dataLock = new ReentrantLock(true);
 
     /**
      *
@@ -164,14 +166,18 @@ public GraphCacheList(String name, String procedure) {
      */
     public String getCachedData(int index) {
       if (index >= data.size()) {
-        synchronized (data) {
+        dataLock.lock();
+        
+        try {
           if (index >= data.size()) {
             getProcedureInfo();
           }
+        } finally {
+          dataLock.unlock();
         }
       }
+      
       return data.get(index);
-
     }
 
     /**
diff --git a/src/main/java/redis/clients/jedis/mcf/CircuitBreakerFailoverBase.java b/src/main/java/redis/clients/jedis/mcf/CircuitBreakerFailoverBase.java
index 4ef383e6494..91ad29d9a06 100644
--- a/src/main/java/redis/clients/jedis/mcf/CircuitBreakerFailoverBase.java
+++ b/src/main/java/redis/clients/jedis/mcf/CircuitBreakerFailoverBase.java
@@ -1,6 +1,8 @@
 package redis.clients.jedis.mcf;
 
 import io.github.resilience4j.circuitbreaker.CircuitBreaker;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 import redis.clients.jedis.annots.Experimental;
 import redis.clients.jedis.exceptions.JedisConnectionException;
 import redis.clients.jedis.providers.MultiClusterPooledConnectionProvider;
@@ -17,6 +19,7 @@
  */
 @Experimental
 public class CircuitBreakerFailoverBase implements AutoCloseable {
+    private final Lock lock = new ReentrantLock(true);
 
     protected final MultiClusterPooledConnectionProvider provider;
 
@@ -32,29 +35,34 @@ public void close() {
     /**
      * Functional interface wrapped in retry and circuit breaker logic to handle open circuit breaker failure scenarios
      */
-    protected synchronized void clusterFailover(CircuitBreaker circuitBreaker) {
+    protected void clusterFailover(CircuitBreaker circuitBreaker) {
+        lock.lock();
+        
+        try {
+            // Check state to handle race conditions since incrementActiveMultiClusterIndex() is non-idempotent
+            if (!CircuitBreaker.State.FORCED_OPEN.equals(circuitBreaker.getState())) {
 
-        // Check state to handle race conditions since incrementActiveMultiClusterIndex() is non-idempotent
-        if (!CircuitBreaker.State.FORCED_OPEN.equals(circuitBreaker.getState())) {
+                // Transitions state machine to a FORCED_OPEN state, stopping state transition, metrics and event publishing.
+                // To recover/transition from this forced state the user will need to manually failback
+                circuitBreaker.transitionToForcedOpenState();
 
-            // Transitions state machine to a FORCED_OPEN state, stopping state transition, metrics and event publishing.
-            // To recover/transition from this forced state the user will need to manually failback
-            circuitBreaker.transitionToForcedOpenState();
+                // Incrementing the activeMultiClusterIndex will allow subsequent calls to the executeCommand()
+                // to use the next cluster's connection pool - according to the configuration's prioritization/order
+                int activeMultiClusterIndex = provider.incrementActiveMultiClusterIndex();
 
-            // Incrementing the activeMultiClusterIndex will allow subsequent calls to the executeCommand()
-            // to use the next cluster's connection pool - according to the configuration's prioritization/order
-            int activeMultiClusterIndex = provider.incrementActiveMultiClusterIndex();
+                // Implementation is optionally provided during configuration. Typically, used for activeMultiClusterIndex persistence or custom logging
+                provider.runClusterFailoverPostProcessor(activeMultiClusterIndex);
+            }
 
-            // Implementation is optionally provided during configuration. Typically, used for activeMultiClusterIndex persistence or custom logging
-            provider.runClusterFailoverPostProcessor(activeMultiClusterIndex);
-        }
-
-        // Once the priority list is exhausted only a manual failback can open the circuit breaker so all subsequent operations will fail
-        else if (provider.isLastClusterCircuitBreakerForcedOpen()) {
-            throw new JedisConnectionException("Cluster/database endpoint could not failover since the MultiClusterClientConfig was not " +
-                                               "provided with an additional cluster/database endpoint according to its prioritized sequence. " +
-                                               "If applicable, consider failing back OR restarting with an available cluster/database endpoint");
+            // Once the priority list is exhausted only a manual failback can open the circuit breaker so all subsequent operations will fail
+            else if (provider.isLastClusterCircuitBreakerForcedOpen()) {
+                throw new JedisConnectionException("Cluster/database endpoint could not failover since the MultiClusterClientConfig was not " +
+                                                   "provided with an additional cluster/database endpoint according to its prioritized sequence. " +
+                                                   "If applicable, consider failing back OR restarting with an available cluster/database endpoint");
+            }
+        } finally {
+            lock.unlock();
         }
     }
 
-}
\ No newline at end of file
+}
diff --git a/src/main/java/redis/clients/jedis/providers/MultiClusterPooledConnectionProvider.java b/src/main/java/redis/clients/jedis/providers/MultiClusterPooledConnectionProvider.java
index 47b03c77733..9ddf8e810a0 100644
--- a/src/main/java/redis/clients/jedis/providers/MultiClusterPooledConnectionProvider.java
+++ b/src/main/java/redis/clients/jedis/providers/MultiClusterPooledConnectionProvider.java
@@ -13,6 +13,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 import java.util.function.Consumer;
 
 import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
@@ -54,6 +56,8 @@ public class MultiClusterPooledConnectionProvider implements ConnectionProvider
      * provided at startup via the MultiClusterClientConfig. All traffic will be routed according to this index.
      */
     private volatile Integer activeMultiClusterIndex = 1;
+    
+    private final Lock activeClusterIndexLock = new ReentrantLock(true);
 
     /**
      * Indicates the final cluster/database endpoint (connection pool), according to the pre-configured list
@@ -162,8 +166,9 @@ public int incrementActiveMultiClusterIndex() {
 
         // Field-level synchronization is used to avoid the edge case in which
         // setActiveMultiClusterIndex(int multiClusterIndex) is called at the same time
-        synchronized (activeMultiClusterIndex) {
-
+        activeClusterIndexLock.lock();
+        
+        try {
             String originalClusterName = getClusterCircuitBreaker().getName();
 
             // Only increment if it can pass this validation otherwise we will need to check for NULL in the data path
@@ -185,6 +190,8 @@ public int incrementActiveMultiClusterIndex() {
                 incrementActiveMultiClusterIndex();
 
             else log.warn("Cluster/database endpoint successfully updated from '{}' to '{}'", originalClusterName, circuitBreaker.getName());
+        } finally {
+            activeClusterIndexLock.unlock();
         }
 
         return activeMultiClusterIndex;
@@ -229,11 +236,13 @@ public void validateTargetConnection(int multiClusterIndex) {
      * Special care should be taken to confirm cluster/database availability AND
      * potentially cross-cluster replication BEFORE using this capability.
      */
-    public synchronized void setActiveMultiClusterIndex(int multiClusterIndex) {
+    public void setActiveMultiClusterIndex(int multiClusterIndex) {
 
         // Field-level synchronization is used to avoid the edge case in which
         // incrementActiveMultiClusterIndex() is called at the same time
-        synchronized (activeMultiClusterIndex) {
+        activeClusterIndexLock.lock();
+        
+        try {
 
             // Allows an attempt to reset the current cluster from a FORCED_OPEN to CLOSED state in the event that no failover is possible
             if (activeMultiClusterIndex == multiClusterIndex &&
@@ -256,6 +265,8 @@ public synchronized void setActiveMultiClusterIndex(int multiClusterIndex) {
 
             activeMultiClusterIndex = multiClusterIndex;
             lastClusterCircuitBreakerForcedOpen = false;
+        } finally {
+            activeClusterIndexLock.unlock();
         }
     }
 
diff --git a/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java b/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java
index 5058f07179a..f2f07464609 100644
--- a/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java
+++ b/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java
@@ -5,6 +5,8 @@
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
 import org.slf4j.Logger;
@@ -43,7 +45,7 @@ public class SentineledConnectionProvider implements ConnectionProvider {
 
   private final long subscribeRetryWaitTimeMillis;
 
-  private final Object initPoolLock = new Object();
+  private final Lock initPoolLock = new ReentrantLock(true);
 
   public SentineledConnectionProvider(String masterName, final JedisClientConfig masterClientConfig,
       Set sentinels, final JedisClientConfig sentinelClientConfig) {
@@ -95,7 +97,9 @@ public HostAndPort getCurrentMaster() {
   }
 
   private void initMaster(HostAndPort master) {
-    synchronized (initPoolLock) {
+    initPoolLock.lock();
+    
+    try {
       if (!master.equals(currentMaster)) {
         currentMaster = master;
 
@@ -114,6 +118,8 @@ private void initMaster(HostAndPort master) {
           existingPool.close();
         }
       }
+    } finally {
+      initPoolLock.unlock();
     }
   }
 

From 773651c896fc17195763ab37e4baf30253da6c28 Mon Sep 17 00:00:00 2001
From: zyfx595701088 <50442211+zyfx595701088@users.noreply.github.com>
Date: Tue, 16 Jul 2024 20:04:25 +0800
Subject: [PATCH 15/63] Modify the judgment that reads a response as empty to
 isEmpty method (#3888)

Co-authored-by: zhangshuai 
---
 src/main/java/redis/clients/jedis/util/RedisInputStream.java | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/main/java/redis/clients/jedis/util/RedisInputStream.java b/src/main/java/redis/clients/jedis/util/RedisInputStream.java
index a0dad9d4370..f7e320f1642 100644
--- a/src/main/java/redis/clients/jedis/util/RedisInputStream.java
+++ b/src/main/java/redis/clients/jedis/util/RedisInputStream.java
@@ -14,6 +14,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.math.BigInteger;
+
 import redis.clients.jedis.exceptions.JedisConnectionException;
 
 /**
@@ -84,7 +85,7 @@ public String readLine() {
     }
 
     final String reply = sb.toString();
-    if (reply.length() == 0) {
+    if (reply.isEmpty()) {
       throw new JedisConnectionException("It seems like server has closed the connection.");
     }
 

From 2274416b771c161f3f659ca70633a90376f3c42c Mon Sep 17 00:00:00 2001
From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com>
Date: Tue, 16 Jul 2024 20:45:50 +0600
Subject: [PATCH 16/63] JedisConnectionException contains HostAndPort from
 DefaultJedisSocketFactory (#3896)

---
 src/main/java/redis/clients/jedis/Connection.java               | 2 +-
 .../java/redis/clients/jedis/DefaultJedisSocketFactory.java     | 2 +-
 .../jedis/providers/MultiClusterPooledConnectionProvider.java   | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/main/java/redis/clients/jedis/Connection.java b/src/main/java/redis/clients/jedis/Connection.java
index f9fe5594829..9325de80d7c 100644
--- a/src/main/java/redis/clients/jedis/Connection.java
+++ b/src/main/java/redis/clients/jedis/Connection.java
@@ -343,7 +343,7 @@ protected void flush() {
 
   protected Object readProtocolWithCheckingBroken() {
     if (broken) {
-      throw new JedisConnectionException("Attempting to read from a broken connection");
+      throw new JedisConnectionException("Attempting to read from a broken connection.");
     }
 
     try {
diff --git a/src/main/java/redis/clients/jedis/DefaultJedisSocketFactory.java b/src/main/java/redis/clients/jedis/DefaultJedisSocketFactory.java
index a2d963e2214..c9ef6646ba6 100644
--- a/src/main/java/redis/clients/jedis/DefaultJedisSocketFactory.java
+++ b/src/main/java/redis/clients/jedis/DefaultJedisSocketFactory.java
@@ -60,7 +60,7 @@ private Socket connectToFirstSuccessfulHost(HostAndPort hostAndPort) throws Exce
       Collections.shuffle(hosts);
     }
 
-    JedisConnectionException jce = new JedisConnectionException("Failed to connect to any host resolved for DNS name.");
+    JedisConnectionException jce = new JedisConnectionException("Failed to connect to " + hostAndPort + ".");
     for (InetAddress host : hosts) {
       try {
         Socket socket = new Socket();
diff --git a/src/main/java/redis/clients/jedis/providers/MultiClusterPooledConnectionProvider.java b/src/main/java/redis/clients/jedis/providers/MultiClusterPooledConnectionProvider.java
index 9ddf8e810a0..eb443bca1e5 100644
--- a/src/main/java/redis/clients/jedis/providers/MultiClusterPooledConnectionProvider.java
+++ b/src/main/java/redis/clients/jedis/providers/MultiClusterPooledConnectionProvider.java
@@ -178,7 +178,7 @@ public int incrementActiveMultiClusterIndex() {
 
                 throw new JedisConnectionException("Cluster/database endpoint could not failover since the MultiClusterClientConfig was not " +
                                                    "provided with an additional cluster/database endpoint according to its prioritized sequence. " +
-                                                   "If applicable, consider failing back OR restarting with an available cluster/database endpoint");
+                                                   "If applicable, consider failing back OR restarting with an available cluster/database endpoint.");
             }
             else activeMultiClusterIndex++;
 

From 28e7c73ba6bb8532dd1397928a735d0fc7a43813 Mon Sep 17 00:00:00 2001
From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com>
Date: Wed, 17 Jul 2024 13:10:48 +0100
Subject: [PATCH 17/63] Json tabbed code sample (#3895)

Co-authored-by: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com>
---
 .../java/io/redis/examples/JsonExample.java   | 509 ++++++++++++++++++
 1 file changed, 509 insertions(+)
 create mode 100644 src/test/java/io/redis/examples/JsonExample.java

diff --git a/src/test/java/io/redis/examples/JsonExample.java b/src/test/java/io/redis/examples/JsonExample.java
new file mode 100644
index 00000000000..4fd635b6515
--- /dev/null
+++ b/src/test/java/io/redis/examples/JsonExample.java
@@ -0,0 +1,509 @@
+// EXAMPLE: json_tutorial
+// REMOVE_START
+package io.redis.examples;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.List;
+
+// REMOVE_END
+// HIDE_START
+import redis.clients.jedis.UnifiedJedis;
+import redis.clients.jedis.json.Path2;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+// HIDE_END
+
+// HIDE_START
+public class JsonExample {
+    @Test
+    public void run() {
+        UnifiedJedis jedis = new UnifiedJedis("redis://localhost:6379");
+// HIDE_END
+
+        //REMOVE_START
+        // Clear any keys here before using them in tests.
+        jedis.del("bike", "bike:1", "crashes", "newbike", "riders", "bikes:inventory");
+        //REMOVE_END
+
+        // STEP_START set_get
+        String res1 = jedis.jsonSet("bike", new Path2("$"), "\"Hyperion\"");
+        System.out.println(res1);   // >>> OK
+
+        Object res2 = jedis.jsonGet("bike", new Path2("$"));
+        System.out.println(res2);   // >>> ["Hyperion"]
+
+        List> res3 = jedis.jsonType("bike", new Path2("$"));
+        System.out.println(res3);   // >>> [class java.lang.String]
+        // STEP_END
+
+        // Tests for 'set_get' step.
+        // REMOVE_START
+        Assert.assertEquals("OK", res1);
+        Assert.assertEquals("[\"Hyperion\"]", res2.toString());
+        Assert.assertEquals("[class java.lang.String]", res3.toString());
+        // REMOVE_END
+
+
+        // STEP_START str
+        List res4 = jedis.jsonStrLen("bike", new Path2("$"));
+        System.out.println(res4);   // >>> [8]
+
+        List res5 = jedis.jsonStrAppend("bike", new Path2("$"), " (Enduro bikes)");
+        System.out.println(res5);   // >>> [23]
+
+        Object res6 = jedis.jsonGet("bike", new Path2("$"));
+        System.out.println(res6);   // >>> ["Hyperion (Enduro bikes)"]
+        // STEP_END
+
+        // Tests for 'str' step.
+        // REMOVE_START
+        Assert.assertEquals("[8]", res4.toString());
+        Assert.assertEquals("[23]", res5.toString());
+        Assert.assertEquals("[\"Hyperion (Enduro bikes)\"]", res6.toString());
+        // REMOVE_END
+
+
+        // STEP_START num
+        String res7 = jedis.jsonSet("crashes", new Path2("$"), 0);
+        System.out.println(res7);   // >>> OK
+
+        Object res8 = jedis.jsonNumIncrBy("crashes", new Path2("$"), 1);
+        System.out.println(res8);   // >>> [1]
+
+        Object res9 = jedis.jsonNumIncrBy("crashes", new Path2("$"), 1.5);
+        System.out.println(res9);   // >>> [2.5]
+
+        Object res10 = jedis.jsonNumIncrBy("crashes", new Path2("$"), -0.75);
+        System.out.println(res10);   // >>> [1.75]
+        // STEP_END
+
+        // Tests for 'num' step.
+        // REMOVE_START
+        Assert.assertEquals("OK", res7);
+        Assert.assertEquals("[1]", res8.toString());
+        Assert.assertEquals("[2.5]", res9.toString());
+        Assert.assertEquals("[1.75]", res10.toString());
+        // REMOVE_END
+
+
+        // STEP_START arr
+        String res11 = jedis.jsonSet("newbike", new Path2("$"),
+            new JSONArray()
+                .put("Deimos")
+                .put(new JSONObject().put("crashes", 0))
+                .put((Object) null)
+        );
+        System.out.println(res11);  // >>> OK
+        
+        Object res12 = jedis.jsonGet("newbike", new Path2("$"));
+        System.out.println(res12);  // >>> [["Deimos",{"crashes":0},null]]
+
+        Object res13 = jedis.jsonGet("newbike", new Path2("$[1].crashes"));
+        System.out.println(res13);  // >>> [0]
+
+        long res14 = jedis.jsonDel("newbike", new Path2("$.[-1]"));
+        System.out.println(res14);  // >>> 1
+
+        Object res15 = jedis.jsonGet("newbike", new Path2("$"));
+        System.out.println(res15);  // >>> [["Deimos",{"crashes":0}]]
+        // STEP_END
+
+        // Tests for 'arr' step.
+        // REMOVE_START
+        Assert.assertEquals("OK", res11);
+        Assert.assertEquals("[[\"Deimos\",{\"crashes\":0},null]]", res12.toString());
+        Assert.assertEquals("[0]", res13.toString());
+        Assert.assertEquals(1, res14);
+        Assert.assertEquals("[[\"Deimos\",{\"crashes\":0}]]", res15.toString());
+        // REMOVE_END
+
+
+        // STEP_START arr2
+        String res16 = jedis.jsonSet("riders", new Path2("$"), new JSONArray());
+        System.out.println(res16);  // >>> OK
+
+        List res17 = jedis.jsonArrAppendWithEscape("riders", new Path2("$"), "Norem");
+        System.out.println(res17);  // >>> [1]
+
+        Object res18 = jedis.jsonGet("riders", new Path2("$"));
+        System.out.println(res18);  // >>> [["Norem"]]
+
+        List res19 = jedis.jsonArrInsertWithEscape(
+            "riders", new Path2("$"), 1, "Prickett", "Royce", "Castilla"
+        );
+        System.out.println(res19);  // >>> [4]
+
+        Object res20 = jedis.jsonGet("riders", new Path2("$"));
+        System.out.println(res20);
+        // >>> [["Norem","Prickett","Royce","Castilla"]]
+        
+        List res21 = jedis.jsonArrTrim("riders", new Path2("$"), 1, 1);
+        System.out.println(res21);  // >>> [1]
+
+        Object res22 = jedis.jsonGet("riders", new Path2("$"));
+        System.out.println(res22);  // >>> [["Prickett"]]
+
+        Object res23 = jedis.jsonArrPop("riders", new Path2("$"));
+        System.out.println(res23);  // >>> [Prickett]
+
+        Object res24 = jedis.jsonArrPop("riders", new Path2("$"));
+        System.out.println(res24);  // >>> [null]
+        // STEP_END
+
+        // Tests for 'arr2' step.
+        // REMOVE_START
+        Assert.assertEquals("OK", res16);
+        Assert.assertEquals("[1]", res17.toString());
+        Assert.assertEquals("[[\"Norem\"]]", res18.toString());
+        Assert.assertEquals("[4]", res19.toString());
+        Assert.assertEquals("[[\"Norem\",\"Prickett\",\"Royce\",\"Castilla\"]]", res20.toString());
+        Assert.assertEquals("[1]", res21.toString());
+        Assert.assertEquals("[[\"Prickett\"]]", res22.toString());
+        Assert.assertEquals("[Prickett]", res23.toString());
+        Assert.assertEquals("[null]", res24.toString());
+        // REMOVE_END
+
+
+        // STEP_START obj
+        String res25 = jedis.jsonSet("bike:1", new Path2("$"),
+            new JSONObject()
+                .put("model", "Deimos")
+                .put("brand", "Ergonom")
+                .put("price", 4972)
+        );
+        System.out.println(res25);  // >>> OK
+
+        List res26 = jedis.jsonObjLen("bike:1", new Path2("$"));
+        System.out.println(res26);  // >>> [3]
+
+        List> res27 = jedis.jsonObjKeys("bike:1", new Path2("$"));
+        System.out.println(res27);  // >>> [[price, model, brand]]
+        // STEP_END
+
+        // Tests for 'obj' step.
+        // REMOVE_START
+        Assert.assertEquals("OK", res25);
+        Assert.assertEquals("[3]", res26.toString());
+        Assert.assertEquals("[[price, model, brand]]", res27.toString());
+        // REMOVE_END
+
+        // STEP_START set_bikes
+        String inventory_json = "{"
+        + "    \"inventory\": {"
+        + "        \"mountain_bikes\": ["
+        + "            {"
+        + "                \"id\": \"bike:1\","
+        + "                \"model\": \"Phoebe\","
+        + "                \"description\": \"This is a mid-travel trail slayer that is a "
+        + "fantastic daily driver or one bike quiver. The Shimano Claris 8-speed groupset "
+        + "gives plenty of gear range to tackle hills and there\u2019s room for mudguards "
+        + "and a rack too.  This is the bike for the rider who wants trail manners with "
+        + "low fuss ownership.\","
+        + "                \"price\": 1920,"
+        + "                \"specs\": {\"material\": \"carbon\", \"weight\": 13.1},"
+        + "                \"colors\": [\"black\", \"silver\"]"
+        + "            },"
+        + "            {"
+        + "                \"id\": \"bike:2\","
+        + "                \"model\": \"Quaoar\","
+        + "                \"description\": \"Redesigned for the 2020 model year, this "
+        + "bike impressed our testers and is the best all-around trail bike we've ever "
+        + "tested. The Shimano gear system effectively does away with an external cassette, "
+        + "so is super low maintenance in terms of wear and tear. All in all it's an "
+        + "impressive package for the price, making it very competitive.\","
+        + "                \"price\": 2072,"
+        + "                \"specs\": {\"material\": \"aluminium\", \"weight\": 7.9},"
+        + "                \"colors\": [\"black\", \"white\"]"
+        + "            },"
+        + "            {"
+        + "                \"id\": \"bike:3\","
+        + "                \"model\": \"Weywot\","
+        + "                \"description\": \"This bike gives kids aged six years and older "
+        + "a durable and uberlight mountain bike for their first experience on tracks and easy "
+        + "cruising through forests and fields. A set of powerful Shimano hydraulic disc brakes "
+        + "provide ample stopping ability. If you're after a budget option, this is one of the "
+        + "best bikes you could get.\","
+        + "                \"price\": 3264,"
+        + "                \"specs\": {\"material\": \"alloy\", \"weight\": 13.8}"
+        + "            }"
+        + "        ],"
+        + "        \"commuter_bikes\": ["
+        + "            {"
+        + "                \"id\": \"bike:4\","
+        + "                \"model\": \"Salacia\","
+        + "                \"description\": \"This bike is a great option for anyone who just "
+        + "wants a bike to get about on With a slick-shifting Claris gears from Shimano\u2019s, "
+        + "this is a bike which doesn\u2019t break the bank and delivers craved performance.  "
+        + "It\u2019s for the rider who wants both efficiency and capability.\","
+        + "                \"price\": 1475,"
+        + "                \"specs\": {\"material\": \"aluminium\", \"weight\": 16.6},"
+        + "                \"colors\": [\"black\", \"silver\"]"
+        + "            },"
+        + "            {"
+        + "                \"id\": \"bike:5\","
+        + "                \"model\": \"Mimas\","
+        + "                \"description\": \"A real joy to ride, this bike got very high scores "
+        + "in last years Bike of the year report. The carefully crafted 50-34 tooth chainset "
+        + "and 11-32 tooth cassette give an easy-on-the-legs bottom gear for climbing, and the "
+        + "high-quality Vittoria Zaffiro tires give balance and grip.It includes a low-step "
+        + "frame , our memory foam seat, bump-resistant shocks and conveniently placed thumb "
+        + "throttle. Put it all together and you get a bike that helps redefine what can be "
+        + "done for this price.\","
+        + "                \"price\": 3941,"
+        + "                \"specs\": {\"material\": \"alloy\", \"weight\": 11.6}"
+        + "            }"
+        + "        ]"
+        + "    }"
+        + "}";
+
+        String res28 = jedis.jsonSet("bikes:inventory", new Path2("$"), inventory_json);
+        System.out.println(res28);  // >>> OK
+        // STEP_END
+
+        // Tests for 'set_bikes' step.
+        // REMOVE_START
+        Assert.assertEquals("OK", res28);
+        // REMOVE_END
+
+
+        // STEP_START get_bikes
+        Object res29 = jedis.jsonGet("bikes:inventory", new Path2("$.inventory.*"));
+        System.out.println(res29);
+        // >>> [[{"specs":{"material":"carbon","weight":13.1},"price":1920, ...
+        // STEP_END
+
+        // Tests for 'get_bikes' step.
+        // REMOVE_START
+        Assert.assertEquals(
+            "[[{\"specs\":{\"material\":\"carbon\",\"weight\":13.1},\"price\":1920,"
+            + "\"description\":\"This is a mid-travel trail slayer that is a "
+            + "fantastic daily driver or one bike quiver. The Shimano Claris 8-speed "
+            + "groupset gives plenty of gear range to tackle hills and "
+            + "there\\u2019s room for mudguards and a rack too.  This is the bike "
+            + "for the rider who wants trail manners with low fuss "
+            + "ownership.\",\"model\":\"Phoebe\",\"id\":\"bike:1\",\"colors\":"
+            + "[\"black\",\"silver\"]},{\"specs\":{\"material\":\"aluminium\",\"weight\":7.9},"
+            + "\"price\":2072,\"description\":\"Redesigned for the 2020 model year, this "
+            + "bike impressed our testers and is the best all-around trail bike we've "
+            + "ever tested. The Shimano gear system effectively does away with an "
+            + "external cassette, so is super low maintenance in terms of wear and tear. "
+            + "All in all it's an impressive package for the price, making it very "
+            + "competitive.\",\"model\":\"Quaoar\",\"id\":\"bike:2\",\"colors\":"
+            + "[\"black\",\"white\"]},{\"specs\":{\"material\":\"alloy\",\"weight\":13.8},"
+            + "\"price\":3264,\"description\":\"This bike gives kids aged six years and "
+            + "older a durable and uberlight mountain bike for their first experience "
+            + "on tracks and easy cruising through forests and fields. A set of "
+            + "powerful Shimano hydraulic disc brakes provide ample stopping ability. "
+            + "If you're after a budget option, this is one of the best bikes you could "
+            + "get.\",\"model\":\"Weywot\",\"id\":\"bike:3\"}],[{\"specs\":"
+            + "{\"material\":\"aluminium\",\"weight\":16.6},\"price\":1475,\"description\":"
+            + "\"This bike is a great option for anyone who just wants a bike to get about "
+            + "on With a slick-shifting Claris gears from Shimano\\u2019s, this is a bike "
+            + "which doesn\\u2019t break the bank and delivers craved performance.  "
+            + "It\\u2019s for the rider who wants both efficiency and "
+            + "capability.\",\"model\":\"Salacia\",\"id\":\"bike:4\",\"colors\":"
+            + "[\"black\",\"silver\"]},{\"specs\":{\"material\":\"alloy\",\"weight\":11.6},"
+            + "\"price\":3941,\"description\":\"A real joy to ride, this bike got very "
+            + "high scores in last years Bike of the year report. The carefully crafted "
+            + "50-34 tooth chainset and 11-32 tooth cassette give an easy-on-the-legs "
+            + "bottom gear for climbing, and the high-quality Vittoria Zaffiro tires "
+            + "give balance and grip.It includes a low-step frame , our memory foam "
+            + "seat, bump-resistant shocks and conveniently placed thumb throttle. Put "
+            + "it all together and you get a bike that helps redefine what can be done "
+            + "for this price.\",\"model\":\"Mimas\",\"id\":\"bike:5\"}]]",
+             res29.toString()
+        );
+        // REMOVE_END
+
+
+        // STEP_START get_mtnbikes
+        Object res30 = jedis.jsonGet(
+            "bikes:inventory", new Path2("$.inventory.mountain_bikes[*].model")
+        );
+        System.out.println(res30);  // >>> ["Phoebe","Quaoar","Weywot"]
+
+        Object res31 = jedis.jsonGet(
+            "bikes:inventory", new Path2("$.inventory[\"mountain_bikes\"][*].model")
+        );
+        System.out.println(res31);  // >>> ["Phoebe","Quaoar","Weywot"]
+
+        Object res32 = jedis.jsonGet(
+            "bikes:inventory", new Path2("$..mountain_bikes[*].model")
+        );
+        System.out.println(res32);  // >>> ["Phoebe","Quaoar","Weywot"]
+        // STEP_END
+
+        // Tests for 'get_mtnbikes' step.
+        // REMOVE_START
+        Assert.assertEquals("[\"Phoebe\",\"Quaoar\",\"Weywot\"]", res30.toString());
+        Assert.assertEquals("[\"Phoebe\",\"Quaoar\",\"Weywot\"]", res31.toString());
+        Assert.assertEquals("[\"Phoebe\",\"Quaoar\",\"Weywot\"]", res32.toString());
+        // REMOVE_END
+
+
+        // STEP_START get_models
+        Object res33 = jedis.jsonGet("bikes:inventory", new Path2("$..model"));
+        System.out.println(res33);
+        // >>> ["Phoebe","Quaoar","Weywot","Salacia","Mimas"]
+        // STEP_END
+
+        // Tests for 'get_models' step.
+        // REMOVE_START
+        Assert.assertEquals("[\"Phoebe\",\"Quaoar\",\"Weywot\",\"Salacia\",\"Mimas\"]", res33.toString());
+        // REMOVE_END
+
+
+        // STEP_START get2mtnbikes
+        Object res34 = jedis.jsonGet(
+            "bikes:inventory", new Path2("$..mountain_bikes[0:2].model")
+        );
+        System.out.println(res34);  // >>> ["Phoebe","Quaoar"]
+        // STEP_END
+
+        // Tests for 'get2mtnbikes' step.
+        // REMOVE_START
+        Assert.assertEquals("[\"Phoebe\",\"Quaoar\"]", res34.toString());
+        // REMOVE_END
+
+
+        // STEP_START filter1
+        Object res35 = jedis.jsonGet(
+            "bikes:inventory",
+            new Path2("$..mountain_bikes[?(@.price < 3000 && @.specs.weight < 10)]")
+        );
+        System.out.println(res35);
+        // >>> [{"specs":{"material":"aluminium","weight":7.9},"price":2072,...
+        // STEP_END
+
+        // Tests for 'filter1' step.
+        // REMOVE_START
+        Assert.assertEquals(
+            "[{\"specs\":{\"material\":\"aluminium\",\"weight\":7.9},\"price\":2072,"
+            + "\"description\":\"Redesigned for the 2020 model year, this bike impressed "
+            + "our testers and is the best all-around trail bike we've ever tested. The "
+            + "Shimano gear system effectively does away with an external cassette, "
+            + "so is super low maintenance in terms of wear and tear. All in all it's an "
+            + "impressive package for the price, making it very competitive.\",\"model\":"
+            + "\"Quaoar\",\"id\":\"bike:2\",\"colors\":[\"black\",\"white\"]}]",
+            res35.toString()
+        );
+        // REMOVE_END
+
+
+        // STEP_START filter2
+        Object res36 = jedis.jsonGet(
+            "bikes:inventory", new Path2("$..[?(@.specs.material == 'alloy')].model")
+        );
+        System.out.println(res36);  // >>> ["Weywot","Mimas"]
+        // STEP_END
+
+        // Tests for 'filter2' step.
+        // REMOVE_START
+        Assert.assertEquals("[\"Weywot\",\"Mimas\"]", res36.toString());
+        // REMOVE_END
+
+
+        // STEP_START filter3
+        Object res37 = jedis.jsonGet(
+            "bikes:inventory", new Path2("$..[?(@.specs.material =~ '(?i)al')].model")
+        );
+        System.out.println(res37);
+        // >>> ["Quaoar","Weywot","Salacia","Mimas"]
+        // STEP_END
+
+        // Tests for 'filter3' step.
+        // REMOVE_START
+        Assert.assertEquals("[\"Quaoar\",\"Weywot\",\"Salacia\",\"Mimas\"]", res37.toString());
+        // REMOVE_END
+
+
+        // STEP_START filter4
+        jedis.jsonSet(
+            "bikes:inventory", new Path2("$.inventory.mountain_bikes[0].regex_pat"),
+            "\"(?i)al\""
+        );
+        jedis.jsonSet(
+            "bikes:inventory", new Path2("$.inventory.mountain_bikes[1].regex_pat"),
+            "\"(?i)al\""
+        );
+        jedis.jsonSet(
+            "bikes:inventory", new Path2("$.inventory.mountain_bikes[2].regex_pat"),
+            "\"(?i)al\""
+        );
+        
+        Object res38 = jedis.jsonGet(
+            "bikes:inventory",
+            new Path2("$.inventory.mountain_bikes[?(@.specs.material =~ @.regex_pat)].model")
+        );
+        System.out.println(res38);  // >>> ["Quaoar","Weywot"]
+        // STEP_END
+
+        // Tests for 'filter4' step.
+        // REMOVE_START
+        Assert.assertEquals("[\"Quaoar\",\"Weywot\"]", res38.toString());
+        // REMOVE_END
+
+
+        // STEP_START update_bikes
+        Object res39 = jedis.jsonGet("bikes:inventory", new Path2("$..price"));
+        System.out.println(res39);
+        // >>> [1920,2072,3264,1475,3941]
+
+        Object res40 = jedis.jsonNumIncrBy("bikes:inventory", new Path2("$..price"), -100);
+        System.out.println(res40);  // >>> [1820,1972,3164,1375,3841]
+
+        Object res41 = jedis.jsonNumIncrBy("bikes:inventory", new Path2("$..price"), 100);
+        System.out.println(res41);  // >>> [1920,2072,3264,1475,3941]
+        // STEP_END
+
+        // Tests for 'update_bikes' step.
+        // REMOVE_START
+        Assert.assertEquals("[1920,2072,3264,1475,3941]", res39.toString());
+        Assert.assertEquals("[1820,1972,3164,1375,3841]", res40.toString());
+        Assert.assertEquals("[1920,2072,3264,1475,3941]", res41.toString());
+        // REMOVE_END
+
+
+        // STEP_START update_filters1
+        jedis.jsonSet("bikes:inventory", new Path2("$.inventory.*[?(@.price<2000)].price"), 1500);
+        Object res42 = jedis.jsonGet("bikes:inventory", new Path2("$..price"));
+        System.out.println(res42);  // >>> [1500,2072,3264,1500,3941]
+        // STEP_END
+
+        // Tests for 'update_filters1' step.
+        // REMOVE_START
+        Assert.assertEquals("[1500,2072,3264,1500,3941]", res42.toString());
+        // REMOVE_END
+
+
+        // STEP_START update_filters2
+        List res43 = jedis.jsonArrAppendWithEscape(
+            "bikes:inventory", new Path2("$.inventory.*[?(@.price<2000)].colors"),
+            "\"pink\""
+        );
+        System.out.println(res43);  // >>> [3, 3]
+
+        Object res44 = jedis.jsonGet("bikes:inventory", new Path2("$..[*].colors"));
+        System.out.println(res44);
+        // >>> [["black","silver","\"pink\""],["black","white"],["black","silver","\"pink\""]]
+        // STEP_END
+
+        // Tests for 'update_filters2' step.
+        // REMOVE_START
+        Assert.assertEquals("[3, 3]", res43.toString());
+        Assert.assertEquals(
+            "[[\"black\",\"silver\",\"\\\"pink\\\"\"],[\"black\",\"white\"],"
+            + "[\"black\",\"silver\",\"\\\"pink\\\"\"]]",
+            res44.toString());
+        // REMOVE_END
+
+// HIDE_START
+        jedis.close();
+    }
+}
+// HIDE_END
+

From fc603d261d542859936125348f67537b46e9dba0 Mon Sep 17 00:00:00 2001
From: Igor Malinovskiy 
Date: Wed, 17 Jul 2024 14:43:35 +0200
Subject: [PATCH 18/63] Add Scenario tests (#3847)

* Add POC DMC restart test

* Cleanup

* More tests

* Add missing files

* Clean up scenario tests

* Address review suggestions
---
 pom.xml                                       |   6 +
 .../redis/clients/jedis/EndpointConfig.java   |   6 +-
 .../scenario/ClusterTopologyRefreshTest.java  | 100 ++++++++++
 .../scenario/ConnectionInterruptionTest.java  | 182 ++++++++++++++++++
 .../redis/clients/jedis/scenario/FakeApp.java |  65 +++++++
 .../jedis/scenario/FaultInjectionClient.java  | 124 ++++++++++++
 .../jedis/scenario/RecommendedSettings.java   |  31 +++
 7 files changed, 513 insertions(+), 1 deletion(-)
 create mode 100644 src/test/java/redis/clients/jedis/scenario/ClusterTopologyRefreshTest.java
 create mode 100644 src/test/java/redis/clients/jedis/scenario/ConnectionInterruptionTest.java
 create mode 100644 src/test/java/redis/clients/jedis/scenario/FakeApp.java
 create mode 100644 src/test/java/redis/clients/jedis/scenario/FaultInjectionClient.java
 create mode 100644 src/test/java/redis/clients/jedis/scenario/RecommendedSettings.java

diff --git a/pom.xml b/pom.xml
index cee45b01e9d..fef8874ecd8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -133,6 +133,12 @@
 			2.38.0 
 			test
 		
+		
+			org.apache.httpcomponents.client5
+			httpclient5-fluent
+			5.3.1
+			test
+		
 
 		
 		
diff --git a/src/test/java/redis/clients/jedis/EndpointConfig.java b/src/test/java/redis/clients/jedis/EndpointConfig.java
index 5cb322224dd..42a44a3c47e 100644
--- a/src/test/java/redis/clients/jedis/EndpointConfig.java
+++ b/src/test/java/redis/clients/jedis/EndpointConfig.java
@@ -1,6 +1,8 @@
 package redis.clients.jedis;
 
+import com.google.gson.FieldNamingPolicy;
 import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
 import com.google.gson.reflect.TypeToken;
 import redis.clients.jedis.util.JedisURIHelper;
 
@@ -118,7 +120,9 @@ protected String getURISchema(boolean tls) {
     }
 
     public static HashMap loadFromJSON(String filePath) throws Exception {
-        Gson gson = new Gson();
+        Gson gson = new GsonBuilder().setFieldNamingPolicy(
+            FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
+
         HashMap configs;
         try (FileReader reader = new FileReader(filePath)) {
             configs = gson.fromJson(reader, new TypeToken>() {
diff --git a/src/test/java/redis/clients/jedis/scenario/ClusterTopologyRefreshTest.java b/src/test/java/redis/clients/jedis/scenario/ClusterTopologyRefreshTest.java
new file mode 100644
index 00000000000..e4fc9a8b0aa
--- /dev/null
+++ b/src/test/java/redis/clients/jedis/scenario/ClusterTopologyRefreshTest.java
@@ -0,0 +1,100 @@
+package redis.clients.jedis.scenario;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import redis.clients.jedis.*;
+import redis.clients.jedis.providers.ClusterConnectionProvider;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+public class ClusterTopologyRefreshTest {
+
+  private static final Logger log = LoggerFactory.getLogger(ClusterTopologyRefreshTest.class);
+
+  private static EndpointConfig endpoint;
+
+  private final FaultInjectionClient faultClient = new FaultInjectionClient();
+
+  @BeforeClass
+  public static void beforeClass() {
+    try {
+      ClusterTopologyRefreshTest.endpoint = HostAndPorts.getRedisEndpoint("re-single-shard-oss-cluster");
+    } catch (IllegalArgumentException e) {
+      log.warn("Skipping test because no Redis endpoint is configured");
+      org.junit.Assume.assumeTrue(false);
+    }
+  }
+
+  @Test
+  public void testWithPool() {
+    Set jedisClusterNode = new HashSet<>();
+    jedisClusterNode.add(endpoint.getHostAndPort());
+
+    JedisClientConfig config = endpoint.getClientConfigBuilder()
+        .socketTimeoutMillis(RecommendedSettings.DEFAULT_TIMEOUT_MS)
+        .connectionTimeoutMillis(RecommendedSettings.DEFAULT_TIMEOUT_MS).build();
+
+
+    ClusterConnectionProvider provider = new ClusterConnectionProvider(jedisClusterNode, config, RecommendedSettings.poolConfig);
+    ClusterConnectionProvider spyProvider = spy(provider);
+
+    try (JedisCluster client = new JedisCluster(spyProvider,
+        RecommendedSettings.MAX_RETRIES, RecommendedSettings.MAX_TOTAL_RETRIES_DURATION)) {
+      assertEquals("Was this BDB used to run this test before?", 1,
+          client.getClusterNodes().size());
+
+      AtomicLong commandsExecuted = new AtomicLong();
+
+      // Start thread that imitates an application that uses the client
+      FakeApp fakeApp = new FakeApp(client, (UnifiedJedis c) -> {
+        long i = commandsExecuted.getAndIncrement();
+        client.set(String.valueOf(i), String.valueOf(i));
+        return true;
+      });
+
+      Thread t = new Thread(fakeApp);
+      t.start();
+
+      HashMap params = new HashMap<>();
+      params.put("bdb_id", endpoint.getBdbId());
+      params.put("actions", "[\"reshard\",\"failover\"]");
+
+      FaultInjectionClient.TriggerActionResponse actionResponse = null;
+
+      try {
+        log.info("Triggering Resharding and Failover");
+        actionResponse = faultClient.triggerAction("sequence_of_actions", params);
+      } catch (IOException e) {
+        fail("Fault Injection Server error:" + e.getMessage());
+      }
+
+      log.info("Action id: {}", actionResponse.getActionId());
+      fakeApp.setAction(actionResponse);
+
+      try {
+        t.join();
+      } catch (InterruptedException e) {
+        throw new RuntimeException(e);
+      }
+
+      assertTrue(fakeApp.capturedExceptions().isEmpty());
+
+      log.info("Commands executed: {}", commandsExecuted.get());
+      for (long i = 0; i < commandsExecuted.get(); i++) {
+        assertTrue(client.exists(String.valueOf(i)));
+      }
+
+      verify(spyProvider, atLeast(2)).renewSlotCache(any(Connection.class));
+    }
+  }
+
+}
diff --git a/src/test/java/redis/clients/jedis/scenario/ConnectionInterruptionTest.java b/src/test/java/redis/clients/jedis/scenario/ConnectionInterruptionTest.java
new file mode 100644
index 00000000000..ee584931ca6
--- /dev/null
+++ b/src/test/java/redis/clients/jedis/scenario/ConnectionInterruptionTest.java
@@ -0,0 +1,182 @@
+package redis.clients.jedis.scenario;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import redis.clients.jedis.*;
+import redis.clients.jedis.exceptions.JedisConnectionException;
+import redis.clients.jedis.providers.ConnectionProvider;
+import redis.clients.jedis.providers.PooledConnectionProvider;
+import redis.clients.jedis.util.SafeEncoder;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static org.junit.Assert.*;
+
+@RunWith(Parameterized.class)
+public class ConnectionInterruptionTest {
+
+  private static final Logger log = LoggerFactory.getLogger(ConnectionInterruptionTest.class);
+
+  private static EndpointConfig endpoint;
+
+  private final FaultInjectionClient faultClient = new FaultInjectionClient();
+
+  @Parameterized.Parameters
+  public static Iterable data() {
+    return Arrays.asList("dmc_restart", "network_failure");
+  }
+
+  @Parameterized.Parameter
+  public String triggerAction;
+
+  @BeforeClass
+  public static void beforeClass() {
+    try {
+      ConnectionInterruptionTest.endpoint = HostAndPorts.getRedisEndpoint("re-standalone");
+    } catch (IllegalArgumentException e) {
+      log.warn("Skipping test because no Redis endpoint is configured");
+      org.junit.Assume.assumeTrue(false);
+    }
+  }
+
+  @Test
+  public void testWithPool() {
+    ConnectionProvider connectionProvider = new PooledConnectionProvider(endpoint.getHostAndPort(),
+        endpoint.getClientConfigBuilder().build(), RecommendedSettings.poolConfig);
+
+    UnifiedJedis client = new UnifiedJedis(connectionProvider, RecommendedSettings.MAX_RETRIES,
+        RecommendedSettings.MAX_TOTAL_RETRIES_DURATION);
+    String keyName = "counter";
+    client.set(keyName, "0");
+    assertEquals("0", client.get(keyName));
+
+    AtomicLong commandsExecuted = new AtomicLong();
+
+    // Start thread that imitates an application that uses the client
+    FakeApp fakeApp = new FakeApp(client, (UnifiedJedis c) -> {
+      assertTrue(client.incr(keyName) > 0);
+      long currentCount = commandsExecuted.getAndIncrement();
+      log.info("Command executed {}", currentCount);
+      return true;
+    });
+    fakeApp.setKeepExecutingForSeconds(RecommendedSettings.DEFAULT_TIMEOUT_MS/1000 * 2);
+    Thread t = new Thread(fakeApp);
+    t.start();
+
+    HashMap params = new HashMap<>();
+    params.put("bdb_id", endpoint.getBdbId());
+
+    FaultInjectionClient.TriggerActionResponse actionResponse = null;
+
+    try {
+      log.info("Triggering {}", triggerAction);
+      actionResponse = faultClient.triggerAction(triggerAction, params);
+    } catch (IOException e) {
+      fail("Fault Injection Server error:" + e.getMessage());
+    }
+
+    log.info("Action id: {}", actionResponse.getActionId());
+    fakeApp.setAction(actionResponse);
+
+    try {
+      t.join();
+    } catch (InterruptedException e) {
+      throw new RuntimeException(e);
+    }
+
+    log.info("Commands executed: {}", commandsExecuted.get());
+    assertEquals(commandsExecuted.get(), Long.parseLong(client.get(keyName)));
+    assertTrue(fakeApp.capturedExceptions().isEmpty());
+
+    client.close();
+  }
+
+  @Test
+  public void testWithPubSub() {
+    ConnectionProvider connectionProvider = new PooledConnectionProvider(endpoint.getHostAndPort(),
+        endpoint.getClientConfigBuilder().build(), RecommendedSettings.poolConfig);
+
+    UnifiedJedis client = new UnifiedJedis(connectionProvider, RecommendedSettings.MAX_RETRIES,
+        RecommendedSettings.MAX_TOTAL_RETRIES_DURATION);
+
+    AtomicLong messagesSent = new AtomicLong();
+    AtomicLong messagesReceived = new AtomicLong();
+
+    final Thread subscriberThread = getSubscriberThread(messagesReceived, connectionProvider);
+
+    // Start thread that imitates a publisher that uses the client
+    FakeApp fakeApp = new FakeApp(client, (UnifiedJedis c) -> {
+      log.info("Publishing message");
+      long consumed = client.publish("test", String.valueOf(messagesSent.getAndIncrement()));
+      return consumed > 0;
+    });
+    fakeApp.setKeepExecutingForSeconds(10);
+    Thread t = new Thread(fakeApp);
+    t.start();
+
+    HashMap params = new HashMap<>();
+    params.put("bdb_id", endpoint.getBdbId());
+
+    FaultInjectionClient.TriggerActionResponse actionResponse = null;
+
+    try {
+      log.info("Triggering {}", triggerAction);
+      actionResponse = faultClient.triggerAction(triggerAction, params);
+    } catch (IOException e) {
+      fail("Fault Injection Server error:" + e.getMessage());
+    }
+
+    log.info("Action id: {}", actionResponse.getActionId());
+    fakeApp.setAction(actionResponse);
+
+    try {
+      t.join();
+    } catch (InterruptedException e) {
+      throw new RuntimeException(e);
+    }
+
+    if (subscriberThread.isAlive())
+      subscriberThread.interrupt();
+
+    assertEquals(messagesSent.get() - 1, messagesReceived.get());
+    assertTrue(fakeApp.capturedExceptions().isEmpty());
+
+    client.close();
+  }
+
+  private static Thread getSubscriberThread(AtomicLong messagesReceived,
+      ConnectionProvider connectionProvider) {
+    final JedisPubSubBase pubSub = new JedisPubSubBase() {
+
+      @Override
+      public void onMessage(String channel, String message) {
+        messagesReceived.incrementAndGet();
+        log.info("Received message: {}", message);
+      }
+
+      @Override
+      protected String encode(byte[] raw) {
+        return SafeEncoder.encode(raw);
+      }
+    };
+
+    final Thread subscriberThread = new Thread(() -> {
+      try {
+        pubSub.proceed(connectionProvider.getConnection(), "test");
+        fail("PubSub should have been interrupted");
+      } catch (JedisConnectionException e) {
+        log.info("Expected exception in Subscriber: {}", e.getMessage());
+        assertTrue(e.getMessage().contains("Unexpected end of stream."));
+      }
+    });
+    subscriberThread.start();
+    return subscriberThread;
+  }
+}
diff --git a/src/test/java/redis/clients/jedis/scenario/FakeApp.java b/src/test/java/redis/clients/jedis/scenario/FakeApp.java
new file mode 100644
index 00000000000..7e505862a24
--- /dev/null
+++ b/src/test/java/redis/clients/jedis/scenario/FakeApp.java
@@ -0,0 +1,65 @@
+package redis.clients.jedis.scenario;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import redis.clients.jedis.UnifiedJedis;
+import redis.clients.jedis.exceptions.JedisConnectionException;
+import redis.clients.jedis.exceptions.JedisException;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+
+public class FakeApp implements Runnable {
+
+  private static final Logger log = LoggerFactory.getLogger(FakeApp.class);
+
+  public void setKeepExecutingForSeconds(int keepExecutingForSeconds) {
+    this.keepExecutingForSeconds = keepExecutingForSeconds;
+  }
+
+  private int keepExecutingForSeconds = 60;
+
+  private FaultInjectionClient.TriggerActionResponse actionResponse = null;
+  private final UnifiedJedis client;
+  private final ExecutedAction action;
+  private List exceptions = new ArrayList<>();
+
+  @FunctionalInterface
+  public interface ExecutedAction {
+    boolean run(UnifiedJedis client);
+  }
+
+  public FakeApp(UnifiedJedis client, ExecutedAction action) {
+    this.client = client;
+    this.action = action;
+  }
+
+  public void setAction(FaultInjectionClient.TriggerActionResponse actionResponse) {
+    this.actionResponse = actionResponse;
+  }
+
+  public List capturedExceptions() {
+    return exceptions;
+  }
+
+  public void run() {
+    log.info("Starting FakeApp");
+
+    int checkEachSeconds = 5;
+    int timeoutSeconds = 120;
+
+    while (actionResponse == null || !actionResponse.isCompleted(
+        Duration.ofSeconds(checkEachSeconds), Duration.ofSeconds(keepExecutingForSeconds),
+        Duration.ofSeconds(timeoutSeconds))) {
+      try {
+        boolean success = action.run(client);
+
+        if (!success) break;
+      } catch (JedisConnectionException e) {
+        log.error("Error executing action", e);
+        exceptions.add(e);
+      }
+    }
+  }
+}
diff --git a/src/test/java/redis/clients/jedis/scenario/FaultInjectionClient.java b/src/test/java/redis/clients/jedis/scenario/FaultInjectionClient.java
new file mode 100644
index 00000000000..c4e1c5717bb
--- /dev/null
+++ b/src/test/java/redis/clients/jedis/scenario/FaultInjectionClient.java
@@ -0,0 +1,124 @@
+package redis.clients.jedis.scenario;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.concurrent.TimeUnit;
+
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.fluent.Request;
+import com.google.gson.Gson;
+import org.apache.hc.client5.http.fluent.Response;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
+import org.apache.hc.core5.http.ContentType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class FaultInjectionClient {
+
+  private static final String BASE_URL;
+
+  static {
+    BASE_URL = System.getenv().getOrDefault("FAULT_INJECTION_API_URL", "http://127.0.0.1:20324");
+  }
+
+  private static final Logger log = LoggerFactory.getLogger(FaultInjectionClient.class);
+
+  public static class TriggerActionResponse {
+    private final String actionId;
+
+    private Instant lastRequestTime = null;
+
+    private Instant completedAt = null;
+
+    private Instant firstRequestAt = null;
+
+    public TriggerActionResponse(String actionId) {
+      this.actionId = actionId;
+    }
+
+    public String getActionId() {
+      return actionId;
+    }
+
+    public boolean isCompleted(Duration checkInterval, Duration delayAfter, Duration timeout) {
+      if (completedAt != null) {
+        return Duration.between(completedAt, Instant.now()).compareTo(delayAfter) >= 0;
+      }
+
+      if (firstRequestAt != null && Duration.between(firstRequestAt, Instant.now())
+          .compareTo(timeout) >= 0) {
+        throw new RuntimeException("Timeout");
+      }
+
+      if (lastRequestTime == null || Duration.between(lastRequestTime, Instant.now())
+          .compareTo(checkInterval) >= 0) {
+        lastRequestTime = Instant.now();
+
+        if (firstRequestAt == null) {
+          firstRequestAt = lastRequestTime;
+        }
+
+        CloseableHttpClient httpClient = getHttpClient();
+
+        Request request = Request.get(BASE_URL + "/action/" + actionId);
+
+        try {
+          Response response = request.execute(httpClient);
+          String result = response.returnContent().asString();
+
+          log.info("Action status: {}", result);
+
+          if (result.contains("success")) {
+            completedAt = Instant.now();
+            return Duration.between(completedAt, Instant.now()).compareTo(delayAfter) >= 0;
+          }
+
+        } catch (IOException e) {
+          throw new RuntimeException("Fault injection proxy error ", e);
+        }
+      }
+      return false;
+    }
+  }
+
+  private static CloseableHttpClient getHttpClient() {
+    RequestConfig requestConfig = RequestConfig.custom()
+        .setConnectionRequestTimeout(5000, TimeUnit.MILLISECONDS)
+        .setResponseTimeout(5000, TimeUnit.MILLISECONDS).build();
+
+    return HttpClientBuilder.create()
+        .setDefaultRequestConfig(requestConfig).build();
+  }
+
+  public TriggerActionResponse triggerAction(String actionType, HashMap parameters)
+      throws IOException {
+    Gson gson = new GsonBuilder().setFieldNamingPolicy(
+        FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
+
+    HashMap payload = new HashMap<>();
+    payload.put("type", actionType);
+    payload.put("parameters", parameters);
+
+    String jsonString = gson.toJson(payload);
+
+    CloseableHttpClient httpClient = getHttpClient();
+    Request request = Request.post(BASE_URL + "/action");
+    request.bodyString(jsonString, ContentType.APPLICATION_JSON);
+
+    try {
+      String result = request.execute(httpClient).returnContent().asString();
+      return gson.fromJson(result, new TypeToken() {
+      }.getType());
+    } catch (IOException e) {
+      e.printStackTrace();
+      throw e;
+    }
+  }
+
+}
diff --git a/src/test/java/redis/clients/jedis/scenario/RecommendedSettings.java b/src/test/java/redis/clients/jedis/scenario/RecommendedSettings.java
new file mode 100644
index 00000000000..aa1671ad9e3
--- /dev/null
+++ b/src/test/java/redis/clients/jedis/scenario/RecommendedSettings.java
@@ -0,0 +1,31 @@
+package redis.clients.jedis.scenario;
+
+import redis.clients.jedis.ConnectionPoolConfig;
+
+import java.time.Duration;
+
+public class RecommendedSettings {
+
+  public static ConnectionPoolConfig poolConfig;
+
+  static {
+    poolConfig = new ConnectionPoolConfig();
+    ConnectionPoolConfig poolConfig = new ConnectionPoolConfig();
+    poolConfig.setMaxTotal(8);
+    poolConfig.setMaxIdle(8);
+    poolConfig.setMinIdle(0);
+    poolConfig.setBlockWhenExhausted(true);
+    poolConfig.setMaxWait(Duration.ofSeconds(1));
+    poolConfig.setTestWhileIdle(true);
+    poolConfig.setTimeBetweenEvictionRuns(Duration.ofSeconds(1));
+  }
+
+  public static int MAX_RETRIES = 5;
+
+  public static Duration MAX_TOTAL_RETRIES_DURATION = Duration.ofSeconds(10);
+
+  public static int DEFAULT_TIMEOUT_MS = 5000;
+
+
+
+}

From bd14f2096a823bc7daf706b78beeeacd63220c47 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 25 Jul 2024 12:33:23 +0600
Subject: [PATCH 19/63] Bump jackson.version from 2.17.1 to 2.17.2 (#3902)

Bumps `jackson.version` from 2.17.1 to 2.17.2.

Updates `com.fasterxml.jackson.core:jackson-databind` from 2.17.1 to 2.17.2
- [Commits](https://github.com/FasterXML/jackson/commits)

Updates `com.fasterxml.jackson.datatype:jackson-datatype-jsr310` from 2.17.1 to 2.17.2

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-type: direct:development
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.datatype:jackson-datatype-jsr310
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index fef8874ecd8..6cfd4a2a2d7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -49,7 +49,7 @@
 		redis.clients.jedis
 		1.7.36
 		1.7.1
-		2.17.1
+		2.17.2
 		3.2.5
 	
 

From 51266d5197774af511e7dce9bf7a056613d8b359 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 25 Jul 2024 12:33:36 +0600
Subject: [PATCH 20/63] Bump com.kohlschutter.junixsocket:junixsocket-core from
 2.9.1 to 2.10.0 (#3901)

Bumps [com.kohlschutter.junixsocket:junixsocket-core](https://github.com/kohlschutter/junixsocket) from 2.9.1 to 2.10.0.
- [Release notes](https://github.com/kohlschutter/junixsocket/releases)
- [Commits](https://github.com/kohlschutter/junixsocket/compare/junixsocket-2.9.1...junixsocket-2.10.0)

---
updated-dependencies:
- dependency-name: com.kohlschutter.junixsocket:junixsocket-core
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 6cfd4a2a2d7..a8fa81fdfc2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -79,7 +79,7 @@
 		
 			com.kohlschutter.junixsocket
 			junixsocket-core
-			2.9.1
+			2.10.0
 			pom
 			test
 		

From 0b04243fdfc8caeb767ac8340aa86b0cf8daff47 Mon Sep 17 00:00:00 2001
From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com>
Date: Fri, 26 Jul 2024 14:03:19 +0600
Subject: [PATCH 21/63] [TEMPORARY] [TEST] Disable FT.CONFIG tests (#3907)

Disable FT.CONFIG tests
---
 .../modules/RedisModulesPipelineTest.java      | 18 ++++++++++--------
 .../jedis/modules/search/SearchConfigTest.java |  4 ++++
 2 files changed, 14 insertions(+), 8 deletions(-)

diff --git a/src/test/java/redis/clients/jedis/modules/RedisModulesPipelineTest.java b/src/test/java/redis/clients/jedis/modules/RedisModulesPipelineTest.java
index db0ba8223c1..4c9ad92f85b 100644
--- a/src/test/java/redis/clients/jedis/modules/RedisModulesPipelineTest.java
+++ b/src/test/java/redis/clients/jedis/modules/RedisModulesPipelineTest.java
@@ -66,10 +66,11 @@ public void search() {
     Response explain = p.ftExplain(index, new Query("@title:title_val"));
     Response> explainCLI = p.ftExplainCLI(index, new Query("@title:title_val"));
     Response> info = p.ftInfo(index);
-    Response configSet = p.ftConfigSet("timeout", "100");
-    Response> configGet = p.ftConfigGet("*");
-    Response configSetIndex = p.ftConfigSet(index, "timeout", "100");
-    Response> configGetIndex = p.ftConfigGet(index, "*");
+//    // @org.junit.Ignore
+//    Response configSet = p.ftConfigSet("timeout", "100");
+//    Response> configGet = p.ftConfigGet("*");
+//    Response configSetIndex = p.ftConfigSet(index, "timeout", "100");
+//    Response> configGetIndex = p.ftConfigGet(index, "*");
     Response synUpdate = p.ftSynUpdate(index, "foo", "bar");
     Response>> synDump = p.ftSynDump(index);
 
@@ -85,10 +86,11 @@ public void search() {
     assertNotNull(explain.get());
     assertNotNull(explainCLI.get().get(0));
     assertEquals(index, info.get().get("index_name"));
-    assertEquals("OK", configSet.get());
-    assertEquals("100", configGet.get().get("TIMEOUT"));
-    assertEquals("OK", configSetIndex.get());
-    assertEquals("100", configGetIndex.get().get("TIMEOUT"));
+//    // @org.junit.Ignore
+//    assertEquals("OK", configSet.get());
+//    assertEquals("100", configGet.get().get("TIMEOUT"));
+//    assertEquals("OK", configSetIndex.get());
+//    assertEquals("100", configGetIndex.get().get("TIMEOUT"));
     assertEquals("OK", synUpdate.get());
     Map> expected = new HashMap<>();
     expected.put("bar", Collections.singletonList("foo"));
diff --git a/src/test/java/redis/clients/jedis/modules/search/SearchConfigTest.java b/src/test/java/redis/clients/jedis/modules/search/SearchConfigTest.java
index 4e93303a4a9..2f14e58be95 100644
--- a/src/test/java/redis/clients/jedis/modules/search/SearchConfigTest.java
+++ b/src/test/java/redis/clients/jedis/modules/search/SearchConfigTest.java
@@ -6,6 +6,7 @@
 import java.util.Collections;
 import java.util.Map;
 import org.junit.BeforeClass;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -30,6 +31,7 @@ public SearchConfigTest(RedisProtocol protocol) {
     super(protocol);
   }
 
+  @Ignore
   @Test
   public void config() {
     Map map = client.ftConfigGet("TIMEOUT");
@@ -42,6 +44,7 @@ public void config() {
     }
   }
 
+  @Ignore
   @Test
   public void configOnTimeout() {
     // confirm default
@@ -57,6 +60,7 @@ public void configOnTimeout() {
     }
   }
 
+  @Ignore
   @Test
   public void dialectConfig() {
     // confirm default

From 7b121c4f35ce5d733f157030974c1ee70ee2b710 Mon Sep 17 00:00:00 2001
From: Igor Malinovskiy 
Date: Fri, 26 Jul 2024 14:09:25 +0200
Subject: [PATCH 22/63] Added support for ADDSCORES argument in FT.AGGREGATE
 (#3908)

---
 .../clients/jedis/search/SearchProtocol.java    |  2 +-
 .../jedis/search/aggr/AggregationBuilder.java   |  5 +++++
 .../jedis/modules/search/AggregationTest.java   | 17 +++++++++++++++++
 3 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/main/java/redis/clients/jedis/search/SearchProtocol.java b/src/main/java/redis/clients/jedis/search/SearchProtocol.java
index 64482f4fbb2..17f5df1349d 100644
--- a/src/main/java/redis/clients/jedis/search/SearchProtocol.java
+++ b/src/main/java/redis/clients/jedis/search/SearchProtocol.java
@@ -56,7 +56,7 @@ public enum SearchKeyword implements Rawable {
     LANGUAGE_FIELD, SCORE, SCORE_FIELD, SCORER, PARAMS, AS, DIALECT, SLOP, TIMEOUT, INORDER,
     EXPANDER, MAXTEXTFIELDS, SKIPINITIALSCAN, WITHSUFFIXTRIE, NOSTEM, NOINDEX, PHONETIC, WEIGHT,
     CASESENSITIVE, LOAD, APPLY, GROUPBY, MAXIDLE, WITHCURSOR, DISTANCE, TERMS, INCLUDE, EXCLUDE,
-    SEARCH, AGGREGATE, QUERY, LIMITED, COUNT, REDUCE, INDEXMISSING, INDEXEMPTY;
+    SEARCH, AGGREGATE, QUERY, LIMITED, COUNT, REDUCE, INDEXMISSING, INDEXEMPTY, ADDSCORES;
 
     private final byte[] raw;
 
diff --git a/src/main/java/redis/clients/jedis/search/aggr/AggregationBuilder.java b/src/main/java/redis/clients/jedis/search/aggr/AggregationBuilder.java
index 7c8b5473392..ec478b33671 100644
--- a/src/main/java/redis/clients/jedis/search/aggr/AggregationBuilder.java
+++ b/src/main/java/redis/clients/jedis/search/aggr/AggregationBuilder.java
@@ -170,6 +170,11 @@ public AggregationBuilder timeout(long timeout) {
     return this;
   }
 
+  public AggregationBuilder addScores() {
+    aggrArgs.add(SearchKeyword.ADDSCORES);
+    return this;
+  }
+
   public AggregationBuilder params(Map params) {
     aggrArgs.add(SearchKeyword.PARAMS);
     aggrArgs.add(params.size() << 1);
diff --git a/src/test/java/redis/clients/jedis/modules/search/AggregationTest.java b/src/test/java/redis/clients/jedis/modules/search/AggregationTest.java
index 98e811472ed..ad960209e30 100644
--- a/src/test/java/redis/clients/jedis/modules/search/AggregationTest.java
+++ b/src/test/java/redis/clients/jedis/modules/search/AggregationTest.java
@@ -200,6 +200,23 @@ public void testAggregationBuilderVerbatim() {
     assertEquals(0, res.getTotalResults());
   }
 
+  @Test
+  public void testAggregationBuilderAddScores() {
+    Schema sc = new Schema();
+    sc.addSortableTextField("name", 1.0);
+    sc.addSortableNumericField("age");
+    client.ftCreate(index, IndexOptions.defaultOptions(), sc);
+    addDocument(new Document("data1").set("name", "Adam").set("age", 33));
+    addDocument(new Document("data2").set("name", "Sara").set("age", 44));
+
+    AggregationBuilder r = new AggregationBuilder("sara").addScores()
+        .apply("@__score * 100", "normalized_score").dialect(3);
+
+    AggregationResult res = client.ftAggregate(index, r);
+    assertEquals(2, res.getRow(0).getLong("__score"));
+    assertEquals(200, res.getRow(0).getLong("normalized_score"));
+  }
+
   @Test
   public void testAggregationBuilderTimeout() {
     Schema sc = new Schema();

From e14b8995099d472cc96d395c608876290a213134 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 29 Jul 2024 10:44:02 +0600
Subject: [PATCH 23/63] Bump org.apache.maven.plugins:maven-jar-plugin from
 3.4.1 to 3.4.2 (#3910)

Bumps [org.apache.maven.plugins:maven-jar-plugin](https://github.com/apache/maven-jar-plugin) from 3.4.1 to 3.4.2.
- [Release notes](https://github.com/apache/maven-jar-plugin/releases)
- [Commits](https://github.com/apache/maven-jar-plugin/compare/maven-jar-plugin-3.4.1...maven-jar-plugin-3.4.2)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-jar-plugin
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index a8fa81fdfc2..181fc6db127 100644
--- a/pom.xml
+++ b/pom.xml
@@ -278,7 +278,7 @@
 			
 			
 				maven-jar-plugin
-				3.4.1
+				3.4.2
 				
 					
 						${project.build.outputDirectory}/META-INF/MANIFEST.MF

From 085624517ba1a9740f23bf22d8c2af344c3e53b1 Mon Sep 17 00:00:00 2001
From: anotherJJz <470623352@qq.com>
Date: Mon, 12 Aug 2024 17:35:56 +0800
Subject: [PATCH 24/63] Support execute the read-only command on replica nodes
 (#3848)

Co-authored-by: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com>
---
 .../java/redis/clients/jedis/Connection.java  |  5 +++
 .../jedis/DefaultJedisClientConfig.java       | 25 +++++++++++---
 .../clients/jedis/JedisClientConfig.java      |  4 +++
 .../redis/clients/jedis/JedisCluster.java     |  8 +++++
 .../clients/jedis/JedisClusterInfoCache.java  | 34 +++++++++++++++++++
 .../executors/ClusterCommandExecutor.java     | 11 +++++-
 .../providers/ClusterConnectionProvider.java  | 26 ++++++++++++++
 .../redis/clients/jedis/JedisClusterTest.java | 27 +++++++++++++++
 8 files changed, 135 insertions(+), 5 deletions(-)

diff --git a/src/main/java/redis/clients/jedis/Connection.java b/src/main/java/redis/clients/jedis/Connection.java
index 9325de80d7c..8a313532581 100644
--- a/src/main/java/redis/clients/jedis/Connection.java
+++ b/src/main/java/redis/clients/jedis/Connection.java
@@ -435,6 +435,11 @@ private void initializeFromClientConfig(final JedisClientConfig config) {
         }
       }
 
+      // set readonly flag to ALL connections (including master nodes) when enable read from replica
+      if (config.isReadOnlyForReplica()) {
+        fireAndForgetMsg.add(new CommandArguments(Command.READONLY));
+      }
+
       for (CommandArguments arg : fireAndForgetMsg) {
         sendCommand(arg);
       }
diff --git a/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java b/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java
index 6d62646a5e7..98fa4677d84 100644
--- a/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java
+++ b/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java
@@ -26,11 +26,13 @@ public final class DefaultJedisClientConfig implements JedisClientConfig {
 
   private final ClientSetInfoConfig clientSetInfoConfig;
 
+  private final boolean readOnlyForReplica;
+
   private DefaultJedisClientConfig(RedisProtocol protocol, int connectionTimeoutMillis, int soTimeoutMillis,
       int blockingSocketTimeoutMillis, Supplier credentialsProvider, int database,
       String clientName, boolean ssl, SSLSocketFactory sslSocketFactory, SSLParameters sslParameters,
       HostnameVerifier hostnameVerifier, HostAndPortMapper hostAndPortMapper,
-      ClientSetInfoConfig clientSetInfoConfig) {
+      ClientSetInfoConfig clientSetInfoConfig, boolean readOnlyForReplica) {
     this.redisProtocol = protocol;
     this.connectionTimeoutMillis = connectionTimeoutMillis;
     this.socketTimeoutMillis = soTimeoutMillis;
@@ -44,6 +46,7 @@ private DefaultJedisClientConfig(RedisProtocol protocol, int connectionTimeoutMi
     this.hostnameVerifier = hostnameVerifier;
     this.hostAndPortMapper = hostAndPortMapper;
     this.clientSetInfoConfig = clientSetInfoConfig;
+    this.readOnlyForReplica = readOnlyForReplica;
   }
 
   @Override
@@ -122,6 +125,11 @@ public ClientSetInfoConfig getClientSetInfoConfig() {
     return clientSetInfoConfig;
   }
 
+  @Override
+  public boolean isReadOnlyForReplica() {
+    return readOnlyForReplica;
+  }
+
   public static Builder builder() {
     return new Builder();
   }
@@ -149,6 +157,8 @@ public static class Builder {
 
     private ClientSetInfoConfig clientSetInfoConfig = ClientSetInfoConfig.DEFAULT;
 
+    private boolean readOnlyForReplicas = false;
+
     private Builder() {
     }
 
@@ -160,7 +170,8 @@ public DefaultJedisClientConfig build() {
 
       return new DefaultJedisClientConfig(redisProtocol, connectionTimeoutMillis, socketTimeoutMillis,
           blockingSocketTimeoutMillis, credentialsProvider, database, clientName, ssl,
-          sslSocketFactory, sslParameters, hostnameVerifier, hostAndPortMapper, clientSetInfoConfig);
+          sslSocketFactory, sslParameters, hostnameVerifier, hostAndPortMapper, clientSetInfoConfig,
+          readOnlyForReplicas);
     }
 
     /**
@@ -255,6 +266,11 @@ public Builder clientSetInfoConfig(ClientSetInfoConfig setInfoConfig) {
       this.clientSetInfoConfig = setInfoConfig;
       return this;
     }
+
+    public Builder readOnlyForReplicas() {
+      this.readOnlyForReplicas = true;
+      return this;
+    }
   }
 
   public static DefaultJedisClientConfig create(int connectionTimeoutMillis, int soTimeoutMillis,
@@ -264,7 +280,8 @@ public static DefaultJedisClientConfig create(int connectionTimeoutMillis, int s
     return new DefaultJedisClientConfig(null,
         connectionTimeoutMillis, soTimeoutMillis, blockingSocketTimeoutMillis,
         new DefaultRedisCredentialsProvider(new DefaultRedisCredentials(user, password)), database,
-        clientName, ssl, sslSocketFactory, sslParameters, hostnameVerifier, hostAndPortMapper, null);
+        clientName, ssl, sslSocketFactory, sslParameters, hostnameVerifier, hostAndPortMapper, null,
+        false);
   }
 
   public static DefaultJedisClientConfig copyConfig(JedisClientConfig copy) {
@@ -273,6 +290,6 @@ public static DefaultJedisClientConfig copyConfig(JedisClientConfig copy) {
         copy.getBlockingSocketTimeoutMillis(), copy.getCredentialsProvider(),
         copy.getDatabase(), copy.getClientName(), copy.isSsl(), copy.getSslSocketFactory(),
         copy.getSslParameters(), copy.getHostnameVerifier(), copy.getHostAndPortMapper(),
-        copy.getClientSetInfoConfig());
+        copy.getClientSetInfoConfig(), copy.isReadOnlyForReplica());
   }
 }
diff --git a/src/main/java/redis/clients/jedis/JedisClientConfig.java b/src/main/java/redis/clients/jedis/JedisClientConfig.java
index 0ad6e979f61..57b172cb34b 100644
--- a/src/main/java/redis/clients/jedis/JedisClientConfig.java
+++ b/src/main/java/redis/clients/jedis/JedisClientConfig.java
@@ -80,6 +80,10 @@ default HostAndPortMapper getHostAndPortMapper() {
     return null;
   }
 
+  default boolean isReadOnlyForReplica() {
+    return false;
+  }
+
   /**
    * Modify the behavior of internally executing CLIENT SETINFO command.
    * @return CLIENT SETINFO config
diff --git a/src/main/java/redis/clients/jedis/JedisCluster.java b/src/main/java/redis/clients/jedis/JedisCluster.java
index 6c5843c16ed..68d8f4205f6 100644
--- a/src/main/java/redis/clients/jedis/JedisCluster.java
+++ b/src/main/java/redis/clients/jedis/JedisCluster.java
@@ -7,6 +7,7 @@
 
 import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
 
+import redis.clients.jedis.executors.ClusterCommandExecutor;
 import redis.clients.jedis.providers.ClusterConnectionProvider;
 import redis.clients.jedis.util.JedisClusterCRC16;
 
@@ -266,4 +267,11 @@ public ClusterPipeline pipelined() {
   public AbstractTransaction transaction(boolean doMulti) {
     throw new UnsupportedOperationException();
   }
+
+  public final  T executeCommandToReplica(CommandObject commandObject) {
+    if (!(executor instanceof ClusterCommandExecutor)) {
+      throw new UnsupportedOperationException("Support only execute to replica in ClusterCommandExecutor");
+    }
+    return ((ClusterCommandExecutor) executor).executeCommandToReplica(commandObject);
+  }
 }
diff --git a/src/main/java/redis/clients/jedis/JedisClusterInfoCache.java b/src/main/java/redis/clients/jedis/JedisClusterInfoCache.java
index da88462ef49..79be093ffb5 100644
--- a/src/main/java/redis/clients/jedis/JedisClusterInfoCache.java
+++ b/src/main/java/redis/clients/jedis/JedisClusterInfoCache.java
@@ -38,6 +38,7 @@ public class JedisClusterInfoCache {
   private final Map nodes = new HashMap<>();
   private final ConnectionPool[] slots = new ConnectionPool[Protocol.CLUSTER_HASHSLOTS];
   private final HostAndPort[] slotNodes = new HostAndPort[Protocol.CLUSTER_HASHSLOTS];
+  private final List[] replicaSlots;
 
   private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
   private final Lock r = rwl.readLock();
@@ -85,6 +86,11 @@ public JedisClusterInfoCache(final JedisClientConfig clientConfig,
       topologyRefreshExecutor.scheduleWithFixedDelay(new TopologyRefreshTask(), topologyRefreshPeriod.toMillis(),
           topologyRefreshPeriod.toMillis(), TimeUnit.MILLISECONDS);
     }
+    if (clientConfig.isReadOnlyForReplica()) {
+      replicaSlots = new ArrayList[Protocol.CLUSTER_HASHSLOTS];
+    } else {
+      replicaSlots = null;
+    }
   }
 
   /**
@@ -144,6 +150,8 @@ public void discoverClusterNodesAndSlots(Connection jedis) {
           setupNodeIfNotExist(targetNode);
           if (i == MASTER_NODE_INDEX) {
             assignSlotsToNode(slotNums, targetNode);
+          } else if (clientConfig.isReadOnlyForReplica()) {
+            assignSlotsToReplicaNode(slotNums, targetNode);
           }
         }
       }
@@ -236,6 +244,8 @@ private void discoverClusterSlots(Connection jedis) {
           setupNodeIfNotExist(targetNode);
           if (i == MASTER_NODE_INDEX) {
             assignSlotsToNode(slotNums, targetNode);
+          } else if (clientConfig.isReadOnlyForReplica()) {
+            assignSlotsToReplicaNode(slotNums, targetNode);
           }
         }
       }
@@ -307,6 +317,21 @@ public void assignSlotsToNode(List targetSlots, HostAndPort targetNode)
     }
   }
 
+  public void assignSlotsToReplicaNode(List targetSlots, HostAndPort targetNode) {
+    w.lock();
+    try {
+      ConnectionPool targetPool = setupNodeIfNotExist(targetNode);
+      for (Integer slot : targetSlots) {
+        if (replicaSlots[slot] == null) {
+          replicaSlots[slot] = new ArrayList<>();
+        }
+        replicaSlots[slot].add(targetPool);
+      }
+    } finally {
+      w.unlock();
+    }
+  }
+
   public ConnectionPool getNode(String nodeKey) {
     r.lock();
     try {
@@ -338,6 +363,15 @@ public HostAndPort getSlotNode(int slot) {
     }
   }
 
+  public List getSlotReplicaPools(int slot) {
+    r.lock();
+    try {
+      return replicaSlots[slot];
+    } finally {
+      r.unlock();
+    }
+  }
+
   public Map getNodes() {
     r.lock();
     try {
diff --git a/src/main/java/redis/clients/jedis/executors/ClusterCommandExecutor.java b/src/main/java/redis/clients/jedis/executors/ClusterCommandExecutor.java
index 375db99e140..e5049672b89 100644
--- a/src/main/java/redis/clients/jedis/executors/ClusterCommandExecutor.java
+++ b/src/main/java/redis/clients/jedis/executors/ClusterCommandExecutor.java
@@ -73,6 +73,14 @@ public final  T broadcastCommand(CommandObject commandObject) {
 
   @Override
   public final  T executeCommand(CommandObject commandObject) {
+    return doExecuteCommand(commandObject, false);
+  }
+
+  public final  T executeCommandToReplica(CommandObject commandObject) {
+    return doExecuteCommand(commandObject, true);
+  }
+
+  private  T doExecuteCommand(CommandObject commandObject, boolean toReplica) {
     Instant deadline = Instant.now().plus(maxTotalRetriesDuration);
 
     JedisRedirectionException redirect = null;
@@ -88,7 +96,8 @@ public final  T executeCommand(CommandObject commandObject) {
             connection.executeCommand(Protocol.Command.ASKING);
           }
         } else {
-          connection = provider.getConnection(commandObject.getArguments());
+          connection = toReplica ? provider.getReplicaConnection(commandObject.getArguments())
+              : provider.getConnection(commandObject.getArguments());
         }
 
         return execute(connection, commandObject);
diff --git a/src/main/java/redis/clients/jedis/providers/ClusterConnectionProvider.java b/src/main/java/redis/clients/jedis/providers/ClusterConnectionProvider.java
index c21640713d9..925645e169e 100644
--- a/src/main/java/redis/clients/jedis/providers/ClusterConnectionProvider.java
+++ b/src/main/java/redis/clients/jedis/providers/ClusterConnectionProvider.java
@@ -6,6 +6,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ThreadLocalRandom;
+
 import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
 
 import redis.clients.jedis.ClusterCommandArguments;
@@ -102,6 +104,11 @@ public Connection getConnection(CommandArguments args) {
     return slot >= 0 ? getConnectionFromSlot(slot) : getConnection();
   }
 
+  public Connection getReplicaConnection(CommandArguments args) {
+    final int slot = ((ClusterCommandArguments) args).getCommandHashSlot();
+    return slot >= 0 ? getReplicaConnectionFromSlot(slot) : getConnection();
+  }
+
   @Override
   public Connection getConnection() {
     // In antirez's redis-rb-cluster implementation, getRandomConnection always
@@ -158,6 +165,25 @@ public Connection getConnectionFromSlot(int slot) {
     }
   }
 
+  public Connection getReplicaConnectionFromSlot(int slot) {
+    List connectionPools = cache.getSlotReplicaPools(slot);
+    ThreadLocalRandom random = ThreadLocalRandom.current();
+    if (connectionPools != null && !connectionPools.isEmpty()) {
+      // pick up randomly a connection
+      int idx = random.nextInt(connectionPools.size());
+      return connectionPools.get(idx).getResource();
+    }
+
+    renewSlotCache();
+    connectionPools = cache.getSlotReplicaPools(slot);
+    if (connectionPools != null && !connectionPools.isEmpty()) {
+      int idx = random.nextInt(connectionPools.size());
+      return connectionPools.get(idx).getResource();
+    }
+
+    return getConnectionFromSlot(slot);
+  }
+
   @Override
   public Map getConnectionMap() {
     return Collections.unmodifiableMap(getNodes());
diff --git a/src/test/java/redis/clients/jedis/JedisClusterTest.java b/src/test/java/redis/clients/jedis/JedisClusterTest.java
index 8297eb90c63..2308bb4b079 100644
--- a/src/test/java/redis/clients/jedis/JedisClusterTest.java
+++ b/src/test/java/redis/clients/jedis/JedisClusterTest.java
@@ -199,6 +199,33 @@ public void testReadonlyAndReadwrite() throws Exception {
     nodeSlave2.flushDB();
   }
 
+  @Test
+  public void testReadFromReplicas() throws Exception {
+    node1.clusterMeet(LOCAL_IP, nodeInfoSlave2.getPort());
+    JedisClusterTestUtil.waitForClusterReady(node1, node2, node3, nodeSlave2);
+
+    for (String nodeInfo : node2.clusterNodes().split("\n")) {
+      if (nodeInfo.contains("myself")) {
+        nodeSlave2.clusterReplicate(nodeInfo.split(" ")[0]);
+        break;
+      }
+    }
+
+    DefaultJedisClientConfig READ_REPLICAS_CLIENT_CONFIG
+        = DefaultJedisClientConfig.builder().password("cluster").readOnlyForReplicas().build();
+    ClusterCommandObjects commandObjects = new ClusterCommandObjects();
+    try (JedisCluster jedisCluster = new JedisCluster(nodeInfo1, READ_REPLICAS_CLIENT_CONFIG,
+        DEFAULT_REDIRECTIONS, DEFAULT_POOL_CONFIG)) {
+      assertEquals("OK", jedisCluster.set("test", "read-from-replicas"));
+
+      assertEquals("read-from-replicas", jedisCluster.executeCommandToReplica(commandObjects.get("test")));
+      // TODO: ensure data being served from replica node(s)
+    }
+
+    nodeSlave2.clusterReset(ClusterResetType.SOFT);
+    nodeSlave2.flushDB();
+  }
+
   /**
    * slot->nodes 15363 node3 e
    */

From 2af43de03e773617b663d145bae3508dc95e1262 Mon Sep 17 00:00:00 2001
From: Krzysztof Wolny 
Date: Mon, 12 Aug 2024 13:34:58 +0200
Subject: [PATCH 25/63] Fixed typo in Javadoc (#3917)

---
 src/main/java/redis/clients/jedis/Jedis.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/redis/clients/jedis/Jedis.java b/src/main/java/redis/clients/jedis/Jedis.java
index 66d64fa6eb0..750eccf5531 100644
--- a/src/main/java/redis/clients/jedis/Jedis.java
+++ b/src/main/java/redis/clients/jedis/Jedis.java
@@ -3431,7 +3431,7 @@ public String shutdownAbort() {
    * All the fields are in the form field:value
    *
    * 
-   * edis_version:0.07
+   * redis_version:0.07
    * connected_clients:1
    * connected_slaves:0
    * used_memory:3187

From cd9a1ab9e7769f3a029c8a8e61daa8bfe14c8d18 Mon Sep 17 00:00:00 2001
From: anotherJJz <470623352@qq.com>
Date: Mon, 12 Aug 2024 23:26:30 +0800
Subject: [PATCH 26/63] Get enriched Connection information (#3745)

* Adds a new method `toIdentityString()` that returns the identifier info for the connection

* Update src/main/java/redis/clients/jedis/Connection.java

Co-authored-by: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com>

* fix NPE about invoke `toIdentityString` before connect

* add test case

---------

Co-authored-by: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com>
Co-authored-by: anotherJJz <470622352@qq.com>
---
 .../java/redis/clients/jedis/Connection.java  | 60 +++++++++++++++++++
 .../redis/clients/jedis/ConnectionTest.java   | 11 ++++
 2 files changed, 71 insertions(+)

diff --git a/src/main/java/redis/clients/jedis/Connection.java b/src/main/java/redis/clients/jedis/Connection.java
index 8a313532581..d0a8a625f8c 100644
--- a/src/main/java/redis/clients/jedis/Connection.java
+++ b/src/main/java/redis/clients/jedis/Connection.java
@@ -5,6 +5,7 @@
 import java.io.Closeable;
 import java.io.IOException;
 import java.net.Socket;
+import java.net.SocketAddress;
 import java.net.SocketException;
 import java.nio.ByteBuffer;
 import java.nio.CharBuffer;
@@ -37,6 +38,8 @@ public class Connection implements Closeable {
   private int soTimeout = 0;
   private int infiniteSoTimeout = 0;
   private boolean broken = false;
+  private boolean strValActive;
+  private String strVal;
 
   public Connection() {
     this(Protocol.DEFAULT_HOST, Protocol.DEFAULT_PORT);
@@ -72,6 +75,63 @@ public String toString() {
     return "Connection{" + socketFactory + "}";
   }
 
+  public String toIdentityString() {
+    if (strValActive == broken && strVal != null) {
+      return strVal;
+    }
+
+    int id = hashCode();
+    String classInfo = getClass().toString();
+
+    if (socket == null) {
+      StringBuilder buf = new StringBuilder(56)
+          .append("[")
+          .append(classInfo)
+          .append(", id: 0x")
+          .append(id)
+          .append(']');
+      return buf.toString();
+    }
+
+    SocketAddress remoteAddr = socket.getRemoteSocketAddress();
+    SocketAddress localAddr = socket.getLocalSocketAddress();
+    if (remoteAddr != null) {
+      StringBuilder buf = new StringBuilder(101)
+          .append("[")
+          .append(classInfo)
+          .append(", id: 0x")
+          .append(id)
+          .append(", L:")
+          .append(localAddr)
+          .append(broken? " ! " : " - ")
+          .append("R:")
+          .append(remoteAddr)
+          .append(']');
+      strVal = buf.toString();
+    } else if (localAddr != null) {
+      StringBuilder buf = new StringBuilder(64)
+          .append("[")
+          .append(classInfo)
+          .append(", id: 0x")
+          .append(id)
+          .append(", L:")
+          .append(localAddr)
+          .append(']');
+      strVal = buf.toString();
+    } else {
+      StringBuilder buf = new StringBuilder(56)
+          .append("[")
+          .append(classInfo)
+          .append(", id: 0x")
+          .append(id)
+          .append(']');
+      strVal = buf.toString();
+    }
+
+    strValActive = broken;
+    return strVal;
+  }
+
   public final RedisProtocol getRedisProtocol() {
     return protocol;
   }
diff --git a/src/test/java/redis/clients/jedis/ConnectionTest.java b/src/test/java/redis/clients/jedis/ConnectionTest.java
index 28eba8100cf..c57248db53b 100644
--- a/src/test/java/redis/clients/jedis/ConnectionTest.java
+++ b/src/test/java/redis/clients/jedis/ConnectionTest.java
@@ -1,6 +1,7 @@
 package redis.clients.jedis;
 
 import org.junit.After;
+import org.junit.Assert;
 import org.junit.Test;
 
 import redis.clients.jedis.exceptions.JedisConnectionException;
@@ -40,4 +41,14 @@ public void checkCloseable() {
     client.connect();
     client.close();
   }
+
+  @Test
+  public void checkIdentityString() {
+    client = new Connection("localhost", 6379);
+    Assert.assertFalse(client.toIdentityString().contains("6379"));
+    client.connect();
+    Assert.assertTrue(client.toIdentityString().contains("6379"));
+    client.close();
+    Assert.assertTrue(client.toIdentityString().contains("6379"));
+  }
 }

From 4a1f35b1e4175a8181b3af8e56df54f125e8305a Mon Sep 17 00:00:00 2001
From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com>
Date: Sun, 18 Aug 2024 22:00:49 +0600
Subject: [PATCH 27/63] Fix empty LUA table reply (#3924)

* Fix empty LUA table reply

* processMapKeyValueReply returns List

* make PROTOCOL_EMPTY_MAP unmodifiable
---
 .../redis/clients/jedis/BuilderFactory.java   |  7 ++++--
 .../java/redis/clients/jedis/Protocol.java    | 22 ++++++++++++-------
 .../commands/jedis/ScriptingCommandsTest.java |  6 +++++
 3 files changed, 25 insertions(+), 10 deletions(-)

diff --git a/src/main/java/redis/clients/jedis/BuilderFactory.java b/src/main/java/redis/clients/jedis/BuilderFactory.java
index 18eeb64afda..a48168357d1 100644
--- a/src/main/java/redis/clients/jedis/BuilderFactory.java
+++ b/src/main/java/redis/clients/jedis/BuilderFactory.java
@@ -385,7 +385,9 @@ public Object build(Object data) {
 
       if (data instanceof List) {
         final List list = (List) data;
-        if (list.isEmpty()) return Collections.emptyMap();
+        if (list.isEmpty()) {
+          return list == Protocol.PROTOCOL_EMPTY_MAP ? Collections.emptyMap() : Collections.emptyList();
+        }
 
         if (list.get(0) instanceof KeyValue) {
           return ((List) data).stream()
@@ -397,8 +399,9 @@ public Object build(Object data) {
         }
       } else if (data instanceof byte[]) {
         return STRING.build(data);
+      } else {
+        return data;
       }
-      return data;
     }
   };
 
diff --git a/src/main/java/redis/clients/jedis/Protocol.java b/src/main/java/redis/clients/jedis/Protocol.java
index 448bd7ff123..d9c9872e1ce 100644
--- a/src/main/java/redis/clients/jedis/Protocol.java
+++ b/src/main/java/redis/clients/jedis/Protocol.java
@@ -4,6 +4,7 @@
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
 
@@ -49,6 +50,8 @@ public final class Protocol {
   public static final byte[] POSITIVE_INFINITY_BYTES = "+inf".getBytes();
   public static final byte[] NEGATIVE_INFINITY_BYTES = "-inf".getBytes();
 
+  static final List PROTOCOL_EMPTY_MAP = Collections.unmodifiableList(new ArrayList<>(0));
+
   private static final String ASK_PREFIX = "ASK ";
   private static final String MOVED_PREFIX = "MOVED ";
   private static final String CLUSTERDOWN_PREFIX = "CLUSTERDOWN ";
@@ -192,7 +195,6 @@ private static byte[] processBulkReply(final RedisInputStream is) {
   }
 
   private static List processMultiBulkReply(final RedisInputStream is) {
-  // private static List processMultiBulkReply(final int num, final RedisInputStream is) {
     final int num = is.readIntCrLf();
     if (num == -1) return null;
     final List ret = new ArrayList<>(num);
@@ -206,16 +208,20 @@ private static List processMultiBulkReply(final RedisInputStream is) {
     return ret;
   }
 
-  // private static List processMultiBulkReply(final RedisInputStream is) {
-  // private static List processMultiBulkReply(final int num, final RedisInputStream is) {
   private static List processMapKeyValueReply(final RedisInputStream is) {
     final int num = is.readIntCrLf();
-    if (num == -1) return null;
-    final List ret = new ArrayList<>(num);
-    for (int i = 0; i < num; i++) {
-      ret.add(new KeyValue(process(is), process(is)));
+    switch (num) {
+      case -1:
+        return null;
+      case 0:
+        return PROTOCOL_EMPTY_MAP;
+      default:
+        final List ret = new ArrayList<>(num);
+        for (int i = 0; i < num; i++) {
+          ret.add(new KeyValue(process(is), process(is)));
+        }
+        return ret;
     }
-    return ret;
   }
 
   public static Object read(final RedisInputStream is) {
diff --git a/src/test/java/redis/clients/jedis/commands/jedis/ScriptingCommandsTest.java b/src/test/java/redis/clients/jedis/commands/jedis/ScriptingCommandsTest.java
index 5f26baec587..9f4c60de0bb 100644
--- a/src/test/java/redis/clients/jedis/commands/jedis/ScriptingCommandsTest.java
+++ b/src/test/java/redis/clients/jedis/commands/jedis/ScriptingCommandsTest.java
@@ -328,6 +328,12 @@ public void scriptExistsWithBrokenConnection() {
     deadClient.close();
   }
 
+  @Test
+  public void emptyLuaTableReply() {
+    Object reply = jedis.eval("return {}");
+    assertEquals(Collections.emptyList(), reply);
+  }
+
   @Test
   public void functionLoadAndDelete() {
     String engine = "Lua";

From 16c9064b4c975c389b8eed19ffabf529f7cb04af Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 21 Aug 2024 17:23:23 +0600
Subject: [PATCH 28/63] Bump org.apache.maven.plugins:maven-release-plugin from
 3.0.1 to 3.1.1 (#3890)

Bumps [org.apache.maven.plugins:maven-release-plugin](https://github.com/apache/maven-release) from 3.0.1 to 3.1.1.
- [Release notes](https://github.com/apache/maven-release/releases)
- [Commits](https://github.com/apache/maven-release/compare/maven-release-3.0.1...maven-release-3.1.1)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-release-plugin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 181fc6db127..47dc459c822 100644
--- a/pom.xml
+++ b/pom.xml
@@ -255,7 +255,7 @@
 			
 			
 				maven-release-plugin
-				3.0.1
+				3.1.1
 			
 			
 				org.sonatype.plugins

From 9aae73af9cbafacc61b58a9cbca9ff7a001ec3ed Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 21 Aug 2024 18:00:47 +0600
Subject: [PATCH 29/63] Bump net.javacrumbs.json-unit:json-unit from 2.38.0 to
 2.40.1 (#3903)

Bumps [net.javacrumbs.json-unit:json-unit](https://github.com/lukas-krecan/JsonUnit) from 2.38.0 to 2.40.1.
- [Commits](https://github.com/lukas-krecan/JsonUnit/compare/json-unit-parent-2.38.0...json-unit-parent-2.40.1)

---
updated-dependencies:
- dependency-name: net.javacrumbs.json-unit:json-unit
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 47dc459c822..2a0ae516812 100644
--- a/pom.xml
+++ b/pom.xml
@@ -130,7 +130,7 @@
 		
 			net.javacrumbs.json-unit
 			json-unit
-			2.38.0 
+			2.40.1 
 			test
 		
 		

From 5d2767c2cfdce6b70645eff139f30b19c289ce65 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 21 Aug 2024 18:00:57 +0600
Subject: [PATCH 30/63] Bump org.apache.maven.plugins:maven-javadoc-plugin from
 3.7.0 to 3.8.0 (#3909)

Bumps [org.apache.maven.plugins:maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) from 3.7.0 to 3.8.0.
- [Release notes](https://github.com/apache/maven-javadoc-plugin/releases)
- [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.7.0...maven-javadoc-plugin-3.8.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-javadoc-plugin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 2a0ae516812..17eb62af247 100644
--- a/pom.xml
+++ b/pom.xml
@@ -237,7 +237,7 @@
 			
 			
 				maven-javadoc-plugin
-				3.7.0
+				3.8.0
 				
 					8
 					false

From 5570879dcddd78e7dc7d256ec6b8794b01c55e21 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 21 Aug 2024 18:01:11 +0600
Subject: [PATCH 31/63] Bump org.hamcrest:hamcrest from 2.2 to 3.0 (#3914)

Bumps [org.hamcrest:hamcrest](https://github.com/hamcrest/JavaHamcrest) from 2.2 to 3.0.
- [Release notes](https://github.com/hamcrest/JavaHamcrest/releases)
- [Changelog](https://github.com/hamcrest/JavaHamcrest/blob/master/CHANGES.md)
- [Commits](https://github.com/hamcrest/JavaHamcrest/compare/v2.2...v3.0)

---
updated-dependencies:
- dependency-name: org.hamcrest:hamcrest
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 17eb62af247..f309ce9298c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -100,7 +100,7 @@
 		
 			org.hamcrest
 			hamcrest
-			2.2
+			3.0
 			test
 		
 		

From 35505f1318f82dc2d6df2e20cd42077086b8f4e8 Mon Sep 17 00:00:00 2001
From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com>
Date: Wed, 21 Aug 2024 18:04:03 +0600
Subject: [PATCH 32/63] Support automatic namespacing (#3781)

* Proof-of-concept for automatic key prefixing

* Iteration on key-prefixing POC

- Demonstrated automatic key-prefixing for all subclasses of
  UnifiedJedis: JedisCluster, JedisPooled, and JedisSentineled
- Key-prefixing is possible as long as the underlying CommandObjects can
  be customized.
- CommandObjects cannot use commandArguments in its constructor since
  in the specific case of key-prefixing, commandArguments depends on the
  child constructor running first. So we lose caching of argument-less
  CommandObjects.
- Based on this POC, the minimum changes required to jedis would be:
  - public constructors that allow UnifiedJedis and its subclasses to
    take a custom CommandObjects.
  - Consistent use of supplied CommandObjects throughout code (e.g. in
    Pipeline, Transaction, etc).
  - Removal of caching of argument-less CommandObjects in the
    constructor of CommandObjects.
- Applications can then supply CommandObjects with custom behavior as
  necessary. Sample classes that implement the behavior of prefixed keys,
  etc are provided but these can be supplied by the application as long
  as required constructors are available.

* Second iteration on key-prefixing POC

- Restore cached key-less commands in CommandObjects
- Support Transactions
- New constructors do not take CommandExecutor
- Requested JavaDoc regarding new constructors specifying RedisProtocol
- New classes moved into 'prefix' packages
- De-duplicate prefixing code

* Support automatic key prefixing by handler interface

---------

Co-authored-by: R-J Lim 
---
 .../clients/jedis/AbstractTransaction.java    |  1 +
 .../clients/jedis/ClusterCommandObjects.java  |  4 +-
 .../redis/clients/jedis/CommandArguments.java | 12 ++++
 .../jedis/CommandKeyArgumentPreProcessor.java | 13 ++++
 .../redis/clients/jedis/CommandObjects.java   | 22 ++++++-
 .../java/redis/clients/jedis/Pipeline.java    | 19 ++++--
 .../clients/jedis/ReliableTransaction.java    | 37 ++++++++---
 .../clients/jedis/ShardedCommandObjects.java  |  4 +-
 .../java/redis/clients/jedis/Transaction.java | 38 ++++++++---
 .../redis/clients/jedis/TransactionBase.java  |  1 +
 .../redis/clients/jedis/UnifiedJedis.java     |  9 ++-
 .../jedis/mcf/MultiClusterTransaction.java    |  4 +-
 .../util/PrefixedKeyArgumentPreProcessor.java | 47 ++++++++++++++
 .../prefix/JedisClusterPrefixedKeysTest.java  | 28 ++++++++
 .../prefix/JedisPooledPrefixedKeysTest.java   | 15 +++++
 .../JedisSentineledPrefixedKeysTest.java      | 25 +++++++
 .../jedis/prefix/PrefixedKeysTest.java        | 65 +++++++++++++++++++
 17 files changed, 316 insertions(+), 28 deletions(-)
 create mode 100644 src/main/java/redis/clients/jedis/CommandKeyArgumentPreProcessor.java
 create mode 100644 src/main/java/redis/clients/jedis/util/PrefixedKeyArgumentPreProcessor.java
 create mode 100644 src/test/java/redis/clients/jedis/prefix/JedisClusterPrefixedKeysTest.java
 create mode 100644 src/test/java/redis/clients/jedis/prefix/JedisPooledPrefixedKeysTest.java
 create mode 100644 src/test/java/redis/clients/jedis/prefix/JedisSentineledPrefixedKeysTest.java
 create mode 100644 src/test/java/redis/clients/jedis/prefix/PrefixedKeysTest.java

diff --git a/src/main/java/redis/clients/jedis/AbstractTransaction.java b/src/main/java/redis/clients/jedis/AbstractTransaction.java
index a4369075405..33afbb6e0b5 100644
--- a/src/main/java/redis/clients/jedis/AbstractTransaction.java
+++ b/src/main/java/redis/clients/jedis/AbstractTransaction.java
@@ -5,6 +5,7 @@
 
 public abstract class AbstractTransaction extends PipeliningBase implements Closeable {
 
+  @Deprecated
   protected AbstractTransaction() {
     super(new CommandObjects());
   }
diff --git a/src/main/java/redis/clients/jedis/ClusterCommandObjects.java b/src/main/java/redis/clients/jedis/ClusterCommandObjects.java
index 02cec4c3fd5..466e9d37f94 100644
--- a/src/main/java/redis/clients/jedis/ClusterCommandObjects.java
+++ b/src/main/java/redis/clients/jedis/ClusterCommandObjects.java
@@ -16,7 +16,9 @@ public class ClusterCommandObjects extends CommandObjects {
 
   @Override
   protected ClusterCommandArguments commandArguments(ProtocolCommand command) {
-    return new ClusterCommandArguments(command);
+    ClusterCommandArguments comArgs = new ClusterCommandArguments(command);
+    if (keyPreProcessor != null) comArgs.setKeyArgumentPreProcessor(keyPreProcessor);
+    return comArgs;
   }
 
   private static final String CLUSTER_UNSUPPORTED_MESSAGE = "Not supported in cluster mode.";
diff --git a/src/main/java/redis/clients/jedis/CommandArguments.java b/src/main/java/redis/clients/jedis/CommandArguments.java
index c630ae76dec..763a60e0260 100644
--- a/src/main/java/redis/clients/jedis/CommandArguments.java
+++ b/src/main/java/redis/clients/jedis/CommandArguments.java
@@ -5,6 +5,7 @@
 import java.util.Collection;
 import java.util.Iterator;
 
+import redis.clients.jedis.annots.Experimental;
 import redis.clients.jedis.args.Rawable;
 import redis.clients.jedis.args.RawableFactory;
 import redis.clients.jedis.commands.ProtocolCommand;
@@ -13,6 +14,7 @@
 
 public class CommandArguments implements Iterable {
 
+  private CommandKeyArgumentPreProcessor keyPreProc = null;
   private final ArrayList args;
 
   private boolean blocking;
@@ -30,6 +32,11 @@ public ProtocolCommand getCommand() {
     return (ProtocolCommand) args.get(0);
   }
 
+  @Experimental
+  void setKeyArgumentPreProcessor(CommandKeyArgumentPreProcessor keyPreProcessor) {
+    this.keyPreProc = keyPreProcessor;
+  }
+
   public CommandArguments add(Rawable arg) {
     args.add(arg);
     return this;
@@ -100,6 +107,10 @@ public CommandArguments addObjects(Collection args) {
   }
 
   public CommandArguments key(Object key) {
+    if (keyPreProc != null) {
+      key = keyPreProc.actualKey(key);
+    }
+
     if (key instanceof Rawable) {
       Rawable raw = (Rawable) key;
       processKey(raw.getRaw());
@@ -115,6 +126,7 @@ public CommandArguments key(Object key) {
     } else {
       throw new IllegalArgumentException("\"" + key.toString() + "\" is not a valid argument.");
     }
+
     return this;
   }
 
diff --git a/src/main/java/redis/clients/jedis/CommandKeyArgumentPreProcessor.java b/src/main/java/redis/clients/jedis/CommandKeyArgumentPreProcessor.java
new file mode 100644
index 00000000000..e1b66c8dde6
--- /dev/null
+++ b/src/main/java/redis/clients/jedis/CommandKeyArgumentPreProcessor.java
@@ -0,0 +1,13 @@
+package redis.clients.jedis;
+
+import redis.clients.jedis.annots.Experimental;
+
+@Experimental
+public interface CommandKeyArgumentPreProcessor {
+
+  /**
+   * @param paramKey key name in application
+   * @return key name in Redis server
+   */
+  Object actualKey(Object paramKey);
+}
diff --git a/src/main/java/redis/clients/jedis/CommandObjects.java b/src/main/java/redis/clients/jedis/CommandObjects.java
index 5c6af322dc3..b89f06f62e3 100644
--- a/src/main/java/redis/clients/jedis/CommandObjects.java
+++ b/src/main/java/redis/clients/jedis/CommandObjects.java
@@ -14,6 +14,7 @@
 
 import redis.clients.jedis.Protocol.Command;
 import redis.clients.jedis.Protocol.Keyword;
+import redis.clients.jedis.annots.Experimental;
 import redis.clients.jedis.args.*;
 import redis.clients.jedis.bloom.*;
 import redis.clients.jedis.bloom.RedisBloomProtocol.*;
@@ -52,18 +53,25 @@ protected RedisProtocol getProtocol() {
     return protocol;
   }
 
+  protected volatile CommandKeyArgumentPreProcessor keyPreProcessor = null;
+  private JedisBroadcastAndRoundRobinConfig broadcastAndRoundRobinConfig = null;
   private Lock mapperLock = new ReentrantLock(true);    
   private volatile JsonObjectMapper jsonObjectMapper;
   private final AtomicInteger searchDialect = new AtomicInteger(0);
 
-  private JedisBroadcastAndRoundRobinConfig broadcastAndRoundRobinConfig = null;
+  @Experimental
+  void setKeyArgumentPreProcessor(CommandKeyArgumentPreProcessor keyPreProcessor) {
+    this.keyPreProcessor = keyPreProcessor;
+  }
 
   void setBroadcastAndRoundRobinConfig(JedisBroadcastAndRoundRobinConfig config) {
     this.broadcastAndRoundRobinConfig = config;
   }
 
   protected CommandArguments commandArguments(ProtocolCommand command) {
-    return new CommandArguments(command);
+    CommandArguments comArgs = new CommandArguments(command);
+    if (keyPreProcessor != null) comArgs.setKeyArgumentPreProcessor(keyPreProcessor);
+    return comArgs;
   }
 
   private final CommandObject PING_COMMAND_OBJECT = new CommandObject<>(commandArguments(PING), BuilderFactory.STRING);
@@ -4424,6 +4432,16 @@ public final CommandObject tFunctionCallAsync(String library, String fun
   }
   // RedisGears commands
 
+  // Transaction commands
+  public final CommandObject watch(String... keys) {
+    return new CommandObject<>(commandArguments(WATCH).keys((Object[]) keys), BuilderFactory.STRING);
+  }
+
+  public final CommandObject watch(byte[]... keys) {
+    return new CommandObject<>(commandArguments(WATCH).keys((Object[]) keys), BuilderFactory.STRING);
+  }
+  // Transaction commands
+
   /**
    * Get the instance for JsonObjectMapper if not null, otherwise a new instance reference with
    * default implementation will be created and returned.
diff --git a/src/main/java/redis/clients/jedis/Pipeline.java b/src/main/java/redis/clients/jedis/Pipeline.java
index af6d39ee2d2..3ed5db41af3 100644
--- a/src/main/java/redis/clients/jedis/Pipeline.java
+++ b/src/main/java/redis/clients/jedis/Pipeline.java
@@ -29,12 +29,23 @@ public Pipeline(Connection connection) {
   }
 
   public Pipeline(Connection connection, boolean closeConnection) {
-    super(new CommandObjects());
+    this(connection, closeConnection, createCommandObjects(connection));
+  }
+
+  private static CommandObjects createCommandObjects(Connection connection) {
+    CommandObjects commandObjects = new CommandObjects();
+    RedisProtocol proto = connection.getRedisProtocol();
+    if (proto != null) commandObjects.setProtocol(proto);
+    return commandObjects;
+  }
+
+  Pipeline(Connection connection, boolean closeConnection, CommandObjects commandObjects) {
+    super(commandObjects);
     this.connection = connection;
     this.closeConnection = closeConnection;
-    RedisProtocol proto = this.connection.getRedisProtocol();
-    if (proto != null) this.commandObjects.setProtocol(proto);
-    setGraphCommands(new GraphCommandObjects(this.connection));
+    GraphCommandObjects graphCommandObjects = new GraphCommandObjects(this.connection);
+    graphCommandObjects.setBaseCommandArgumentsCreator(protocolCommand -> commandObjects.commandArguments(protocolCommand));
+    setGraphCommands(graphCommandObjects);
   }
 
   @Override
diff --git a/src/main/java/redis/clients/jedis/ReliableTransaction.java b/src/main/java/redis/clients/jedis/ReliableTransaction.java
index c750bdb9d97..6fe41570b6b 100644
--- a/src/main/java/redis/clients/jedis/ReliableTransaction.java
+++ b/src/main/java/redis/clients/jedis/ReliableTransaction.java
@@ -4,7 +4,6 @@
 import static redis.clients.jedis.Protocol.Command.EXEC;
 import static redis.clients.jedis.Protocol.Command.MULTI;
 import static redis.clients.jedis.Protocol.Command.UNWATCH;
-import static redis.clients.jedis.Protocol.Command.WATCH;
 
 import java.util.ArrayList;
 import java.util.LinkedList;
@@ -17,8 +16,7 @@
 import redis.clients.jedis.graph.GraphCommandObjects;
 
 /**
- * ReliableTransaction is a transaction where commands are immediately sent to Redis server and the
- * 'QUEUED' reply checked.
+ * A transaction where commands are immediately sent to Redis server and the {@code QUEUED} reply checked.
  */
 public class ReliableTransaction extends TransactionBase {
 
@@ -66,12 +64,37 @@ public ReliableTransaction(Connection connection, boolean doMulti) {
    * @param closeConnection should the 'connection' be closed when 'close()' is called?
    */
   public ReliableTransaction(Connection connection, boolean doMulti, boolean closeConnection) {
+    this(connection, doMulti, closeConnection, createCommandObjects(connection));
+  }
+
+  /**
+   * Creates a new transaction.
+   *
+   * A user wanting to WATCH/UNWATCH keys followed by a call to MULTI ({@link #multi()}) it should
+   * be {@code doMulti=false}.
+   *
+   * @param connection connection
+   * @param commandObjects command objects
+   * @param doMulti {@code false} should be set to enable manual WATCH, UNWATCH and MULTI
+   * @param closeConnection should the 'connection' be closed when 'close()' is called?
+   */
+  ReliableTransaction(Connection connection, boolean doMulti, boolean closeConnection, CommandObjects commandObjects) {
+    super(commandObjects);
     this.connection = connection;
     this.closeConnection = closeConnection;
-    setGraphCommands(new GraphCommandObjects(this.connection));
+    GraphCommandObjects graphCommandObjects = new GraphCommandObjects(this.connection);
+    graphCommandObjects.setBaseCommandArgumentsCreator(protocolCommand -> commandObjects.commandArguments(protocolCommand));
+    setGraphCommands(graphCommandObjects);
     if (doMulti) multi();
   }
 
+  private static CommandObjects createCommandObjects(Connection connection) {
+    CommandObjects commandObjects = new CommandObjects();
+    RedisProtocol proto = connection.getRedisProtocol();
+    if (proto != null) commandObjects.setProtocol(proto);
+    return commandObjects;
+  }
+
   @Override
   public final void multi() {
     connection.sendCommand(MULTI);
@@ -84,16 +107,14 @@ public final void multi() {
 
   @Override
   public String watch(final String... keys) {
-    connection.sendCommand(WATCH, keys);
-    String status = connection.getStatusCodeReply();
+    String status = connection.executeCommand(commandObjects.watch(keys));
     inWatch = true;
     return status;
   }
 
   @Override
   public String watch(final byte[]... keys) {
-    connection.sendCommand(WATCH, keys);
-    String status = connection.getStatusCodeReply();
+    String status = connection.executeCommand(commandObjects.watch(keys));
     inWatch = true;
     return status;
   }
diff --git a/src/main/java/redis/clients/jedis/ShardedCommandObjects.java b/src/main/java/redis/clients/jedis/ShardedCommandObjects.java
index 44604c00eba..c0f2cc1fd1c 100644
--- a/src/main/java/redis/clients/jedis/ShardedCommandObjects.java
+++ b/src/main/java/redis/clients/jedis/ShardedCommandObjects.java
@@ -34,7 +34,9 @@ public ShardedCommandObjects(Hashing algo, Pattern tagPattern) {
 
   @Override
   protected ShardedCommandArguments commandArguments(ProtocolCommand command) {
-    return new ShardedCommandArguments(algo, tagPattern, command);
+    ShardedCommandArguments comArgs = new ShardedCommandArguments(algo, tagPattern, command);
+    if (keyPreProcessor != null) comArgs.setKeyArgumentPreProcessor(keyPreProcessor);
+    return comArgs;
   }
 
   @Override
diff --git a/src/main/java/redis/clients/jedis/Transaction.java b/src/main/java/redis/clients/jedis/Transaction.java
index 0dccd655a00..106047836c0 100644
--- a/src/main/java/redis/clients/jedis/Transaction.java
+++ b/src/main/java/redis/clients/jedis/Transaction.java
@@ -4,7 +4,6 @@
 import static redis.clients.jedis.Protocol.Command.EXEC;
 import static redis.clients.jedis.Protocol.Command.MULTI;
 import static redis.clients.jedis.Protocol.Command.UNWATCH;
-import static redis.clients.jedis.Protocol.Command.WATCH;
 
 import java.util.ArrayList;
 import java.util.LinkedList;
@@ -16,7 +15,7 @@
 import redis.clients.jedis.graph.GraphCommandObjects;
 
 /**
- * A pipeline based transaction.
+ * A transaction based on pipelining.
  */
 public class Transaction extends TransactionBase {
 
@@ -59,7 +58,7 @@ public Transaction(Connection connection) {
    * @param doMulti {@code false} should be set to enable manual WATCH, UNWATCH and MULTI
    */
   public Transaction(Connection connection, boolean doMulti) {
-    this(connection, doMulti, false);
+    this(connection, doMulti, false, createCommandObjects(connection));
   }
 
   /**
@@ -73,12 +72,37 @@ public Transaction(Connection connection, boolean doMulti) {
    * @param closeConnection should the 'connection' be closed when 'close()' is called?
    */
   public Transaction(Connection connection, boolean doMulti, boolean closeConnection) {
+    this(connection, doMulti, closeConnection, createCommandObjects(connection));
+  }
+
+  /**
+   * Creates a new transaction.
+   *
+   * A user wanting to WATCH/UNWATCH keys followed by a call to MULTI ({@link #multi()}) it should
+   * be {@code doMulti=false}.
+   *
+   * @param connection connection
+   * @param commandObjects command objects
+   * @param doMulti {@code false} should be set to enable manual WATCH, UNWATCH and MULTI
+   * @param closeConnection should the 'connection' be closed when 'close()' is called?
+   */
+  Transaction(Connection connection, boolean doMulti, boolean closeConnection, CommandObjects commandObjects) {
+    super(commandObjects);
     this.connection = connection;
     this.closeConnection = closeConnection;
-    setGraphCommands(new GraphCommandObjects(this.connection));
+    GraphCommandObjects graphCommandObjects = new GraphCommandObjects(this.connection);
+    graphCommandObjects.setBaseCommandArgumentsCreator(protocolCommand -> commandObjects.commandArguments(protocolCommand));
+    setGraphCommands(graphCommandObjects);
     if (doMulti) multi();
   }
 
+  private static CommandObjects createCommandObjects(Connection connection) {
+    CommandObjects commandObjects = new CommandObjects();
+    RedisProtocol proto = connection.getRedisProtocol();
+    if (proto != null) commandObjects.setProtocol(proto);
+    return commandObjects;
+  }
+
   @Override
   public final void multi() {
     connection.sendCommand(MULTI);
@@ -88,16 +112,14 @@ public final void multi() {
 
   @Override
   public String watch(final String... keys) {
-    connection.sendCommand(WATCH, keys);
-    String status = connection.getStatusCodeReply();
+    String status = connection.executeCommand(commandObjects.watch(keys));
     inWatch = true;
     return status;
   }
 
   @Override
   public String watch(final byte[]... keys) {
-    connection.sendCommand(WATCH, keys);
-    String status = connection.getStatusCodeReply();
+    String status = connection.executeCommand(commandObjects.watch(keys));
     inWatch = true;
     return status;
   }
diff --git a/src/main/java/redis/clients/jedis/TransactionBase.java b/src/main/java/redis/clients/jedis/TransactionBase.java
index efdf332700b..e2c4b9080d7 100644
--- a/src/main/java/redis/clients/jedis/TransactionBase.java
+++ b/src/main/java/redis/clients/jedis/TransactionBase.java
@@ -6,6 +6,7 @@
 @Deprecated
 public abstract class TransactionBase extends AbstractTransaction {
 
+  @Deprecated
   protected TransactionBase() {
     super();
   }
diff --git a/src/main/java/redis/clients/jedis/UnifiedJedis.java b/src/main/java/redis/clients/jedis/UnifiedJedis.java
index 398b1302ba2..e3bc4aba50a 100644
--- a/src/main/java/redis/clients/jedis/UnifiedJedis.java
+++ b/src/main/java/redis/clients/jedis/UnifiedJedis.java
@@ -5019,7 +5019,7 @@ public PipelineBase pipelined() {
     } else if (provider instanceof MultiClusterPooledConnectionProvider) {
       return new MultiClusterPipeline((MultiClusterPooledConnectionProvider) provider, commandObjects);
     } else {
-      return new Pipeline(provider.getConnection(), true);
+      return new Pipeline(provider.getConnection(), true, commandObjects);
     }
   }
 
@@ -5040,7 +5040,7 @@ public AbstractTransaction transaction(boolean doMulti) {
     } else if (provider instanceof MultiClusterPooledConnectionProvider) {
       return new MultiClusterTransaction((MultiClusterPooledConnectionProvider) provider, doMulti, commandObjects);
     } else {
-      return new Transaction(provider.getConnection(), doMulti, true);
+      return new Transaction(provider.getConnection(), doMulti, true, commandObjects);
     }
   }
 
@@ -5084,6 +5084,11 @@ public Object executeCommand(CommandArguments args) {
     return executeCommand(new CommandObject<>(args, BuilderFactory.RAW_OBJECT));
   }
 
+  @Experimental
+  public void setKeyArgumentPreProcessor(CommandKeyArgumentPreProcessor keyPreProcessor) {
+    this.commandObjects.setKeyArgumentPreProcessor(keyPreProcessor);
+  }
+
   public void setJsonObjectMapper(JsonObjectMapper jsonObjectMapper) {
     this.commandObjects.setJsonObjectMapper(jsonObjectMapper);
   }
diff --git a/src/main/java/redis/clients/jedis/mcf/MultiClusterTransaction.java b/src/main/java/redis/clients/jedis/mcf/MultiClusterTransaction.java
index 7356f9ba79e..d4a460432d3 100644
--- a/src/main/java/redis/clients/jedis/mcf/MultiClusterTransaction.java
+++ b/src/main/java/redis/clients/jedis/mcf/MultiClusterTransaction.java
@@ -94,7 +94,7 @@ public final void multi() {
    */
   @Override
   public final String watch(String... keys) {
-    appendCommand(new CommandObject<>(new CommandArguments(WATCH).addObjects((Object[]) keys), NO_OP_BUILDER));
+    appendCommand(commandObjects.watch(keys));
     extraCommandCount.incrementAndGet();
     inWatch = true;
     return null;
@@ -106,7 +106,7 @@ public final String watch(String... keys) {
    */
   @Override
   public final String watch(byte[]... keys) {
-    appendCommand(new CommandObject<>(new CommandArguments(WATCH).addObjects((Object[]) keys), NO_OP_BUILDER));
+    appendCommand(commandObjects.watch(keys));
     extraCommandCount.incrementAndGet();
     inWatch = true;
     return null;
diff --git a/src/main/java/redis/clients/jedis/util/PrefixedKeyArgumentPreProcessor.java b/src/main/java/redis/clients/jedis/util/PrefixedKeyArgumentPreProcessor.java
new file mode 100644
index 00000000000..ab6072873ad
--- /dev/null
+++ b/src/main/java/redis/clients/jedis/util/PrefixedKeyArgumentPreProcessor.java
@@ -0,0 +1,47 @@
+package redis.clients.jedis.util;
+
+import redis.clients.jedis.CommandKeyArgumentPreProcessor;
+import redis.clients.jedis.annots.Experimental;
+import redis.clients.jedis.args.Rawable;
+import redis.clients.jedis.args.RawableFactory;
+
+@Experimental
+public class PrefixedKeyArgumentPreProcessor implements CommandKeyArgumentPreProcessor {
+
+  private final byte[] prefixBytes;
+  private final String prefixString;
+
+  public PrefixedKeyArgumentPreProcessor(String prefix) {
+    this(prefix, SafeEncoder.encode(prefix));
+  }
+
+  public PrefixedKeyArgumentPreProcessor(String prefixString, byte[] prefixBytes) {
+    this.prefixString = prefixString;
+    this.prefixBytes = prefixBytes;
+  }
+
+  @Override
+  public Object actualKey(Object paramKey) {
+    return prefixKey(paramKey, prefixString, prefixBytes);
+  }
+
+  private static Object prefixKey(Object key, String prefixString, byte[] prefixBytes) {
+    if (key instanceof Rawable) {
+      byte[] raw = ((Rawable) key).getRaw();
+      return RawableFactory.from(prefixKeyWithBytes(raw, prefixBytes));
+    } else if (key instanceof byte[]) {
+      return prefixKeyWithBytes((byte[]) key, prefixBytes);
+    } else if (key instanceof String) {
+      String raw = (String) key;
+      return prefixString + raw;
+    }
+    throw new IllegalArgumentException("\"" + key.toString() + "\" is not a valid argument.");
+  }
+
+  private static byte[] prefixKeyWithBytes(byte[] key, byte[] prefixBytes) {
+    byte[] namespaced = new byte[prefixBytes.length + key.length];
+    System.arraycopy(prefixBytes, 0, namespaced, 0, prefixBytes.length);
+    System.arraycopy(key, 0, namespaced, prefixBytes.length, key.length);
+    return namespaced;
+  }
+}
diff --git a/src/test/java/redis/clients/jedis/prefix/JedisClusterPrefixedKeysTest.java b/src/test/java/redis/clients/jedis/prefix/JedisClusterPrefixedKeysTest.java
new file mode 100644
index 00000000000..0c4040de305
--- /dev/null
+++ b/src/test/java/redis/clients/jedis/prefix/JedisClusterPrefixedKeysTest.java
@@ -0,0 +1,28 @@
+package redis.clients.jedis.prefix;
+
+import java.util.stream.Collectors;
+import java.util.Set;
+import org.junit.Test;
+
+import redis.clients.jedis.HostAndPorts;
+import redis.clients.jedis.DefaultJedisClientConfig;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisClientConfig;
+import redis.clients.jedis.JedisCluster;
+
+public class JedisClusterPrefixedKeysTest extends PrefixedKeysTest {
+
+  private static final JedisClientConfig CLIENT_CONFIG = DefaultJedisClientConfig.builder().password("cluster").build();
+  private static final Set NODES = HostAndPorts.getStableClusterServers().stream().collect(Collectors.toSet());
+
+  @Override
+  JedisCluster nonPrefixingJedis() {
+    return new JedisCluster(NODES, CLIENT_CONFIG);
+  }
+
+  @Override
+  @Test(expected = UnsupportedOperationException.class)
+  public void prefixesKeysInTransaction() {
+    super.prefixesKeysInTransaction();
+  }
+}
diff --git a/src/test/java/redis/clients/jedis/prefix/JedisPooledPrefixedKeysTest.java b/src/test/java/redis/clients/jedis/prefix/JedisPooledPrefixedKeysTest.java
new file mode 100644
index 00000000000..e1c99bfb0b5
--- /dev/null
+++ b/src/test/java/redis/clients/jedis/prefix/JedisPooledPrefixedKeysTest.java
@@ -0,0 +1,15 @@
+package redis.clients.jedis.prefix;
+
+import redis.clients.jedis.EndpointConfig;
+import redis.clients.jedis.HostAndPorts;
+import redis.clients.jedis.JedisPooled;
+
+public class JedisPooledPrefixedKeysTest extends PrefixedKeysTest {
+
+  private static final EndpointConfig ENDPOINT = HostAndPorts.getRedisEndpoint("standalone1");
+
+  @Override
+  JedisPooled nonPrefixingJedis() {
+    return new JedisPooled(ENDPOINT.getHostAndPort(), ENDPOINT.getClientConfigBuilder().timeoutMillis(500).build());
+  }
+}
diff --git a/src/test/java/redis/clients/jedis/prefix/JedisSentineledPrefixedKeysTest.java b/src/test/java/redis/clients/jedis/prefix/JedisSentineledPrefixedKeysTest.java
new file mode 100644
index 00000000000..94c53982a2e
--- /dev/null
+++ b/src/test/java/redis/clients/jedis/prefix/JedisSentineledPrefixedKeysTest.java
@@ -0,0 +1,25 @@
+package redis.clients.jedis.prefix;
+
+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 JedisSentineledPrefixedKeysTest extends PrefixedKeysTest {
+
+  private static final String MASTER_NAME = "mymaster";
+  private static final JedisClientConfig MASTER_CLIENT_CONFIG = DefaultJedisClientConfig.builder().password("foobared").build();
+  private static final Set SENTINEL_NODES = new HashSet<>(
+      Arrays.asList(HostAndPorts.getSentinelServers().get(1), HostAndPorts.getSentinelServers().get(3)));
+  private static final JedisClientConfig SENTINEL_CLIENT_CONFIG = DefaultJedisClientConfig.builder().build();
+
+  @Override
+  JedisSentineled nonPrefixingJedis() {
+    return new JedisSentineled(MASTER_NAME, MASTER_CLIENT_CONFIG, SENTINEL_NODES, SENTINEL_CLIENT_CONFIG);
+  }
+}
diff --git a/src/test/java/redis/clients/jedis/prefix/PrefixedKeysTest.java b/src/test/java/redis/clients/jedis/prefix/PrefixedKeysTest.java
new file mode 100644
index 00000000000..66fc2fec025
--- /dev/null
+++ b/src/test/java/redis/clients/jedis/prefix/PrefixedKeysTest.java
@@ -0,0 +1,65 @@
+package redis.clients.jedis.prefix;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.After;
+import org.junit.Test;
+
+import redis.clients.jedis.AbstractPipeline;
+import redis.clients.jedis.AbstractTransaction;
+import redis.clients.jedis.UnifiedJedis;
+import redis.clients.jedis.resps.Tuple;
+import redis.clients.jedis.util.PrefixedKeyArgumentPreProcessor;
+import redis.clients.jedis.util.SafeEncoder;
+
+public abstract class PrefixedKeysTest {
+
+    abstract T nonPrefixingJedis();
+
+    T prefixingJedis() {
+        T jedis = nonPrefixingJedis();
+        jedis.setKeyArgumentPreProcessor(new PrefixedKeyArgumentPreProcessor("test-prefix:"));
+        return jedis;
+    }
+
+    @After
+    public void cleanUp() {
+        try (UnifiedJedis jedis = prefixingJedis()) {
+            jedis.flushAll();
+        }
+    }
+
+    @Test
+    public void prefixesKeys() {
+        try (UnifiedJedis jedis = prefixingJedis()) {
+            jedis.set("foo1", "bar1");
+            jedis.set(SafeEncoder.encode("foo2"), SafeEncoder.encode("bar2"));
+            AbstractPipeline pipeline = jedis.pipelined();
+            pipeline.incr("foo3");
+            pipeline.zadd("foo4", 1234, "bar4");
+            pipeline.sync();
+        }
+
+        try (UnifiedJedis jedis = nonPrefixingJedis()) {
+            assertEquals("bar1", jedis.get("test-prefix:foo1"));
+            assertEquals("bar2", jedis.get("test-prefix:foo2"));
+            assertEquals("1", jedis.get("test-prefix:foo3"));
+            assertEquals(new Tuple("bar4", 1234d), jedis.zpopmax("test-prefix:foo4"));
+        }
+    }
+
+    @Test
+    public void prefixesKeysInTransaction() {
+        try (UnifiedJedis jedis = prefixingJedis()) {
+            AbstractTransaction transaction = jedis.multi();
+            transaction.set("foo1", "bar1-from-transaction");
+            transaction.hset("foo2", "bar2-key", "bar2-value");
+            transaction.exec();
+        }
+
+        try (UnifiedJedis jedis = nonPrefixingJedis()) {
+            assertEquals("bar1-from-transaction", jedis.get("test-prefix:foo1"));
+            assertEquals("bar2-value", jedis.hget("test-prefix:foo2", "bar2-key"));
+        }
+    }
+}

From b648c79c6aeba2cd57e3832e75a34c33d74fecc0 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 21 Aug 2024 18:25:13 +0600
Subject: [PATCH 33/63] Bump org.apache.maven.plugins:maven-surefire-plugin
 from 3.2.5 to 3.3.1 (#3891)

Bumps [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.2.5 to 3.3.1.
- [Release notes](https://github.com/apache/maven-surefire/releases)
- [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.2.5...surefire-3.3.1)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-surefire-plugin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com>
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index f309ce9298c..9fbcdb79b8f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -50,7 +50,7 @@
 		1.7.36
 		1.7.1
 		2.17.2
-		3.2.5
+		3.3.1
 	
 
 	

From a19bd20322663de6769e98cbb3fb740d4f9effb3 Mon Sep 17 00:00:00 2001
From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com>
Date: Wed, 21 Aug 2024 18:26:10 +0600
Subject: [PATCH 34/63] Tag Connection.toIdentityString() as Experimental

---
 src/main/java/redis/clients/jedis/Connection.java | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/main/java/redis/clients/jedis/Connection.java b/src/main/java/redis/clients/jedis/Connection.java
index d0a8a625f8c..4e1c42e57cf 100644
--- a/src/main/java/redis/clients/jedis/Connection.java
+++ b/src/main/java/redis/clients/jedis/Connection.java
@@ -16,6 +16,7 @@
 
 import redis.clients.jedis.Protocol.Command;
 import redis.clients.jedis.Protocol.Keyword;
+import redis.clients.jedis.annots.Experimental;
 import redis.clients.jedis.args.ClientAttributeOption;
 import redis.clients.jedis.args.Rawable;
 import redis.clients.jedis.commands.ProtocolCommand;
@@ -75,6 +76,7 @@ public String toString() {
     return "Connection{" + socketFactory + "}";
   }
 
+  @Experimental
   public String toIdentityString() {
     if (strValActive == broken && strVal != null) {
       return strVal;

From a8983dbcd2eb2443b08208ab7c810e14cc552d22 Mon Sep 17 00:00:00 2001
From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com>
Date: Wed, 21 Aug 2024 22:38:46 +0600
Subject: [PATCH 35/63] Revert "Creating CODEOWNERS for the examples (#3570)"
 (#3897)

This reverts commit de872942dde45abdc083d585494c9a59926aa721.
---
 .github/CODEOWNERS | 1 -
 1 file changed, 1 deletion(-)
 delete mode 100644 .github/CODEOWNERS

diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
deleted file mode 100644
index aaabe28b987..00000000000
--- a/.github/CODEOWNERS
+++ /dev/null
@@ -1 +0,0 @@
-src/test/java/io/redis/examples/* @dmaier-redislabs

From 2a43ab55b1678bd975b0dbbe1512ba1342c701b0 Mon Sep 17 00:00:00 2001
From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com>
Date: Thu, 22 Aug 2024 14:40:28 +0600
Subject: [PATCH 36/63] Modify Connection.toIdentityString and test (#3931)

---
 .../java/redis/clients/jedis/Connection.java  | 56 +++++--------------
 .../redis/clients/jedis/ConnectionTest.java   | 28 ++++++++--
 2 files changed, 37 insertions(+), 47 deletions(-)

diff --git a/src/main/java/redis/clients/jedis/Connection.java b/src/main/java/redis/clients/jedis/Connection.java
index 4e1c42e57cf..96ddac20c87 100644
--- a/src/main/java/redis/clients/jedis/Connection.java
+++ b/src/main/java/redis/clients/jedis/Connection.java
@@ -73,7 +73,7 @@ public Connection(final JedisSocketFactory socketFactory, JedisClientConfig clie
 
   @Override
   public String toString() {
-    return "Connection{" + socketFactory + "}";
+    return getClass().getSimpleName() + "{" + socketFactory + "}";
   }
 
   @Experimental
@@ -82,52 +82,22 @@ public String toIdentityString() {
       return strVal;
     }
 
+    String className = getClass().getSimpleName();
     int id = hashCode();
-    String classInfo = getClass().toString();
 
     if (socket == null) {
-      StringBuilder buf = new StringBuilder(56)
-          .append("[")
-          .append(classInfo)
-          .append(", id: 0x")
-          .append(id)
-          .append(']');
-      return buf.toString();
+      return String.format("%s{id: 0x%X}", className, id);
     }
 
     SocketAddress remoteAddr = socket.getRemoteSocketAddress();
     SocketAddress localAddr = socket.getLocalSocketAddress();
     if (remoteAddr != null) {
-      StringBuilder buf = new StringBuilder(101)
-          .append("[")
-          .append(classInfo)
-          .append(", id: 0x")
-          .append(id)
-          .append(", L:")
-          .append(localAddr)
-          .append(broken? " ! " : " - ")
-          .append("R:")
-          .append(remoteAddr)
-          .append(']');
-      strVal = buf.toString();
+      strVal = String.format("%s{id: 0x%X, L:%s %c R:%s}", className, id,
+          localAddr, (broken ? '!' : '-'), remoteAddr);
     } else if (localAddr != null) {
-      StringBuilder buf = new StringBuilder(64)
-          .append("[")
-          .append(classInfo)
-          .append(", id: 0x")
-          .append(id)
-          .append(", L:")
-          .append(localAddr)
-          .append(']');
-      strVal = buf.toString();
+      strVal = String.format("%s{id: 0x%X, L:%s}", className, id, localAddr);
     } else {
-      StringBuilder buf = new StringBuilder(56)
-          .append("[")
-          .append(classInfo)
-          .append(", id: 0x")
-          .append(id)
-          .append(']');
-      strVal = buf.toString();
+      strVal = String.format("%s{id: 0x%X}", className, id);
     }
 
     strValActive = broken;
@@ -156,7 +126,7 @@ public void setSoTimeout(int soTimeout) {
       try {
         this.socket.setSoTimeout(soTimeout);
       } catch (SocketException ex) {
-        broken = true;
+        setBroken();
         throw new JedisConnectionException(ex);
       }
     }
@@ -169,7 +139,7 @@ public void setTimeoutInfinite() {
       }
       socket.setSoTimeout(infiniteSoTimeout);
     } catch (SocketException ex) {
-      broken = true;
+      setBroken();
       throw new JedisConnectionException(ex);
     }
   }
@@ -178,7 +148,7 @@ public void rollbackTimeout() {
     try {
       socket.setSoTimeout(this.soTimeout);
     } catch (SocketException ex) {
-      broken = true;
+      setBroken();
       throw new JedisConnectionException(ex);
     }
   }
@@ -245,7 +215,7 @@ public void sendCommand(final CommandArguments args) {
          */
       }
       // Any other exceptions related to connection?
-      broken = true;
+      setBroken();
       throw ex;
     }
   }
@@ -398,7 +368,7 @@ protected void flush() {
     try {
       outputStream.flush();
     } catch (IOException ex) {
-      broken = true;
+      setBroken();
       throw new JedisConnectionException(ex);
     }
   }
@@ -414,7 +384,7 @@ protected Object readProtocolWithCheckingBroken() {
 //      System.out.println(redis.clients.jedis.util.SafeEncoder.encodeObject(read));
 //      return read;
     } catch (JedisConnectionException exc) {
-      broken = true;
+      setBroken();
       throw exc;
     }
   }
diff --git a/src/test/java/redis/clients/jedis/ConnectionTest.java b/src/test/java/redis/clients/jedis/ConnectionTest.java
index c57248db53b..2fce9d62ace 100644
--- a/src/test/java/redis/clients/jedis/ConnectionTest.java
+++ b/src/test/java/redis/clients/jedis/ConnectionTest.java
@@ -1,7 +1,9 @@
 package redis.clients.jedis;
 
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.hamcrest.Matchers;
 import org.junit.After;
-import org.junit.Assert;
 import org.junit.Test;
 
 import redis.clients.jedis.exceptions.JedisConnectionException;
@@ -45,10 +47,28 @@ public void checkCloseable() {
   @Test
   public void checkIdentityString() {
     client = new Connection("localhost", 6379);
-    Assert.assertFalse(client.toIdentityString().contains("6379"));
+
+    String idString = "id: 0x" + Integer.toHexString(client.hashCode()).toUpperCase();
+
+    String identityString = client.toIdentityString();
+    assertThat(identityString, Matchers.startsWith("Connection{"));
+    assertThat(identityString, Matchers.endsWith("}"));
+    assertThat(identityString, Matchers.containsString(idString));
+
     client.connect();
-    Assert.assertTrue(client.toIdentityString().contains("6379"));
+    identityString = client.toIdentityString();
+    assertThat(identityString, Matchers.startsWith("Connection{"));
+    assertThat(identityString, Matchers.endsWith("}"));
+    assertThat(identityString, Matchers.containsString(idString));
+    assertThat(identityString, Matchers.containsString(", L:"));
+    assertThat(identityString, Matchers.containsString(" - R:"));
+
     client.close();
-    Assert.assertTrue(client.toIdentityString().contains("6379"));
+    identityString = client.toIdentityString();
+    assertThat(identityString, Matchers.startsWith("Connection{"));
+    assertThat(identityString, Matchers.endsWith("}"));
+    assertThat(identityString, Matchers.containsString(idString));
+    assertThat(identityString, Matchers.containsString(", L:"));
+    assertThat(identityString, Matchers.containsString(" ! R:"));
   }
 }

From 3b6b2b0cbb0ea7c8d51c25add55ad42a4ff3646d Mon Sep 17 00:00:00 2001
From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com>
Date: Thu, 22 Aug 2024 15:43:06 +0600
Subject: [PATCH 37/63] Accept null replies for BZPOPMAX and BZPOPMIN commands
 (#3930)

---
 .../java/redis/clients/jedis/BuilderFactory.java   | 14 ++++++--------
 .../commands/jedis/SortedSetCommandsTest.java      |  8 ++++++++
 .../unified/SortedSetCommandsTestBase.java         |  8 ++++++++
 .../cluster/ClusterSortedSetCommandsTest.java      |  5 +++++
 4 files changed, 27 insertions(+), 8 deletions(-)

diff --git a/src/main/java/redis/clients/jedis/BuilderFactory.java b/src/main/java/redis/clients/jedis/BuilderFactory.java
index a48168357d1..adce27f1f41 100644
--- a/src/main/java/redis/clients/jedis/BuilderFactory.java
+++ b/src/main/java/redis/clients/jedis/BuilderFactory.java
@@ -598,10 +598,9 @@ public String toString() {
     @Override
     @SuppressWarnings("unchecked")
     public KeyValue build(Object data) {
-      List l = (List) data; // never null
-      if (l.isEmpty()) {
-        return null;
-      }
+      if (data == null) return null;
+      List l = (List) data;
+      if (l.isEmpty()) return null;
       return KeyValue.of(STRING.build(l.get(0)), new Tuple(BINARY.build(l.get(1)), DOUBLE.build(l.get(2))));
     }
 
@@ -615,10 +614,9 @@ public String toString() {
     @Override
     @SuppressWarnings("unchecked")
     public KeyValue build(Object data) {
-      List l = (List) data; // never null
-      if (l.isEmpty()) {
-        return null;
-      }
+      if (data == null) return null;
+      List l = (List) data;
+      if (l.isEmpty()) return null;
       return KeyValue.of(BINARY.build(l.get(0)), new Tuple(BINARY.build(l.get(1)), DOUBLE.build(l.get(2))));
     }
 
diff --git a/src/test/java/redis/clients/jedis/commands/jedis/SortedSetCommandsTest.java b/src/test/java/redis/clients/jedis/commands/jedis/SortedSetCommandsTest.java
index 68606840287..9e4cd71a53d 100644
--- a/src/test/java/redis/clients/jedis/commands/jedis/SortedSetCommandsTest.java
+++ b/src/test/java/redis/clients/jedis/commands/jedis/SortedSetCommandsTest.java
@@ -1527,12 +1527,16 @@ public void infinity() {
 
   @Test
   public void bzpopmax() {
+    assertNull(jedis.bzpopmax(1, "foo", "bar"));
+
     jedis.zadd("foo", 1d, "a", ZAddParams.zAddParams().nx());
     jedis.zadd("foo", 10d, "b", ZAddParams.zAddParams().nx());
     jedis.zadd("bar", 0.1d, "c", ZAddParams.zAddParams().nx());
     assertEquals(new KeyValue<>("foo", new Tuple("b", 10d)), jedis.bzpopmax(0, "foo", "bar"));
 
     // Binary
+    assertNull(jedis.bzpopmax(1, bfoo, bbar));
+
     jedis.zadd(bfoo, 1d, ba);
     jedis.zadd(bfoo, 10d, bb);
     jedis.zadd(bbar, 0.1d, bc);
@@ -1543,12 +1547,16 @@ public void bzpopmax() {
 
   @Test
   public void bzpopmin() {
+    assertNull(jedis.bzpopmin(1, "bar", "foo"));
+
     jedis.zadd("foo", 1d, "a", ZAddParams.zAddParams().nx());
     jedis.zadd("foo", 10d, "b", ZAddParams.zAddParams().nx());
     jedis.zadd("bar", 0.1d, "c", ZAddParams.zAddParams().nx());
     assertEquals(new KeyValue<>("bar", new Tuple("c", 0.1)), jedis.bzpopmin(0, "bar", "foo"));
 
     // Binary
+    assertNull(jedis.bzpopmin(1, bbar, bfoo));
+
     jedis.zadd(bfoo, 1d, ba);
     jedis.zadd(bfoo, 10d, bb);
     jedis.zadd(bbar, 0.1d, bc);
diff --git a/src/test/java/redis/clients/jedis/commands/unified/SortedSetCommandsTestBase.java b/src/test/java/redis/clients/jedis/commands/unified/SortedSetCommandsTestBase.java
index 3f56a91459e..126a884993f 100644
--- a/src/test/java/redis/clients/jedis/commands/unified/SortedSetCommandsTestBase.java
+++ b/src/test/java/redis/clients/jedis/commands/unified/SortedSetCommandsTestBase.java
@@ -1513,12 +1513,16 @@ public void infinity() {
 
   @Test
   public void bzpopmax() {
+    assertNull(jedis.bzpopmax(1, "foo", "bar"));
+
     jedis.zadd("foo", 1d, "a", ZAddParams.zAddParams().nx());
     jedis.zadd("foo", 10d, "b", ZAddParams.zAddParams().nx());
     jedis.zadd("bar", 0.1d, "c", ZAddParams.zAddParams().nx());
     assertEquals(new KeyValue<>("foo", new Tuple("b", 10d)), jedis.bzpopmax(0, "foo", "bar"));
 
     // Binary
+    assertNull(jedis.bzpopmax(1, bfoo, bbar));
+
     jedis.zadd(bfoo, 1d, ba);
     jedis.zadd(bfoo, 10d, bb);
     jedis.zadd(bbar, 0.1d, bc);
@@ -1529,12 +1533,16 @@ public void bzpopmax() {
 
   @Test
   public void bzpopmin() {
+    assertNull(jedis.bzpopmin(1, "bar", "foo"));
+
     jedis.zadd("foo", 1d, "a", ZAddParams.zAddParams().nx());
     jedis.zadd("foo", 10d, "b", ZAddParams.zAddParams().nx());
     jedis.zadd("bar", 0.1d, "c", ZAddParams.zAddParams().nx());
     assertEquals(new KeyValue<>("bar", new Tuple("c", 0.1)), jedis.bzpopmin(0, "bar", "foo"));
 
     // Binary
+    assertNull(jedis.bzpopmin(1, bbar, bfoo));
+
     jedis.zadd(bfoo, 1d, ba);
     jedis.zadd(bfoo, 10d, bb);
     jedis.zadd(bbar, 0.1d, bc);
diff --git a/src/test/java/redis/clients/jedis/commands/unified/cluster/ClusterSortedSetCommandsTest.java b/src/test/java/redis/clients/jedis/commands/unified/cluster/ClusterSortedSetCommandsTest.java
index 9151b0c52b0..b08d7b17734 100644
--- a/src/test/java/redis/clients/jedis/commands/unified/cluster/ClusterSortedSetCommandsTest.java
+++ b/src/test/java/redis/clients/jedis/commands/unified/cluster/ClusterSortedSetCommandsTest.java
@@ -3,6 +3,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 import static redis.clients.jedis.util.AssertUtil.assertByteArrayListEquals;
 
 import java.util.ArrayList;
@@ -154,6 +155,8 @@ public void zintertoreParams() {
   @Test
   @Override
   public void bzpopmax() {
+    assertNull(jedis.bzpopmax(1, "f{:}oo", "b{:}ar"));
+
     jedis.zadd("f{:}oo", 1d, "a", ZAddParams.zAddParams().nx());
     jedis.zadd("f{:}oo", 10d, "b", ZAddParams.zAddParams().nx());
     jedis.zadd("b{:}ar", 0.1d, "c", ZAddParams.zAddParams().nx());
@@ -163,6 +166,8 @@ public void bzpopmax() {
   @Test
   @Override
   public void bzpopmin() {
+    assertNull(jedis.bzpopmin(1, "ba{:}r", "fo{:}o"));
+
     jedis.zadd("fo{:}o", 1d, "a", ZAddParams.zAddParams().nx());
     jedis.zadd("fo{:}o", 10d, "b", ZAddParams.zAddParams().nx());
     jedis.zadd("ba{:}r", 0.1d, "c", ZAddParams.zAddParams().nx());

From beadf6aa6f05d155eaedf6196b7d7aee29aa0aa9 Mon Sep 17 00:00:00 2001
From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com>
Date: Thu, 22 Aug 2024 15:49:40 +0600
Subject: [PATCH 38/63] Rename readonly config param to specify Redis Cluster
 (#3932)

* Rename readonly config param to specify Redis Cluster

* clarify javadoc
---
 .../java/redis/clients/jedis/Connection.java  |  4 ++--
 .../jedis/DefaultJedisClientConfig.java       | 20 +++++++++----------
 .../clients/jedis/JedisClientConfig.java      | 11 ++++++++--
 .../clients/jedis/JedisClusterInfoCache.java  |  6 +++---
 .../redis/clients/jedis/JedisClusterTest.java |  4 ++--
 5 files changed, 26 insertions(+), 19 deletions(-)

diff --git a/src/main/java/redis/clients/jedis/Connection.java b/src/main/java/redis/clients/jedis/Connection.java
index 96ddac20c87..9bffd716440 100644
--- a/src/main/java/redis/clients/jedis/Connection.java
+++ b/src/main/java/redis/clients/jedis/Connection.java
@@ -467,8 +467,8 @@ private void initializeFromClientConfig(final JedisClientConfig config) {
         }
       }
 
-      // set readonly flag to ALL connections (including master nodes) when enable read from replica
-      if (config.isReadOnlyForReplica()) {
+      // set READONLY flag to ALL connections (including master nodes) when enable read from replica
+      if (config.isReadOnlyForRedisClusterReplicas()) {
         fireAndForgetMsg.add(new CommandArguments(Command.READONLY));
       }
 
diff --git a/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java b/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java
index 98fa4677d84..f26513fa587 100644
--- a/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java
+++ b/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java
@@ -26,13 +26,13 @@ public final class DefaultJedisClientConfig implements JedisClientConfig {
 
   private final ClientSetInfoConfig clientSetInfoConfig;
 
-  private final boolean readOnlyForReplica;
+  private final boolean readOnlyForRedisClusterReplicas;
 
   private DefaultJedisClientConfig(RedisProtocol protocol, int connectionTimeoutMillis, int soTimeoutMillis,
       int blockingSocketTimeoutMillis, Supplier credentialsProvider, int database,
       String clientName, boolean ssl, SSLSocketFactory sslSocketFactory, SSLParameters sslParameters,
       HostnameVerifier hostnameVerifier, HostAndPortMapper hostAndPortMapper,
-      ClientSetInfoConfig clientSetInfoConfig, boolean readOnlyForReplica) {
+      ClientSetInfoConfig clientSetInfoConfig, boolean readOnlyForRedisClusterReplicas) {
     this.redisProtocol = protocol;
     this.connectionTimeoutMillis = connectionTimeoutMillis;
     this.socketTimeoutMillis = soTimeoutMillis;
@@ -46,7 +46,7 @@ private DefaultJedisClientConfig(RedisProtocol protocol, int connectionTimeoutMi
     this.hostnameVerifier = hostnameVerifier;
     this.hostAndPortMapper = hostAndPortMapper;
     this.clientSetInfoConfig = clientSetInfoConfig;
-    this.readOnlyForReplica = readOnlyForReplica;
+    this.readOnlyForRedisClusterReplicas = readOnlyForRedisClusterReplicas;
   }
 
   @Override
@@ -126,8 +126,8 @@ public ClientSetInfoConfig getClientSetInfoConfig() {
   }
 
   @Override
-  public boolean isReadOnlyForReplica() {
-    return readOnlyForReplica;
+  public boolean isReadOnlyForRedisClusterReplicas() {
+    return readOnlyForRedisClusterReplicas;
   }
 
   public static Builder builder() {
@@ -157,7 +157,7 @@ public static class Builder {
 
     private ClientSetInfoConfig clientSetInfoConfig = ClientSetInfoConfig.DEFAULT;
 
-    private boolean readOnlyForReplicas = false;
+    private boolean readOnlyForRedisClusterReplicas = false;
 
     private Builder() {
     }
@@ -171,7 +171,7 @@ public DefaultJedisClientConfig build() {
       return new DefaultJedisClientConfig(redisProtocol, connectionTimeoutMillis, socketTimeoutMillis,
           blockingSocketTimeoutMillis, credentialsProvider, database, clientName, ssl,
           sslSocketFactory, sslParameters, hostnameVerifier, hostAndPortMapper, clientSetInfoConfig,
-          readOnlyForReplicas);
+          readOnlyForRedisClusterReplicas);
     }
 
     /**
@@ -267,8 +267,8 @@ public Builder clientSetInfoConfig(ClientSetInfoConfig setInfoConfig) {
       return this;
     }
 
-    public Builder readOnlyForReplicas() {
-      this.readOnlyForReplicas = true;
+    public Builder readOnlyForRedisClusterReplicas() {
+      this.readOnlyForRedisClusterReplicas = true;
       return this;
     }
   }
@@ -290,6 +290,6 @@ public static DefaultJedisClientConfig copyConfig(JedisClientConfig copy) {
         copy.getBlockingSocketTimeoutMillis(), copy.getCredentialsProvider(),
         copy.getDatabase(), copy.getClientName(), copy.isSsl(), copy.getSslSocketFactory(),
         copy.getSslParameters(), copy.getHostnameVerifier(), copy.getHostAndPortMapper(),
-        copy.getClientSetInfoConfig(), copy.isReadOnlyForReplica());
+        copy.getClientSetInfoConfig(), copy.isReadOnlyForRedisClusterReplicas());
   }
 }
diff --git a/src/main/java/redis/clients/jedis/JedisClientConfig.java b/src/main/java/redis/clients/jedis/JedisClientConfig.java
index 57b172cb34b..abe1f352376 100644
--- a/src/main/java/redis/clients/jedis/JedisClientConfig.java
+++ b/src/main/java/redis/clients/jedis/JedisClientConfig.java
@@ -58,7 +58,7 @@ default String getClientName() {
   }
 
   /**
-   * @return true - to create a TLS connection. false - otherwise.
+   * @return {@code true} - to create TLS connection(s). {@code false} - otherwise.
    */
   default boolean isSsl() {
     return false;
@@ -80,7 +80,14 @@ default HostAndPortMapper getHostAndPortMapper() {
     return null;
   }
 
-  default boolean isReadOnlyForReplica() {
+  /**
+   * Execute READONLY command to connections.
+   * 

+ * READONLY command is specific to Redis Cluster replica nodes. So this config param is only + * intended for Redis Cluster connections. + * @return {@code true} - to execute READONLY command to connection(s). {@code false} - otherwise. + */ + default boolean isReadOnlyForRedisClusterReplicas() { return false; } diff --git a/src/main/java/redis/clients/jedis/JedisClusterInfoCache.java b/src/main/java/redis/clients/jedis/JedisClusterInfoCache.java index 79be093ffb5..2c4ea3b3c52 100644 --- a/src/main/java/redis/clients/jedis/JedisClusterInfoCache.java +++ b/src/main/java/redis/clients/jedis/JedisClusterInfoCache.java @@ -86,7 +86,7 @@ public JedisClusterInfoCache(final JedisClientConfig clientConfig, topologyRefreshExecutor.scheduleWithFixedDelay(new TopologyRefreshTask(), topologyRefreshPeriod.toMillis(), topologyRefreshPeriod.toMillis(), TimeUnit.MILLISECONDS); } - if (clientConfig.isReadOnlyForReplica()) { + if (clientConfig.isReadOnlyForRedisClusterReplicas()) { replicaSlots = new ArrayList[Protocol.CLUSTER_HASHSLOTS]; } else { replicaSlots = null; @@ -150,7 +150,7 @@ public void discoverClusterNodesAndSlots(Connection jedis) { setupNodeIfNotExist(targetNode); if (i == MASTER_NODE_INDEX) { assignSlotsToNode(slotNums, targetNode); - } else if (clientConfig.isReadOnlyForReplica()) { + } else if (clientConfig.isReadOnlyForRedisClusterReplicas()) { assignSlotsToReplicaNode(slotNums, targetNode); } } @@ -244,7 +244,7 @@ private void discoverClusterSlots(Connection jedis) { setupNodeIfNotExist(targetNode); if (i == MASTER_NODE_INDEX) { assignSlotsToNode(slotNums, targetNode); - } else if (clientConfig.isReadOnlyForReplica()) { + } else if (clientConfig.isReadOnlyForRedisClusterReplicas()) { assignSlotsToReplicaNode(slotNums, targetNode); } } diff --git a/src/test/java/redis/clients/jedis/JedisClusterTest.java b/src/test/java/redis/clients/jedis/JedisClusterTest.java index 2308bb4b079..6ebec4e73fc 100644 --- a/src/test/java/redis/clients/jedis/JedisClusterTest.java +++ b/src/test/java/redis/clients/jedis/JedisClusterTest.java @@ -211,8 +211,8 @@ public void testReadFromReplicas() throws Exception { } } - DefaultJedisClientConfig READ_REPLICAS_CLIENT_CONFIG - = DefaultJedisClientConfig.builder().password("cluster").readOnlyForReplicas().build(); + DefaultJedisClientConfig READ_REPLICAS_CLIENT_CONFIG = DefaultJedisClientConfig.builder() + .password("cluster").readOnlyForRedisClusterReplicas().build(); ClusterCommandObjects commandObjects = new ClusterCommandObjects(); try (JedisCluster jedisCluster = new JedisCluster(nodeInfo1, READ_REPLICAS_CLIENT_CONFIG, DEFAULT_REDIRECTIONS, DEFAULT_POOL_CONFIG)) { From 53b8ad5658df5514046d9931d223b35dee10dc7a Mon Sep 17 00:00:00 2001 From: Igor Malinovskiy Date: Thu, 22 Aug 2024 15:16:55 +0200 Subject: [PATCH 39/63] Fix codecov upload (#3933) * Try to fix codecov upload * Add token --- .github/workflows/integration.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 5b78b3124b3..f294af71018 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -55,6 +55,8 @@ jobs: env: JVM_OPTS: -Xmx3200m TERM: dumb - - name: Codecov - run: | - bash <(curl -s https://codecov.io/bash) + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} From ac2a4e0e88d0757a3597793d4d2097e9515559fe Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Sun, 25 Aug 2024 14:03:45 +0600 Subject: [PATCH 40/63] [DOC] RediSearch usage with FTCreateParams and FTSearchParams classes (#3934) * Update redisearch.md * Update redisjson.md * fix spell check --- .github/wordlist.txt | 2 ++ docs/redisearch.md | 46 ++++++++++++++++++++++++++++++++++++++++---- docs/redisjson.md | 20 +++++++++++++++++-- 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/.github/wordlist.txt b/.github/wordlist.txt index 7d0d85f49d5..d60e297814e 100644 --- a/.github/wordlist.txt +++ b/.github/wordlist.txt @@ -20,6 +20,8 @@ EVAL EVALSHA Failback Failover +FTCreateParams +FTSearchParams GSON GenericObjectPool GenericObjectPoolConfig diff --git a/docs/redisearch.md b/docs/redisearch.md index 4292f75048a..c446de0b41b 100644 --- a/docs/redisearch.md +++ b/docs/redisearch.md @@ -1,6 +1,6 @@ # RediSearch Jedis Quick Start -To use RediSearch features with Jedis, you'll need to use and implementation of RediSearchCommands. +To use RediSearch features with Jedis, you'll need to use an implementation of RediSearchCommands. ## Creating the RediSearch client @@ -22,6 +22,8 @@ JedisCluster client = new JedisCluster(nodes); ## Indexing and querying +### Indexing + Defining a schema for an index and creating it: ```java @@ -37,6 +39,23 @@ IndexDefinition def = new IndexDefinition() client.ftCreate("item-index", IndexOptions.defaultOptions().setDefinition(def), sc); ``` +Alternatively, we can create the same index using FTCreateParams: + +```java +client.ftCreate("item-index", + + FTCreateParams.createParams() + .prefix("item:", "product:") + .filter("@price>100"), + + TextField.of("title").weight(5.0), + TextField.of("body"), + NumericField.of("price") +); +``` + +### Inserting + Adding documents to the index: ```java @@ -49,18 +68,37 @@ fields.put("price", 1337); client.hset("item:hw", RediSearchUtil.toStringMap(fields)); ``` +Another way to insert documents: + +```java +client.hsetObject("item:hw", fields); +``` + +### Querying + Searching the index: ```java -// creating a complex query Query q = new Query("hello world") .addFilter(new Query.NumericFilter("price", 0, 1000)) .limit(0, 5); -// actual search SearchResult sr = client.ftSearch("item-index", q); +``` + +Alternative searching using FTSearchParams: -// aggregation query +```java +SearchResult sr = client.ftSearch("item-index", + "hello world", + FTSearchParams.searchParams() + .filter("price", 0, 1000) + .limit(0, 5)); +``` + +Aggregation query: + +```java AggregationBuilder ab = new AggregationBuilder("hello") .apply("@price/1000", "k") .groupBy("@state", Reducers.avg("@k").as("avgprice")) diff --git a/docs/redisjson.md b/docs/redisjson.md index 6409e726f94..6d9db6e20c9 100644 --- a/docs/redisjson.md +++ b/docs/redisjson.md @@ -81,7 +81,7 @@ If we want to be able to query this JSON, we'll need to create an index. Let's c 3. Then we actually create the index, called "student-index", by calling `ftCreate()`. ```java -Schema schema = new Schema().addTextField("$.firstName", 1.0).addTextField("$" + ".lastName", 1.0); +Schema schema = new Schema().addTextField("$.firstName", 1.0).addTextField("$.lastName", 1.0); IndexDefinition rule = new IndexDefinition(IndexDefinition.Type.JSON) .setPrefixes(new String[]{"student:"}); @@ -89,13 +89,29 @@ IndexDefinition rule = new IndexDefinition(IndexDefinition.Type.JSON) client.ftCreate("student-index", IndexOptions.defaultOptions().setDefinition(rule), schema); ``` +Alternatively creating the same index using FTCreateParams: + +```java +client.ftCreate("student-index", + FTCreateParams.createParams().on(IndexDataType.JSON).prefix("student:"), + TextField.of("$.firstName"), TextField.of("$.lastName")); +``` + With an index now defined, we can query our JSON. Let's find all students whose name begins with "maya": ```java -Query q = new Query("@\\$\\" + ".firstName:maya*"); +Query q = new Query("@\\$\\.firstName:maya*"); SearchResult mayaSearch = client.ftSearch("student-index", q); ``` +Same query can be done using FTSearchParams: + +```java +SearchResult mayaSearch = client.ftSearch("student-index", + "@\\$\\.firstName:maya*", + FTSearchParams.searchParams()); +``` + We can then iterate over our search results: ```java From 11274decd4a94555a8d9746c041d7927cfeadeb2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 14:03:02 +0600 Subject: [PATCH 41/63] Bump org.apache.maven.plugins:maven-gpg-plugin from 3.2.4 to 3.2.5 (#3936) Bumps [org.apache.maven.plugins:maven-gpg-plugin](https://github.com/apache/maven-gpg-plugin) from 3.2.4 to 3.2.5. - [Release notes](https://github.com/apache/maven-gpg-plugin/releases) - [Commits](https://github.com/apache/maven-gpg-plugin/compare/maven-gpg-plugin-3.2.4...maven-gpg-plugin-3.2.5) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-gpg-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9fbcdb79b8f..3ae8ea9c7fd 100644 --- a/pom.xml +++ b/pom.xml @@ -312,7 +312,7 @@ maven-gpg-plugin - 3.2.4 + 3.2.5 --pinentry-mode From 81c9f32cdc0b1fa72a6f356a319226b7cbaf9d91 Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Tue, 27 Aug 2024 16:58:43 +0600 Subject: [PATCH 42/63] Decoding FT.SEARCH reply can be disabled at field level (#3926) * Decoding FT.SEARCH reply can be disabled at field level * added javadoc in Document * make the pr not breaking --- .../redis/clients/jedis/CommandObjects.java | 32 +++--- .../redis/clients/jedis/search/Document.java | 101 +++++++++++++----- .../clients/jedis/search/FTSearchParams.java | 62 ++++++----- .../clients/jedis/search/SearchResult.java | 35 +++++- .../modules/search/SearchWithParamsTest.java | 39 ++++++- 5 files changed, 200 insertions(+), 69 deletions(-) diff --git a/src/main/java/redis/clients/jedis/CommandObjects.java b/src/main/java/redis/clients/jedis/CommandObjects.java index b89f06f62e3..89eb3e16ca9 100644 --- a/src/main/java/redis/clients/jedis/CommandObjects.java +++ b/src/main/java/redis/clients/jedis/CommandObjects.java @@ -3384,19 +3384,20 @@ public final CommandObject ftDropIndexDD(String indexName) { public final CommandObject ftSearch(String indexName, String query) { return new CommandObject<>(checkAndRoundRobinSearchCommand(SearchCommand.SEARCH, indexName).add(query), - getSearchResultBuilder(() -> new SearchResultBuilder(true, false, true))); + getSearchResultBuilder(null, () -> new SearchResultBuilder(true, false, true))); } public final CommandObject ftSearch(String indexName, String query, FTSearchParams params) { return new CommandObject<>(checkAndRoundRobinSearchCommand(SearchCommand.SEARCH, indexName) .add(query).addParams(params.dialectOptional(searchDialect.get())), - getSearchResultBuilder(() -> new SearchResultBuilder(!params.getNoContent(), params.getWithScores(), true))); + getSearchResultBuilder(params.getReturnFieldDecodeMap(), () -> new SearchResultBuilder( + !params.getNoContent(), params.getWithScores(), true, params.getReturnFieldDecodeMap()))); } public final CommandObject ftSearch(String indexName, Query query) { return new CommandObject<>(checkAndRoundRobinSearchCommand(SearchCommand.SEARCH, indexName) - .addParams(query.dialectOptional(searchDialect.get())), getSearchResultBuilder(() -> - new SearchResultBuilder(!query.getNoContent(), query.getWithScores(), true))); + .addParams(query.dialectOptional(searchDialect.get())), getSearchResultBuilder(null, + () -> new SearchResultBuilder(!query.getNoContent(), query.getWithScores(), true))); } @Deprecated @@ -3405,8 +3406,8 @@ public final CommandObject ftSearch(byte[] indexName, Query query) throw new UnsupportedOperationException("binary ft.search is not implemented with resp3."); } return new CommandObject<>(checkAndRoundRobinSearchCommand(commandArguments(SearchCommand.SEARCH), indexName) - .addParams(query.dialectOptional(searchDialect.get())), getSearchResultBuilder(() -> - new SearchResultBuilder(!query.getNoContent(), query.getWithScores(), false))); + .addParams(query.dialectOptional(searchDialect.get())), getSearchResultBuilder(null, + () -> new SearchResultBuilder(!query.getNoContent(), query.getWithScores(), false))); } public final CommandObject ftExplain(String indexName, Query query) { @@ -3449,20 +3450,27 @@ public final CommandObject>> ftProfi String indexName, FTProfileParams profileParams, Query query) { return new CommandObject<>(checkAndRoundRobinSearchCommand(SearchCommand.PROFILE, indexName) .add(SearchKeyword.SEARCH).addParams(profileParams).add(SearchKeyword.QUERY) - .addParams(query.dialectOptional(searchDialect.get())), new SearchProfileResponseBuilder<>( - getSearchResultBuilder(() -> new SearchResultBuilder(!query.getNoContent(), query.getWithScores(), true)))); + .addParams(query.dialectOptional(searchDialect.get())), + new SearchProfileResponseBuilder<>(getSearchResultBuilder(null, + () -> new SearchResultBuilder(!query.getNoContent(), query.getWithScores(), true)))); } public final CommandObject>> ftProfileSearch( String indexName, FTProfileParams profileParams, String query, FTSearchParams searchParams) { return new CommandObject<>(checkAndRoundRobinSearchCommand(SearchCommand.PROFILE, indexName) .add(SearchKeyword.SEARCH).addParams(profileParams).add(SearchKeyword.QUERY).add(query) - .addParams(searchParams.dialectOptional(searchDialect.get())), new SearchProfileResponseBuilder<>( - getSearchResultBuilder(() -> new SearchResultBuilder(!searchParams.getNoContent(), searchParams.getWithScores(), true)))); + .addParams(searchParams.dialectOptional(searchDialect.get())), + new SearchProfileResponseBuilder<>(getSearchResultBuilder(searchParams.getReturnFieldDecodeMap(), + () -> new SearchResultBuilder(!searchParams.getNoContent(), searchParams.getWithScores(), true, + searchParams.getReturnFieldDecodeMap())))); } - private Builder getSearchResultBuilder(Supplier> resp2) { - if (protocol == RedisProtocol.RESP3) return SearchResult.SEARCH_RESULT_BUILDER; + private Builder getSearchResultBuilder( + Map isReturnFieldDecode, Supplier> resp2) { + if (protocol == RedisProtocol.RESP3) { + return isReturnFieldDecode == null ? SearchResult.SEARCH_RESULT_BUILDER + : new SearchResult.PerFieldDecoderSearchResultBuilder(isReturnFieldDecode); + } return resp2.get(); } diff --git a/src/main/java/redis/clients/jedis/search/Document.java b/src/main/java/redis/clients/jedis/search/Document.java index 20149e581ab..4c7d5690232 100644 --- a/src/main/java/redis/clients/jedis/search/Document.java +++ b/src/main/java/redis/clients/jedis/search/Document.java @@ -7,6 +7,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; + import redis.clients.jedis.Builder; import redis.clients.jedis.BuilderFactory; import redis.clients.jedis.util.KeyValue; @@ -50,24 +51,6 @@ public Iterable> getProperties() { return fields.entrySet(); } - public static Document load(String id, double score, byte[] payload, List fields) { - return Document.load(id, score, fields, true); - } - - public static Document load(String id, double score, List fields, boolean decode) { - Document ret = new Document(id, score); - if (fields != null) { - for (int i = 0; i < fields.size(); i += 2) { - byte[] rawKey = fields.get(i); - byte[] rawValue = fields.get(i + 1); - String key = SafeEncoder.encode(rawKey); - Object value = rawValue == null ? null : decode ? SafeEncoder.encode(rawValue) : rawValue; - ret.set(key, value); - } - } - return ret; - } - /** * @return the document's id */ @@ -102,16 +85,22 @@ public Object get(String key) { */ public String getString(String key) { Object value = fields.get(key); - if (value instanceof String) { + if (value == null) { + return null; + } else if (value instanceof String) { return (String) value; + } else if (value instanceof byte[]) { + return SafeEncoder.encode((byte[]) value); + } else { + return String.valueOf(value); } - return value instanceof byte[] ? SafeEncoder.encode((byte[]) value) : value.toString(); } public boolean hasProperty(String key) { return fields.containsKey(key); } + // TODO: private ?? public Document set(String key, Object value) { fields.put(key, value); return this; @@ -122,7 +111,9 @@ public Document set(String key, Object value) { * * @param score new score to set * @return the document itself + * @deprecated */ + @Deprecated public Document setScore(float score) { this.score = (double) score; return this; @@ -134,13 +125,58 @@ public String toString() { ", properties:" + this.getProperties(); } - static Builder SEARCH_DOCUMENT = new Builder() { + /// RESP2 --> + public static Document load(String id, double score, byte[] payload, List fields) { + return Document.load(id, score, fields, true); + } + + public static Document load(String id, double score, List fields, boolean decode) { + return load(id, score, fields, decode, null); + } + + /** + * Parse document object from FT.SEARCH reply. + * @param id + * @param score + * @param fields + * @param decode + * @param isFieldDecode checked only if {@code decode=true} + * @return document + */ + public static Document load(String id, double score, List fields, boolean decode, + Map isFieldDecode) { + Document ret = new Document(id, score); + if (fields != null) { + for (int i = 0; i < fields.size(); i += 2) { + byte[] rawKey = fields.get(i); + byte[] rawValue = fields.get(i + 1); + String key = SafeEncoder.encode(rawKey); + Object value = rawValue == null ? null + : (decode && (isFieldDecode == null || !Boolean.FALSE.equals(isFieldDecode.get(key)))) + ? SafeEncoder.encode(rawValue) : rawValue; + ret.set(key, value); + } + } + return ret; + } + /// <-- RESP2 + + /// RESP3 --> + // TODO: final + static Builder SEARCH_DOCUMENT = new PerFieldDecoderDocumentBuilder((Map) null); + + static final class PerFieldDecoderDocumentBuilder extends Builder { private static final String ID_STR = "id"; private static final String SCORE_STR = "score"; - // private static final String FIELDS_STR = "fields"; private static final String FIELDS_STR = "extra_attributes"; + private final Map isFieldDecode; + + public PerFieldDecoderDocumentBuilder(Map isFieldDecode) { + this.isFieldDecode = isFieldDecode != null ? isFieldDecode : Collections.emptyMap(); + } + @Override public Document build(Object data) { List list = (List) data; @@ -157,13 +193,28 @@ public Document build(Object data) { score = BuilderFactory.DOUBLE.build(kv.getValue()); break; case FIELDS_STR: - fields = BuilderFactory.ENCODED_OBJECT_MAP.build(kv.getValue()); + fields = makeFieldsMap(isFieldDecode, kv.getValue()); break; } } -// assert id != null; -// if (fields == null) fields = Collections.emptyMap(); return new Document(id, score, fields); } }; + + private static Map makeFieldsMap(Map isDecode, Object data) { + if (data == null) return null; + + final List list = (List) data; + + Map map = new HashMap<>(list.size(), 1f); + list.stream().filter((kv) -> (kv != null && kv.getKey() != null && kv.getValue() != null)) + .forEach((kv) -> { + String key = BuilderFactory.STRING.build(kv.getKey()); + map.put(key, + (Boolean.FALSE.equals(isDecode.get(key)) ? BuilderFactory.RAW_OBJECT + : BuilderFactory.AGGRESSIVE_ENCODED_OBJECT).build(kv.getValue())); + }); + return map; + } + /// <-- RESP3 } diff --git a/src/main/java/redis/clients/jedis/search/FTSearchParams.java b/src/main/java/redis/clients/jedis/search/FTSearchParams.java index 2d4c6e00576..03c905edb3c 100644 --- a/src/main/java/redis/clients/jedis/search/FTSearchParams.java +++ b/src/main/java/redis/clients/jedis/search/FTSearchParams.java @@ -24,8 +24,7 @@ public class FTSearchParams implements IParams { private final List filters = new LinkedList<>(); private Collection inKeys; private Collection inFields; - private Collection returnFields; - private Collection returnFieldNames; + private Collection returnFieldsNames; private boolean summarize; private SummarizeParams summarizeParams; private boolean highlight; @@ -43,6 +42,9 @@ public class FTSearchParams implements IParams { private Map params; private Integer dialect; + /// non command parameters + private Map returnFieldDecodeMap = null; + public FTSearchParams() { } @@ -78,17 +80,15 @@ public void addParams(CommandArguments args) { args.add(INFIELDS).add(inFields.size()).addObjects(inFields); } - if (returnFieldNames != null && !returnFieldNames.isEmpty()) { + if (returnFieldsNames != null && !returnFieldsNames.isEmpty()) { args.add(RETURN); LazyRawable returnCountObject = new LazyRawable(); args.add(returnCountObject); // holding a place for setting the total count later. int returnCount = 0; - for (FieldName fn : returnFieldNames) { + for (FieldName fn : returnFieldsNames) { returnCount += fn.addCommandArguments(args); } returnCountObject.setRaw(Protocol.toByteArray(returnCount)); - } else if (returnFields != null && !returnFields.isEmpty()) { - args.add(RETURN).add(returnFields.size()).addObjects(returnFields); } if (summarizeParams != null) { @@ -256,21 +256,15 @@ public FTSearchParams inFields(Collection fields) { * @return the query object itself */ public FTSearchParams returnFields(String... fields) { - if (returnFieldNames != null) { - Arrays.stream(fields).forEach(f -> returnFieldNames.add(FieldName.of(f))); - } else { - if (returnFields == null) { - returnFields = new ArrayList<>(); - } - Arrays.stream(fields).forEach(f -> returnFields.add(f)); + if (returnFieldsNames == null) { + returnFieldsNames = new ArrayList<>(); } + Arrays.stream(fields).forEach(f -> returnFieldsNames.add(FieldName.of(f))); return this; } public FTSearchParams returnField(FieldName field) { - initReturnFieldNames(); - returnFieldNames.add(field); - return this; + return returnFields(Collections.singleton(field)); } public FTSearchParams returnFields(FieldName... fields) { @@ -278,19 +272,30 @@ public FTSearchParams returnFields(FieldName... fields) { } public FTSearchParams returnFields(Collection fields) { - initReturnFieldNames(); - returnFieldNames.addAll(fields); + if (returnFieldsNames == null) { + returnFieldsNames = new ArrayList<>(); + } + returnFieldsNames.addAll(fields); return this; } - private void initReturnFieldNames() { - if (returnFieldNames == null) { - returnFieldNames = new ArrayList<>(); - } - if (returnFields != null) { - returnFields.forEach(f -> returnFieldNames.add(FieldName.of(f))); - returnFields = null; + public FTSearchParams returnField(String field, boolean decode) { + returnFields(field); + addReturnFieldDecode(field, decode); + return this; + } + + public FTSearchParams returnField(FieldName field, boolean decode) { + returnFields(field); + addReturnFieldDecode(field.getAttribute() != null ? field.getAttribute() : field.getName(), decode); + return this; + } + + private void addReturnFieldDecode(String returnName, boolean decode) { + if (returnFieldDecodeMap == null) { + returnFieldDecodeMap = new HashMap<>(); } + returnFieldDecodeMap.put(returnName, decode); } public FTSearchParams summarize() { @@ -436,14 +441,21 @@ public FTSearchParams dialectOptional(int dialect) { return this; } + @Internal public boolean getNoContent() { return noContent; } + @Internal public boolean getWithScores() { return withScores; } + @Internal + public Map getReturnFieldDecodeMap() { + return returnFieldDecodeMap; + } + /** * NumericFilter wraps a range filter on a numeric field. It can be inclusive or exclusive */ diff --git a/src/main/java/redis/clients/jedis/search/SearchResult.java b/src/main/java/redis/clients/jedis/search/SearchResult.java index cc28cc942a5..55afbe0b249 100644 --- a/src/main/java/redis/clients/jedis/search/SearchResult.java +++ b/src/main/java/redis/clients/jedis/search/SearchResult.java @@ -3,9 +3,13 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; + import redis.clients.jedis.Builder; import redis.clients.jedis.BuilderFactory; +import redis.clients.jedis.annots.Internal; import redis.clients.jedis.util.KeyValue; /** @@ -43,10 +47,18 @@ public static class SearchResultBuilder extends Builder { private final boolean hasScores; private final boolean decode; + private final Map isFieldDecode; + public SearchResultBuilder(boolean hasContent, boolean hasScores, boolean decode) { + this(hasContent, hasScores, decode, null); + } + + public SearchResultBuilder(boolean hasContent, boolean hasScores, boolean decode, + Map isFieldDecode) { this.hasContent = hasContent; this.hasScores = hasScores; this.decode = decode; + this.isFieldDecode = isFieldDecode; } @Override @@ -75,18 +87,34 @@ public SearchResult build(Object data) { double score = hasScores ? BuilderFactory.DOUBLE.build(resp.get(i + scoreOffset)) : 1.0; List fields = hasContent ? (List) resp.get(i + contentOffset) : null; - documents.add(Document.load(id, score, fields, decode)); + documents.add(Document.load(id, score, fields, decode, isFieldDecode)); } return new SearchResult(totalResults, documents); } } - public static Builder SEARCH_RESULT_BUILDER = new Builder() { + /// RESP3 --> + // TODO: final + public static Builder SEARCH_RESULT_BUILDER + = new PerFieldDecoderSearchResultBuilder(Document.SEARCH_DOCUMENT); + + @Internal + public static final class PerFieldDecoderSearchResultBuilder extends Builder { private static final String TOTAL_RESULTS_STR = "total_results"; private static final String RESULTS_STR = "results"; + private final Builder documentBuilder; + + public PerFieldDecoderSearchResultBuilder(Map isFieldDecode) { + this(new Document.PerFieldDecoderDocumentBuilder(isFieldDecode)); + } + + private PerFieldDecoderSearchResultBuilder(Builder builder) { + this.documentBuilder = Objects.requireNonNull(builder); + } + @Override public SearchResult build(Object data) { List list = (List) data; @@ -100,7 +128,7 @@ public SearchResult build(Object data) { break; case RESULTS_STR: results = ((List) kv.getValue()).stream() - .map(Document.SEARCH_DOCUMENT::build) + .map(documentBuilder::build) .collect(Collectors.toList()); break; } @@ -108,4 +136,5 @@ public SearchResult build(Object data) { return new SearchResult(totalResults, results); } }; + /// <-- RESP3 } 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 f4c584d5075..9f190a06ae4 100644 --- a/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java +++ b/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java @@ -960,7 +960,7 @@ public void tagFieldParams() { } @Test - public void testReturnFields() { + public void returnFields() { assertOK(client.ftCreate(index, TextField.of("field1"), TextField.of("field2"))); Map doc = new HashMap<>(); @@ -975,10 +975,16 @@ public void testReturnFields() { Document ret = res.getDocuments().get(0); assertEquals("value1", ret.get("field1")); assertNull(ret.get("field2")); + + res = client.ftSearch(index, "*", FTSearchParams.searchParams().returnField("field1", true)); + assertEquals("value1", res.getDocuments().get(0).get("field1")); + + res = client.ftSearch(index, "*", FTSearchParams.searchParams().returnField("field1", false)); + assertArrayEquals("value1".getBytes(), (byte[]) res.getDocuments().get(0).get("field1")); } @Test - public void returnWithFieldNames() { + public void returnFieldsNames() { assertOK(client.ftCreate(index, TextField.of("a"), TextField.of("b"), TextField.of("c"))); Map map = new HashMap<>(); @@ -989,14 +995,39 @@ public void returnWithFieldNames() { // Query SearchResult res = client.ftSearch(index, "*", - FTSearchParams.searchParams().returnFields( - FieldName.of("a"), FieldName.of("b").as("d"))); + FTSearchParams.searchParams() + .returnFields(FieldName.of("a"), + FieldName.of("b").as("d"))); assertEquals(1, res.getTotalResults()); Document doc = res.getDocuments().get(0); assertEquals("value1", doc.get("a")); assertNull(doc.get("b")); assertEquals("value2", doc.get("d")); assertNull(doc.get("c")); + + res = client.ftSearch(index, "*", + FTSearchParams.searchParams() + .returnField(FieldName.of("a")) + .returnField(FieldName.of("b").as("d"))); + assertEquals(1, res.getTotalResults()); + assertEquals("value1", res.getDocuments().get(0).get("a")); + assertEquals("value2", res.getDocuments().get(0).get("d")); + + res = client.ftSearch(index, "*", + FTSearchParams.searchParams() + .returnField(FieldName.of("a"), true) + .returnField(FieldName.of("b").as("d"), true)); + assertEquals(1, res.getTotalResults()); + assertEquals("value1", res.getDocuments().get(0).get("a")); + assertEquals("value2", res.getDocuments().get(0).get("d")); + + res = client.ftSearch(index, "*", + FTSearchParams.searchParams() + .returnField(FieldName.of("a"), false) + .returnField(FieldName.of("b").as("d"), false)); + assertEquals(1, res.getTotalResults()); + assertArrayEquals("value1".getBytes(), (byte[]) res.getDocuments().get(0).get("a")); + assertArrayEquals("value2".getBytes(), (byte[]) res.getDocuments().get(0).get("d")); } @Test From fdf165f5e03f5e3b50dcd902464a5c6c4b8a3654 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 28 Aug 2024 16:12:48 +0100 Subject: [PATCH 43/63] DOC-4094 hash examples (#3940) * DOC-4094 added command examples for HGET and HSET * Delete Bitfield_tutorial --------- Co-authored-by: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> --- .../io/redis/examples/CmdsHashExample.java | 333 ++++++++++++++++++ 1 file changed, 333 insertions(+) create mode 100644 src/test/java/io/redis/examples/CmdsHashExample.java diff --git a/src/test/java/io/redis/examples/CmdsHashExample.java b/src/test/java/io/redis/examples/CmdsHashExample.java new file mode 100644 index 00000000000..cfbb0b340ab --- /dev/null +++ b/src/test/java/io/redis/examples/CmdsHashExample.java @@ -0,0 +1,333 @@ +// EXAMPLE: cmds_hash +// REMOVE_START +package io.redis.examples; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +// REMOVE_END +// HIDE_START +import redis.clients.jedis.UnifiedJedis; +// HIDE_END + +// HIDE_START +public class CmdsHashExample { + @Test + public void run() { + UnifiedJedis jedis = new UnifiedJedis("redis://localhost:6379"); + + //REMOVE_START + // Clear any keys here before using them in tests. + jedis.del("myhash"); + //REMOVE_END +// HIDE_END + + + // STEP_START hdel + + // STEP_END + + // Tests for 'hdel' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START hexists + + // STEP_END + + // Tests for 'hexists' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START hexpire + + // STEP_END + + // Tests for 'hexpire' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START hexpireat + + // STEP_END + + // Tests for 'hexpireat' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START hexpiretime + + // STEP_END + + // Tests for 'hexpiretime' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START hget + Map hGetExampleParams = new HashMap<>(); + hGetExampleParams.put("field1", "foo"); + + long hGetResult1 = jedis.hset("myhash", hGetExampleParams); + System.out.println(hGetResult1); // >>> 1 + + String hGetResult2 = jedis.hget("myhash", "field1"); + System.out.println(hGetResult2); // >>> foo + + String hGetResult3 = jedis.hget("myhash", "field2"); + System.out.println(hGetResult3); // >>> null + // STEP_END + + // Tests for 'hget' step. + // REMOVE_START + Assert.assertEquals(1, hGetResult1); + Assert.assertEquals("foo", hGetResult2); + Assert.assertNull(hGetResult3); + jedis.del("myhash"); + // REMOVE_END + + + // STEP_START hgetall + + // STEP_END + + // Tests for 'hgetall' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START hincrby + + // STEP_END + + // Tests for 'hincrby' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START hincrbyfloat + + // STEP_END + + // Tests for 'hincrbyfloat' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START hkeys + + // STEP_END + + // Tests for 'hkeys' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START hlen + + // STEP_END + + // Tests for 'hlen' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START hmget + + // STEP_END + + // Tests for 'hmget' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START hmset + + // STEP_END + + // Tests for 'hmset' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START hpersist + + // STEP_END + + // Tests for 'hpersist' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START hpexpire + + // STEP_END + + // Tests for 'hpexpire' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START hpexpireat + + // STEP_END + + // Tests for 'hpexpireat' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START hpexpiretime + + // STEP_END + + // Tests for 'hpexpiretime' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START hpttl + + // STEP_END + + // Tests for 'hpttl' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START hrandfield + + // STEP_END + + // Tests for 'hrandfield' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START hscan + + // STEP_END + + // Tests for 'hscan' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START hset + Map hSetExampleParams = new HashMap<>(); + hSetExampleParams.put("field1", "Hello"); + long hSetResult1 = jedis.hset("myhash", hSetExampleParams); + System.out.println(hSetResult1); // >>> 1 + + String hSetResult2 = jedis.hget("myhash", "field1"); + System.out.println(hSetResult2); // >>> Hello + + hSetExampleParams.clear(); + hSetExampleParams.put("field2", "Hi"); + hSetExampleParams.put("field3", "World"); + long hSetResult3 = jedis.hset("myhash",hSetExampleParams); + System.out.println(hSetResult3); // >>> 2 + + String hSetResult4 = jedis.hget("myhash", "field2"); + System.out.println(hSetResult4); // >>> Hi + + String hSetResult5 = jedis.hget("myhash", "field3"); + System.out.println(hSetResult5); // >>> World + + Map hSetResult6 = jedis.hgetAll("myhash"); + + for (String key: hSetResult6.keySet()) { + System.out.println("Key: " + key + ", Value: " + hSetResult6.get(key)); + } + // >>> Key: field3, Value: World + // >>> Key: field2, Value: Hi + // >>> Key: field1, Value: Hello + // STEP_END + + // Tests for 'hset' step. + // REMOVE_START + Assert.assertEquals(1, hSetResult1); + Assert.assertEquals("Hello", hSetResult2); + Assert.assertEquals(2, hSetResult3); + Assert.assertEquals("Hi", hSetResult4); + Assert.assertEquals("World", hSetResult5); + Assert.assertEquals(3, hSetResult6.size()); + Assert.assertEquals("Hello", hSetResult6.get("field1")); + Assert.assertEquals("Hi", hSetResult6.get("field2")); + Assert.assertEquals("World", hSetResult6.get("field3")); + jedis.del("myhash"); + // REMOVE_END + + + // STEP_START hsetnx + + // STEP_END + + // Tests for 'hsetnx' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START hstrlen + + // STEP_END + + // Tests for 'hstrlen' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START httl + + // STEP_END + + // Tests for 'httl' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START hvals + + // STEP_END + + // Tests for 'hvals' step. + // REMOVE_START + + // REMOVE_END + + +// HIDE_START + + } +} +// HIDE_END + From 4c094726afe782d82fce000660efe78ea967a045 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Sun, 1 Sep 2024 11:23:58 +0100 Subject: [PATCH 44/63] DOC-4080 Sorted Set (ZADD and ZRANGE commands) examples (#3944) * DOC-4080 added ZADD example * DOC-4080 rename file and add ZRANGE examples * DOC-4080 delete sorted set obj after tests * DOC-4080 rename with suffix * DOC-4080 un-hide useful imports --- .../redis/examples/CmdsSortedSetExample.java | 480 ++++++++++++++++++ 1 file changed, 480 insertions(+) create mode 100644 src/test/java/io/redis/examples/CmdsSortedSetExample.java diff --git a/src/test/java/io/redis/examples/CmdsSortedSetExample.java b/src/test/java/io/redis/examples/CmdsSortedSetExample.java new file mode 100644 index 00000000000..19a5c21b8ee --- /dev/null +++ b/src/test/java/io/redis/examples/CmdsSortedSetExample.java @@ -0,0 +1,480 @@ +// EXAMPLE: cmds_sorted_set +// REMOVE_START +package io.redis.examples; + +import org.junit.Assert; +import org.junit.Test; + +// REMOVE_END +// HIDE_START +// HIDE_END +import java.util.HashMap; +import java.util.Map; +import java.util.List; +import redis.clients.jedis.UnifiedJedis; +import redis.clients.jedis.params.ZRangeParams; +import redis.clients.jedis.resps.Tuple; + +// HIDE_START +public class CmdsSortedSetExample { + @Test + public void run() { + UnifiedJedis jedis = new UnifiedJedis("redis://localhost:6379"); + + //REMOVE_START + // Clear any keys here before using them in tests. + jedis.del("myzset"); + //REMOVE_END +// HIDE_END + + + // STEP_START bzmpop + + // STEP_END + + // Tests for 'bzmpop' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START bzpopmax + + // STEP_END + + // Tests for 'bzpopmax' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START bzpopmin + + // STEP_END + + // Tests for 'bzpopmin' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zadd + Map zAddExampleParams = new HashMap<>(); + zAddExampleParams.put("one", 1.0); + long zAddResult1 = jedis.zadd("myzset", zAddExampleParams); + System.out.println(zAddResult1); // >>> 1 + + zAddExampleParams.clear(); + zAddExampleParams.put("uno", 1.0); + long zAddResult2 = jedis.zadd("myzset", zAddExampleParams); + System.out.println(zAddResult2); // >>> 1 + + zAddExampleParams.clear(); + zAddExampleParams.put("two", 2.0); + zAddExampleParams.put("three", 3.0); + long zAddResult3 = jedis.zadd("myzset", zAddExampleParams); + System.out.println(zAddResult3); // >>> 2 + + List zAddResult4 = jedis.zrangeWithScores("myzset", new ZRangeParams(0, -1)); + + for (Tuple item: zAddResult4) { + System.out.println("Element: " + item.getElement() + ", Score: " + item.getScore()); + } + // >>> Element: one, Score: 1.0 + // >>> Element: uno, Score: 1.0 + // >>> Element: two, Score: 2.0 + // >>> Element: three, Score: 3.0 + // STEP_END + + // Tests for 'zadd' step. + // REMOVE_START + Assert.assertEquals(1, zAddResult1); + Assert.assertEquals(1, zAddResult2); + Assert.assertEquals(2, zAddResult3); + Assert.assertEquals(new Tuple("one", 1.0), zAddResult4.get(0)); + Assert.assertEquals(new Tuple("uno", 1.0), zAddResult4.get(1)); + Assert.assertEquals(new Tuple("two", 2.0), zAddResult4.get(2)); + Assert.assertEquals(new Tuple("three", 3.0), zAddResult4.get(3)); + jedis.del("myzset"); + // REMOVE_END + + + // STEP_START zcard + + // STEP_END + + // Tests for 'zcard' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zcount + + // STEP_END + + // Tests for 'zcount' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zdiff + + // STEP_END + + // Tests for 'zdiff' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zdiffstore + + // STEP_END + + // Tests for 'zdiffstore' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zincrby + + // STEP_END + + // Tests for 'zincrby' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zinter + + // STEP_END + + // Tests for 'zinter' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zintercard + + // STEP_END + + // Tests for 'zintercard' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zinterstore + + // STEP_END + + // Tests for 'zinterstore' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zlexcount + + // STEP_END + + // Tests for 'zlexcount' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zmpop + + // STEP_END + + // Tests for 'zmpop' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zmscore + + // STEP_END + + // Tests for 'zmscore' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zpopmax + + // STEP_END + + // Tests for 'zpopmax' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zpopmin + + // STEP_END + + // Tests for 'zpopmin' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zrandmember + + // STEP_END + + // Tests for 'zrandmember' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zrange1 + Map zRangeExampleParams1 = new HashMap<>(); + zRangeExampleParams1.put("one", 1.0); + zRangeExampleParams1.put("two", 2.0); + zRangeExampleParams1.put("three", 3.0); + long zRangeResult1 = jedis.zadd("myzset", zRangeExampleParams1); + System.out.println(zRangeResult1); // >>> 3 + + List zRangeResult2 = jedis.zrange("myzset", new ZRangeParams(0, -1)); + System.out.println(String.join(", ", zRangeResult2)); // >>> one, two, three + + List zRangeResult3 = jedis.zrange("myzset", new ZRangeParams(2, 3)); + System.out.println(String.join(", ", zRangeResult3)); // >> three + + List zRangeResult4 = jedis.zrange("myzset", new ZRangeParams(-2, -1)); + System.out.println(String.join(", ", zRangeResult4)); // >> two, three + // STEP_END + + // Tests for 'zrange1' step. + // REMOVE_START + Assert.assertEquals(3, zRangeResult1); + Assert.assertEquals("one, two, three", String.join(", ", zRangeResult2)); + Assert.assertEquals("three", String.join(", ", zRangeResult3)); + Assert.assertEquals("two, three", String.join(", ", zRangeResult4)); + jedis.del("myzset"); + // REMOVE_END + + + // STEP_START zrange2 + Map zRangeExampleParams2 = new HashMap<>(); + zRangeExampleParams2.put("one", 1.0); + zRangeExampleParams2.put("two", 2.0); + zRangeExampleParams2.put("three", 3.0); + long zRangeResult5 = jedis.zadd("myzset", zRangeExampleParams2); + System.out.println(zRangeResult5); // >>> 3 + + List zRangeResult6 = jedis.zrangeWithScores("myzset", new ZRangeParams(0, 1)); + + for (Tuple item: zRangeResult6) { + System.out.println("Element: " + item.getElement() + ", Score: " + item.getScore()); + } + // >>> Element: one, Score: 1.0 + // >>> Element: two, Score: 2.0 + // STEP_END + + // Tests for 'zrange2' step. + // REMOVE_START + Assert.assertEquals(3, zRangeResult5); + Assert.assertEquals(new Tuple("one", 1.0), zRangeResult6.get(0)); + Assert.assertEquals(new Tuple("two", 2.0), zRangeResult6.get(1)); + jedis.del("myzset"); + // REMOVE_END + + + // STEP_START zrange3 + Map zRangeExampleParams3 = new HashMap<>(); + zRangeExampleParams3.put("one", 1.0); + zRangeExampleParams3.put("two", 2.0); + zRangeExampleParams3.put("three", 3.0); + long zRangeResult7 = jedis.zadd("myzset", zRangeExampleParams3); + System.out.println(zRangeResult7); // >>> 3 + + List zRangeResult8 = jedis.zrangeByScore("myzset", "(1", "+inf", 1, 1); + System.out.println(String.join(", ", zRangeResult8)); // >>> three + // STEP_END + + // Tests for 'zrange3' step. + // REMOVE_START + Assert.assertEquals(3, zRangeResult7); + Assert.assertEquals("three", String.join(", ", zRangeResult8)); + jedis.del("myzset"); + // REMOVE_END + + + // STEP_START zrangebylex + + // STEP_END + + // Tests for 'zrangebylex' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zrangebyscore + + // STEP_END + + // Tests for 'zrangebyscore' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zrangestore + + // STEP_END + + // Tests for 'zrangestore' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zrank + + // STEP_END + + // Tests for 'zrank' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zrem + + // STEP_END + + // Tests for 'zrem' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zremrangebylex + + // STEP_END + + // Tests for 'zremrangebylex' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zremrangebyrank + + // STEP_END + + // Tests for 'zremrangebyrank' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zremrangebyscore + + // STEP_END + + // Tests for 'zremrangebyscore' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zrevrange + + // STEP_END + + // Tests for 'zrevrange' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zrevrangebylex + + // STEP_END + + // Tests for 'zrevrangebylex' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zrevrangebyscore + + // STEP_END + + // Tests for 'zrevrangebyscore' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zrevrank + + // STEP_END + + // Tests for 'zrevrank' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zscan + + // STEP_END + + // Tests for 'zscan' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zscore + + // STEP_END + + // Tests for 'zscore' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zunion + + // STEP_END + + // Tests for 'zunion' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zunionstore + + // STEP_END + + // Tests for 'zunionstore' step. + // REMOVE_START + + // REMOVE_END + + +// HIDE_START + } +} +// HIDE_END + From 708405b75675f11d69317747bba66546466b834a Mon Sep 17 00:00:00 2001 From: Igor Malinovskiy Date: Thu, 5 Sep 2024 10:47:28 +0200 Subject: [PATCH 45/63] Add A-A failover scenario test (#3935) * Add A-A failover scenario test * Update src/main/java/redis/clients/jedis/providers/MultiClusterPooledConnectionProvider.java Co-authored-by: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> * Update src/test/java/redis/clients/jedis/scenario/ActiveActiveFailoverTest.java Co-authored-by: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> * Add missing import --------- Co-authored-by: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> --- .../MultiClusterPooledConnectionProvider.java | 6 + .../redis/clients/jedis/EndpointConfig.java | 4 + .../scenario/ActiveActiveFailoverTest.java | 187 ++++++++++++++++++ .../redis/clients/jedis/scenario/FakeApp.java | 12 +- .../jedis/scenario/MultiThreadedFakeApp.java | 48 +++++ 5 files changed, 251 insertions(+), 6 deletions(-) create mode 100644 src/test/java/redis/clients/jedis/scenario/ActiveActiveFailoverTest.java create mode 100644 src/test/java/redis/clients/jedis/scenario/MultiThreadedFakeApp.java diff --git a/src/main/java/redis/clients/jedis/providers/MultiClusterPooledConnectionProvider.java b/src/main/java/redis/clients/jedis/providers/MultiClusterPooledConnectionProvider.java index eb443bca1e5..097bf636e05 100644 --- a/src/main/java/redis/clients/jedis/providers/MultiClusterPooledConnectionProvider.java +++ b/src/main/java/redis/clients/jedis/providers/MultiClusterPooledConnectionProvider.java @@ -24,6 +24,7 @@ import redis.clients.jedis.*; import redis.clients.jedis.MultiClusterClientConfig.ClusterConfig; import redis.clients.jedis.annots.Experimental; +import redis.clients.jedis.annots.VisibleForTesting; import redis.clients.jedis.exceptions.JedisConnectionException; import redis.clients.jedis.exceptions.JedisValidationException; import redis.clients.jedis.util.Pool; @@ -299,6 +300,11 @@ public Cluster getCluster() { return multiClusterMap.get(activeMultiClusterIndex); } + @VisibleForTesting + public Cluster getCluster(int multiClusterIndex) { + return multiClusterMap.get(multiClusterIndex); + } + public CircuitBreaker getClusterCircuitBreaker() { return multiClusterMap.get(activeMultiClusterIndex).getCircuitBreaker(); } diff --git a/src/test/java/redis/clients/jedis/EndpointConfig.java b/src/test/java/redis/clients/jedis/EndpointConfig.java index 42a44a3c47e..68f927be0e5 100644 --- a/src/test/java/redis/clients/jedis/EndpointConfig.java +++ b/src/test/java/redis/clients/jedis/EndpointConfig.java @@ -31,6 +31,10 @@ public HostAndPort getHostAndPort() { return JedisURIHelper.getHostAndPort(endpoints.get(0)); } + public HostAndPort getHostAndPort(int index) { + return JedisURIHelper.getHostAndPort(endpoints.get(index)); + } + public String getPassword() { return password; } diff --git a/src/test/java/redis/clients/jedis/scenario/ActiveActiveFailoverTest.java b/src/test/java/redis/clients/jedis/scenario/ActiveActiveFailoverTest.java new file mode 100644 index 00000000000..587988b447b --- /dev/null +++ b/src/test/java/redis/clients/jedis/scenario/ActiveActiveFailoverTest.java @@ -0,0 +1,187 @@ +package redis.clients.jedis.scenario; + +import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; +import org.junit.BeforeClass; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import redis.clients.jedis.*; +import redis.clients.jedis.providers.MultiClusterPooledConnectionProvider; +import redis.clients.jedis.exceptions.JedisConnectionException; + +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import static org.junit.Assert.*; + +public class ActiveActiveFailoverTest { + private static final Logger log = LoggerFactory.getLogger(ActiveActiveFailoverTest.class); + + private static EndpointConfig endpoint; + + private final FaultInjectionClient faultClient = new FaultInjectionClient(); + + @BeforeClass + public static void beforeClass() { + try { + ActiveActiveFailoverTest.endpoint = HostAndPorts.getRedisEndpoint("re-active-active"); + } catch (IllegalArgumentException e) { + log.warn("Skipping test because no Redis endpoint is configured"); + org.junit.Assume.assumeTrue(false); + } + } + + @Test + public void testFailover() { + + MultiClusterClientConfig.ClusterConfig[] clusterConfig = new MultiClusterClientConfig.ClusterConfig[2]; + + JedisClientConfig config = endpoint.getClientConfigBuilder() + .socketTimeoutMillis(RecommendedSettings.DEFAULT_TIMEOUT_MS) + .connectionTimeoutMillis(RecommendedSettings.DEFAULT_TIMEOUT_MS).build(); + + clusterConfig[0] = new MultiClusterClientConfig.ClusterConfig(endpoint.getHostAndPort(0), + config, RecommendedSettings.poolConfig); + clusterConfig[1] = new MultiClusterClientConfig.ClusterConfig(endpoint.getHostAndPort(1), + config, RecommendedSettings.poolConfig); + + MultiClusterClientConfig.Builder builder = new MultiClusterClientConfig.Builder(clusterConfig); + + builder.circuitBreakerSlidingWindowType(CircuitBreakerConfig.SlidingWindowType.TIME_BASED); + builder.circuitBreakerSlidingWindowSize(1); // SLIDING WINDOW SIZE IN SECONDS + builder.circuitBreakerSlidingWindowMinCalls(1); + builder.circuitBreakerFailureRateThreshold(10.0f); // percentage of failures to trigger circuit breaker + + builder.retryWaitDuration(10); + builder.retryMaxAttempts(1); + builder.retryWaitDurationExponentialBackoffMultiplier(1); + + class FailoverReporter implements Consumer { + + String currentClusterName = "not set"; + + boolean failoverHappened = false; + + Instant failoverAt = null; + + public String getCurrentClusterName() { + return currentClusterName; + } + + @Override + public void accept(String clusterName) { + this.currentClusterName = clusterName; + log.info( + "\n\n====FailoverEvent=== \nJedis failover to cluster: {}\n====FailoverEvent===\n\n", + clusterName); + + failoverHappened = true; + failoverAt = Instant.now(); + } + } + + MultiClusterPooledConnectionProvider provider = new MultiClusterPooledConnectionProvider( + builder.build()); + FailoverReporter reporter = new FailoverReporter(); + provider.setClusterFailoverPostProcessor(reporter); + provider.setActiveMultiClusterIndex(1); + + UnifiedJedis client = new UnifiedJedis(provider); + + AtomicLong retryingThreadsCounter = new AtomicLong(0); + AtomicLong failedCommandsAfterFailover = new AtomicLong(0); + AtomicReference lastFailedCommandAt = new AtomicReference<>(); + + // Start thread that imitates an application that uses the client + MultiThreadedFakeApp fakeApp = new MultiThreadedFakeApp(client, (UnifiedJedis c) -> { + + long threadId = Thread.currentThread().getId(); + + int attempt = 0; + int maxTries = 500; + int retryingDelay = 5; + while (true) { + try { + Map executionInfo = new HashMap() {{ + put("threadId", String.valueOf(threadId)); + put("cluster", reporter.getCurrentClusterName()); + }}; + client.xadd("execution_log", StreamEntryID.NEW_ENTRY, executionInfo); + + if (attempt > 0) { + log.info("Thread {} recovered after {} ms. Threads still not recovered: {}", threadId, + attempt * retryingDelay, retryingThreadsCounter.decrementAndGet()); + } + + break; + } catch (JedisConnectionException e) { + + if (reporter.failoverHappened) { + long failedCommands = failedCommandsAfterFailover.incrementAndGet(); + lastFailedCommandAt.set(Instant.now()); + log.warn( + "Thread {} failed to execute command after failover. Failed commands after failover: {}", + threadId, failedCommands); + } + + if (attempt == 0) { + long failedThreads = retryingThreadsCounter.incrementAndGet(); + log.warn("Thread {} failed to execute command. Failed threads: {}", threadId, + failedThreads); + } + try { + Thread.sleep(retryingDelay); + } catch (InterruptedException ie) { + throw new RuntimeException(ie); + } + if (++attempt == maxTries) throw e; + } + } + return true; + }, 18); + fakeApp.setKeepExecutingForSeconds(30); + Thread t = new Thread(fakeApp); + t.start(); + + HashMap params = new HashMap<>(); + params.put("bdb_id", endpoint.getBdbId()); + params.put("rlutil_command", "pause_bdb"); + + FaultInjectionClient.TriggerActionResponse actionResponse = null; + + try { + log.info("Triggering bdb_pause"); + actionResponse = faultClient.triggerAction("execute_rlutil_command", params); + } catch (IOException e) { + fail("Fault Injection Server error:" + e.getMessage()); + } + + log.info("Action id: {}", actionResponse.getActionId()); + fakeApp.setAction(actionResponse); + + try { + t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + ConnectionPool pool = provider.getCluster(1).getConnectionPool(); + + log.info("First connection pool state: active: {}, idle: {}", pool.getNumActive(), + pool.getNumIdle()); + log.info("Full failover time: {} s", + Duration.between(reporter.failoverAt, lastFailedCommandAt.get()).getSeconds()); + + assertEquals(0, pool.getNumActive()); + assertTrue(fakeApp.capturedExceptions().isEmpty()); + + client.close(); + } + +} diff --git a/src/test/java/redis/clients/jedis/scenario/FakeApp.java b/src/test/java/redis/clients/jedis/scenario/FakeApp.java index 7e505862a24..4ea6ffc1f9e 100644 --- a/src/test/java/redis/clients/jedis/scenario/FakeApp.java +++ b/src/test/java/redis/clients/jedis/scenario/FakeApp.java @@ -12,18 +12,18 @@ public class FakeApp implements Runnable { - private static final Logger log = LoggerFactory.getLogger(FakeApp.class); + protected static final Logger log = LoggerFactory.getLogger(FakeApp.class); public void setKeepExecutingForSeconds(int keepExecutingForSeconds) { this.keepExecutingForSeconds = keepExecutingForSeconds; } - private int keepExecutingForSeconds = 60; + protected int keepExecutingForSeconds = 60; - private FaultInjectionClient.TriggerActionResponse actionResponse = null; - private final UnifiedJedis client; - private final ExecutedAction action; - private List exceptions = new ArrayList<>(); + protected FaultInjectionClient.TriggerActionResponse actionResponse = null; + protected final UnifiedJedis client; + protected final ExecutedAction action; + protected List exceptions = new ArrayList<>(); @FunctionalInterface public interface ExecutedAction { diff --git a/src/test/java/redis/clients/jedis/scenario/MultiThreadedFakeApp.java b/src/test/java/redis/clients/jedis/scenario/MultiThreadedFakeApp.java new file mode 100644 index 00000000000..4c03fc2cf27 --- /dev/null +++ b/src/test/java/redis/clients/jedis/scenario/MultiThreadedFakeApp.java @@ -0,0 +1,48 @@ +package redis.clients.jedis.scenario; + +import redis.clients.jedis.UnifiedJedis; +import redis.clients.jedis.exceptions.JedisConnectionException; + +import java.time.Duration; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +public class MultiThreadedFakeApp extends FakeApp { + + private final ExecutorService executorService; + + public MultiThreadedFakeApp(UnifiedJedis client, FakeApp.ExecutedAction action, int numThreads) { + super(client, action); + this.executorService = Executors.newFixedThreadPool(numThreads); + } + + @Override + public void run() { + log.info("Starting FakeApp"); + + int checkEachSeconds = 5; + int timeoutSeconds = 120; + + while (actionResponse == null || !actionResponse.isCompleted( + Duration.ofSeconds(checkEachSeconds), Duration.ofSeconds(keepExecutingForSeconds), + Duration.ofSeconds(timeoutSeconds))) { + try { + executorService.submit(() -> action.run(client)); + } catch (JedisConnectionException e) { + log.error("Error executing action", e); + exceptions.add(e); + } + } + + executorService.shutdown(); + + try { + if (!executorService.awaitTermination(keepExecutingForSeconds, TimeUnit.SECONDS)) { + executorService.shutdownNow(); + } + } catch (InterruptedException e) { + log.error("Error waiting for executor service to terminate", e); + } + } +} From b874b361cdfc716a5f8b1de34715f90927757a63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2024 00:04:13 +0600 Subject: [PATCH 46/63] Bump org.locationtech.jts:jts-core from 1.19.0 to 1.20.0 (#3948) Bumps org.locationtech.jts:jts-core from 1.19.0 to 1.20.0. --- updated-dependencies: - dependency-name: org.locationtech.jts:jts-core dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3ae8ea9c7fd..753486290cc 100644 --- a/pom.xml +++ b/pom.xml @@ -87,7 +87,7 @@ org.locationtech.jts jts-core - 1.19.0 + 1.20.0 test From 933db735869c29237b5958c139111bacd2d08f0b Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:23:02 +0100 Subject: [PATCH 47/63] DOC-4082 code examples for DEL, EXPIRE, and TTL (#3945) --- .../io/redis/examples/CmdsGenericExample.java | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/test/java/io/redis/examples/CmdsGenericExample.java diff --git a/src/test/java/io/redis/examples/CmdsGenericExample.java b/src/test/java/io/redis/examples/CmdsGenericExample.java new file mode 100644 index 00000000000..c43364f105b --- /dev/null +++ b/src/test/java/io/redis/examples/CmdsGenericExample.java @@ -0,0 +1,113 @@ +// EXAMPLE: cmds_generic +// REMOVE_START +package io.redis.examples; + +import org.junit.Assert; +import org.junit.Test; + +// REMOVE_END +// HIDE_START +import redis.clients.jedis.UnifiedJedis; +import redis.clients.jedis.args.ExpiryOption; +// HIDE_END + +// HIDE_START +public class CmdsGenericExample { + @Test + public void run() { + UnifiedJedis jedis = new UnifiedJedis("redis://localhost:6379"); + + //REMOVE_START + // Clear any keys here before using them in tests. + + //REMOVE_END +// HIDE_END + + // STEP_START del + String delResult1 = jedis.set("key1", "Hello"); + System.out.println(delResult1); // >>> OK + + String delResult2 = jedis.set("key2", "World"); + System.out.println(delResult2); // >>> OK + + long delResult3 = jedis.del("key1", "key2", "key3"); + System.out.println(delResult3); // >>> 2 + // STEP_END + + // Tests for 'del' step. + // REMOVE_START + Assert.assertEquals("OK", delResult1); + Assert.assertEquals("OK", delResult2); + Assert.assertEquals(2, delResult3); + // REMOVE_END + + + // STEP_START expire + String expireResult1 = jedis.set("mykey", "Hello"); + System.out.println(expireResult1); // >>> OK + + long expireResult2 = jedis.expire("mykey", 10); + System.out.println(expireResult2); // >>> 1 + + long expireResult3 = jedis.ttl("mykey"); + System.out.println(expireResult3); // >>> 10 + + String expireResult4 = jedis.set("mykey", "Hello World"); + System.out.println(expireResult4); // >>> OK + + long expireResult5 = jedis.ttl("mykey"); + System.out.println(expireResult5); // >>> -1 + + long expireResult6 = jedis.expire("mykey", 10, ExpiryOption.XX); + System.out.println(expireResult6); // >>> 0 + + long expireResult7 = jedis.ttl("mykey"); + System.out.println(expireResult7); // >>> -1 + + long expireResult8 = jedis.expire("mykey", 10, ExpiryOption.NX); + System.out.println(expireResult8); // >>> 1 + + long expireResult9 = jedis.ttl("mykey"); + System.out.println(expireResult9); // >>> 10 + // STEP_END + + // Tests for 'expire' step. + // REMOVE_START + Assert.assertEquals("OK", expireResult1); + Assert.assertEquals(1, expireResult2); + Assert.assertEquals(10, expireResult3); + Assert.assertEquals("OK", expireResult4); + Assert.assertEquals(-1, expireResult5); + Assert.assertEquals(0, expireResult6); + Assert.assertEquals(-1, expireResult7); + Assert.assertEquals(1, expireResult8); + Assert.assertEquals(10, expireResult9); + jedis.del("mykey"); + // REMOVE_END + + + // STEP_START ttl + String ttlResult1 = jedis.set("mykey", "Hello"); + System.out.println(ttlResult1); // >>> OK + + long ttlResult2 = jedis.expire("mykey", 10); + System.out.println(ttlResult2); // >>> 1 + + long ttlResult3 = jedis.ttl("mykey"); + System.out.println(ttlResult3); // >>> 10 + // STEP_END + + // Tests for 'ttl' step. + // REMOVE_START + Assert.assertEquals("OK", ttlResult1); + Assert.assertEquals(1, ttlResult2); + Assert.assertEquals(10, ttlResult3); + jedis.del("mykey"); + // REMOVE_END + +// HIDE_START + + } +} +// HIDE_END + From bda181cafe9a651f3dfda8e1fdf062e4197f60a3 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:24:57 +0100 Subject: [PATCH 48/63] DOC-4102 INCR command example (#3946) --- .../io/redis/examples/CmdsStringExample.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/test/java/io/redis/examples/CmdsStringExample.java diff --git a/src/test/java/io/redis/examples/CmdsStringExample.java b/src/test/java/io/redis/examples/CmdsStringExample.java new file mode 100644 index 00000000000..a1ba0281e54 --- /dev/null +++ b/src/test/java/io/redis/examples/CmdsStringExample.java @@ -0,0 +1,48 @@ +// EXAMPLE: cmds_string +// REMOVE_START +package io.redis.examples; + +import org.junit.Assert; +import org.junit.Test; + +// REMOVE_END +// HIDE_START +import redis.clients.jedis.UnifiedJedis; +// HIDE_END + +// HIDE_START +public class CmdsStringExample { + @Test + public void run() { + UnifiedJedis jedis = new UnifiedJedis("redis://localhost:6379"); + + //REMOVE_START + // Clear any keys here before using them in tests. + jedis.del("mykey"); + //REMOVE_END +// HIDE_END + + // STEP_START incr + String incrResult1 = jedis.set("mykey", "10"); + System.out.println(incrResult1); // >>> OK + + long incrResult2 = jedis.incr("mykey"); + System.out.println(incrResult2); // >>> 11 + + String incrResult3 = jedis.get("mykey"); + System.out.println(incrResult3); // >>> 11 + // STEP_END + + // Tests for 'incr' step. + // REMOVE_START + Assert.assertEquals("OK", incrResult1); + Assert.assertEquals(11, incrResult2); + Assert.assertEquals("11", incrResult3); + jedis.del("mykey"); + // REMOVE_END + +// HIDE_START + } +} +// HIDE_END + From 2229a34116c1a32a1652149b9f0be52b74c7ae69 Mon Sep 17 00:00:00 2001 From: Thorben <46635221+Gandalf1783@users.noreply.github.com> Date: Sun, 22 Sep 2024 20:34:09 +0200 Subject: [PATCH 49/63] Added JavaDoc for basic JedisCluster constructors (#3304) * Basic Jedis-Cluster constructors commented. More advanced/longer constructors are yet to be commented. * Update src/main/java/redis/clients/jedis/JedisCluster.java Co-authored-by: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> * Update src/main/java/redis/clients/jedis/JedisCluster.java Co-authored-by: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> * Update src/main/java/redis/clients/jedis/JedisCluster.java Co-authored-by: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> * Resolving further issues with PR * Changed overlooked JavaDoc * Format JedisCluster.java * Update JedisCluster.java * Use value annotation --------- Co-authored-by: BuildTools Co-authored-by: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> --- .../redis/clients/jedis/JedisCluster.java | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/main/java/redis/clients/jedis/JedisCluster.java b/src/main/java/redis/clients/jedis/JedisCluster.java index 68d8f4205f6..4d6b6722724 100644 --- a/src/main/java/redis/clients/jedis/JedisCluster.java +++ b/src/main/java/redis/clients/jedis/JedisCluster.java @@ -19,16 +19,38 @@ public class JedisCluster extends UnifiedJedis { * Default timeout in milliseconds. */ public static final int DEFAULT_TIMEOUT = 2000; + + /** + * Default amount of attempts for executing a command + */ public static final int DEFAULT_MAX_ATTEMPTS = 5; + /** + * Creates a JedisCluster instance. The provided node is used to make the first contact with the cluster.
+ * Here, the default timeout of {@value JedisCluster#DEFAULT_TIMEOUT} ms is being used with {@value JedisCluster#DEFAULT_MAX_ATTEMPTS} maximum attempts. + * @param node Node to first connect to. + */ public JedisCluster(HostAndPort node) { this(Collections.singleton(node)); } + /** + * Creates a JedisCluster instance. The provided node is used to make the first contact with the cluster.
+ * Here, the default timeout of {@value JedisCluster#DEFAULT_TIMEOUT} ms is being used with {@value JedisCluster#DEFAULT_MAX_ATTEMPTS} maximum attempts. + * @param node Node to first connect to. + * @param timeout connection and socket timeout in milliseconds. + */ public JedisCluster(HostAndPort node, int timeout) { this(Collections.singleton(node), timeout); } + /** + * Creates a JedisCluster instance. The provided node is used to make the first contact with the cluster.
+ * You can specify the timeout and the maximum attempts. + * @param node Node to first connect to. + * @param timeout connection and socket timeout in milliseconds. + * @param maxAttempts maximum attempts for executing a command. + */ public JedisCluster(HostAndPort node, int timeout, int maxAttempts) { this(Collections.singleton(node), timeout, maxAttempts); } @@ -89,14 +111,32 @@ public JedisCluster(HostAndPort node, final JedisClientConfig clientConfig, int this(Collections.singleton(node), clientConfig, maxAttempts, poolConfig); } + /** + * Creates a JedisCluster with multiple entry points. + * Here, the default timeout of {@value JedisCluster#DEFAULT_TIMEOUT} ms is being used with {@value JedisCluster#DEFAULT_MAX_ATTEMPTS} maximum attempts. + * @param nodes Nodes to connect to. + */ public JedisCluster(Set nodes) { this(nodes, DEFAULT_TIMEOUT); } + /** + * Creates a JedisCluster with multiple entry points. + * Here, the default timeout of {@value JedisCluster#DEFAULT_TIMEOUT} ms is being used with {@value JedisCluster#DEFAULT_MAX_ATTEMPTS} maximum attempts. + * @param nodes Nodes to connect to. + * @param timeout connection and socket timeout in milliseconds. + */ public JedisCluster(Set nodes, int timeout) { this(nodes, DefaultJedisClientConfig.builder().timeoutMillis(timeout).build()); } + /** + * Creates a JedisCluster with multiple entry points.
+ * You can specify the timeout and the maximum attempts. + * @param nodes Nodes to connect to. + * @param timeout connection and socket timeout in milliseconds. + * @param maxAttempts maximum attempts for executing a command. + */ public JedisCluster(Set nodes, int timeout, int maxAttempts) { this(nodes, DefaultJedisClientConfig.builder().timeoutMillis(timeout).build(), maxAttempts); } @@ -206,6 +246,19 @@ public JedisCluster(Set clusterNodes, JedisClientConfig clientConfi maxAttempts, maxTotalRetriesDuration, clientConfig.getRedisProtocol()); } + /** + * Creates a JedisCluster with multiple entry points.
+ * You can specify the timeout and the maximum attempts.
+ * + * Additionally, you are free to provide a {@link JedisClientConfig} instance.
+ * You can use the {@link DefaultJedisClientConfig#builder()} builder pattern to customize your configuration, including socket timeouts, + * username and passwords as well as SSL related parameters. + * + * @param clusterNodes Nodes to connect to. + * @param clientConfig Client configuration parameters. + * @param maxAttempts maximum attempts for executing a command. + * @param maxTotalRetriesDuration Maximum time used for reconnecting. + */ public JedisCluster(Set clusterNodes, JedisClientConfig clientConfig, int maxAttempts, Duration maxTotalRetriesDuration, GenericObjectPoolConfig poolConfig) { this(new ClusterConnectionProvider(clusterNodes, clientConfig, poolConfig), maxAttempts, maxTotalRetriesDuration, @@ -223,10 +276,20 @@ private JedisCluster(ClusterConnectionProvider provider, int maxAttempts, Durati super(provider, maxAttempts, maxTotalRetriesDuration, protocol); } + /** + * 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. + * @return the map of all connections. + */ public Map getClusterNodes() { return ((ClusterConnectionProvider) provider).getNodes(); } + /** + * Returns the connection for one of the 16,384 slots. + * @param slot the slot to retrieve the connection for. + * @return connection of the provided slot. {@code close()} of this connection must be called after use. + */ public Connection getConnectionFromSlot(int slot) { return ((ClusterConnectionProvider) provider).getConnectionFromSlot(slot); } From 1effa7ac89208ea94af65bb5ff493b5d3d5ce38b Mon Sep 17 00:00:00 2001 From: asc at DI <104837209+ascdi@users.noreply.github.com> Date: Sun, 22 Sep 2024 20:42:46 +0200 Subject: [PATCH 50/63] Add equals and hashCode to Timeseries Params classes (#3959) --- .../clients/jedis/timeseries/TSAddParams.java | 33 +++++++++++++ .../jedis/timeseries/TSAlterParams.java | 30 ++++++++++++ .../jedis/timeseries/TSArithByParams.java | 33 +++++++++++++ .../jedis/timeseries/TSCreateParams.java | 31 ++++++++++++ .../clients/jedis/timeseries/TSGetParams.java | 18 +++++++ .../jedis/timeseries/TSMGetParams.java | 23 +++++++++ .../jedis/timeseries/TSMRangeParams.java | 48 +++++++++++++++++++ .../jedis/timeseries/TSRangeParams.java | 38 +++++++++++++++ 8 files changed, 254 insertions(+) diff --git a/src/main/java/redis/clients/jedis/timeseries/TSAddParams.java b/src/main/java/redis/clients/jedis/timeseries/TSAddParams.java index 0a9713cefb8..487b5301a2b 100644 --- a/src/main/java/redis/clients/jedis/timeseries/TSAddParams.java +++ b/src/main/java/redis/clients/jedis/timeseries/TSAddParams.java @@ -5,6 +5,7 @@ import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; import redis.clients.jedis.CommandArguments; import redis.clients.jedis.params.IParams; @@ -125,4 +126,36 @@ public void addParams(CommandArguments args) { labels.entrySet().forEach((entry) -> args.add(entry.getKey()).add(entry.getValue())); } } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TSAddParams that = (TSAddParams) o; + return ignore == that.ignore && ignoreMaxTimediff == that.ignoreMaxTimediff && + Double.compare(ignoreMaxValDiff, that.ignoreMaxValDiff) == 0 && + Objects.equals(retentionPeriod, that.retentionPeriod) && + encoding == that.encoding && Objects.equals(chunkSize, that.chunkSize) && + duplicatePolicy == that.duplicatePolicy && onDuplicate == that.onDuplicate && + Objects.equals(labels, that.labels); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(retentionPeriod); + result = 31 * result + Objects.hashCode(encoding); + result = 31 * result + Objects.hashCode(chunkSize); + result = 31 * result + Objects.hashCode(duplicatePolicy); + result = 31 * result + Objects.hashCode(onDuplicate); + result = 31 * result + Boolean.hashCode(ignore); + result = 31 * result + Long.hashCode(ignoreMaxTimediff); + result = 31 * result + Double.hashCode(ignoreMaxValDiff); + result = 31 * result + Objects.hashCode(labels); + return result; + } } diff --git a/src/main/java/redis/clients/jedis/timeseries/TSAlterParams.java b/src/main/java/redis/clients/jedis/timeseries/TSAlterParams.java index 50ba9723acc..2a65cc74d91 100644 --- a/src/main/java/redis/clients/jedis/timeseries/TSAlterParams.java +++ b/src/main/java/redis/clients/jedis/timeseries/TSAlterParams.java @@ -6,6 +6,7 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; import redis.clients.jedis.CommandArguments; import redis.clients.jedis.params.IParams; @@ -106,4 +107,33 @@ public void addParams(CommandArguments args) { labels.entrySet().forEach((entry) -> args.add(entry.getKey()).add(entry.getValue())); } } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TSAlterParams that = (TSAlterParams) o; + return ignore == that.ignore && ignoreMaxTimediff == that.ignoreMaxTimediff && + Double.compare(ignoreMaxValDiff, that.ignoreMaxValDiff) == 0 && + Objects.equals(retentionPeriod, that.retentionPeriod) && + Objects.equals(chunkSize, that.chunkSize) && + duplicatePolicy == that.duplicatePolicy && Objects.equals(labels, that.labels); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(retentionPeriod); + result = 31 * result + Objects.hashCode(chunkSize); + result = 31 * result + Objects.hashCode(duplicatePolicy); + result = 31 * result + Boolean.hashCode(ignore); + result = 31 * result + Long.hashCode(ignoreMaxTimediff); + result = 31 * result + Double.hashCode(ignoreMaxValDiff); + result = 31 * result + Objects.hashCode(labels); + return result; + } } diff --git a/src/main/java/redis/clients/jedis/timeseries/TSArithByParams.java b/src/main/java/redis/clients/jedis/timeseries/TSArithByParams.java index 1bc3df1c55b..7703816d256 100644 --- a/src/main/java/redis/clients/jedis/timeseries/TSArithByParams.java +++ b/src/main/java/redis/clients/jedis/timeseries/TSArithByParams.java @@ -5,6 +5,7 @@ import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; import redis.clients.jedis.CommandArguments; import redis.clients.jedis.params.IParams; @@ -117,4 +118,36 @@ public void addParams(CommandArguments args) { labels.entrySet().forEach((entry) -> args.add(entry.getKey()).add(entry.getValue())); } } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TSArithByParams that = (TSArithByParams) o; + return ignore == that.ignore && ignoreMaxTimediff == that.ignoreMaxTimediff && + Double.compare(ignoreMaxValDiff, that.ignoreMaxValDiff) == 0 && + Objects.equals(timestamp, that.timestamp) && + Objects.equals(retentionPeriod, that.retentionPeriod) && + encoding == that.encoding && Objects.equals(chunkSize, that.chunkSize) && + duplicatePolicy == that.duplicatePolicy && Objects.equals(labels, that.labels); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(timestamp); + result = 31 * result + Objects.hashCode(retentionPeriod); + result = 31 * result + Objects.hashCode(encoding); + result = 31 * result + Objects.hashCode(chunkSize); + result = 31 * result + Objects.hashCode(duplicatePolicy); + result = 31 * result + Boolean.hashCode(ignore); + result = 31 * result + Long.hashCode(ignoreMaxTimediff); + result = 31 * result + Double.hashCode(ignoreMaxValDiff); + result = 31 * result + Objects.hashCode(labels); + return result; + } } diff --git a/src/main/java/redis/clients/jedis/timeseries/TSCreateParams.java b/src/main/java/redis/clients/jedis/timeseries/TSCreateParams.java index 0611383d4d2..c9c5face720 100644 --- a/src/main/java/redis/clients/jedis/timeseries/TSCreateParams.java +++ b/src/main/java/redis/clients/jedis/timeseries/TSCreateParams.java @@ -5,6 +5,7 @@ import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; import redis.clients.jedis.CommandArguments; import redis.clients.jedis.params.IParams; @@ -121,4 +122,34 @@ public void addParams(CommandArguments args) { labels.entrySet().forEach((entry) -> args.add(entry.getKey()).add(entry.getValue())); } } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TSCreateParams that = (TSCreateParams) o; + return ignore == that.ignore && ignoreMaxTimediff == that.ignoreMaxTimediff && + Double.compare(ignoreMaxValDiff, that.ignoreMaxValDiff) == 0 && + Objects.equals(retentionPeriod, that.retentionPeriod) && + encoding == that.encoding && Objects.equals(chunkSize, that.chunkSize) && + duplicatePolicy == that.duplicatePolicy && Objects.equals(labels, that.labels); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(retentionPeriod); + result = 31 * result + Objects.hashCode(encoding); + result = 31 * result + Objects.hashCode(chunkSize); + result = 31 * result + Objects.hashCode(duplicatePolicy); + result = 31 * result + Boolean.hashCode(ignore); + result = 31 * result + Long.hashCode(ignoreMaxTimediff); + result = 31 * result + Double.hashCode(ignoreMaxValDiff); + result = 31 * result + Objects.hashCode(labels); + return result; + } } diff --git a/src/main/java/redis/clients/jedis/timeseries/TSGetParams.java b/src/main/java/redis/clients/jedis/timeseries/TSGetParams.java index eb35cf1eae4..3525b0e18f5 100644 --- a/src/main/java/redis/clients/jedis/timeseries/TSGetParams.java +++ b/src/main/java/redis/clients/jedis/timeseries/TSGetParams.java @@ -27,4 +27,22 @@ public void addParams(CommandArguments args) { args.add(LATEST); } } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TSGetParams that = (TSGetParams) o; + return latest == that.latest; + } + + @Override + public int hashCode() { + return Boolean.hashCode(latest); + } } diff --git a/src/main/java/redis/clients/jedis/timeseries/TSMGetParams.java b/src/main/java/redis/clients/jedis/timeseries/TSMGetParams.java index c7d08ffde25..c4272c04c3c 100644 --- a/src/main/java/redis/clients/jedis/timeseries/TSMGetParams.java +++ b/src/main/java/redis/clients/jedis/timeseries/TSMGetParams.java @@ -4,6 +4,7 @@ import static redis.clients.jedis.timeseries.TimeSeriesProtocol.TimeSeriesKeyword.SELECTED_LABELS; import static redis.clients.jedis.timeseries.TimeSeriesProtocol.TimeSeriesKeyword.WITHLABELS; +import java.util.Arrays; import redis.clients.jedis.CommandArguments; import redis.clients.jedis.params.IParams; @@ -55,4 +56,26 @@ public void addParams(CommandArguments args) { } } } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TSMGetParams that = (TSMGetParams) o; + return latest == that.latest && withLabels == that.withLabels && + Arrays.equals(selectedLabels, that.selectedLabels); + } + + @Override + public int hashCode() { + int result = Boolean.hashCode(latest); + result = 31 * result + Boolean.hashCode(withLabels); + result = 31 * result + Arrays.hashCode(selectedLabels); + return result; + } } diff --git a/src/main/java/redis/clients/jedis/timeseries/TSMRangeParams.java b/src/main/java/redis/clients/jedis/timeseries/TSMRangeParams.java index 182c90814b8..cafe091a3a1 100644 --- a/src/main/java/redis/clients/jedis/timeseries/TSMRangeParams.java +++ b/src/main/java/redis/clients/jedis/timeseries/TSMRangeParams.java @@ -7,6 +7,8 @@ import static redis.clients.jedis.timeseries.TimeSeriesProtocol.TimeSeriesKeyword.*; import static redis.clients.jedis.util.SafeEncoder.encode; +import java.util.Arrays; +import java.util.Objects; import redis.clients.jedis.CommandArguments; import redis.clients.jedis.params.IParams; @@ -260,4 +262,50 @@ public void addParams(CommandArguments args) { args.add(GROUPBY).add(groupByLabel).add(REDUCE).add(groupByReduce); } } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TSMRangeParams that = (TSMRangeParams) o; + return latest == that.latest && withLabels == that.withLabels && + bucketDuration == that.bucketDuration && empty == that.empty && + Objects.equals(fromTimestamp, that.fromTimestamp) && + Objects.equals(toTimestamp, that.toTimestamp) && + Arrays.equals(filterByTimestamps, that.filterByTimestamps) && + Arrays.equals(filterByValues, that.filterByValues) && + Arrays.equals(selectedLabels, that.selectedLabels) && + Objects.equals(count, that.count) && Arrays.equals(align, that.align) && + aggregationType == that.aggregationType && + Arrays.equals(bucketTimestamp, that.bucketTimestamp) && + Arrays.equals(filters, that.filters) && + Objects.equals(groupByLabel, that.groupByLabel) && + Objects.equals(groupByReduce, that.groupByReduce); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(fromTimestamp); + result = 31 * result + Objects.hashCode(toTimestamp); + result = 31 * result + Boolean.hashCode(latest); + result = 31 * result + Arrays.hashCode(filterByTimestamps); + result = 31 * result + Arrays.hashCode(filterByValues); + result = 31 * result + Boolean.hashCode(withLabels); + result = 31 * result + Arrays.hashCode(selectedLabels); + result = 31 * result + Objects.hashCode(count); + result = 31 * result + Arrays.hashCode(align); + result = 31 * result + Objects.hashCode(aggregationType); + result = 31 * result + Long.hashCode(bucketDuration); + result = 31 * result + Arrays.hashCode(bucketTimestamp); + result = 31 * result + Boolean.hashCode(empty); + result = 31 * result + Arrays.hashCode(filters); + result = 31 * result + Objects.hashCode(groupByLabel); + result = 31 * result + Objects.hashCode(groupByReduce); + return result; + } } diff --git a/src/main/java/redis/clients/jedis/timeseries/TSRangeParams.java b/src/main/java/redis/clients/jedis/timeseries/TSRangeParams.java index bab70dcbd62..402e2b88e6e 100644 --- a/src/main/java/redis/clients/jedis/timeseries/TSRangeParams.java +++ b/src/main/java/redis/clients/jedis/timeseries/TSRangeParams.java @@ -7,6 +7,8 @@ import static redis.clients.jedis.timeseries.TimeSeriesProtocol.TimeSeriesKeyword.*; import static redis.clients.jedis.util.SafeEncoder.encode; +import java.util.Arrays; +import java.util.Objects; import redis.clients.jedis.CommandArguments; import redis.clients.jedis.params.IParams; @@ -205,4 +207,40 @@ public void addParams(CommandArguments args) { } } } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TSRangeParams that = (TSRangeParams) o; + return latest == that.latest && bucketDuration == that.bucketDuration && empty == that.empty && + Objects.equals(fromTimestamp, that.fromTimestamp) && + Objects.equals(toTimestamp, that.toTimestamp) && + Arrays.equals(filterByTimestamps, that.filterByTimestamps) && + Arrays.equals(filterByValues, that.filterByValues) && + Objects.equals(count, that.count) && Arrays.equals(align, that.align) && + aggregationType == that.aggregationType && + Arrays.equals(bucketTimestamp, that.bucketTimestamp); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(fromTimestamp); + result = 31 * result + Objects.hashCode(toTimestamp); + result = 31 * result + Boolean.hashCode(latest); + result = 31 * result + Arrays.hashCode(filterByTimestamps); + result = 31 * result + Arrays.hashCode(filterByValues); + result = 31 * result + Objects.hashCode(count); + result = 31 * result + Arrays.hashCode(align); + result = 31 * result + Objects.hashCode(aggregationType); + result = 31 * result + Long.hashCode(bucketDuration); + result = 31 * result + Arrays.hashCode(bucketTimestamp); + result = 31 * result + Boolean.hashCode(empty); + return result; + } } From 723fc71d497f16a5c112ca7f545612abd84d2e7c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 00:46:49 +0600 Subject: [PATCH 51/63] Bump org.apache.maven.plugins:maven-gpg-plugin from 3.2.5 to 3.2.6 (#3957) Bumps [org.apache.maven.plugins:maven-gpg-plugin](https://github.com/apache/maven-gpg-plugin) from 3.2.5 to 3.2.6. - [Release notes](https://github.com/apache/maven-gpg-plugin/releases) - [Commits](https://github.com/apache/maven-gpg-plugin/compare/maven-gpg-plugin-3.2.5...maven-gpg-plugin-3.2.6) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-gpg-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 753486290cc..ad2577a3cfd 100644 --- a/pom.xml +++ b/pom.xml @@ -312,7 +312,7 @@ maven-gpg-plugin - 3.2.5 + 3.2.6 --pinentry-mode From e4c50d68c5a16352e96ac6df599c8ad14788c5d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 10:39:31 +0600 Subject: [PATCH 52/63] Bump org.apache.maven.plugins:maven-javadoc-plugin from 3.8.0 to 3.10.0 (#3949) Bumps [org.apache.maven.plugins:maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) from 3.8.0 to 3.10.0. - [Release notes](https://github.com/apache/maven-javadoc-plugin/releases) - [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.8.0...maven-javadoc-plugin-3.10.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-javadoc-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ad2577a3cfd..652704552ad 100644 --- a/pom.xml +++ b/pom.xml @@ -237,7 +237,7 @@ maven-javadoc-plugin - 3.8.0 + 3.10.0 8 false From c1b823b47f3d443115b5634c0bb3f74c3b5cac92 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 10:40:25 +0600 Subject: [PATCH 53/63] Bump org.apache.maven.plugins:maven-surefire-plugin from 3.3.1 to 3.5.0 (#3950) Bumps [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.3.1 to 3.5.0. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.3.1...surefire-3.5.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 652704552ad..f390a5341b4 100644 --- a/pom.xml +++ b/pom.xml @@ -50,7 +50,7 @@ 1.7.36 1.7.1 2.17.2 - 3.3.1 + 3.5.0 From 1683255c993d5e41d117f824c607c700ef615a90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 10:44:43 +0600 Subject: [PATCH 54/63] Bump org.apache.httpcomponents.client5:httpclient5-fluent from 5.3.1 to 5.4 (#3962) Bump org.apache.httpcomponents.client5:httpclient5-fluent Bumps [org.apache.httpcomponents.client5:httpclient5-fluent](https://github.com/apache/httpcomponents-client) from 5.3.1 to 5.4. - [Changelog](https://github.com/apache/httpcomponents-client/blob/master/RELEASE_NOTES.txt) - [Commits](https://github.com/apache/httpcomponents-client/compare/rel/v5.3.1...rel/v5.4) --- updated-dependencies: - dependency-name: org.apache.httpcomponents.client5:httpclient5-fluent dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f390a5341b4..548fe1ca23f 100644 --- a/pom.xml +++ b/pom.xml @@ -136,7 +136,7 @@ org.apache.httpcomponents.client5 httpclient5-fluent - 5.3.1 + 5.4 test From 51e4f3108ec9ac9d2a69bbef073590c44f5c8b3d Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Mon, 23 Sep 2024 13:29:05 +0600 Subject: [PATCH 55/63] Polish #3304 --- .../redis/clients/jedis/JedisCluster.java | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/main/java/redis/clients/jedis/JedisCluster.java b/src/main/java/redis/clients/jedis/JedisCluster.java index 4d6b6722724..1ba62402d77 100644 --- a/src/main/java/redis/clients/jedis/JedisCluster.java +++ b/src/main/java/redis/clients/jedis/JedisCluster.java @@ -222,6 +222,19 @@ public JedisCluster(Set clusterNodes, JedisClientConfig clientConfi Duration.ofMillis((long) clientConfig.getSocketTimeoutMillis() * maxAttempts)); } + /** + * Creates a JedisCluster with multiple entry points.
+ * You can specify the timeout and the maximum attempts.
+ * + * Additionally, you are free to provide a {@link JedisClientConfig} instance.
+ * You can use the {@link DefaultJedisClientConfig#builder()} builder pattern to customize your configuration, including socket timeouts, + * username and passwords as well as SSL related parameters. + * + * @param clusterNodes Nodes to connect to. + * @param clientConfig Client configuration parameters. + * @param maxAttempts maximum attempts for executing a command. + * @param maxTotalRetriesDuration Maximum time used for reconnecting. + */ public JedisCluster(Set clusterNodes, JedisClientConfig clientConfig, int maxAttempts, Duration maxTotalRetriesDuration) { this(new ClusterConnectionProvider(clusterNodes, clientConfig), maxAttempts, maxTotalRetriesDuration, @@ -246,19 +259,6 @@ public JedisCluster(Set clusterNodes, JedisClientConfig clientConfi maxAttempts, maxTotalRetriesDuration, clientConfig.getRedisProtocol()); } - /** - * Creates a JedisCluster with multiple entry points.
- * You can specify the timeout and the maximum attempts.
- * - * Additionally, you are free to provide a {@link JedisClientConfig} instance.
- * You can use the {@link DefaultJedisClientConfig#builder()} builder pattern to customize your configuration, including socket timeouts, - * username and passwords as well as SSL related parameters. - * - * @param clusterNodes Nodes to connect to. - * @param clientConfig Client configuration parameters. - * @param maxAttempts maximum attempts for executing a command. - * @param maxTotalRetriesDuration Maximum time used for reconnecting. - */ public JedisCluster(Set clusterNodes, JedisClientConfig clientConfig, int maxAttempts, Duration maxTotalRetriesDuration, GenericObjectPoolConfig poolConfig) { this(new ClusterConnectionProvider(clusterNodes, clientConfig, poolConfig), maxAttempts, maxTotalRetriesDuration, @@ -266,8 +266,7 @@ public JedisCluster(Set clusterNodes, JedisClientConfig clientConfi } // Uses a fetched connection to process protocol. Should be avoided if possible. - public JedisCluster(ClusterConnectionProvider provider, int maxAttempts, - Duration maxTotalRetriesDuration) { + public JedisCluster(ClusterConnectionProvider provider, int maxAttempts, Duration maxTotalRetriesDuration) { super(provider, maxAttempts, maxTotalRetriesDuration); } From 5f9c90990df9e5960c1c6324636e0a8f6b7af493 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Thu, 26 Sep 2024 10:25:49 +0100 Subject: [PATCH 56/63] DOC-4252 full-text examples (#3963) Co-authored-by: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> --- .../io/redis/examples/QueryFtExample.java | 312 ++++++++++++++++++ 1 file changed, 312 insertions(+) create mode 100644 src/test/java/io/redis/examples/QueryFtExample.java diff --git a/src/test/java/io/redis/examples/QueryFtExample.java b/src/test/java/io/redis/examples/QueryFtExample.java new file mode 100644 index 00000000000..aa00300c94e --- /dev/null +++ b/src/test/java/io/redis/examples/QueryFtExample.java @@ -0,0 +1,312 @@ +// EXAMPLE: query_ft +// REMOVE_START +package io.redis.examples; + +import org.junit.Assert; +import org.junit.Test; +// REMOVE_END +// HIDE_START +import java.util.List; +import redis.clients.jedis.UnifiedJedis; +import redis.clients.jedis.search.*; +import redis.clients.jedis.search.schemafields.*; +import redis.clients.jedis.exceptions.JedisDataException; +import redis.clients.jedis.json.Path2; +// HIDE_END + +// HIDE_START +public class QueryFtExample { + @Test + public void run() { + UnifiedJedis jedis = new UnifiedJedis("redis://localhost:6379"); + + //REMOVE_START + // Clear any keys here before using them in tests. + try {jedis.ftDropIndex("idx:bicycle");} catch (JedisDataException j){} + //REMOVE_END +// HIDE_END + + SchemaField[] schema = { + TextField.of("$.brand").as("brand"), + TextField.of("$.model").as("model"), + TextField.of("$.description").as("description"), + NumericField.of("$.price").as("price"), + TagField.of("$.condition").as("condition") + }; + + jedis.ftCreate("idx:bicycle", + FTCreateParams.createParams() + .on(IndexDataType.JSON) + .addPrefix("bicycle:"), + schema + ); + + String[] bicycleJsons = new String[] { + " {" + + " \"pickup_zone\": \"POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, " + + "-74.0610 40.6678, -74.0610 40.7578))\"," + + " \"store_location\": \"-74.0060,40.7128\"," + + " \"brand\": \"Velorim\"," + + " \"model\": \"Jigger\"," + + " \"price\": 270," + + " \"description\": \"Small and powerful, the Jigger is the best ride for the smallest of tikes! " + + "This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger " + + "is the vehicle of choice for the rare tenacious little rider raring to go.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, " + + "-118.2887 33.9872, -118.2887 34.0972))\"," + + " \"store_location\": \"-118.2437,34.0522\"," + + " \"brand\": \"Bicyk\"," + + " \"model\": \"Hillcraft\"," + + " \"price\": 1200," + + " \"description\": \"Kids want to ride with as little weight as possible. Especially " + + "on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming " + + "off a 24'' bike. The Hillcraft 26 is just the solution they need!\"," + + " \"condition\": \"used\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, " + + "-87.6848 41.8231, -87.6848 41.9331))\"," + + " \"store_location\": \"-87.6298,41.8781\"," + + " \"brand\": \"Nord\"," + + " \"model\": \"Chook air 5\"," + + " \"price\": 815," + + " \"description\": \"The Chook Air 5 gives kids aged six years and older a durable " + + "and uberlight mountain bike for their first experience on tracks and easy cruising through " + + "forests and fields. The lower top tube makes it easy to mount and dismount in any " + + "situation, giving your kids greater safety on the trails.\"," + + " \"condition\": \"used\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, " + + "-80.2433 25.6967, -80.2433 25.8067))\"," + + " \"store_location\": \"-80.1918,25.7617\"," + + " \"brand\": \"Eva\"," + + " \"model\": \"Eva 291\"," + + " \"price\": 3400," + + " \"description\": \"The sister company to Nord, Eva launched in 2005 as the first " + + "and only women-dedicated bicycle brand. Designed by women for women, allEva bikes " + + "are optimized for the feminine physique using analytics from a body metrics database. " + + "If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This " + + "full-suspension, cross-country ride has been designed for velocity. The 291 has " + + "100mm of front and rear travel, a superlight aluminum frame and fast-rolling " + + "29-inch wheels. Yippee!\"," + + " \"condition\": \"used\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, " + + "-122.4644 37.7099, -122.4644 37.8199))\"," + + " \"store_location\": \"-122.4194,37.7749\"," + + " \"brand\": \"Noka Bikes\"," + + " \"model\": \"Kahuna\"," + + " \"price\": 3200," + + " \"description\": \"Whether you want to try your hand at XC racing or are looking " + + "for a lively trail bike that's just as inspiring on the climbs as it is over rougher " + + "ground, the Wilder is one heck of a bike built specifically for short women. Both the " + + "frames and components have been tweaked to include a women’s saddle, different bars " + + "and unique colourway.\"," + + " \"condition\": \"used\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, " + + "-0.1778 51.4024, -0.1778 51.5524))\"," + + " \"store_location\": \"-0.1278,51.5074\"," + + " \"brand\": \"Breakout\"," + + " \"model\": \"XBN 2.1 Alloy\"," + + " \"price\": 810," + + " \"description\": \"The XBN 2.1 Alloy is our entry-level road bike – but that’s " + + "not to say that it’s a basic machine. With an internal weld aluminium frame, a full " + + "carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which " + + "doesn’t break the bank and delivers craved performance.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, " + + "2.1767 48.5516, 2.1767 48.9016))\"," + + " \"store_location\": \"2.3522,48.8566\"," + + " \"brand\": \"ScramBikes\"," + + " \"model\": \"WattBike\"," + + " \"price\": 2300," + + " \"description\": \"The WattBike is the best e-bike for people who still " + + "feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH " + + "Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one " + + "charge. It’s great for tackling hilly terrain or if you just fancy a more " + + "leisurely ride. With three working modes, you can choose between E-bike, " + + "assisted bicycle, and normal bike modes.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, " + + "13.3260 52.2700, 13.3260 52.5700))\"," + + " \"store_location\": \"13.4050,52.5200\"," + + " \"brand\": \"Peaknetic\"," + + " \"model\": \"Secto\"," + + " \"price\": 430," + + " \"description\": \"If you struggle with stiff fingers or a kinked neck or " + + "back after a few minutes on the road, this lightweight, aluminum bike alleviates " + + "those issues and allows you to enjoy the ride. From the ergonomic grips to the " + + "lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. " + + "The rear-inclined seat tube facilitates stability by allowing you to put a foot " + + "on the ground to balance at a stop, and the low step-over frame makes it " + + "accessible for all ability and mobility levels. The saddle is very soft, with " + + "a wide back to support your hip joints and a cutout in the center to redistribute " + + "that pressure. Rim brakes deliver satisfactory braking control, and the wide tires " + + "provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts " + + "facilitate setting up the Roll Low-Entry as your preferred commuter, and the " + + "BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, " + + "1.9450 41.1987, 1.9450 41.4301))\"," + + " \"store_location\": \"2.1734, 41.3851\"," + + " \"brand\": \"nHill\"," + + " \"model\": \"Summit\"," + + " \"price\": 1200," + + " \"description\": \"This budget mountain bike from nHill performs well both " + + "on bike paths and on the trail. The fork with 100mm of travel absorbs rough " + + "terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. " + + "The Shimano Tourney drivetrain offered enough gears for finding a comfortable " + + "pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. " + + "Whether you want an affordable bike that you can take to work, but also take " + + "trail in mountains on the weekends or you’re just after a stable, comfortable " + + "ride for the bike path, the Summit gives a good value for money.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((12.4464 42.1028, 12.5464 42.1028, " + + "12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))\"," + + " \"store_location\": \"12.4964,41.9028\"," + + " \"model\": \"ThrillCycle\"," + + " \"brand\": \"BikeShind\"," + + " \"price\": 815," + + " \"description\": \"An artsy, retro-inspired bicycle that’s as " + + "functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. " + + "A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t " + + "suggest taking it to the mountains. Fenders protect you from mud, and a rear " + + "basket lets you transport groceries, flowers and books. The ThrillCycle comes " + + "with a limited lifetime warranty, so this little guy will last you long " + + "past graduation.\"," + + " \"condition\": \"refurbished\"" + + " }" + }; + + for (int i = 0; i < bicycleJsons.length; i++) { + jedis.jsonSet("bicycle:" + i, Path2.ROOT_PATH, bicycleJsons[i]); + } + + // STEP_START ft1 + SearchResult res1 = jedis.ftSearch("idx:bicycle", "@description: kids"); + System.out.println(res1.getTotalResults()); // >>> 2 + + List docs1 = res1.getDocuments(); + + for (int i = 0; i < docs1.size(); i++) { + System.out.println(docs1.get(i).getId()); + } + // >>> bicycle:2 + // >>> bicycle:1 + // STEP_END + + // Tests for 'ft1' step. + // REMOVE_START + Assert.assertEquals(2, res1.getTotalResults()); + Assert.assertEquals("bicycle:2", docs1.get(0).getId()); + Assert.assertEquals("bicycle:1", docs1.get(1).getId()); + // REMOVE_END + + + // STEP_START ft2 + SearchResult res2 = jedis.ftSearch("idx:bicycle", "@model: ka*"); + System.out.println(res2.getTotalResults()); // >>> 1 + + List docs2 = res2.getDocuments(); + + for (int i = 0; i < docs2.size(); i++) { + System.out.println(docs2.get(i).getId()); + } + // >>> bicycle:4 + // STEP_END + + // Tests for 'ft2' step. + // REMOVE_START + Assert.assertEquals(1, res2.getTotalResults()); + Assert.assertEquals("bicycle:4", docs2.get(0).getId()); + // REMOVE_END + + + // STEP_START ft3 + SearchResult res3 = jedis.ftSearch("idx:bicycle", "@brand: *bikes"); + System.out.println(res3.getTotalResults()); // >>> 2 + + List docs3 = res3.getDocuments(); + + for (int i = 0; i < docs3.size(); i++) { + System.out.println(docs3.get(i).getId()); + } + // >>> bicycle:6 + // >>> bicycle:4 + // STEP_END + + // Tests for 'ft3' step. + // REMOVE_START + Assert.assertEquals(2, res3.getTotalResults()); + Assert.assertEquals("bicycle:6", docs3.get(0).getId()); + Assert.assertEquals("bicycle:4", docs3.get(1).getId()); + // REMOVE_END + + + // STEP_START ft4 + SearchResult res4 = jedis.ftSearch("idx:bicycle", "%optamized%"); + System.out.println(res4.getTotalResults()); // >>> 1 + + List docs4 = res4.getDocuments(); + + for (int i = 0; i < docs4.size(); i++) { + System.out.println(docs4.get(i).getId()); + } + // >>> bicycle:3 + // STEP_END + + // Tests for 'ft4' step. + // REMOVE_START + Assert.assertEquals(1, res4.getTotalResults()); + Assert.assertEquals("bicycle:3", docs4.get(0).getId()); + // REMOVE_END + + + // STEP_START ft5 + SearchResult res5 = jedis.ftSearch("idx:bicycle", "%%optamised%%"); + System.out.println(res5.getTotalResults()); // >>> 1 + + List docs5 = res5.getDocuments(); + + for (int i = 0; i < docs5.size(); i++) { + System.out.println(docs5.get(i).getId()); + } + // >>> bicycle:3 + // STEP_END + + // Tests for 'ft5' step. + // REMOVE_START + Assert.assertEquals(1, res5.getTotalResults()); + Assert.assertEquals("bicycle:3", docs5.get(0).getId()); + // REMOVE_END + +// HIDE_START + jedis.close(); + } +} +// HIDE_END + From 8094063b4dda96cd564b23dc91ba8e60811fc2c7 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:07:21 +0100 Subject: [PATCH 57/63] DOC-4202 exact match search examples (#3964) Co-authored-by: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> --- .../io/redis/examples/QueryEmExample.java | 325 ++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 src/test/java/io/redis/examples/QueryEmExample.java diff --git a/src/test/java/io/redis/examples/QueryEmExample.java b/src/test/java/io/redis/examples/QueryEmExample.java new file mode 100644 index 00000000000..bd6ab923a8d --- /dev/null +++ b/src/test/java/io/redis/examples/QueryEmExample.java @@ -0,0 +1,325 @@ +// EXAMPLE: query_em +// REMOVE_START +package io.redis.examples; + +import org.junit.Assert; +import org.junit.Test; +// REMOVE_END + +// HIDE_START +import java.util.List; +import redis.clients.jedis.UnifiedJedis; +import redis.clients.jedis.json.Path2; +import redis.clients.jedis.search.*; +import redis.clients.jedis.search.schemafields.*; +import redis.clients.jedis.exceptions.JedisDataException; + +public class QueryEmExample { + @Test + public void run() { + UnifiedJedis jedis = new UnifiedJedis("redis://localhost:6379"); + + //REMOVE_START + // Clear any keys here before using them in tests. + try {jedis.ftDropIndex("idx:bicycle");} catch (JedisDataException j){} + try {jedis.ftDropIndex("idx:email");} catch (JedisDataException j){} + //REMOVE_END + + SchemaField[] schema = { + TextField.of("$.brand").as("brand"), + TextField.of("$.model").as("model"), + TextField.of("$.description").as("description"), + NumericField.of("$.price").as("price"), + TagField.of("$.condition").as("condition") + }; + + jedis.ftCreate("idx:bicycle", + FTCreateParams.createParams() + .on(IndexDataType.JSON) + .addPrefix("bicycle:"), + schema + ); + + String[] bicycleJsons = new String[] { + " {" + + " \"pickup_zone\": \"POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, " + + "-74.0610 40.6678, -74.0610 40.7578))\"," + + " \"store_location\": \"-74.0060,40.7128\"," + + " \"brand\": \"Velorim\"," + + " \"model\": \"Jigger\"," + + " \"price\": 270," + + " \"description\": \"Small and powerful, the Jigger is the best ride for the smallest of tikes! " + + "This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger " + + "is the vehicle of choice for the rare tenacious little rider raring to go.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, " + + "-118.2887 33.9872, -118.2887 34.0972))\"," + + " \"store_location\": \"-118.2437,34.0522\"," + + " \"brand\": \"Bicyk\"," + + " \"model\": \"Hillcraft\"," + + " \"price\": 1200," + + " \"description\": \"Kids want to ride with as little weight as possible. Especially " + + "on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming " + + "off a 24'' bike. The Hillcraft 26 is just the solution they need!\"," + + " \"condition\": \"used\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, " + + "-87.6848 41.8231, -87.6848 41.9331))\"," + + " \"store_location\": \"-87.6298,41.8781\"," + + " \"brand\": \"Nord\"," + + " \"model\": \"Chook air 5\"," + + " \"price\": 815," + + " \"description\": \"The Chook Air 5 gives kids aged six years and older a durable " + + "and uberlight mountain bike for their first experience on tracks and easy cruising through " + + "forests and fields. The lower top tube makes it easy to mount and dismount in any " + + "situation, giving your kids greater safety on the trails.\"," + + " \"condition\": \"used\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, " + + "-80.2433 25.6967, -80.2433 25.8067))\"," + + " \"store_location\": \"-80.1918,25.7617\"," + + " \"brand\": \"Eva\"," + + " \"model\": \"Eva 291\"," + + " \"price\": 3400," + + " \"description\": \"The sister company to Nord, Eva launched in 2005 as the first " + + "and only women-dedicated bicycle brand. Designed by women for women, allEva bikes " + + "are optimized for the feminine physique using analytics from a body metrics database. " + + "If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This " + + "full-suspension, cross-country ride has been designed for velocity. The 291 has " + + "100mm of front and rear travel, a superlight aluminum frame and fast-rolling " + + "29-inch wheels. Yippee!\"," + + " \"condition\": \"used\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, " + + "-122.4644 37.7099, -122.4644 37.8199))\"," + + " \"store_location\": \"-122.4194,37.7749\"," + + " \"brand\": \"Noka Bikes\"," + + " \"model\": \"Kahuna\"," + + " \"price\": 3200," + + " \"description\": \"Whether you want to try your hand at XC racing or are looking " + + "for a lively trail bike that's just as inspiring on the climbs as it is over rougher " + + "ground, the Wilder is one heck of a bike built specifically for short women. Both the " + + "frames and components have been tweaked to include a women’s saddle, different bars " + + "and unique colourway.\"," + + " \"condition\": \"used\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, " + + "-0.1778 51.4024, -0.1778 51.5524))\"," + + " \"store_location\": \"-0.1278,51.5074\"," + + " \"brand\": \"Breakout\"," + + " \"model\": \"XBN 2.1 Alloy\"," + + " \"price\": 810," + + " \"description\": \"The XBN 2.1 Alloy is our entry-level road bike – but that’s " + + "not to say that it’s a basic machine. With an internal weld aluminium frame, a full " + + "carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which " + + "doesn’t break the bank and delivers craved performance.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, " + + "2.1767 48.5516, 2.1767 48.9016))\"," + + " \"store_location\": \"2.3522,48.8566\"," + + " \"brand\": \"ScramBikes\"," + + " \"model\": \"WattBike\"," + + " \"price\": 2300," + + " \"description\": \"The WattBike is the best e-bike for people who still " + + "feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH " + + "Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one " + + "charge. It’s great for tackling hilly terrain or if you just fancy a more " + + "leisurely ride. With three working modes, you can choose between E-bike, " + + "assisted bicycle, and normal bike modes.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, " + + "13.3260 52.2700, 13.3260 52.5700))\"," + + " \"store_location\": \"13.4050,52.5200\"," + + " \"brand\": \"Peaknetic\"," + + " \"model\": \"Secto\"," + + " \"price\": 430," + + " \"description\": \"If you struggle with stiff fingers or a kinked neck or " + + "back after a few minutes on the road, this lightweight, aluminum bike alleviates " + + "those issues and allows you to enjoy the ride. From the ergonomic grips to the " + + "lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. " + + "The rear-inclined seat tube facilitates stability by allowing you to put a foot " + + "on the ground to balance at a stop, and the low step-over frame makes it " + + "accessible for all ability and mobility levels. The saddle is very soft, with " + + "a wide back to support your hip joints and a cutout in the center to redistribute " + + "that pressure. Rim brakes deliver satisfactory braking control, and the wide tires " + + "provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts " + + "facilitate setting up the Roll Low-Entry as your preferred commuter, and the " + + "BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, " + + "1.9450 41.1987, 1.9450 41.4301))\"," + + " \"store_location\": \"2.1734, 41.3851\"," + + " \"brand\": \"nHill\"," + + " \"model\": \"Summit\"," + + " \"price\": 1200," + + " \"description\": \"This budget mountain bike from nHill performs well both " + + "on bike paths and on the trail. The fork with 100mm of travel absorbs rough " + + "terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. " + + "The Shimano Tourney drivetrain offered enough gears for finding a comfortable " + + "pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. " + + "Whether you want an affordable bike that you can take to work, but also take " + + "trail in mountains on the weekends or you’re just after a stable, comfortable " + + "ride for the bike path, the Summit gives a good value for money.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((12.4464 42.1028, 12.5464 42.1028, " + + "12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))\"," + + " \"store_location\": \"12.4964,41.9028\"," + + " \"model\": \"ThrillCycle\"," + + " \"brand\": \"BikeShind\"," + + " \"price\": 815," + + " \"description\": \"An artsy, retro-inspired bicycle that’s as " + + "functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. " + + "A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t " + + "suggest taking it to the mountains. Fenders protect you from mud, and a rear " + + "basket lets you transport groceries, flowers and books. The ThrillCycle comes " + + "with a limited lifetime warranty, so this little guy will last you long " + + "past graduation.\"," + + " \"condition\": \"refurbished\"" + + " }" + }; + + for (int i = 0; i < bicycleJsons.length; i++) { + jedis.jsonSet("bicycle:" + i, Path2.ROOT_PATH, bicycleJsons[i]); + } +// HIDE_END + + + // STEP_START em1 + SearchResult res1 = jedis.ftSearch("idx:bicycle", "@price:[270 270]"); + System.out.println(res1.getTotalResults()); // >>> 1 + + List docs1 = res1.getDocuments(); + + for (int i = 0; i < docs1.size(); i++) { + System.out.println(docs1.get(i).getId()); + } + // >>> bicycle:0 + + SearchResult res2 = jedis.ftSearch("idx:bicycle", + "*", + FTSearchParams.searchParams() + .filter("price", 270, 270) + ); + System.out.println(res2.getTotalResults()); // >>> 1 + + List docs2 = res2.getDocuments(); + + for (int i = 0; i < docs2.size(); i++) { + System.out.println(docs2.get(i).getId()); + } + // >>> bicycle:0 + // STEP_END + + // Tests for 'em1' step. + // REMOVE_START + Assert.assertEquals(1, res1.getTotalResults()); + Assert.assertEquals("bicycle:0", docs1.get(0).getId()); + + Assert.assertEquals(1, res2.getTotalResults()); + Assert.assertEquals("bicycle:0", docs2.get(0).getId()); + // REMOVE_END + + + // STEP_START em2 + SearchResult res3 = jedis.ftSearch("idx:bicycle", "@condition:{new}"); + System.out.println(res3.getTotalResults()); // >>> 5 + + List docs3 = res3.getDocuments(); + + for (int i = 0; i < docs3.size(); i++) { + System.out.println(docs3.get(i).getId()); + } + // >>> bicycle:5 + // >>> bicycle:0 + // >>> bicycle:6 + // >>> bicycle:7 + // >>> bicycle:8 + // STEP_END + + // Tests for 'em2' step. + // REMOVE_START + Assert.assertEquals(5, res3.getTotalResults()); + Assert.assertEquals("bicycle:5", docs3.get(0).getId()); + Assert.assertEquals("bicycle:0", docs3.get(1).getId()); + Assert.assertEquals("bicycle:6", docs3.get(2).getId()); + Assert.assertEquals("bicycle:7", docs3.get(3).getId()); + Assert.assertEquals("bicycle:8", docs3.get(4).getId()); + // REMOVE_END + + + // STEP_START em3 + SchemaField[] emailSchema = { + TextField.of("$.email").as("email") + }; + + jedis.ftCreate("idx:email", + new FTCreateParams() + .addPrefix("key:") + .on(IndexDataType.JSON), + emailSchema + ); + + jedis.jsonSet("key:1", Path2.ROOT_PATH, "{\"email\": \"test@redis.com\"}"); + + SearchResult res4 = jedis.ftSearch("idx:email", + RediSearchUtil.escapeQuery("@email{test@redis.com}"), + new FTSearchParams().dialect(2) + ); + System.out.println(res4.getTotalResults()); + // STEP_END + + // Tests for 'em3' step. + // REMOVE_START + jedis.ftDropIndex("idx:email"); + // REMOVE_END + + + // STEP_START em4 + SearchResult res5 = jedis.ftSearch("idx:bicycle", + "@description:\"rough terrain\"" + ); + System.out.println(res5.getTotalResults()); // >>> 1 + + List docs5 = res5.getDocuments(); + + for (int i = 0; i < docs5.size(); i++) { + System.out.println(docs5.get(i).getId()); + } + // >>> bicycle:8 + // STEP_END + + // Tests for 'em4' step. + // REMOVE_START + Assert.assertEquals(1, res5.getTotalResults()); + Assert.assertEquals("bicycle:8", docs5.get(0).getId()); + // REMOVE_END + +// HIDE_START + jedis.close(); + } +} +// HIDE_END + From 98a37e98ee405fcbaa270df3ae49ffeae25f7003 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:28:36 +0100 Subject: [PATCH 58/63] DOC-4296 added aggregation query examples (#3967) Co-authored-by: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> --- .../io/redis/examples/QueryAggExample.java | 353 ++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 src/test/java/io/redis/examples/QueryAggExample.java diff --git a/src/test/java/io/redis/examples/QueryAggExample.java b/src/test/java/io/redis/examples/QueryAggExample.java new file mode 100644 index 00000000000..786478e5ae2 --- /dev/null +++ b/src/test/java/io/redis/examples/QueryAggExample.java @@ -0,0 +1,353 @@ +// EXAMPLE: query_agg +// REMOVE_START +package io.redis.examples; + +import org.junit.Assert; +import org.junit.Test; +// REMOVE_END + +// HIDE_START +import java.util.List; +import java.util.ArrayList; +import redis.clients.jedis.UnifiedJedis; +import redis.clients.jedis.json.Path2; +import redis.clients.jedis.search.FTCreateParams; +import redis.clients.jedis.search.IndexDataType; +import redis.clients.jedis.search.schemafields.*; +import redis.clients.jedis.search.aggr.*; +import redis.clients.jedis.exceptions.JedisDataException; +// HIDE_END + +// HIDE_START +public class QueryAggExample { + @Test + public void run() { + UnifiedJedis jedis = new UnifiedJedis("redis://localhost:6379"); + + //REMOVE_START + // Clear any keys here before using them in tests. + try { jedis.ftDropIndex("idx:bicycle"); } catch (JedisDataException j) {} + //REMOVE_END +// HIDE_END + + SchemaField[] schema = { + TextField.of("$.brand").as("brand"), + TextField.of("$.model").as("model"), + TextField.of("$.description").as("description"), + NumericField.of("$.price").as("price"), + TagField.of("$.condition").as("condition") + }; + + jedis.ftCreate("idx:bicycle", + FTCreateParams.createParams() + .on(IndexDataType.JSON) + .addPrefix("bicycle:"), + schema + ); + + String[] bicycleJsons = new String[] { + " {" + + " \"pickup_zone\": \"POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, " + + "-74.0610 40.6678, -74.0610 40.7578))\"," + + " \"store_location\": \"-74.0060,40.7128\"," + + " \"brand\": \"Velorim\"," + + " \"model\": \"Jigger\"," + + " \"price\": 270," + + " \"description\": \"Small and powerful, the Jigger is the best ride for the smallest of tikes! " + + "This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger " + + "is the vehicle of choice for the rare tenacious little rider raring to go.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, " + + "-118.2887 33.9872, -118.2887 34.0972))\"," + + " \"store_location\": \"-118.2437,34.0522\"," + + " \"brand\": \"Bicyk\"," + + " \"model\": \"Hillcraft\"," + + " \"price\": 1200," + + " \"description\": \"Kids want to ride with as little weight as possible. Especially " + + "on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming " + + "off a 24'' bike. The Hillcraft 26 is just the solution they need!\"," + + " \"condition\": \"used\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, " + + "-87.6848 41.8231, -87.6848 41.9331))\"," + + " \"store_location\": \"-87.6298,41.8781\"," + + " \"brand\": \"Nord\"," + + " \"model\": \"Chook air 5\"," + + " \"price\": 815," + + " \"description\": \"The Chook Air 5 gives kids aged six years and older a durable " + + "and uberlight mountain bike for their first experience on tracks and easy cruising through " + + "forests and fields. The lower top tube makes it easy to mount and dismount in any " + + "situation, giving your kids greater safety on the trails.\"," + + " \"condition\": \"used\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, " + + "-80.2433 25.6967, -80.2433 25.8067))\"," + + " \"store_location\": \"-80.1918,25.7617\"," + + " \"brand\": \"Eva\"," + + " \"model\": \"Eva 291\"," + + " \"price\": 3400," + + " \"description\": \"The sister company to Nord, Eva launched in 2005 as the first " + + "and only women-dedicated bicycle brand. Designed by women for women, allEva bikes " + + "are optimized for the feminine physique using analytics from a body metrics database. " + + "If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This " + + "full-suspension, cross-country ride has been designed for velocity. The 291 has " + + "100mm of front and rear travel, a superlight aluminum frame and fast-rolling " + + "29-inch wheels. Yippee!\"," + + " \"condition\": \"used\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, " + + "-122.4644 37.7099, -122.4644 37.8199))\"," + + " \"store_location\": \"-122.4194,37.7749\"," + + " \"brand\": \"Noka Bikes\"," + + " \"model\": \"Kahuna\"," + + " \"price\": 3200," + + " \"description\": \"Whether you want to try your hand at XC racing or are looking " + + "for a lively trail bike that's just as inspiring on the climbs as it is over rougher " + + "ground, the Wilder is one heck of a bike built specifically for short women. Both the " + + "frames and components have been tweaked to include a women’s saddle, different bars " + + "and unique colourway.\"," + + " \"condition\": \"used\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, " + + "-0.1778 51.4024, -0.1778 51.5524))\"," + + " \"store_location\": \"-0.1278,51.5074\"," + + " \"brand\": \"Breakout\"," + + " \"model\": \"XBN 2.1 Alloy\"," + + " \"price\": 810," + + " \"description\": \"The XBN 2.1 Alloy is our entry-level road bike – but that’s " + + "not to say that it’s a basic machine. With an internal weld aluminium frame, a full " + + "carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which " + + "doesn’t break the bank and delivers craved performance.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, " + + "2.1767 48.5516, 2.1767 48.9016))\"," + + " \"store_location\": \"2.3522,48.8566\"," + + " \"brand\": \"ScramBikes\"," + + " \"model\": \"WattBike\"," + + " \"price\": 2300," + + " \"description\": \"The WattBike is the best e-bike for people who still " + + "feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH " + + "Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one " + + "charge. It’s great for tackling hilly terrain or if you just fancy a more " + + "leisurely ride. With three working modes, you can choose between E-bike, " + + "assisted bicycle, and normal bike modes.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, " + + "13.3260 52.2700, 13.3260 52.5700))\"," + + " \"store_location\": \"13.4050,52.5200\"," + + " \"brand\": \"Peaknetic\"," + + " \"model\": \"Secto\"," + + " \"price\": 430," + + " \"description\": \"If you struggle with stiff fingers or a kinked neck or " + + "back after a few minutes on the road, this lightweight, aluminum bike alleviates " + + "those issues and allows you to enjoy the ride. From the ergonomic grips to the " + + "lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. " + + "The rear-inclined seat tube facilitates stability by allowing you to put a foot " + + "on the ground to balance at a stop, and the low step-over frame makes it " + + "accessible for all ability and mobility levels. The saddle is very soft, with " + + "a wide back to support your hip joints and a cutout in the center to redistribute " + + "that pressure. Rim brakes deliver satisfactory braking control, and the wide tires " + + "provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts " + + "facilitate setting up the Roll Low-Entry as your preferred commuter, and the " + + "BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, " + + "1.9450 41.1987, 1.9450 41.4301))\"," + + " \"store_location\": \"2.1734, 41.3851\"," + + " \"brand\": \"nHill\"," + + " \"model\": \"Summit\"," + + " \"price\": 1200," + + " \"description\": \"This budget mountain bike from nHill performs well both " + + "on bike paths and on the trail. The fork with 100mm of travel absorbs rough " + + "terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. " + + "The Shimano Tourney drivetrain offered enough gears for finding a comfortable " + + "pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. " + + "Whether you want an affordable bike that you can take to work, but also take " + + "trail in mountains on the weekends or you’re just after a stable, comfortable " + + "ride for the bike path, the Summit gives a good value for money.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((12.4464 42.1028, 12.5464 42.1028, " + + "12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))\"," + + " \"store_location\": \"12.4964,41.9028\"," + + " \"model\": \"ThrillCycle\"," + + " \"brand\": \"BikeShind\"," + + " \"price\": 815," + + " \"description\": \"An artsy, retro-inspired bicycle that’s as " + + "functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. " + + "A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t " + + "suggest taking it to the mountains. Fenders protect you from mud, and a rear " + + "basket lets you transport groceries, flowers and books. The ThrillCycle comes " + + "with a limited lifetime warranty, so this little guy will last you long " + + "past graduation.\"," + + " \"condition\": \"refurbished\"" + + " }" + }; + + for (int i = 0; i < bicycleJsons.length; i++) { + jedis.jsonSet("bicycle:" + i, new Path2("$"), bicycleJsons[i]); + } + + // STEP_START agg1 + AggregationResult res1 = jedis.ftAggregate("idx:bicycle", + new AggregationBuilder("@condition:{new}") + .load("__key", "price") + .apply("@price - (@price * 0.1)", "discounted") + ); + + List rows1 = res1.getRows(); + System.out.println(rows1.size()); // >>> 5 + + for (int i = 0; i < rows1.size(); i++) { + System.out.println(rows1.get(i)); + } + // >>> {__key=bicycle:0, discounted=243, price=270} + // >>> {__key=bicycle:5, discounted=729, price=810} + // >>> {__key=bicycle:6, discounted=2070, price=2300} + // >>> {__key=bicycle:7, discounted=387, price=430} + // >>> {__key=bicycle:8, discounted=1080, price=1200} + // STEP_END + + // Tests for 'agg1' step. + // REMOVE_START + Assert.assertEquals(5, rows1.size()); + Assert.assertEquals("{__key=bicycle:0, discounted=243, price=270}", rows1.get(0).toString()); + Assert.assertEquals("{__key=bicycle:5, discounted=729, price=810}", rows1.get(1).toString()); + Assert.assertEquals("{__key=bicycle:6, discounted=2070, price=2300}", rows1.get(2).toString()); + Assert.assertEquals("{__key=bicycle:7, discounted=387, price=430}", rows1.get(3).toString()); + Assert.assertEquals("{__key=bicycle:8, discounted=1080, price=1200}", rows1.get(4).toString()); + // REMOVE_END + + + // STEP_START agg2 + AggregationResult res2 = jedis.ftAggregate("idx:bicycle", + new AggregationBuilder("*") + .load("price") + .apply("@price<1000", "price_category") + .groupBy("@condition", + Reducers.sum("@price_category").as("num_affordable")) + ); + + List rows2 = res2.getRows(); + System.out.println(rows2.size()); // >>> 3 + + for (int i = 0; i < rows2.size(); i++) { + System.out.println(rows2.get(i)); + } + // >>> {condition=refurbished, num_affordable=1} + // >>> {condition=used, num_affordable=1} + // >>> {condition=new, num_affordable=3} + // STEP_END + + // Tests for 'agg2' step. + // REMOVE_START + Assert.assertEquals(3, rows2.size()); + Assert.assertEquals("{condition=refurbished, num_affordable=1}", rows2.get(0).toString()); + Assert.assertEquals("{condition=used, num_affordable=1}", rows2.get(1).toString()); + Assert.assertEquals("{condition=new, num_affordable=3}", rows2.get(2).toString()); + // REMOVE_END + + + // STEP_START agg3 + AggregationResult res3 = jedis.ftAggregate("idx:bicycle", + new AggregationBuilder("*") + .apply("'bicycle'", "type") + .groupBy("@type", Reducers.count().as("num_total")) + ); + + List rows3 = res3.getRows(); + System.out.println(rows3.size()); // >>> 1 + + for (int i = 0; i < rows3.size(); i++) { + System.out.println(rows3.get(i)); + } + // >>> {type=bicycle, num_total=10} + // STEP_END + + // Tests for 'agg3' step. + // REMOVE_START + Assert.assertEquals(1, rows3.size()); + Assert.assertEquals("{type=bicycle, num_total=10}", rows3.get(0).toString()); + // REMOVE_END + + + // STEP_START agg4 + AggregationResult res4 = jedis.ftAggregate("idx:bicycle", + new AggregationBuilder("*") + .load("__key") + .groupBy("@condition", + Reducers.to_list("__key").as("bicycles")) + ); + + List rows4 = res4.getRows(); + System.out.println(rows4.size()); // >>> 3 + + for (int i = 0; i < rows4.size(); i++) { + System.out.println(rows4.get(i)); + } + // >>> {condition=refurbished, bicycles=[bicycle:9]} + // >>> {condition=used, bicycles=[bicycle:3, bicycle:4, bicycle:1, bicycle:2]} + // >>> {condition=new, bicycles=[bicycle:7, bicycle:0, bicycle:5, bicycle:6, bicycle:8]} + // STEP_END + + // Tests for 'agg4' step. + // REMOVE_START + Assert.assertEquals(3, rows4.size()); + + Row test4Row = rows4.get(0); + Assert.assertEquals("refurbished", test4Row.getString("condition")); + + ArrayList test4Bikes = (ArrayList) test4Row.get("bicycles"); + Assert.assertEquals(1, test4Bikes.size()); + Assert.assertTrue(test4Bikes.contains("bicycle:9")); + + test4Row = rows4.get(1); + Assert.assertEquals("used", test4Row.getString("condition")); + + test4Bikes = (ArrayList) test4Row.get("bicycles"); + Assert.assertEquals(4, test4Bikes.size()); + Assert.assertTrue(test4Bikes.contains("bicycle:1")); + Assert.assertTrue(test4Bikes.contains("bicycle:2")); + Assert.assertTrue(test4Bikes.contains("bicycle:3")); + Assert.assertTrue(test4Bikes.contains("bicycle:4")); + + test4Row = rows4.get(2); + Assert.assertEquals("new", test4Row.getString("condition")); + + test4Bikes = (ArrayList) test4Row.get("bicycles"); + Assert.assertEquals(5, test4Bikes.size()); + Assert.assertTrue(test4Bikes.contains("bicycle:0")); + Assert.assertTrue(test4Bikes.contains("bicycle:5")); + Assert.assertTrue(test4Bikes.contains("bicycle:6")); + Assert.assertTrue(test4Bikes.contains("bicycle:7")); + Assert.assertTrue(test4Bikes.contains("bicycle:8")); + // REMOVE_END + +// HIDE_START + jedis.close(); + } +} +// HIDE_END + From 43dd2864777f2061469b4b9de3f9924343387d13 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:13:22 +0100 Subject: [PATCH 59/63] DOC-4244 added range query examples (#3965) Co-authored-by: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> --- .../io/redis/examples/QueryRangeExample.java | 331 ++++++++++++++++++ 1 file changed, 331 insertions(+) create mode 100644 src/test/java/io/redis/examples/QueryRangeExample.java diff --git a/src/test/java/io/redis/examples/QueryRangeExample.java b/src/test/java/io/redis/examples/QueryRangeExample.java new file mode 100644 index 00000000000..5e4edf4d49f --- /dev/null +++ b/src/test/java/io/redis/examples/QueryRangeExample.java @@ -0,0 +1,331 @@ +// EXAMPLE: query_range +// REMOVE_START +package io.redis.examples; + +import org.junit.Assert; +import org.junit.Test; + +// REMOVE_END +// HIDE_START +import java.util.List; +import redis.clients.jedis.UnifiedJedis; +import redis.clients.jedis.search.*; +import redis.clients.jedis.search.schemafields.*; +import redis.clients.jedis.exceptions.JedisDataException; +import redis.clients.jedis.json.Path2; +import redis.clients.jedis.args.SortingOrder; +// HIDE_END + + + +// HIDE_START +public class QueryRangeExample { + @Test + public void run() { + UnifiedJedis jedis = new UnifiedJedis("redis://localhost:6379"); + + //REMOVE_START + // Clear any keys here before using them in tests. + try { jedis.ftDropIndex("idx:bicycle"); } catch (JedisDataException j) {} + //REMOVE_END + + SchemaField[] schema = { + TextField.of("$.brand").as("brand"), + TextField.of("$.model").as("model"), + TextField.of("$.description").as("description"), + NumericField.of("$.price").as("price"), + TagField.of("$.condition").as("condition") + }; + + jedis.ftCreate("idx:bicycle", + FTCreateParams.createParams() + .on(IndexDataType.JSON) + .addPrefix("bicycle:"), + schema + ); + + String[] bicycleJsons = new String[] { + " {" + + " \"pickup_zone\": \"POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, " + + "-74.0610 40.6678, -74.0610 40.7578))\"," + + " \"store_location\": \"-74.0060,40.7128\"," + + " \"brand\": \"Velorim\"," + + " \"model\": \"Jigger\"," + + " \"price\": 270," + + " \"description\": \"Small and powerful, the Jigger is the best ride for the smallest of tikes! " + + "This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger " + + "is the vehicle of choice for the rare tenacious little rider raring to go.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, " + + "-118.2887 33.9872, -118.2887 34.0972))\"," + + " \"store_location\": \"-118.2437,34.0522\"," + + " \"brand\": \"Bicyk\"," + + " \"model\": \"Hillcraft\"," + + " \"price\": 1200," + + " \"description\": \"Kids want to ride with as little weight as possible. Especially " + + "on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming " + + "off a 24'' bike. The Hillcraft 26 is just the solution they need!\"," + + " \"condition\": \"used\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, " + + "-87.6848 41.8231, -87.6848 41.9331))\"," + + " \"store_location\": \"-87.6298,41.8781\"," + + " \"brand\": \"Nord\"," + + " \"model\": \"Chook air 5\"," + + " \"price\": 815," + + " \"description\": \"The Chook Air 5 gives kids aged six years and older a durable " + + "and uberlight mountain bike for their first experience on tracks and easy cruising through " + + "forests and fields. The lower top tube makes it easy to mount and dismount in any " + + "situation, giving your kids greater safety on the trails.\"," + + " \"condition\": \"used\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, " + + "-80.2433 25.6967, -80.2433 25.8067))\"," + + " \"store_location\": \"-80.1918,25.7617\"," + + " \"brand\": \"Eva\"," + + " \"model\": \"Eva 291\"," + + " \"price\": 3400," + + " \"description\": \"The sister company to Nord, Eva launched in 2005 as the first " + + "and only women-dedicated bicycle brand. Designed by women for women, allEva bikes " + + "are optimized for the feminine physique using analytics from a body metrics database. " + + "If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This " + + "full-suspension, cross-country ride has been designed for velocity. The 291 has " + + "100mm of front and rear travel, a superlight aluminum frame and fast-rolling " + + "29-inch wheels. Yippee!\"," + + " \"condition\": \"used\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, " + + "-122.4644 37.7099, -122.4644 37.8199))\"," + + " \"store_location\": \"-122.4194,37.7749\"," + + " \"brand\": \"Noka Bikes\"," + + " \"model\": \"Kahuna\"," + + " \"price\": 3200," + + " \"description\": \"Whether you want to try your hand at XC racing or are looking " + + "for a lively trail bike that's just as inspiring on the climbs as it is over rougher " + + "ground, the Wilder is one heck of a bike built specifically for short women. Both the " + + "frames and components have been tweaked to include a women’s saddle, different bars " + + "and unique colourway.\"," + + " \"condition\": \"used\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, " + + "-0.1778 51.4024, -0.1778 51.5524))\"," + + " \"store_location\": \"-0.1278,51.5074\"," + + " \"brand\": \"Breakout\"," + + " \"model\": \"XBN 2.1 Alloy\"," + + " \"price\": 810," + + " \"description\": \"The XBN 2.1 Alloy is our entry-level road bike – but that’s " + + "not to say that it’s a basic machine. With an internal weld aluminium frame, a full " + + "carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which " + + "doesn’t break the bank and delivers craved performance.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, " + + "2.1767 48.5516, 2.1767 48.9016))\"," + + " \"store_location\": \"2.3522,48.8566\"," + + " \"brand\": \"ScramBikes\"," + + " \"model\": \"WattBike\"," + + " \"price\": 2300," + + " \"description\": \"The WattBike is the best e-bike for people who still " + + "feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH " + + "Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one " + + "charge. It’s great for tackling hilly terrain or if you just fancy a more " + + "leisurely ride. With three working modes, you can choose between E-bike, " + + "assisted bicycle, and normal bike modes.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, " + + "13.3260 52.2700, 13.3260 52.5700))\"," + + " \"store_location\": \"13.4050,52.5200\"," + + " \"brand\": \"Peaknetic\"," + + " \"model\": \"Secto\"," + + " \"price\": 430," + + " \"description\": \"If you struggle with stiff fingers or a kinked neck or " + + "back after a few minutes on the road, this lightweight, aluminum bike alleviates " + + "those issues and allows you to enjoy the ride. From the ergonomic grips to the " + + "lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. " + + "The rear-inclined seat tube facilitates stability by allowing you to put a foot " + + "on the ground to balance at a stop, and the low step-over frame makes it " + + "accessible for all ability and mobility levels. The saddle is very soft, with " + + "a wide back to support your hip joints and a cutout in the center to redistribute " + + "that pressure. Rim brakes deliver satisfactory braking control, and the wide tires " + + "provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts " + + "facilitate setting up the Roll Low-Entry as your preferred commuter, and the " + + "BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, " + + "1.9450 41.1987, 1.9450 41.4301))\"," + + " \"store_location\": \"2.1734, 41.3851\"," + + " \"brand\": \"nHill\"," + + " \"model\": \"Summit\"," + + " \"price\": 1200," + + " \"description\": \"This budget mountain bike from nHill performs well both " + + "on bike paths and on the trail. The fork with 100mm of travel absorbs rough " + + "terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. " + + "The Shimano Tourney drivetrain offered enough gears for finding a comfortable " + + "pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. " + + "Whether you want an affordable bike that you can take to work, but also take " + + "trail in mountains on the weekends or you’re just after a stable, comfortable " + + "ride for the bike path, the Summit gives a good value for money.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((12.4464 42.1028, 12.5464 42.1028, " + + "12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))\"," + + " \"store_location\": \"12.4964,41.9028\"," + + " \"model\": \"ThrillCycle\"," + + " \"brand\": \"BikeShind\"," + + " \"price\": 815," + + " \"description\": \"An artsy, retro-inspired bicycle that’s as " + + "functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. " + + "A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t " + + "suggest taking it to the mountains. Fenders protect you from mud, and a rear " + + "basket lets you transport groceries, flowers and books. The ThrillCycle comes " + + "with a limited lifetime warranty, so this little guy will last you long " + + "past graduation.\"," + + " \"condition\": \"refurbished\"" + + " }" + }; + + for (int i = 0; i < bicycleJsons.length; i++) { + jedis.jsonSet("bicycle:" + i, Path2.ROOT_PATH, bicycleJsons[i]); + } +// HIDE_END + + + // STEP_START range1 + SearchResult res1 = jedis.ftSearch("idx:bicycle", "@price:[500 1000]"); + System.out.println(res1.getTotalResults()); // >>> 3 + + List docs1 = res1.getDocuments(); + + for (int i = 0; i < docs1.size(); i++) { + System.out.println(docs1.get(i).getId()); + } + // >>> bicycle:2 + // >>> bicycle:5 + // >>> bicycle:9 + // STEP_END + + // Tests for 'range1' step. + // REMOVE_START + Assert.assertEquals(3, res1.getTotalResults()); + Assert.assertEquals("bicycle:2", docs1.get(0).getId()); + Assert.assertEquals("bicycle:5", docs1.get(1).getId()); + Assert.assertEquals("bicycle:9", docs1.get(2).getId()); + // REMOVE_END + + + // STEP_START range2 + SearchResult res2 = jedis.ftSearch("idx:bicycle", + "*", + FTSearchParams.searchParams() + .filter("price", 500, 1000) + ); + System.out.println(res2.getTotalResults()); // >>> 3 + + List docs2 = res2.getDocuments(); + + for (int i = 0; i < docs2.size(); i++) { + System.out.println(docs2.get(i).getId()); + } + // >>> bicycle:2 + // >>> bicycle:5 + // >>> bicycle:9 + // STEP_END + + // Tests for 'range2' step. + // REMOVE_START + Assert.assertEquals(3, res2.getTotalResults()); + Assert.assertEquals("bicycle:2", docs2.get(0).getId()); + Assert.assertEquals("bicycle:5", docs2.get(1).getId()); + Assert.assertEquals("bicycle:9", docs2.get(2).getId()); + // REMOVE_END + + + // STEP_START range3 + SearchResult res3 = jedis.ftSearch("idx:bicycle", + "*", + FTSearchParams.searchParams() + .filter("price", 1000, true, Double.POSITIVE_INFINITY, false) + ); + System.out.println(res3.getTotalResults()); // >>> 5 + + List docs3 = res3.getDocuments(); + + for (int i = 0; i < docs3.size(); i++) { + System.out.println(docs3.get(i).getId()); + } + // >>> bicycle:1 + // >>> bicycle:4 + // >>> bicycle:6 + // >>> bicycle:3 + // >>> bicycle:8 + // STEP_END + + // Tests for 'range3' step. + // REMOVE_START + Assert.assertEquals(5, res3.getTotalResults()); + Assert.assertEquals("bicycle:1", docs3.get(0).getId()); + Assert.assertEquals("bicycle:4", docs3.get(1).getId()); + Assert.assertEquals("bicycle:6", docs3.get(2).getId()); + Assert.assertEquals("bicycle:3", docs3.get(3).getId()); + Assert.assertEquals("bicycle:8", docs3.get(4).getId()); + // REMOVE_END + + + // STEP_START range4 + SearchResult res4 = jedis.ftSearch("idx:bicycle", + "@price:[-inf 2000]", + FTSearchParams.searchParams() + .sortBy("price", SortingOrder.ASC) + .limit(0, 5) + ); + System.out.println(res4.getTotalResults()); // >>> 7 + + List docs4 = res4.getDocuments(); + + for (int i = 0; i < docs4.size(); i++) { + System.out.println(docs4.get(i).getId()); + } + // >>> bicycle:0 + // >>> bicycle:7 + // >>> bicycle:5 + // >>> bicycle:2 + // >>> bicycle:9 + // STEP_END + + // Tests for 'range4' step. + // REMOVE_START + Assert.assertEquals(7, res4.getTotalResults()); + Assert.assertEquals("bicycle:0", docs4.get(0).getId()); + Assert.assertEquals("bicycle:7", docs4.get(1).getId()); + Assert.assertEquals("bicycle:5", docs4.get(2).getId()); + Assert.assertEquals("bicycle:2", docs4.get(3).getId()); + Assert.assertEquals("bicycle:9", docs4.get(4).getId()); + // REMOVE_END + +// HIDE_START + jedis.close(); + } +} +// HIDE_END + From cc8c8ce6108309405569f93370ea5839dade57ad Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:14:04 +0100 Subject: [PATCH 60/63] DOC-4262 geo query examples (#3966) Co-authored-by: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> --- .../io/redis/examples/QueryGeoExample.java | 299 ++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 src/test/java/io/redis/examples/QueryGeoExample.java diff --git a/src/test/java/io/redis/examples/QueryGeoExample.java b/src/test/java/io/redis/examples/QueryGeoExample.java new file mode 100644 index 00000000000..47b93f75796 --- /dev/null +++ b/src/test/java/io/redis/examples/QueryGeoExample.java @@ -0,0 +1,299 @@ +// EXAMPLE: query_geo +// REMOVE_START +package io.redis.examples; + +import org.junit.Assert; +import org.junit.Test; +// REMOVE_END +// HIDE_START +import java.util.List; +import redis.clients.jedis.UnifiedJedis; +import redis.clients.jedis.search.*; +import redis.clients.jedis.search.schemafields.*; +import redis.clients.jedis.search.schemafields.GeoShapeField.CoordinateSystem; +import redis.clients.jedis.exceptions.JedisDataException; +import redis.clients.jedis.json.Path2; +// HIDE_END + +// HIDE_START +public class QueryGeoExample { + @Test + public void run() { + UnifiedJedis jedis = new UnifiedJedis("redis://localhost:6379"); + + //REMOVE_START + // Clear any keys here before using them in tests. + try { jedis.ftDropIndex("idx:bicycle"); } catch (JedisDataException j) {} + //REMOVE_END +// HIDE_END + + SchemaField[] schema = { + TextField.of("$.brand").as("brand"), + TextField.of("$.model").as("model"), + TextField.of("$.description").as("description"), + NumericField.of("$.price").as("price"), + TagField.of("$.condition").as("condition"), + GeoField.of("$.store_location").as("store_location"), + GeoShapeField.of("$.pickup_zone", CoordinateSystem.FLAT).as("pickup_zone") + }; + + jedis.ftCreate("idx:bicycle", + FTCreateParams.createParams() + .on(IndexDataType.JSON) + .addPrefix("bicycle:"), + schema + ); + + String[] bicycleJsons = new String[] { + " {" + + " \"pickup_zone\": \"POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, " + + "-74.0610 40.6678, -74.0610 40.7578))\"," + + " \"store_location\": \"-74.0060,40.7128\"," + + " \"brand\": \"Velorim\"," + + " \"model\": \"Jigger\"," + + " \"price\": 270," + + " \"description\": \"Small and powerful, the Jigger is the best ride for the smallest of tikes! " + + "This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger " + + "is the vehicle of choice for the rare tenacious little rider raring to go.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, " + + "-118.2887 33.9872, -118.2887 34.0972))\"," + + " \"store_location\": \"-118.2437,34.0522\"," + + " \"brand\": \"Bicyk\"," + + " \"model\": \"Hillcraft\"," + + " \"price\": 1200," + + " \"description\": \"Kids want to ride with as little weight as possible. Especially " + + "on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming " + + "off a 24'' bike. The Hillcraft 26 is just the solution they need!\"," + + " \"condition\": \"used\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, " + + "-87.6848 41.8231, -87.6848 41.9331))\"," + + " \"store_location\": \"-87.6298,41.8781\"," + + " \"brand\": \"Nord\"," + + " \"model\": \"Chook air 5\"," + + " \"price\": 815," + + " \"description\": \"The Chook Air 5 gives kids aged six years and older a durable " + + "and uberlight mountain bike for their first experience on tracks and easy cruising through " + + "forests and fields. The lower top tube makes it easy to mount and dismount in any " + + "situation, giving your kids greater safety on the trails.\"," + + " \"condition\": \"used\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, " + + "-80.2433 25.6967, -80.2433 25.8067))\"," + + " \"store_location\": \"-80.1918,25.7617\"," + + " \"brand\": \"Eva\"," + + " \"model\": \"Eva 291\"," + + " \"price\": 3400," + + " \"description\": \"The sister company to Nord, Eva launched in 2005 as the first " + + "and only women-dedicated bicycle brand. Designed by women for women, allEva bikes " + + "are optimized for the feminine physique using analytics from a body metrics database. " + + "If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This " + + "full-suspension, cross-country ride has been designed for velocity. The 291 has " + + "100mm of front and rear travel, a superlight aluminum frame and fast-rolling " + + "29-inch wheels. Yippee!\"," + + " \"condition\": \"used\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, " + + "-122.4644 37.7099, -122.4644 37.8199))\"," + + " \"store_location\": \"-122.4194,37.7749\"," + + " \"brand\": \"Noka Bikes\"," + + " \"model\": \"Kahuna\"," + + " \"price\": 3200," + + " \"description\": \"Whether you want to try your hand at XC racing or are looking " + + "for a lively trail bike that's just as inspiring on the climbs as it is over rougher " + + "ground, the Wilder is one heck of a bike built specifically for short women. Both the " + + "frames and components have been tweaked to include a women’s saddle, different bars " + + "and unique colourway.\"," + + " \"condition\": \"used\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, " + + "-0.1778 51.4024, -0.1778 51.5524))\"," + + " \"store_location\": \"-0.1278,51.5074\"," + + " \"brand\": \"Breakout\"," + + " \"model\": \"XBN 2.1 Alloy\"," + + " \"price\": 810," + + " \"description\": \"The XBN 2.1 Alloy is our entry-level road bike – but that’s " + + "not to say that it’s a basic machine. With an internal weld aluminium frame, a full " + + "carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which " + + "doesn’t break the bank and delivers craved performance.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, " + + "2.1767 48.5516, 2.1767 48.9016))\"," + + " \"store_location\": \"2.3522,48.8566\"," + + " \"brand\": \"ScramBikes\"," + + " \"model\": \"WattBike\"," + + " \"price\": 2300," + + " \"description\": \"The WattBike is the best e-bike for people who still " + + "feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH " + + "Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one " + + "charge. It’s great for tackling hilly terrain or if you just fancy a more " + + "leisurely ride. With three working modes, you can choose between E-bike, " + + "assisted bicycle, and normal bike modes.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, " + + "13.3260 52.2700, 13.3260 52.5700))\"," + + " \"store_location\": \"13.4050,52.5200\"," + + " \"brand\": \"Peaknetic\"," + + " \"model\": \"Secto\"," + + " \"price\": 430," + + " \"description\": \"If you struggle with stiff fingers or a kinked neck or " + + "back after a few minutes on the road, this lightweight, aluminum bike alleviates " + + "those issues and allows you to enjoy the ride. From the ergonomic grips to the " + + "lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. " + + "The rear-inclined seat tube facilitates stability by allowing you to put a foot " + + "on the ground to balance at a stop, and the low step-over frame makes it " + + "accessible for all ability and mobility levels. The saddle is very soft, with " + + "a wide back to support your hip joints and a cutout in the center to redistribute " + + "that pressure. Rim brakes deliver satisfactory braking control, and the wide tires " + + "provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts " + + "facilitate setting up the Roll Low-Entry as your preferred commuter, and the " + + "BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, " + + "1.9450 41.1987, 1.9450 41.4301))\"," + + " \"store_location\": \"2.1734, 41.3851\"," + + " \"brand\": \"nHill\"," + + " \"model\": \"Summit\"," + + " \"price\": 1200," + + " \"description\": \"This budget mountain bike from nHill performs well both " + + "on bike paths and on the trail. The fork with 100mm of travel absorbs rough " + + "terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. " + + "The Shimano Tourney drivetrain offered enough gears for finding a comfortable " + + "pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. " + + "Whether you want an affordable bike that you can take to work, but also take " + + "trail in mountains on the weekends or you’re just after a stable, comfortable " + + "ride for the bike path, the Summit gives a good value for money.\"," + + " \"condition\": \"new\"" + + " }", + + " {" + + " \"pickup_zone\": \"POLYGON((12.4464 42.1028, 12.5464 42.1028, " + + "12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))\"," + + " \"store_location\": \"12.4964,41.9028\"," + + " \"model\": \"ThrillCycle\"," + + " \"brand\": \"BikeShind\"," + + " \"price\": 815," + + " \"description\": \"An artsy, retro-inspired bicycle that’s as " + + "functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. " + + "A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t " + + "suggest taking it to the mountains. Fenders protect you from mud, and a rear " + + "basket lets you transport groceries, flowers and books. The ThrillCycle comes " + + "with a limited lifetime warranty, so this little guy will last you long " + + "past graduation.\"," + + " \"condition\": \"refurbished\"" + + " }" + }; + + for (int i = 0; i < bicycleJsons.length; i++) { + jedis.jsonSet("bicycle:" + i, Path2.ROOT_PATH, bicycleJsons[i]); + } + + // STEP_START geo1 + SearchResult res1 = jedis.ftSearch("idx:bicycle", + "@store_location:[$lon $lat $radius $units]", + FTSearchParams.searchParams() + .addParam("lon", -0.1778) + .addParam("lat", 51.5524) + .addParam("radius", 20) + .addParam("units", "mi") + .dialect(2) + ); + System.out.println(res1.getTotalResults()); // >>> 1 + + List docs1 = res1.getDocuments(); + + for (int i = 0; i < docs1.size(); i++) { + System.out.println(docs1.get(i).getId()); + } + // >>> bicycle:5 + // STEP_END + + // Tests for 'geo1' step. + // REMOVE_START + Assert.assertEquals(1, res1.getTotalResults()); + Assert.assertEquals("bicycle:5", docs1.get(0).getId()); + // REMOVE_END + + + // STEP_START geo2 + SearchResult res2 = jedis.ftSearch("idx:bicycle", + "@pickup_zone:[CONTAINS $bike]", + FTSearchParams.searchParams() + .addParam("bike", "POINT(-0.1278 51.5074)") + .dialect(3) + ); + System.out.println(res2.getTotalResults()); // >>> 1 + + List docs2 = res2.getDocuments(); + + for (int i = 0; i < docs2.size(); i++) { + System.out.println(docs2.get(i).getId()); + } + // >>> bicycle:5 + // STEP_END + + // Tests for 'geo2' step. + // REMOVE_START + Assert.assertEquals(1, res2.getTotalResults()); + Assert.assertEquals("bicycle:5", docs2.get(0).getId()); + // REMOVE_END + + + // STEP_START geo3 + SearchResult res3 = jedis.ftSearch("idx:bicycle", + "@pickup_zone:[WITHIN $europe]", + FTSearchParams.searchParams() + .addParam("europe", "POLYGON((-25 35, 40 35, 40 70, -25 70, -25 35))") + .dialect(3) + ); + System.out.println(res3.getTotalResults()); // >>> 5 + + List docs3 = res3.getDocuments(); + + for (int i = 0; i < docs3.size(); i++) { + System.out.println(docs3.get(i).getId()); + } + // >>> bicycle:5 + // >>> bicycle:6 + // >>> bicycle:7 + // >>> bicycle:8 + // >>> bicycle:9 + // STEP_END + + // Tests for 'geo3' step. + // REMOVE_START + Assert.assertEquals(5, res3.getTotalResults()); + Assert.assertEquals("bicycle:5", docs3.get(0).getId()); + Assert.assertEquals("bicycle:6", docs3.get(1).getId()); + Assert.assertEquals("bicycle:7", docs3.get(2).getId()); + Assert.assertEquals("bicycle:8", docs3.get(3).getId()); + Assert.assertEquals("bicycle:9", docs3.get(4).getId()); + // REMOVE_END + +// HIDE_START + jedis.close(); + } +} +// HIDE_END + From 251831ccd15ca7ac300f08b2a36c0a903ef00680 Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Thu, 26 Sep 2024 22:06:48 +0600 Subject: [PATCH 61/63] Deprecate Triggers and Functions feature (#3968) * Use redis:8.0-M01 image * Deprecate TFUNCTION commands * Deprecate TFCALL and TFCALLASYNC commands * Deprecate EVERYTHING about Gears module * Revert "Use redis:8.0-M01 image" --- .../redis/clients/jedis/CommandObjects.java | 5 +++++ .../redis/clients/jedis/UnifiedJedis.java | 5 +++++ .../jedis/gears/RedisGearsCommands.java | 15 +++++++------- .../jedis/gears/RedisGearsProtocol.java | 9 ++++++--- .../jedis/gears/TFunctionListParams.java | 1 + .../jedis/gears/TFunctionLoadParams.java | 1 + .../jedis/gears/resps/FunctionInfo.java | 2 ++ .../jedis/gears/resps/FunctionStreamInfo.java | 2 ++ .../jedis/gears/resps/GearsLibraryInfo.java | 3 +++ .../jedis/gears/resps/StreamTriggerInfo.java | 2 ++ .../jedis/gears/resps/TriggerInfo.java | 2 ++ ...jectsTriggersAndFunctionsCommandsTest.java | 2 ++ ...JedisTriggersAndFunctionsCommandsTest.java | 3 ++- .../jedis/modules/gears/GearsTest.java | 20 ++++++++++--------- 14 files changed, 52 insertions(+), 20 deletions(-) diff --git a/src/main/java/redis/clients/jedis/CommandObjects.java b/src/main/java/redis/clients/jedis/CommandObjects.java index 89eb3e16ca9..5a66f803a85 100644 --- a/src/main/java/redis/clients/jedis/CommandObjects.java +++ b/src/main/java/redis/clients/jedis/CommandObjects.java @@ -4414,26 +4414,31 @@ public final CommandObject> graphConfigGet(String configName // RedisGraph commands // RedisGears commands + @Deprecated public final CommandObject tFunctionLoad(String libraryCode, TFunctionLoadParams params) { return new CommandObject<>(commandArguments(GearsCommand.TFUNCTION).add(GearsKeyword.LOAD) .addParams(params).add(libraryCode), BuilderFactory.STRING); } + @Deprecated public final CommandObject tFunctionDelete(String libraryName) { return new CommandObject<>(commandArguments(GearsCommand.TFUNCTION).add(GearsKeyword.DELETE) .add(libraryName), BuilderFactory.STRING); } + @Deprecated public final CommandObject> tFunctionList(TFunctionListParams params) { return new CommandObject<>(commandArguments(GearsCommand.TFUNCTION).add(GearsKeyword.LIST) .addParams(params), GearsLibraryInfo.GEARS_LIBRARY_INFO_LIST); } + @Deprecated public final CommandObject tFunctionCall(String library, String function, List keys, List args) { return new CommandObject<>(commandArguments(GearsCommand.TFCALL).add(library + "." + function) .add(keys.size()).keys(keys).addObjects(args), BuilderFactory.AGGRESSIVE_ENCODED_OBJECT); } + @Deprecated public final CommandObject tFunctionCallAsync(String library, String function, List keys, List args) { return new CommandObject<>(commandArguments(GearsCommand.TFCALLASYNC).add(library + "." + function) .add(keys.size()).keys(keys).addObjects(args), BuilderFactory.AGGRESSIVE_ENCODED_OBJECT); diff --git a/src/main/java/redis/clients/jedis/UnifiedJedis.java b/src/main/java/redis/clients/jedis/UnifiedJedis.java index e3bc4aba50a..50d18351d78 100644 --- a/src/main/java/redis/clients/jedis/UnifiedJedis.java +++ b/src/main/java/redis/clients/jedis/UnifiedJedis.java @@ -4984,26 +4984,31 @@ public Map graphConfigGet(String configName) { // RedisGraph commands // RedisGears commands + @Deprecated @Override public String tFunctionLoad(String libraryCode, TFunctionLoadParams params) { return executeCommand(commandObjects.tFunctionLoad(libraryCode, params)); } + @Deprecated @Override public String tFunctionDelete(String libraryName) { return executeCommand(commandObjects.tFunctionDelete(libraryName)); } + @Deprecated @Override public List tFunctionList(TFunctionListParams params) { return executeCommand(commandObjects.tFunctionList(params)); } + @Deprecated @Override public Object tFunctionCall(String library, String function, List keys, List args) { return executeCommand(commandObjects.tFunctionCall(library, function, keys, args)); } + @Deprecated @Override public Object tFunctionCallAsync(String library, String function, List keys, List args) { return executeCommand(commandObjects.tFunctionCallAsync(library, function, keys, args)); diff --git a/src/main/java/redis/clients/jedis/gears/RedisGearsCommands.java b/src/main/java/redis/clients/jedis/gears/RedisGearsCommands.java index f5adb42c9e1..e196835ab01 100644 --- a/src/main/java/redis/clients/jedis/gears/RedisGearsCommands.java +++ b/src/main/java/redis/clients/jedis/gears/RedisGearsCommands.java @@ -4,23 +4,24 @@ import java.util.List; +@Deprecated public interface RedisGearsCommands { - default String tFunctionLoad(String libraryCode) { + @Deprecated default String tFunctionLoad(String libraryCode) { return tFunctionLoad(libraryCode, TFunctionLoadParams.loadParams()); } - String tFunctionLoad(String libraryCode, TFunctionLoadParams params); + @Deprecated String tFunctionLoad(String libraryCode, TFunctionLoadParams params); - default List tFunctionList() { + @Deprecated default List tFunctionList() { return tFunctionList(TFunctionListParams.listParams()); } - List tFunctionList(TFunctionListParams params); + @Deprecated List tFunctionList(TFunctionListParams params); - String tFunctionDelete(String libraryName); + @Deprecated String tFunctionDelete(String libraryName); - Object tFunctionCall(String library, String function, List keys, List args); + @Deprecated Object tFunctionCall(String library, String function, List keys, List args); - Object tFunctionCallAsync(String library, String function, List keys, List args); + @Deprecated Object tFunctionCallAsync(String library, String function, List keys, List args); } diff --git a/src/main/java/redis/clients/jedis/gears/RedisGearsProtocol.java b/src/main/java/redis/clients/jedis/gears/RedisGearsProtocol.java index fc43f29e669..dfb73b04a99 100644 --- a/src/main/java/redis/clients/jedis/gears/RedisGearsProtocol.java +++ b/src/main/java/redis/clients/jedis/gears/RedisGearsProtocol.java @@ -4,13 +4,15 @@ import redis.clients.jedis.commands.ProtocolCommand; import redis.clients.jedis.util.SafeEncoder; +@Deprecated public class RedisGearsProtocol { + @Deprecated public enum GearsCommand implements ProtocolCommand { - TFUNCTION, - TFCALL, - TFCALLASYNC; + @Deprecated TFUNCTION, + @Deprecated TFCALL, + @Deprecated TFCALLASYNC; private final byte[] raw; @@ -24,6 +26,7 @@ public byte[] getRaw() { } } + @Deprecated public enum GearsKeyword implements Rawable { CONFIG, diff --git a/src/main/java/redis/clients/jedis/gears/TFunctionListParams.java b/src/main/java/redis/clients/jedis/gears/TFunctionListParams.java index d3f867e92b4..e32bb436397 100644 --- a/src/main/java/redis/clients/jedis/gears/TFunctionListParams.java +++ b/src/main/java/redis/clients/jedis/gears/TFunctionListParams.java @@ -6,6 +6,7 @@ import java.util.Collections; +@Deprecated public class TFunctionListParams implements IParams { private boolean withCode = false; private int verbose; diff --git a/src/main/java/redis/clients/jedis/gears/TFunctionLoadParams.java b/src/main/java/redis/clients/jedis/gears/TFunctionLoadParams.java index d155727ec5c..b8b0363b6ed 100644 --- a/src/main/java/redis/clients/jedis/gears/TFunctionLoadParams.java +++ b/src/main/java/redis/clients/jedis/gears/TFunctionLoadParams.java @@ -4,6 +4,7 @@ import redis.clients.jedis.gears.RedisGearsProtocol.GearsKeyword; import redis.clients.jedis.params.IParams; +@Deprecated public class TFunctionLoadParams implements IParams { private boolean replace = false; private String config; diff --git a/src/main/java/redis/clients/jedis/gears/resps/FunctionInfo.java b/src/main/java/redis/clients/jedis/gears/resps/FunctionInfo.java index ccb8785812b..24d47b0e4eb 100644 --- a/src/main/java/redis/clients/jedis/gears/resps/FunctionInfo.java +++ b/src/main/java/redis/clients/jedis/gears/resps/FunctionInfo.java @@ -10,6 +10,7 @@ import static redis.clients.jedis.BuilderFactory.*; +@Deprecated public class FunctionInfo { private final String name; private final String description; @@ -40,6 +41,7 @@ public FunctionInfo(String name, String description, boolean isAsync, List> FUNCTION_INFO_LIST = new Builder>() { @Override public List build(Object data) { diff --git a/src/main/java/redis/clients/jedis/gears/resps/FunctionStreamInfo.java b/src/main/java/redis/clients/jedis/gears/resps/FunctionStreamInfo.java index f4b607d6a32..577a0992fc6 100644 --- a/src/main/java/redis/clients/jedis/gears/resps/FunctionStreamInfo.java +++ b/src/main/java/redis/clients/jedis/gears/resps/FunctionStreamInfo.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.stream.Collectors; +@Deprecated public class FunctionStreamInfo { private final String name; private final String idToReadFrom; @@ -67,6 +68,7 @@ public FunctionStreamInfo(String name, String idToReadFrom, String lastError, this.pendingIds = pendingIds; } + @Deprecated public static final Builder> STREAM_INFO_LIST = new Builder>() { @Override public List build(Object data) { diff --git a/src/main/java/redis/clients/jedis/gears/resps/GearsLibraryInfo.java b/src/main/java/redis/clients/jedis/gears/resps/GearsLibraryInfo.java index 5ed6515ff68..d16a607cd2f 100644 --- a/src/main/java/redis/clients/jedis/gears/resps/GearsLibraryInfo.java +++ b/src/main/java/redis/clients/jedis/gears/resps/GearsLibraryInfo.java @@ -12,6 +12,7 @@ import static redis.clients.jedis.gears.resps.StreamTriggerInfo.STREAM_TRIGGER_INFO_LIST; import static redis.clients.jedis.gears.resps.TriggerInfo.KEYSPACE_TRIGGER_INFO_LIST; +@Deprecated public class GearsLibraryInfo { private final String apiVersion; private final List clusterFunctions; @@ -90,6 +91,7 @@ public String getUser() { return user; } + @Deprecated public static final Builder GEARS_LIBRARY_INFO = new Builder() { @Override public GearsLibraryInfo build(Object data) { @@ -171,6 +173,7 @@ public GearsLibraryInfo build(Object data) { } }; + @Deprecated public static final Builder> GEARS_LIBRARY_INFO_LIST = new Builder>() { @Override public List build(Object data) { diff --git a/src/main/java/redis/clients/jedis/gears/resps/StreamTriggerInfo.java b/src/main/java/redis/clients/jedis/gears/resps/StreamTriggerInfo.java index be526e0e714..202d4bba4fb 100644 --- a/src/main/java/redis/clients/jedis/gears/resps/StreamTriggerInfo.java +++ b/src/main/java/redis/clients/jedis/gears/resps/StreamTriggerInfo.java @@ -11,6 +11,7 @@ import static redis.clients.jedis.BuilderFactory.*; import static redis.clients.jedis.gears.resps.FunctionStreamInfo.STREAM_INFO_LIST; +@Deprecated public class StreamTriggerInfo { private final String name; private final String description; @@ -60,6 +61,7 @@ public StreamTriggerInfo(String name, String description, String prefix, this(name, description, prefix, window, trim, Collections.emptyList()); } + @Deprecated public static final Builder> STREAM_TRIGGER_INFO_LIST = new Builder>() { @Override public List build(Object data) { diff --git a/src/main/java/redis/clients/jedis/gears/resps/TriggerInfo.java b/src/main/java/redis/clients/jedis/gears/resps/TriggerInfo.java index 8a5b4704842..775af5cecd4 100644 --- a/src/main/java/redis/clients/jedis/gears/resps/TriggerInfo.java +++ b/src/main/java/redis/clients/jedis/gears/resps/TriggerInfo.java @@ -11,6 +11,7 @@ import static redis.clients.jedis.BuilderFactory.LONG; import static redis.clients.jedis.BuilderFactory.STRING; +@Deprecated public class TriggerInfo { private final String name; private final String description; @@ -80,6 +81,7 @@ public TriggerInfo(String name, String description, String lastError, long numFi this.totalExecutionTime = totalExecutionTime; } + @Deprecated public static final Builder> KEYSPACE_TRIGGER_INFO_LIST = new Builder>() { @Override public List build(Object data) { diff --git a/src/test/java/redis/clients/jedis/commands/commandobjects/CommandObjectsTriggersAndFunctionsCommandsTest.java b/src/test/java/redis/clients/jedis/commands/commandobjects/CommandObjectsTriggersAndFunctionsCommandsTest.java index 6403f57404c..ce198f46271 100644 --- a/src/test/java/redis/clients/jedis/commands/commandobjects/CommandObjectsTriggersAndFunctionsCommandsTest.java +++ b/src/test/java/redis/clients/jedis/commands/commandobjects/CommandObjectsTriggersAndFunctionsCommandsTest.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.List; import org.junit.After; +import org.junit.Ignore; import org.junit.Test; import redis.clients.jedis.RedisProtocol; @@ -19,6 +20,7 @@ /** * Tests related to Triggers and functions commands. */ +@Ignore public class CommandObjectsTriggersAndFunctionsCommandsTest extends CommandObjectsModulesTestBase { public CommandObjectsTriggersAndFunctionsCommandsTest(RedisProtocol protocol) { diff --git a/src/test/java/redis/clients/jedis/mocked/unified/UnifiedJedisTriggersAndFunctionsCommandsTest.java b/src/test/java/redis/clients/jedis/mocked/unified/UnifiedJedisTriggersAndFunctionsCommandsTest.java index 5dec794c65a..62fd7bf7cd6 100644 --- a/src/test/java/redis/clients/jedis/mocked/unified/UnifiedJedisTriggersAndFunctionsCommandsTest.java +++ b/src/test/java/redis/clients/jedis/mocked/unified/UnifiedJedisTriggersAndFunctionsCommandsTest.java @@ -9,12 +9,13 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; - +import org.junit.Ignore; import org.junit.Test; import redis.clients.jedis.gears.TFunctionListParams; import redis.clients.jedis.gears.TFunctionLoadParams; import redis.clients.jedis.gears.resps.GearsLibraryInfo; +@Ignore public class UnifiedJedisTriggersAndFunctionsCommandsTest extends UnifiedJedisMockedTestBase { @Test diff --git a/src/test/java/redis/clients/jedis/modules/gears/GearsTest.java b/src/test/java/redis/clients/jedis/modules/gears/GearsTest.java index 117094b9b8d..e32e5262795 100644 --- a/src/test/java/redis/clients/jedis/modules/gears/GearsTest.java +++ b/src/test/java/redis/clients/jedis/modules/gears/GearsTest.java @@ -1,9 +1,19 @@ package redis.clients.jedis.modules.gears; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + import org.hamcrest.Matchers; import org.junit.After; import org.junit.Assert; import org.junit.BeforeClass; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -15,15 +25,6 @@ import redis.clients.jedis.modules.RedisModuleCommandsTestBase; import redis.clients.jedis.gears.resps.GearsLibraryInfo; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.*; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; - import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -31,6 +32,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +@Ignore @RunWith(Parameterized.class) public class GearsTest extends RedisModuleCommandsTestBase { From d22df4b0f350f243b2e7713caac50cb33a1a1030 Mon Sep 17 00:00:00 2001 From: Igor Malinovskiy Date: Fri, 27 Sep 2024 14:20:38 +0200 Subject: [PATCH 62/63] Improve QueryRangeExample and QueryGeoExample (#3970) * Improve QueryRangeExample - Do not rely on random order of unsorted search results - Print prices for clarity * Fix assertion in QueryGeoExample --- .../io/redis/examples/QueryGeoExample.java | 22 ++--- .../io/redis/examples/QueryRangeExample.java | 86 +++++++++++-------- 2 files changed, 59 insertions(+), 49 deletions(-) diff --git a/src/test/java/io/redis/examples/QueryGeoExample.java b/src/test/java/io/redis/examples/QueryGeoExample.java index 47b93f75796..1f034b43f2a 100644 --- a/src/test/java/io/redis/examples/QueryGeoExample.java +++ b/src/test/java/io/redis/examples/QueryGeoExample.java @@ -7,6 +7,8 @@ // REMOVE_END // HIDE_START import java.util.List; +import java.util.stream.Stream; + import redis.clients.jedis.UnifiedJedis; import redis.clients.jedis.search.*; import redis.clients.jedis.search.schemafields.*; @@ -223,8 +225,8 @@ public void run() { List docs1 = res1.getDocuments(); - for (int i = 0; i < docs1.size(); i++) { - System.out.println(docs1.get(i).getId()); + for (Document document : docs1) { + System.out.println(document.getId()); } // >>> bicycle:5 // STEP_END @@ -247,8 +249,8 @@ public void run() { List docs2 = res2.getDocuments(); - for (int i = 0; i < docs2.size(); i++) { - System.out.println(docs2.get(i).getId()); + for (Document document : docs2) { + System.out.println(document.getId()); } // >>> bicycle:5 // STEP_END @@ -271,8 +273,8 @@ public void run() { List docs3 = res3.getDocuments(); - for (int i = 0; i < docs3.size(); i++) { - System.out.println(docs3.get(i).getId()); + for (Document document : docs3) { + System.out.println(document.getId()); } // >>> bicycle:5 // >>> bicycle:6 @@ -284,11 +286,9 @@ public void run() { // Tests for 'geo3' step. // REMOVE_START Assert.assertEquals(5, res3.getTotalResults()); - Assert.assertEquals("bicycle:5", docs3.get(0).getId()); - Assert.assertEquals("bicycle:6", docs3.get(1).getId()); - Assert.assertEquals("bicycle:7", docs3.get(2).getId()); - Assert.assertEquals("bicycle:8", docs3.get(3).getId()); - Assert.assertEquals("bicycle:9", docs3.get(4).getId()); + Assert.assertArrayEquals( + Stream.of("bicycle:5", "bicycle:6", "bicycle:7", "bicycle:8", "bicycle:9").sorted() + .toArray(), docs3.stream().map(Document::getId).sorted().toArray()); // REMOVE_END // HIDE_START diff --git a/src/test/java/io/redis/examples/QueryRangeExample.java b/src/test/java/io/redis/examples/QueryRangeExample.java index 5e4edf4d49f..46554e089d5 100644 --- a/src/test/java/io/redis/examples/QueryRangeExample.java +++ b/src/test/java/io/redis/examples/QueryRangeExample.java @@ -8,6 +8,10 @@ // REMOVE_END // HIDE_START import java.util.List; +// REMOVE_START +import java.util.stream.Stream; +// REMOVE_END + import redis.clients.jedis.UnifiedJedis; import redis.clients.jedis.search.*; import redis.clients.jedis.search.schemafields.*; @@ -212,25 +216,28 @@ public void run() { // STEP_START range1 - SearchResult res1 = jedis.ftSearch("idx:bicycle", "@price:[500 1000]"); + SearchResult res1 = jedis.ftSearch( + "idx:bicycle", "@price:[500 1000]", + FTSearchParams.searchParams().returnFields("price")); System.out.println(res1.getTotalResults()); // >>> 3 List docs1 = res1.getDocuments(); - for (int i = 0; i < docs1.size(); i++) { - System.out.println(docs1.get(i).getId()); + for (Document document : docs1) { + System.out.println(document.getId() + " : price " + document.getString("price")); } - // >>> bicycle:2 - // >>> bicycle:5 - // >>> bicycle:9 + // >>> bicycle:2 : price 815 + // >>> bicycle:5 : price 810 + // >>> bicycle:9 : price 815 // STEP_END // Tests for 'range1' step. // REMOVE_START Assert.assertEquals(3, res1.getTotalResults()); - Assert.assertEquals("bicycle:2", docs1.get(0).getId()); - Assert.assertEquals("bicycle:5", docs1.get(1).getId()); - Assert.assertEquals("bicycle:9", docs1.get(2).getId()); + Assert.assertArrayEquals( + Stream.of("bicycle:5", "bicycle:9", "bicycle:2").sorted().toArray(), + docs1.stream().map(Document::getId).sorted().toArray() + ); // REMOVE_END @@ -238,26 +245,28 @@ public void run() { SearchResult res2 = jedis.ftSearch("idx:bicycle", "*", FTSearchParams.searchParams() - .filter("price", 500, 1000) + .returnFields("price") + .filter("price", 500, 1000) ); System.out.println(res2.getTotalResults()); // >>> 3 List docs2 = res2.getDocuments(); - for (int i = 0; i < docs2.size(); i++) { - System.out.println(docs2.get(i).getId()); + for (Document document : docs2) { + System.out.println(document.getId() + " : price " + document.getString("price")); } - // >>> bicycle:2 - // >>> bicycle:5 - // >>> bicycle:9 + // >>> bicycle:2 : price 815 + // >>> bicycle:5 : price 810 + // >>> bicycle:9 : price 815 // STEP_END // Tests for 'range2' step. // REMOVE_START Assert.assertEquals(3, res2.getTotalResults()); - Assert.assertEquals("bicycle:2", docs2.get(0).getId()); - Assert.assertEquals("bicycle:5", docs2.get(1).getId()); - Assert.assertEquals("bicycle:9", docs2.get(2).getId()); + Assert.assertArrayEquals( + Stream.of("bicycle:5", "bicycle:9", "bicycle:2").sorted().toArray(), + docs2.stream().map(Document::getId).sorted().toArray() + ); // REMOVE_END @@ -265,30 +274,30 @@ public void run() { SearchResult res3 = jedis.ftSearch("idx:bicycle", "*", FTSearchParams.searchParams() - .filter("price", 1000, true, Double.POSITIVE_INFINITY, false) + .returnFields("price") + .filter("price", 1000, true, Double.POSITIVE_INFINITY, false) ); System.out.println(res3.getTotalResults()); // >>> 5 List docs3 = res3.getDocuments(); - for (int i = 0; i < docs3.size(); i++) { - System.out.println(docs3.get(i).getId()); + for (Document document : docs3) { + System.out.println(document.getId() + " : price " + document.getString("price")); } - // >>> bicycle:1 - // >>> bicycle:4 - // >>> bicycle:6 - // >>> bicycle:3 - // >>> bicycle:8 + // >>> bicycle:1 : price 1200 + // >>> bicycle:4 : price 3200 + // >>> bicycle:6 : price 2300 + // >>> bicycle:3 : price 3400 + // >>> bicycle:8 : price 1200 // STEP_END // Tests for 'range3' step. // REMOVE_START Assert.assertEquals(5, res3.getTotalResults()); - Assert.assertEquals("bicycle:1", docs3.get(0).getId()); - Assert.assertEquals("bicycle:4", docs3.get(1).getId()); - Assert.assertEquals("bicycle:6", docs3.get(2).getId()); - Assert.assertEquals("bicycle:3", docs3.get(3).getId()); - Assert.assertEquals("bicycle:8", docs3.get(4).getId()); + Assert.assertArrayEquals( + Stream.of("bicycle:1", "bicycle:4", "bicycle:6", "bicycle:3", "bicycle:8").sorted() + .toArray(), + docs3.stream().map(Document::getId).sorted().toArray()); // REMOVE_END @@ -296,6 +305,7 @@ public void run() { SearchResult res4 = jedis.ftSearch("idx:bicycle", "@price:[-inf 2000]", FTSearchParams.searchParams() + .returnFields("price") .sortBy("price", SortingOrder.ASC) .limit(0, 5) ); @@ -303,14 +313,14 @@ public void run() { List docs4 = res4.getDocuments(); - for (int i = 0; i < docs4.size(); i++) { - System.out.println(docs4.get(i).getId()); + for (Document document : docs4) { + System.out.println(document.getId() + " : price " + document.getString("price")); } - // >>> bicycle:0 - // >>> bicycle:7 - // >>> bicycle:5 - // >>> bicycle:2 - // >>> bicycle:9 + // >>> bicycle:0 : price 270 + // >>> bicycle:7 : price 430 + // >>> bicycle:5 : price 810 + // >>> bicycle:2 : price 815 + // >>> bicycle:9 : price 815 // STEP_END // Tests for 'range4' step. 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 63/63] 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 548fe1ca23f..2da7b4eb0a4 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 763a60e0260..da51c098e1f 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 b4931f26346..c44a0be7de7 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 9bffd716440..2860866c6ee 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 3500a211722..cc53df56f0d 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 5899b22260d..40d4861f98a 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 c9ef6646ba6..0d41693d0fd 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 1ba62402d77..db8d17ee158 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 2c4ea3b3c52..ec63c5206ad 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 0e07ccc2860..0ff5bebe1c3 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 c6d022e0942..c3429319e7a 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 0ea0221c1ad..26f208a03b2 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 d9c9872e1ce..cd6e41581fb 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 00000000000..a2b9e5b74c3 --- /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 50d18351d78..db5a52c231d 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 e0c642e6309..0d170840859 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 75153868618..be266f58aa2 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 813ddd021be..4a2ec782a72 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 00000000000..84b4d2ef81e --- /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 00000000000..0bf4592b594 --- /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 00000000000..ab907dfbdec --- /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 00000000000..f157d95a94e --- /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 00000000000..36c308db8de --- /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 00000000000..0286783dfc7 --- /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 00000000000..dedd88374ea --- /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 00000000000..e689ea0d774 --- /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 00000000000..908b004cbb6 --- /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 00000000000..5577cc07580 --- /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 00000000000..47f9ca0ccce --- /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 00000000000..217b04263e6 --- /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 00000000000..b75c7338ba5 --- /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 00000000000..2daf6393c76 --- /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 00000000000..d74aee56cd4 --- /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 00000000000..25fd89cff10 --- /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 00000000000..abd1e73b9e8 --- /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 00000000000..94a745e1bf8 --- /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 6b89d9c77b7..60b1f9c1236 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 925645e169e..d7e2f9fee73 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 f7b90e29535..ddbd768f9b2 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 f2f07464609..dedf34fb692 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 6bbd1599a89..a565d38048c 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 f7e320f1642..5baf1b32251 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 6ebec4e73fc..f3dfe630e58 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 0746c2d37c8..bb6656a8122 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 d3a5853f421..4ef4f969bb3 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 00000000000..8ee05800112 --- /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 f7ab13df25b..f6bedc17410 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 00000000000..a0b7d68381d --- /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 00000000000..d2032e5d238 --- /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 00000000000..db53b085be4 --- /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 00000000000..89114d154f6 --- /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 00000000000..d7b2bd49899 --- /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 00000000000..133efcb3fc1 --- /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 00000000000..82da0b14afb --- /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 00000000000..b8df3910cd6 --- /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 00000000000..0c9db2dbbab --- /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 00000000000..388113307b6 --- /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 00000000000..b154e88a05d --- /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 9f190a06ae4..03d4dc62dd5 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")