From af30f2c40b9a87be7a69e5c508d4240cfed60110 Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Tue, 21 Mar 2023 16:09:25 +0100 Subject: [PATCH] A few Redis usability improvements - Add support for ts.add(key, val, args) - without timestamp - Add support for Jackson Polymorphic serialization/deserialization - Fix NPE when using empty sorted set --- .../deployment/devmode/IncrementResource.java | 6 ++ .../devmode/RedisClientDevModeTestCase.java | 6 ++ .../ReactiveTimeSeriesCommands.java | 14 ++++ .../timeseries/TimeSeriesCommands.java | 13 +++ .../datasource/AbstractSortedSetCommands.java | 2 +- .../AbstractTimeSeriesCommands.java | 7 ++ .../BlockingTimeSeriesCommandsImpl.java | 5 ++ .../redis/runtime/datasource/Marshaller.java | 21 ++--- .../ReactiveTimeSeriesCommandsImpl.java | 6 ++ .../redis/datasource/ListCommandTest.java | 80 +++++++++++++++++++ .../datasource/SortedSetCommandsTest.java | 1 + .../datasource/TimeSeriesCommandsTest.java | 9 +++ 12 files changed, 159 insertions(+), 11 deletions(-) diff --git a/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/devmode/IncrementResource.java b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/devmode/IncrementResource.java index 20727dd2d95eb..7dd6cb2ba68ce 100644 --- a/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/devmode/IncrementResource.java +++ b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/devmode/IncrementResource.java @@ -24,6 +24,12 @@ public int increment() { return (int) commands.incrby("counter-dev-mode", INCREMENT); } + @GET + @Path("/val") + public int value() { + return commands.get("counter-dev-mode"); + } + @GET @Path("/keys") public int verifyPreloading() { diff --git a/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/devmode/RedisClientDevModeTestCase.java b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/devmode/RedisClientDevModeTestCase.java index 860194ee8e014..b1d3fa86b2fba 100644 --- a/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/devmode/RedisClientDevModeTestCase.java +++ b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/devmode/RedisClientDevModeTestCase.java @@ -3,10 +3,12 @@ import java.util.function.Function; import java.util.function.Supplier; +import org.awaitility.Awaitility; import org.hamcrest.Matchers; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -47,6 +49,10 @@ public String apply(String s) { return s.replace("INCREMENT = 1", "INCREMENT = 10"); } }); + + Awaitility.await() + .untilAsserted(() -> Assertions.assertEquals("2", RestAssured.get("/inc/val").andReturn().asString())); + RestAssured.get("/inc") .then() .statusCode(200) diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/ReactiveTimeSeriesCommands.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/ReactiveTimeSeriesCommands.java index 18f437897daf7..bb4e71c4f05e4 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/ReactiveTimeSeriesCommands.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/ReactiveTimeSeriesCommands.java @@ -79,6 +79,20 @@ public interface ReactiveTimeSeriesCommands extends ReactiveRedisCommands { **/ Uni tsAdd(K key, double value); + /** + * Execute the command TS.ADD. + * Summary: Append a sample to a time series + * Group: time series + *

+ * Unlike {@link #tsAdd(Object, long, double, AddArgs)}, set the timestamp according to the server clock. + * + * @param key the key name for the time series must not be {@code null} + * @param value the numeric data value of the sample. + * @param args the creation arguments. + * @return A uni emitting {@code null} when the operation completes + */ + Uni tsAdd(K key, double value, AddArgs args); + /** * Execute the command TS.ALTER. * Summary: Update the retention, chunk size, duplicate policy, and labels of an existing time series diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/TimeSeriesCommands.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/TimeSeriesCommands.java index 42f309eff59d7..d0c9c86b91563 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/TimeSeriesCommands.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/TimeSeriesCommands.java @@ -73,6 +73,19 @@ public interface TimeSeriesCommands extends RedisCommands { */ void tsAdd(K key, double value); + /** + * Execute the command TS.ADD. + * Summary: Append a sample to a time series + * Group: time series + *

+ * Unlike {@link #tsAdd(Object, long, double, AddArgs)}, set the timestamp according to the server clock. + * + * @param key the key name for the time series must not be {@code null} + * @param value the numeric data value of the sample. + * @param args the creation arguments. + */ + void tsAdd(K key, double value, AddArgs args); + /** * Execute the command TS.ALTER. * Summary: Update the retention, chunk size, duplicate policy, and labels of an existing time series diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractSortedSetCommands.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractSortedSetCommands.java index b6bf0d93c508d..9d74dc8722e87 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractSortedSetCommands.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractSortedSetCommands.java @@ -734,7 +734,7 @@ protected String getScoreAsString(double score) { final List> decodeAsListOfScoredValues(Response response) { List> list = new ArrayList<>(); - if (!response.iterator().hasNext()) { + if (response == null || !response.iterator().hasNext()) { return Collections.emptyList(); } if (response.iterator().next().type() == ResponseType.BULK) { diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractTimeSeriesCommands.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractTimeSeriesCommands.java index 9ed22059cde73..7657d25eb9e10 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractTimeSeriesCommands.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractTimeSeriesCommands.java @@ -67,6 +67,13 @@ Uni _tsAdd(K key, double value) { return execute(cmd); } + Uni _tsAdd(K key, double value, AddArgs args) { + nonNull(key, "key"); + nonNull(args, "args"); + RedisCommand cmd = RedisCommand.of(Command.TS_ADD).put(marshaller.encode(key)).put("*").put(value).putArgs(args); + return execute(cmd); + } + Uni _tsAlter(K key, AlterArgs args) { nonNull(key, "key"); nonNull(args, "args"); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingTimeSeriesCommandsImpl.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingTimeSeriesCommandsImpl.java index 4023dd0816e17..d50693b157d0d 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingTimeSeriesCommandsImpl.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingTimeSeriesCommandsImpl.java @@ -45,6 +45,11 @@ public void tsAdd(K key, long timestamp, double value, AddArgs args) { reactive.tsAdd(key, timestamp, value, args).await().atMost(timeout); } + @Override + public void tsAdd(K key, double value, AddArgs args) { + reactive.tsAdd(key, value, args).await().atMost(timeout); + } + @Override public void tsAdd(K key, long timestamp, double value) { reactive.tsAdd(key, timestamp, value).await().atMost(timeout); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Marshaller.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Marshaller.java index 6f02bd1b15889..d16d8b94f7fc9 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Marshaller.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Marshaller.java @@ -6,13 +6,13 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import io.quarkus.redis.datasource.codecs.Codec; @@ -25,13 +25,13 @@ public class Marshaller { private static final Map, Codec> DEFAULT_CODECS; static { - DEFAULT_CODECS = new HashMap<>(); - DEFAULT_CODECS.put(String.class, Codecs.StringCodec.INSTANCE); - DEFAULT_CODECS.put(Integer.class, Codecs.IntegerCodec.INSTANCE); - DEFAULT_CODECS.put(Double.class, Codecs.DoubleCodec.INSTANCE); + DEFAULT_CODECS = Map.of( + String.class, Codecs.StringCodec.INSTANCE, + Integer.class, Codecs.IntegerCodec.INSTANCE, + Double.class, Codecs.DoubleCodec.INSTANCE); } - Map, Codec> codecs = new HashMap<>(); + Map, Codec> codecs = new ConcurrentHashMap<>(); public Marshaller(Class... hints) { doesNotContainNull(hints, "hints"); @@ -51,11 +51,12 @@ public byte[] encode(Object o) { } Class clazz = o.getClass(); Codec codec = codec(clazz); - if (codec != null) { - return codec.encode(o); - } else { - throw new IllegalArgumentException("Unable to encode object of type " + clazz); + if (codec == null) { + // Default to JSON. + codec = new Codecs.JsonCodec<>(clazz); + codecs.put(clazz, codec); } + return codec.encode(o); } @SafeVarargs diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveTimeSeriesCommandsImpl.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveTimeSeriesCommandsImpl.java index e42591451cf6b..cc29f58e7fc90 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveTimeSeriesCommandsImpl.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveTimeSeriesCommandsImpl.java @@ -62,6 +62,12 @@ public Uni tsAdd(K key, long timestamp, double value, AddArgs args) { .replaceWithVoid(); } + @Override + public Uni tsAdd(K key, double value, AddArgs args) { + return super._tsAdd(key, value, args) + .replaceWithVoid(); + } + @Override public Uni tsAdd(K key, long timestamp, double value) { return super._tsAdd(key, timestamp, value) diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/ListCommandTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/ListCommandTest.java index d07f0b84e823b..5b1b983cea2cf 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/ListCommandTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/ListCommandTest.java @@ -10,6 +10,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + import io.quarkus.redis.datasource.list.KeyValue; import io.quarkus.redis.datasource.list.LPosArgs; import io.quarkus.redis.datasource.list.ListCommands; @@ -352,4 +356,80 @@ void sort() { assertThat(commands.lpop("dest2", 100)).containsExactly("1", "2", "3", "4", "5", "5", "6", "7", "8", "9"); } + @Test + void testJacksonPolymorphism() { + var cmd = ds.list(Animal.class); + + var cat = new Cat(); + cat.setId("1234"); + cat.setName("the cat"); + + var rabbit = new Rabbit(); + rabbit.setName("roxanne"); + rabbit.setColor("grey"); + + cmd.lpush(key, cat, rabbit); + + var shouldBeACat = cmd.rpop(key); + var shouldBeARabbit = cmd.rpop(key); + + assertThat(shouldBeACat).isInstanceOf(Cat.class) + .satisfies(animal -> { + assertThat(animal.getName()).isEqualTo("the cat"); + assertThat(((Cat) animal).getId()).isEqualTo("1234"); + }); + + assertThat(shouldBeARabbit).isInstanceOf(Rabbit.class) + .satisfies(animal -> { + assertThat(animal.getName()).isEqualTo("roxanne"); + assertThat(((Rabbit) animal).getColor()).isEqualTo("grey"); + }); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) + @JsonSubTypes({ + @JsonSubTypes.Type(value = Cat.class, name = "Cat"), + @JsonSubTypes.Type(value = Rabbit.class, name = "Rabbit") + }) + public static class Animal { + + private String name; + + public String getName() { + return name; + } + + public Animal setName(String name) { + this.name = name; + return this; + } + } + + public static class Rabbit extends Animal { + + private String color; + + public String getColor() { + return color; + } + + public Rabbit setColor(String color) { + this.color = color; + return this; + } + } + + public static class Cat extends Animal { + private String id; + + public String getId() { + return id; + } + + public Cat setId(String id) { + this.id = id; + return this; + } + } } diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SortedSetCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SortedSetCommandsTest.java index b5f14cfad65de..6f4a92d929099 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SortedSetCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SortedSetCommandsTest.java @@ -472,6 +472,7 @@ void zrevrange() { @RequiresRedis6OrHigher void zrevrangeWithScoreEmpty() { assertThat(ds.sortedSet(String.class).zrangeWithScores("top-products", 0, 2, new ZRangeArgs().rev())).isEmpty(); + assertThat(ds.sortedSet(String.class).zrangeWithScores("missing", 0, 2)).isEmpty(); } @Test diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TimeSeriesCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TimeSeriesCommandsTest.java index 5ae481afb579e..12524a810764b 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TimeSeriesCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TimeSeriesCommandsTest.java @@ -72,6 +72,15 @@ void testCreationAndAddingDataPoint() throws InterruptedException { assertThat(list).hasSize(1); } + @Test + void testAdd() throws InterruptedException { + ts.tsAdd(key, 25.0); + Thread.sleep(10); // Make sure the timestamp is different + ts.tsAdd(key, 30.5, new AddArgs().label("foo", "bar")); + var list = ts.tsRange(key, TimeSeriesRange.fromTimeSeries()); + assertThat(list).hasSize(2); + } + @Test void testCreationWhileAdding() { long timestamp = System.currentTimeMillis() - 1000;