From 5927a3b91bf0dd2dd87f3d026fdeea02545b63ff Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Fri, 22 Jul 2022 15:26:51 +0200 Subject: [PATCH] Implement Redis Stream command support --- docs/src/main/asciidoc/redis-reference.adoc | 2 +- .../src/etc/RedisCommandGenerator.java | 81 +++ .../datasource/ReactiveRedisDataSource.java | 24 + .../redis/datasource/RedisDataSource.java | 24 + .../redis/datasource/keys/CopyArgs.java | 7 +- .../datasource/stream/ClaimedMessages.java | 28 + .../stream/ReactiveStreamCommands.java | 593 +++++++++++++++++ .../ReactiveTransactionalStreamCommands.java | 617 +++++++++++++++++ .../datasource/stream/StreamCommands.java | 585 ++++++++++++++++ .../redis/datasource/stream/StreamEntry.java | 14 + .../datasource/stream/StreamMessage.java | 44 ++ .../redis/datasource/stream/StreamRange.java | 34 + .../stream/TransactionalStreamCommands.java | 554 +++++++++++++++ .../redis/datasource/stream/XAddArgs.java | 137 ++++ .../redis/datasource/stream/XClaimArgs.java | 136 ++++ .../datasource/stream/XGroupCreateArgs.java | 55 ++ .../datasource/stream/XGroupSetIdArgs.java | 41 ++ .../redis/datasource/stream/XReadArgs.java | 55 ++ .../datasource/stream/XReadGroupArgs.java | 71 ++ .../redis/datasource/stream/XTrimArgs.java | 103 +++ .../ReactiveTransactionalRedisDataSource.java | 29 + .../TransactionalRedisDataSource.java | 29 + .../datasource/AbstractStreamCommands.java | 430 ++++++++++++ .../BlockingRedisDataSourceImpl.java | 6 + .../BlockingStreamCommandsImpl.java | 188 ++++++ ...ckingTransactionalRedisDataSourceImpl.java | 8 + ...ockingTransactionalStreamCommandsImpl.java | 184 +++++ .../ReactiveRedisDataSourceImpl.java | 6 + .../ReactiveStreamCommandsImpl.java | 330 +++++++++ ...ctiveTransactionalRedisDataSourceImpl.java | 8 + ...activeTransactionalStreamCommandsImpl.java | 222 +++++++ .../redis/datasource/StreamCommandsTest.java | 628 ++++++++++++++++++ .../TransactionalStreamCommandsTest.java | 85 +++ .../redis/generator/RedisApiGenerator.java | 6 +- 34 files changed, 5358 insertions(+), 6 deletions(-) create mode 100755 extensions/redis-client/runtime/src/etc/RedisCommandGenerator.java create mode 100644 extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/ClaimedMessages.java create mode 100644 extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/ReactiveStreamCommands.java create mode 100644 extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/ReactiveTransactionalStreamCommands.java create mode 100644 extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamCommands.java create mode 100644 extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamEntry.java create mode 100644 extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamMessage.java create mode 100644 extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamRange.java create mode 100644 extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/TransactionalStreamCommands.java create mode 100644 extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XAddArgs.java create mode 100644 extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XClaimArgs.java create mode 100644 extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupCreateArgs.java create mode 100644 extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupSetIdArgs.java create mode 100644 extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XReadArgs.java create mode 100644 extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XReadGroupArgs.java create mode 100644 extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XTrimArgs.java create mode 100644 extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractStreamCommands.java create mode 100644 extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingStreamCommandsImpl.java create mode 100644 extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingTransactionalStreamCommandsImpl.java create mode 100644 extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveStreamCommandsImpl.java create mode 100644 extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveTransactionalStreamCommandsImpl.java create mode 100644 extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/StreamCommandsTest.java create mode 100644 extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalStreamCommandsTest.java diff --git a/docs/src/main/asciidoc/redis-reference.adoc b/docs/src/main/asciidoc/redis-reference.adoc index 4afba3b3e2c64..022bd6afc58d3 100644 --- a/docs/src/main/asciidoc/redis-reference.adoc +++ b/docs/src/main/asciidoc/redis-reference.adoc @@ -274,8 +274,8 @@ As mentioned above, the API is divided into groups: - pubsub - `pubsub()` - set - `.set(memberType)` - sorted-set - `.sortedSet(memberType)` -- stream (not available yet) - string - `.value(valueType)` +- stream - `.stream(`valueType`) - transactions - `withTransaction` - json - `.json()` (requires the https://redis.com/modules/redis-json/[RedisJSON] module on the server side) - bloom - `.bloom()` (requires the https://redis.com/modules/redis-bloom/[RedisBloom] module on the server side) diff --git a/extensions/redis-client/runtime/src/etc/RedisCommandGenerator.java b/extensions/redis-client/runtime/src/etc/RedisCommandGenerator.java new file mode 100755 index 0000000000000..d3a9c9b4ad326 --- /dev/null +++ b/extensions/redis-client/runtime/src/etc/RedisCommandGenerator.java @@ -0,0 +1,81 @@ +///usr/bin/env jbang "$0" "$@" ; exit $? +//DEPS io.smallrye.reactive:smallrye-mutiny-vertx-redis-client:2.24.1 +//DEPS info.picocli:picocli:4.6.3 +//DEPS org.slf4j:slf4j-simple:1.7.36 + +import static java.lang.System.*; + +import java.util.List; +import java.util.concurrent.Callable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.vertx.mutiny.core.Vertx; +import io.vertx.mutiny.redis.client.Redis; +import io.vertx.mutiny.redis.client.RedisAPI; +import io.vertx.mutiny.redis.client.Response; +import io.vertx.redis.client.RedisOptions; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + + +@Command(name = "RedisCommandGenerator", mixinStandardHelpOptions = true, version = "RedisCommandGenerator 0.1", description = "Generate REdis Command Javadoc and signatures") +public class RedisCommandGenerator implements Callable { + + static Logger logger = LoggerFactory.getLogger("👻 >> "); + + + @Option(names = {"--redis"}, + description = "Redis connection string (redis://localhost:6379). Start Redis with: `docker run -p 6379:6379 redis:latest`", + defaultValue = "redis://localhost:6379") + private String url; + + @Option(names = "--command", description = "The command name from https://redis.io/commands/", required = true) + private String command; + + public Integer call() { + logger.info("Connecting to Redis"); + + Vertx vertx = Vertx.vertx(); + Redis client = Redis.createClient(vertx, new RedisOptions().setConnectionString(url)); + RedisAPI api = RedisAPI.api(client); + + Response response = api.commandAndAwait(List.of("DOCS", command.toLowerCase())); + System.out.println(javadoc(command, response.get(command))); + + vertx.closeAndAwait(); + return 0; + } + + private String javadoc(String cmd, Response response) { + String content = "/**\n"; + content += String.format(" * Execute the command $s.\n", cmd.toLowerCase(), cmd.toUpperCase()); + content += String.format(" * Summary: %s\n", response.get("summary").toString()); + content += String.format(" * Group: %s\n", response.get("group").toString()); + if (response.get("since") != null) { + content += String.format(" * Requires Redis %s+\n", response.get("since").toString()); + } + boolean deprecated = false; + if (response.get("deprecated_since") != null) { + content += String.format(" * Deprecated since Redis %s\n", response.get("deprecated_since").toString()); + deprecated = true; + } + content += " *

\n"; + for (Response arg: response.get("arguments")) { + content += String.format(" * @param %s %s\n", arg.get("name").toString(), arg.get("type")); + } + content += " * @return TODO\n"; + if (deprecated) { + content += " * @deprecated"; + } + content += " */\n"; + return content; + } + + public static void main(String... args) { + int exitCode = new CommandLine(new RedisCommandGenerator()).execute(args); + System.exit(exitCode); + } +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/ReactiveRedisDataSource.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/ReactiveRedisDataSource.java index 7c4b684dac706..b898666b7edc5 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/ReactiveRedisDataSource.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/ReactiveRedisDataSource.java @@ -19,6 +19,7 @@ import io.quarkus.redis.datasource.search.ReactiveSearchCommands; import io.quarkus.redis.datasource.set.ReactiveSetCommands; import io.quarkus.redis.datasource.sortedset.ReactiveSortedSetCommands; +import io.quarkus.redis.datasource.stream.ReactiveStreamCommands; import io.quarkus.redis.datasource.string.ReactiveStringCommands; import io.quarkus.redis.datasource.timeseries.ReactiveTimeSeriesCommands; import io.quarkus.redis.datasource.topk.ReactiveTopKCommands; @@ -381,6 +382,29 @@ default ReactiveBitMapCommands bitmap() { return bitmap(String.class); } + /** + * Gets the object to execute commands manipulating streams. + * + * @param redisKeyType the class of the keys + * @param fieldType the class of the fields included in the message exchanged on the streams + * @param valueType the class of the values included in the message exchanged on the streams + * @param the type of the redis key + * @param the type of the fields (map's keys) + * @param the type of the value + * @return the object to execute commands manipulating streams. + */ + ReactiveStreamCommands stream(Class redisKeyType, Class fieldType, Class valueType); + + /** + * Gets the object to execute commands manipulating streams, using a string key, and string fields. + * + * @param the type of the value + * @return the object to execute commands manipulating streams. + */ + default ReactiveStreamCommands stream(Class typeOfValue) { + return stream(String.class, String.class, typeOfValue); + } + /** * Gets the object to publish and receive messages. * diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/RedisDataSource.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/RedisDataSource.java index cab5e3246702a..52d9852a9b46c 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/RedisDataSource.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/RedisDataSource.java @@ -20,6 +20,7 @@ import io.quarkus.redis.datasource.search.SearchCommands; import io.quarkus.redis.datasource.set.SetCommands; import io.quarkus.redis.datasource.sortedset.SortedSetCommands; +import io.quarkus.redis.datasource.stream.StreamCommands; import io.quarkus.redis.datasource.string.StringCommands; import io.quarkus.redis.datasource.timeseries.TimeSeriesCommands; import io.quarkus.redis.datasource.topk.TopKCommands; @@ -379,6 +380,29 @@ default BitMapCommands bitmap() { return bitmap(String.class); } + /** + * Gets the object to execute commands manipulating streams. + * + * @param redisKeyType the class of the keys + * @param fieldType the class of the fields included in the message exchanged on the streams + * @param valueType the class of the values included in the message exchanged on the streams + * @param the type of the redis key + * @param the type of the fields (map's keys) + * @param the type of the value + * @return the object to execute commands manipulating streams. + */ + StreamCommands stream(Class redisKeyType, Class fieldType, Class valueType); + + /** + * Gets the object to execute commands manipulating streams, using a string key, and string fields. + * + * @param the type of the value + * @return the object to execute commands manipulating streams. + */ + default StreamCommands stream(Class typeOfValue) { + return stream(String.class, String.class, typeOfValue); + } + /** * Gets the object to manipulate JSON values. * This group requires the RedisJSON module. diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/keys/CopyArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/keys/CopyArgs.java index 6719025a69e74..fb21c77d6ef0a 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/keys/CopyArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/keys/CopyArgs.java @@ -3,10 +3,12 @@ import java.util.ArrayList; import java.util.List; +import io.quarkus.redis.datasource.RedisCommandExtraArguments; + /** * Arguments for the Redis COPY command. */ -public class CopyArgs { +public class CopyArgs implements RedisCommandExtraArguments { private long destinationDb = -1; @@ -34,6 +36,7 @@ public CopyArgs replace(boolean replace) { return this; } + @Override public List toArgs() { List args = new ArrayList<>(); if (destinationDb != -1) { @@ -47,4 +50,4 @@ public List toArgs() { return args; } -} \ No newline at end of file +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/ClaimedMessages.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/ClaimedMessages.java new file mode 100644 index 0000000000000..8eca18e527eea --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/ClaimedMessages.java @@ -0,0 +1,28 @@ +package io.quarkus.redis.datasource.stream; + +import java.util.List; + +/** + * Represents claimed messages + * + * @param the type of the key + * @param the field type for the payload + * @param the value type for the payload + */ +public class ClaimedMessages { + private final String id; + private final List> messages; + + public ClaimedMessages(String id, List> messages) { + this.id = id; + this.messages = messages; + } + + public String getId() { + return this.id; + } + + public List> getMessages() { + return this.messages; + } +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/ReactiveStreamCommands.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/ReactiveStreamCommands.java new file mode 100644 index 0000000000000..20266c54fbd2d --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/ReactiveStreamCommands.java @@ -0,0 +1,593 @@ +package io.quarkus.redis.datasource.stream; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +import io.smallrye.mutiny.Uni; + +/** + * Allows executing commands manipulating streams. + * See the stream command list for further information about these + * commands. + *

+ *

+ * The messages are represented as {@code Map}. + * + * @param the type of the keys, often {@link String} + * @param the key type of the messages, generally {@link String} + * @param the value type of the messages + */ +public interface ReactiveStreamCommands { + + /** + * Execute the command XACK. + * Summary: Marks a pending message as correctly processed, effectively removing it from the pending entries list + * of the consumer group. Return value of the command is the number of messages successfully acknowledged, that is, + * the IDs we were actually able to resolve in the PEL. + *

+ * The {@code XACK} command removes one or multiple messages from the Pending Entries List (PEL) of a stream consumer + * group. A message is pending, and as such stored inside the PEL, when it was delivered to some consumer, normally + * as a side effect of calling {@code XREADGROUP}, or when a consumer took ownership of a message + * calling {@code XCLAIM}. The pending message was delivered to some consumer but the server is yet not sure it was + * processed at least once. So new calls to {@code XREADGROUP} to grab the messages history for a consumer + * (for instance using an ID of 0), will return such message. Similarly, the pending message will be listed by the + * {@code XPENDING} command, that inspects the PEL. + *

+ * Once a consumer successfully processes a message, it should call {@code XACK} so that such message does not get + * processed again, and as a side effect, the PEL entry about this message is also purged, releasing memory from + * the Redis server. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key the key + * @param group the name of the consumer group + * @param ids the message ids to acknowledge + * @return A {@link io.smallrye.mutiny.Uni} emitting the number of acknowledged messages. Certain message IDs may no + * longer be part of the PEL (for example because they have already been acknowledged), and XACK will not count them + * as successfully acknowledged. + */ + Uni xack(K key, String group, String... ids); + + /** + * Execute the command XADD. + * Summary: Appends the specified stream entry to the stream at the specified key. If the key does not exist, as a + * side effect of running this command the key is created with a stream value. The creation of stream's key can be + * disabled with the {@code NOMKSTREAM} option. + *

+ * An entry is composed of a list of field-value pairs. The field-value pairs are stored in the same order they are + * given by the user. Commands that read the stream, such as {@code XRANGE} or {@code XREAD}, are guaranteed to + * return the fields and values exactly in the same order they were added by {@code XADD}. + *

+ * {@code XADD} is the only Redis command that can add data to a stream, but there are other commands, such as + * {@code XDEL} and {@code XTRIM}, that are able to remove data from a stream. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key the key + * @param payload the payload to write to the stream, must not be {@code null} + * @return A {@link io.smallrye.mutiny.Uni} emitting the id of the added message + */ + Uni xadd(K key, Map payload); + + /** + * Execute the command XADD. + * Summary: Appends the specified stream entry to the stream at the specified key. If the key does not exist, as a + * side effect of running this command the key is created with a stream value. The creation of stream's key can be + * disabled with the {@code NOMKSTREAM} option. + *

+ * An entry is composed of a list of field-value pairs. The field-value pairs are stored in the same order they are + * given by the user. Commands that read the stream, such as {@code XRANGE} or {@code XREAD}, are guaranteed to + * return the fields and values exactly in the same order they were added by {@code XADD}. + *

+ * {@code XADD} is the only Redis command that can add data to a stream, but there are other commands, such as + * {@code XDEL} and {@code XTRIM}, that are able to remove data from a stream. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key the key + * @param args the extra parameters + * @param payload the payload to write to the stream, must not be {@code null} + * @return A {@link io.smallrye.mutiny.Uni} emitting the id of the added message + */ + Uni xadd(K key, XAddArgs args, Map payload); + + /** + * Execute the command XAUTOCLAIM. + * Summary: Changes (or acquires) ownership of messages in a consumer group, as if the messages were delivered to + * the specified consumer. + *

+ * This command transfers ownership of pending stream entries that match the specified criteria. Conceptually, + * {@code XAUTOCLAIM} is equivalent to calling {@code XPENDING} and then {@code XCLAIM}, but provides a more + * straightforward way to deal with message delivery failures via {@code SCAN}-like semantics. + *

+ * Like {@code XCLAIM}, the command operates on the stream entries at {@code key} and in the context of the provided + * {@code group}. It transfers ownership to @{code consumer} of messages pending for more than {@code min-idle-time} + * milliseconds and having an equal or greater ID than {@code start}. + *

+ * Group: stream + * Requires Redis 6.2.0+ + *

+ * + * @param key key the key + * @param group string the consumer group + * @param consumer string the consumer id + * @param minIdleTime the min pending time of the message to claim + * @param start the min id of the message to claim + * @return A {@link io.smallrye.mutiny.Uni} emitting the claimed messages + */ + Uni> xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start); + + /** + * Execute the command XAUTOCLAIM. + * Summary: Changes (or acquires) ownership of messages in a consumer group, as if the messages were delivered to + * the specified consumer. + *

+ * This command transfers ownership of pending stream entries that match the specified criteria. Conceptually, + * {@code XAUTOCLAIM} is equivalent to calling {@code XPENDING} and then {@code XCLAIM}, but provides a more + * straightforward way to deal with message delivery failures via {@code SCAN}-like semantics. + *

+ * Like {@code XCLAIM}, the command operates on the stream entries at {@code key} and in the context of the provided + * {@code group}. It transfers ownership to @{code consumer} of messages pending for more than {@code min-idle-time} + * milliseconds and having an equal or greater ID than {@code start}. + *

+ * Group: stream + * Requires Redis 6.2.0+ + *

+ * + * @param key key the key + * @param group string the consumer group + * @param consumer string the consumer id + * @param minIdleTime the min pending time of the message to claim + * @param start the min id of the message to claim + * @param count the upper limit of the number of entries to claim, default is 100. + * @return A {@link io.smallrye.mutiny.Uni} emitting the claimed messages + */ + Uni> xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start, + int count); + + /** + * Execute the command XAUTOCLAIM. + * Summary: Changes (or acquires) ownership of messages in a consumer group, as if the messages were delivered to + * the specified consumer. + *

+ * This command transfers ownership of pending stream entries that match the specified criteria. Conceptually, + * {@code XAUTOCLAIM} is equivalent to calling {@code XPENDING} and then {@code XCLAIM}, but provides a more + * straightforward way to deal with message delivery failures via {@code SCAN}-like semantics. + *

+ * Like {@code XCLAIM}, the command operates on the stream entries at {@code key} and in the context of the provided + * {@code group}. It transfers ownership to @{code consumer} of messages pending for more than {@code min-idle-time} + * milliseconds and having an equal or greater ID than {@code start}. + *

+ * Group: stream + * Requires Redis 6.2.0+ + *

+ * + * @param key key the key + * @param group string the consumer group + * @param consumer string the consumer id + * @param minIdleTime the min pending time of the message to claim + * @param start the min id of the message to claim + * @param count the upper limit of the number of entries to claim, default is 100. + * @param justId if {@code true} the returned structure would only contain the id of the messages and not the payloads + * @return A {@link io.smallrye.mutiny.Uni} emitting the claimed messages + */ + Uni> xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start, + int count, boolean justId); + + /** + * Execute the command XCLAIM. + * Summary: In the context of a stream consumer group, this command changes the ownership of a pending message, so + * that the new owner is the consumer specified as the command argument. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param group string the consumer group + * @param consumer string the consumer id + * @param minIdleTime the min pending time of the message to claim + * @param id the message ids to claim, must not be empty, must not contain {@code null} + * @return A {@link io.smallrye.mutiny.Uni} emitting the claimed messages + */ + Uni>> xclaim(K key, String group, String consumer, Duration minIdleTime, String... id); + + /** + * Execute the command XCLAIM. + * Summary: In the context of a stream consumer group, this command changes the ownership of a pending message, so + * that the new owner is the consumer specified as the command argument. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param group string the consumer group + * @param consumer string the consumer id + * @param minIdleTime the min pending time of the message to claim + * @param args the extra command parameters + * @param id the message ids to claim, must not be empty, must not contain {@code null} + * @return A {@link io.smallrye.mutiny.Uni} emitting the claimed messages + */ + Uni>> xclaim(K key, String group, String consumer, Duration minIdleTime, XClaimArgs args, + String... id); + + /** + * Execute the command XDEL. + * Summary: Removes the specified entries from a stream, and returns the number of entries deleted. This number may + * be less than the number of IDs passed to the command in the case where some of the specified IDs do not exist in + * the stream. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param id the message ids, must not be empty, must not contain {@code null} + * @return A {@link io.smallrye.mutiny.Uni} emitting the number of deleted messages + */ + Uni xdel(K key, String... id); + + /** + * Execute the command XGROUP CREATE. + * Summary: Create a new consumer group uniquely identified by {@code groupname} for the stream stored at {@code key} + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param from the last delivered entry in the stream from the new group's perspective. The special ID {@code $} + * is the ID of the last entry in the stream, but you can substitute it with any valid ID. + * @return A {@link io.smallrye.mutiny.Uni} emitting {@code null} when the operation completes + */ + Uni xgroupCreate(K key, String groupname, String from); + + /** + * Execute the command XGROUP CREATE. + * Summary: Create a new consumer group uniquely identified by {@code groupname} for the stream stored at {@code key} + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param from the last delivered entry in the stream from the new group's perspective. The special ID {@code $} + * is the ID of the last entry in the stream, but you can substitute it with any valid ID. + * @param args the extra command parameters + * @return A {@link io.smallrye.mutiny.Uni} emitting {@code null} when the operation completes + */ + Uni xgroupCreate(K key, String groupname, String from, XGroupCreateArgs args); + + /** + * Execute the command XGROUP CREATECONSUMER. + * Summary: Create a consumer named {@code consumername} in the consumer group {@code groupname} of the stream + * that's stored at {@code key}. + *

+ * Consumers are also created automatically whenever an operation, such as {@code XREADGROUP}, references a consumer + * that doesn't exist. + *

+ * Group: stream + * Requires Redis 6.2.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param consumername the consumer name + * @return A {@link io.smallrye.mutiny.Uni} emitting {@code true} if the consumer was created, {@code false} otherwise. + */ + Uni xgroupCreateConsumer(K key, String groupname, String consumername); + + /** + * Execute the command XGROUP DELCONSUMER. + * Summary: Deletes a consumer from the consumer group. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param consumername the consumer name + * @return A {@link io.smallrye.mutiny.Uni} emitting the number of pending messages that the consumer had before it + * was deleted + */ + Uni xgroupDelConsumer(K key, String groupname, String consumername); + + /** + * Execute the command XGROUP DESTROY. + * Summary: Completely destroys a consumer group. The consumer group will be destroyed even if there are active + * consumers, and pending messages, so make sure to call this command only when really needed. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @return A {@link io.smallrye.mutiny.Uni} emitting {@code true} if the consumer group was destroyed, + * {@code false} otherwise. + */ + Uni xgroupDestroy(K key, String groupname); + + /** + * Execute the command XGROUP SETID. + * Summary: Set the last delivered ID for a consumer group. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param from the last delivered entry in the stream from the new group's perspective. The special ID {@code $} + * is the ID of the last entry in the stream, but you can substitute it with any valid ID. + * @return A {@link io.smallrye.mutiny.Uni} emitting {@code null} when the operation completes + */ + Uni xgroupSetId(K key, String groupname, String from); + + /** + * Execute the command XGROUP SETID. + * Summary: Set the last delivered ID for a consumer group. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param from the last delivered entry in the stream from the new group's perspective. The special ID {@code $} + * is the ID of the last entry in the stream, but you can substitute it with any valid ID. + * @param args the extra command parameters + * @return A {@link io.smallrye.mutiny.Uni} emitting {@code null} when the operation completes + */ + Uni xgroupSetId(K key, String groupname, String from, XGroupSetIdArgs args); + + /** + * Execute the command XLEN. + * Summary: Returns the number of entries inside a stream. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @return A {@link io.smallrye.mutiny.Uni} emitting the number of messages in the stream + */ + Uni xlen(K key); + + /** + * Execute the command XRANGE. + * Summary: The command returns the stream entries matching a given range of IDs. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param range the range, must not be {@code null} + * @param count the max number of entries to return + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages from the given range. + */ + Uni>> xrange(K key, StreamRange range, int count); + + /** + * Execute the command XRANGE. + * Summary: The command returns the stream entries matching a given range of IDs. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param range the range, must not be {@code null} + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages from the given range. + */ + Uni>> xrange(K key, StreamRange range); + + /** + * Execute the command XREAD. + * Summary: Read data from one or multiple streams, only returning entries with an ID greater than the last received + * ID reported by the caller. This command has an option to block if items are not available, in a similar fashion + * to {@code BRPOP} or {@code BZPOPMIN} and others. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key of the stream + * @param id the last read id + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages. + */ + Uni>> xread(K key, String id); + + /** + * Execute the command XREAD. + * Summary: Read data from one or multiple streams, only returning entries with an ID greater than the last received + * ID reported by the caller. This command has an option to block if items are not available, in a similar fashion + * to {@code BRPOP} or {@code BZPOPMIN} and others. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param lastIdsPerStream the map of key -> id indicating the last received id per stream to read + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages. + */ + Uni>> xread(Map lastIdsPerStream); + + /** + * Execute the command XREAD. + * Summary: Read data from one or multiple streams, only returning entries with an ID greater than the last received + * ID reported by the caller. This command has an option to block if items are not available, in a similar fashion + * to {@code BRPOP} or {@code BZPOPMIN} and others. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key of the stream + * @param id the last read id + * @param args the extra parameter + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages. + */ + Uni>> xread(K key, String id, XReadArgs args); + + /** + * Execute the command XREAD. + * Summary: Read data from one or multiple streams, only returning entries with an ID greater than the last received + * ID reported by the caller. This command has an option to block if items are not available, in a similar fashion + * to {@code BRPOP} or {@code BZPOPMIN} and others. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param lastIdsPerStream the map of key -> id indicating the last received id per stream to read + * @param args the extra parameter + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages. + */ + Uni>> xread(Map lastIdsPerStream, XReadArgs args); + + /** + * Execute the command XREADGROUP. + * Summary: The {@code XREADGROUP} command is a special version of the {@code XREAD} command with support for + * consumer groups. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param group the group name + * @param consumer the consumer name + * @param key the stream key + * @param id the last read id + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages. + */ + Uni>> xreadgroup(String group, String consumer, K key, String id); + + /** + * Execute the command XREADGROUP. + * Summary: The {@code XREADGROUP} command is a special version of the {@code XREAD} command with support for + * consumer groups. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param group the group name + * @param consumer the consumer name + * @param lastIdsPerStream the map of key -> id indicating the last received id per stream to read + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages. + */ + Uni>> xreadgroup(String group, String consumer, Map lastIdsPerStream); + + /** + * Execute the command XREADGROUP. + * Summary: The {@code XREADGROUP} command is a special version of the {@code XREAD} command with support for + * consumer groups. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param group the group name + * @param consumer the consumer name + * @param key the stream key + * @param id the last read id + * @param args the extra parameter + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages. + */ + Uni>> xreadgroup(String group, String consumer, K key, String id, XReadGroupArgs args); + + /** + * Execute the command XREADGROUP. + * Summary: The {@code XREADGROUP} command is a special version of the {@code XREAD} command with support for + * consumer groups. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param group the group name + * @param consumer the consumer name + * @param lastIdsPerStream the map of key -> id indicating the last received id per stream to read + * @param args the extra parameter + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages. + */ + Uni>> xreadgroup(String group, String consumer, Map lastIdsPerStream, + XReadGroupArgs args); + + /** + * Execute the command XREVRANGE. + * Summary: This command is exactly like {@code XRANGE}, but with the notable difference of returning the entries + * in reverse order, and also taking the start-end range in reverse order: in {@code XREVRANGE} you need to state + * the end ID and later the start ID, and the command will produce all the element between (or exactly like) the + * two IDs, starting from the end side. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param range the range, must not be {@code null} + * @param count the max number of entries to return + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages from the given range. + */ + Uni>> xrevrange(K key, StreamRange range, int count); + + /** + * Execute the command XRANGE. + * Summary: This command is exactly like {@code XRANGE}, but with the notable difference of returning the entries + * in reverse order, and also taking the start-end range in reverse order: in {@code XREVRANGE} you need to state + * the end ID and later the start ID, and the command will produce all the element between (or exactly like) the + * two IDs, starting from the end side. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param range the range, must not be {@code null} + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages from the given range. + */ + Uni>> xrevrange(K key, StreamRange range); + + /** + * Execute the command XTRIM. + * Summary: Trims the stream by evicting older entries (entries with lower IDs) if needed. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key the key + * @param threshold the threshold + * @return A {@link io.smallrye.mutiny.Uni} emitting the number of entries deleted from the stream + */ + Uni xtrim(K key, String threshold); + + /** + * Execute the command XTRIM. + * Summary: Trims the stream by evicting older entries (entries with lower IDs) if needed. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key the key + * @param args the extra parameters + * @return A {@link io.smallrye.mutiny.Uni} emitting the number of entries deleted from the stream + */ + Uni xtrim(K key, XTrimArgs args); + +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/ReactiveTransactionalStreamCommands.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/ReactiveTransactionalStreamCommands.java new file mode 100644 index 0000000000000..75c5f1d41419b --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/ReactiveTransactionalStreamCommands.java @@ -0,0 +1,617 @@ +package io.quarkus.redis.datasource.stream; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +import io.quarkus.redis.datasource.ReactiveTransactionalRedisCommands; +import io.smallrye.mutiny.Uni; + +/** + * Allows executing commands manipulating streams. + * See the stream command list for further information about these + * commands. + *

+ *

+ * The messages are represented as {@code Map}. + * This API is intended to be used in a Redis transaction ({@code MULTI}), thus, all command methods return {@code Uni}. + * + * @param the type of the keys, often {@link String} + * @param the key type of the messages, generally {@link String} + * @param the value type of the messages + */ +public interface ReactiveTransactionalStreamCommands extends ReactiveTransactionalRedisCommands { + + /** + * Execute the command XACK. + * Summary: Marks a pending message as correctly processed, effectively removing it from the pending entries list + * of the consumer group. Return value of the command is the number of messages successfully acknowledged, that is, + * the IDs we were actually able to resolve in the PEL. + *

+ * The {@code XACK} command removes one or multiple messages from the Pending Entries List (PEL) of a stream consumer + * group. A message is pending, and as such stored inside the PEL, when it was delivered to some consumer, normally + * as a side effect of calling {@code XREADGROUP}, or when a consumer took ownership of a message + * calling {@code XCLAIM}. The pending message was delivered to some consumer but the server is yet not sure it was + * processed at least once. So new calls to {@code XREADGROUP} to grab the messages history for a consumer + * (for instance using an ID of 0), will return such message. Similarly, the pending message will be listed by the + * {@code XPENDING} command, that inspects the PEL. + *

+ * Once a consumer successfully processes a message, it should call {@code XACK} so that such message does not get + * processed again, and as a side effect, the PEL entry about this message is also purged, releasing memory from + * the Redis server. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key the key + * @param group the name of the consumer group + * @param ids the message ids to acknowledge + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xack(K key, String group, String... ids); + + /** + * Execute the command XADD. + * Summary: Appends the specified stream entry to the stream at the specified key. If the key does not exist, as a + * side effect of running this command the key is created with a stream value. The creation of stream's key can be + * disabled with the {@code NOMKSTREAM} option. + *

+ * An entry is composed of a list of field-value pairs. The field-value pairs are stored in the same order they are + * given by the user. Commands that read the stream, such as {@code XRANGE} or {@code XREAD}, are guaranteed to + * return the fields and values exactly in the same order they were added by {@code XADD}. + *

+ * {@code XADD} is the only Redis command that can add data to a stream, but there are other commands, such as + * {@code XDEL} and {@code XTRIM}, that are able to remove data from a stream. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key the key + * @param payload the payload to write to the stream, must not be {@code null} + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xadd(K key, Map payload); + + /** + * Execute the command XADD. + * Summary: Appends the specified stream entry to the stream at the specified key. If the key does not exist, as a + * side effect of running this command the key is created with a stream value. The creation of stream's key can be + * disabled with the {@code NOMKSTREAM} option. + *

+ * An entry is composed of a list of field-value pairs. The field-value pairs are stored in the same order they are + * given by the user. Commands that read the stream, such as {@code XRANGE} or {@code XREAD}, are guaranteed to + * return the fields and values exactly in the same order they were added by {@code XADD}. + *

+ * {@code XADD} is the only Redis command that can add data to a stream, but there are other commands, such as + * {@code XDEL} and {@code XTRIM}, that are able to remove data from a stream. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key the key + * @param args the extra parameters + * @param payload the payload to write to the stream, must not be {@code null} + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xadd(K key, XAddArgs args, Map payload); + + /** + * Execute the command XAUTOCLAIM. + * Summary: Changes (or acquires) ownership of messages in a consumer group, as if the messages were delivered to + * the specified consumer. + *

+ * This command transfers ownership of pending stream entries that match the specified criteria. Conceptually, + * {@code XAUTOCLAIM} is equivalent to calling {@code XPENDING} and then {@code XCLAIM}, but provides a more + * straightforward way to deal with message delivery failures via {@code SCAN}-like semantics. + *

+ * Like {@code XCLAIM}, the command operates on the stream entries at {@code key} and in the context of the provided + * {@code group}. It transfers ownership to @{code consumer} of messages pending for more than {@code min-idle-time} + * milliseconds and having an equal or greater ID than {@code start}. + *

+ * Group: stream + * Requires Redis 6.2.0+ + *

+ * + * @param key key the key + * @param group string the consumer group + * @param consumer string the consumer id + * @param minIdleTime the min pending time of the message to claim + * @param start the min id of the message to claim + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start); + + /** + * Execute the command XAUTOCLAIM. + * Summary: Changes (or acquires) ownership of messages in a consumer group, as if the messages were delivered to + * the specified consumer. + *

+ * This command transfers ownership of pending stream entries that match the specified criteria. Conceptually, + * {@code XAUTOCLAIM} is equivalent to calling {@code XPENDING} and then {@code XCLAIM}, but provides a more + * straightforward way to deal with message delivery failures via {@code SCAN}-like semantics. + *

+ * Like {@code XCLAIM}, the command operates on the stream entries at {@code key} and in the context of the provided + * {@code group}. It transfers ownership to @{code consumer} of messages pending for more than {@code min-idle-time} + * milliseconds and having an equal or greater ID than {@code start}. + *

+ * Group: stream + * Requires Redis 6.2.0+ + *

+ * + * @param key key the key + * @param group string the consumer group + * @param consumer string the consumer id + * @param minIdleTime the min pending time of the message to claim + * @param start the min id of the message to claim + * @param count the upper limit of the number of entries to claim, default is 100. + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start, int count); + + /** + * Execute the command XAUTOCLAIM. + * Summary: Changes (or acquires) ownership of messages in a consumer group, as if the messages were delivered to + * the specified consumer. + *

+ * This command transfers ownership of pending stream entries that match the specified criteria. Conceptually, + * {@code XAUTOCLAIM} is equivalent to calling {@code XPENDING} and then {@code XCLAIM}, but provides a more + * straightforward way to deal with message delivery failures via {@code SCAN}-like semantics. + *

+ * Like {@code XCLAIM}, the command operates on the stream entries at {@code key} and in the context of the provided + * {@code group}. It transfers ownership to @{code consumer} of messages pending for more than {@code min-idle-time} + * milliseconds and having an equal or greater ID than {@code start}. + *

+ * Group: stream + * Requires Redis 6.2.0+ + *

+ * + * @param key key the key + * @param group string the consumer group + * @param consumer string the consumer id + * @param minIdleTime the min pending time of the message to claim + * @param start the min id of the message to claim + * @param count the upper limit of the number of entries to claim, default is 100. + * @param justId if {@code true} the returned structure would only contain the id of the messages and not the payloads + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start, int count, boolean justId); + + /** + * Execute the command XCLAIM. + * Summary: In the context of a stream consumer group, this command changes the ownership of a pending message, so + * that the new owner is the consumer specified as the command argument. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param group string the consumer group + * @param consumer string the consumer id + * @param minIdleTime the min pending time of the message to claim + * @param id the message ids to claim, must not be empty, must not contain {@code null} + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xclaim(K key, String group, String consumer, Duration minIdleTime, String... id); + + /** + * Execute the command XCLAIM. + * Summary: In the context of a stream consumer group, this command changes the ownership of a pending message, so + * that the new owner is the consumer specified as the command argument. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param group string the consumer group + * @param consumer string the consumer id + * @param minIdleTime the min pending time of the message to claim + * @param args the extra command parameters + * @param id the message ids to claim, must not be empty, must not contain {@code null} + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xclaim(K key, String group, String consumer, Duration minIdleTime, XClaimArgs args, String... id); + + /** + * Execute the command XDEL. + * Summary: Removes the specified entries from a stream, and returns the number of entries deleted. This number may + * be less than the number of IDs passed to the command in the case where some of the specified IDs do not exist in + * the stream. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param id the message ids, must not be empty, must not contain {@code null} + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xdel(K key, String... id); + + /** + * Execute the command XGROUP CREATE. + * Summary: Create a new consumer group uniquely identified by {@code groupname} for the stream stored at {@code key} + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param from the last delivered entry in the stream from the new group's perspective. The special ID {@code $} + * is the ID of the last entry in the stream, but you can substitute it with any valid ID. + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xgroupCreate(K key, String groupname, String from); + + /** + * Execute the command XGROUP CREATE. + * Summary: Create a new consumer group uniquely identified by {@code groupname} for the stream stored at {@code key} + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param from the last delivered entry in the stream from the new group's perspective. The special ID {@code $} + * is the ID of the last entry in the stream, but you can substitute it with any valid ID. + * @param args the extra command parameters + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xgroupCreate(K key, String groupname, String from, XGroupCreateArgs args); + + /** + * Execute the command XGROUP CREATECONSUMER. + * Summary: Create a consumer named {@code consumername} in the consumer group {@code groupname} of the stream + * that's stored at {@code key}. + *

+ * Consumers are also created automatically whenever an operation, such as {@code XREADGROUP}, references a consumer + * that doesn't exist. + *

+ * Group: stream + * Requires Redis 6.2.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param consumername the consumer name + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xgroupCreateConsumer(K key, String groupname, String consumername); + + /** + * Execute the command XGROUP DELCONSUMER. + * Summary: Deletes a consumer from the consumer group. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param consumername the consumer name + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xgroupDelConsumer(K key, String groupname, String consumername); + + /** + * Execute the command XGROUP DESTROY. + * Summary: Completely destroys a consumer group. The consumer group will be destroyed even if there are active + * consumers, and pending messages, so make sure to call this command only when really needed. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xgroupDestroy(K key, String groupname); + + /** + * Execute the command XGROUP SETID. + * Summary: Set the last delivered ID for a consumer group. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param from the last delivered entry in the stream from the new group's perspective. The special ID {@code $} + * is the ID of the last entry in the stream, but you can substitute it with any valid ID. + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xgroupSetId(K key, String groupname, String from); + + /** + * Execute the command XGROUP SETID. + * Summary: Set the last delivered ID for a consumer group. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param from the last delivered entry in the stream from the new group's perspective. The special ID {@code $} + * is the ID of the last entry in the stream, but you can substitute it with any valid ID. + * @param args the extra command parameters + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xgroupSetId(K key, String groupname, String from, XGroupSetIdArgs args); + + /** + * Execute the command XLEN. + * Summary: Returns the number of entries inside a stream. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xlen(K key); + + /** + * Execute the command XRANGE. + * Summary: The command returns the stream entries matching a given range of IDs. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param range the range, must not be {@code null} + * @param count the max number of entries to return + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xrange(K key, StreamRange range, int count); + + /** + * Execute the command XRANGE. + * Summary: The command returns the stream entries matching a given range of IDs. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param range the range, must not be {@code null} + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xrange(K key, StreamRange range); + + /** + * Execute the command XREAD. + * Summary: Read data from one or multiple streams, only returning entries with an ID greater than the last received + * ID reported by the caller. This command has an option to block if items are not available, in a similar fashion + * to {@code BRPOP} or {@code BZPOPMIN} and others. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key of the stream + * @param id the last read id + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xread(K key, String id); + + /** + * Execute the command XREAD. + * Summary: Read data from one or multiple streams, only returning entries with an ID greater than the last received + * ID reported by the caller. This command has an option to block if items are not available, in a similar fashion + * to {@code BRPOP} or {@code BZPOPMIN} and others. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param lastIdsPerStream the map of key -> id indicating the last received id per stream to read + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xread(Map lastIdsPerStream); + + /** + * Execute the command XREAD. + * Summary: Read data from one or multiple streams, only returning entries with an ID greater than the last received + * ID reported by the caller. This command has an option to block if items are not available, in a similar fashion + * to {@code BRPOP} or {@code BZPOPMIN} and others. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key of the stream + * @param id the last read id + * @param args the extra parameter + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xread(K key, String id, XReadArgs args); + + /** + * Execute the command XREAD. + * Summary: Read data from one or multiple streams, only returning entries with an ID greater than the last received + * ID reported by the caller. This command has an option to block if items are not available, in a similar fashion + * to {@code BRPOP} or {@code BZPOPMIN} and others. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param lastIdsPerStream the map of key -> id indicating the last received id per stream to read + * @param args the extra parameter + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xread(Map lastIdsPerStream, XReadArgs args); + + /** + * Execute the command XREADGROUP. + * Summary: The {@code XREADGROUP} command is a special version of the {@code XREAD} command with support for + * consumer groups. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param group the group name + * @param consumer the consumer name + * @param key the stream key + * @param id the last read id + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xreadgroup(String group, String consumer, K key, String id); + + /** + * Execute the command XREADGROUP. + * Summary: The {@code XREADGROUP} command is a special version of the {@code XREAD} command with support for + * consumer groups. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param group the group name + * @param consumer the consumer name + * @param lastIdsPerStream the map of key -> id indicating the last received id per stream to read + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xreadgroup(String group, String consumer, Map lastIdsPerStream); + + /** + * Execute the command XREADGROUP. + * Summary: The {@code XREADGROUP} command is a special version of the {@code XREAD} command with support for + * consumer groups. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param group the group name + * @param consumer the consumer name + * @param key the stream key + * @param id the last read id + * @param args the extra parameter + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xreadgroup(String group, String consumer, K key, String id, XReadGroupArgs args); + + /** + * Execute the command XREADGROUP. + * Summary: The {@code XREADGROUP} command is a special version of the {@code XREAD} command with support for + * consumer groups. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param group the group name + * @param consumer the consumer name + * @param lastIdsPerStream the map of key -> id indicating the last received id per stream to read + * @param args the extra parameter + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xreadgroup(String group, String consumer, Map lastIdsPerStream, XReadGroupArgs args); + + /** + * Execute the command XREVRANGE. + * Summary: This command is exactly like {@code XRANGE}, but with the notable difference of returning the entries + * in reverse order, and also taking the start-end range in reverse order: in {@code XREVRANGE} you need to state + * the end ID and later the start ID, and the command will produce all the element between (or exactly like) the + * two IDs, starting from the end side. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param range the range, must not be {@code null} + * @param count the max number of entries to return + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xrevrange(K key, StreamRange range, int count); + + /** + * Execute the command XRANGE. + * Summary: This command is exactly like {@code XRANGE}, but with the notable difference of returning the entries + * in reverse order, and also taking the start-end range in reverse order: in {@code XREVRANGE} you need to state + * the end ID and later the start ID, and the command will produce all the element between (or exactly like) the + * two IDs, starting from the end side. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param range the range, must not be {@code null} + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xrevrange(K key, StreamRange range); + + /** + * Execute the command XTRIM. + * Summary: Trims the stream by evicting older entries (entries with lower IDs) if needed. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key the key + * @param threshold the threshold + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xtrim(K key, String threshold); + + /** + * Execute the command XTRIM. + * Summary: Trims the stream by evicting older entries (entries with lower IDs) if needed. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key the key + * @param args the extra parameters + * @return A {@code Uni} emitting {@code null} when the command has been enqueued successfully in the transaction, a failure + * otherwise. In the case of failure, the transaction is discarded. + */ + Uni xtrim(K key, XTrimArgs args); +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamCommands.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamCommands.java new file mode 100644 index 0000000000000..4b5792649c146 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamCommands.java @@ -0,0 +1,585 @@ +package io.quarkus.redis.datasource.stream; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +import io.quarkus.redis.datasource.RedisCommands; + +/** + * Allows executing commands manipulating streams. + * See the stream command list for further information about these + * commands. + *

+ * + * @param the type of the keys, often {@link String} + * @param the type of the fields composing the payload + * @param the type of the values composing the payload + */ +public interface StreamCommands extends RedisCommands { + + /** + * Execute the command XACK. + * Summary: Marks a pending message as correctly processed, effectively removing it from the pending entries list + * of the consumer group. Return value of the command is the number of messages successfully acknowledged, that is, + * the IDs we were actually able to resolve in the PEL. + *

+ * The {@code XACK} command removes one or multiple messages from the Pending Entries List (PEL) of a stream consumer + * group. A message is pending, and as such stored inside the PEL, when it was delivered to some consumer, normally + * as a side effect of calling {@code XREADGROUP}, or when a consumer took ownership of a message + * calling {@code XCLAIM}. The pending message was delivered to some consumer but the server is yet not sure it was + * processed at least once. So new calls to {@code XREADGROUP} to grab the messages history for a consumer + * (for instance using an ID of 0), will return such message. Similarly, the pending message will be listed by the + * {@code XPENDING} command, that inspects the PEL. + *

+ * Once a consumer successfully processes a message, it should call {@code XACK} so that such message does not get + * processed again, and as a side effect, the PEL entry about this message is also purged, releasing memory from + * the Redis server. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key the key + * @param group the name of the consumer group + * @param ids the message ids to acknowledge + * @return A {@link io.smallrye.mutiny.Uni} emitting the number of acknowledged messages. Certain message IDs may no + * longer be part of the PEL (for example because they have already been acknowledged), and XACK will not count them + * as successfully acknowledged. + */ + int xack(K key, String group, String... ids); + + /** + * Execute the command XADD. + * Summary: Appends the specified stream entry to the stream at the specified key. If the key does not exist, as a + * side effect of running this command the key is created with a stream value. The creation of stream's key can be + * disabled with the {@code NOMKSTREAM} option. + *

+ * An entry is composed of a list of field-value pairs. The field-value pairs are stored in the same order they are + * given by the user. Commands that read the stream, such as {@code XRANGE} or {@code XREAD}, are guaranteed to + * return the fields and values exactly in the same order they were added by {@code XADD}. + *

+ * {@code XADD} is the only Redis command that can add data to a stream, but there are other commands, such as + * {@code XDEL} and {@code XTRIM}, that are able to remove data from a stream. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key the key + * @param payload the payload to write to the stream, must not be {@code null} + * @return A {@link io.smallrye.mutiny.Uni} emitting the id of the added message + */ + String xadd(K key, Map payload); + + /** + * Execute the command XADD. + * Summary: Appends the specified stream entry to the stream at the specified key. If the key does not exist, as a + * side effect of running this command the key is created with a stream value. The creation of stream's key can be + * disabled with the {@code NOMKSTREAM} option. + *

+ * An entry is composed of a list of field-value pairs. The field-value pairs are stored in the same order they are + * given by the user. Commands that read the stream, such as {@code XRANGE} or {@code XREAD}, are guaranteed to + * return the fields and values exactly in the same order they were added by {@code XADD}. + *

+ * {@code XADD} is the only Redis command that can add data to a stream, but there are other commands, such as + * {@code XDEL} and {@code XTRIM}, that are able to remove data from a stream. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key the key + * @param args the extra parameters + * @param payload the payload to write to the stream, must not be {@code null} + * @return A {@link io.smallrye.mutiny.Uni} emitting the id of the added message + */ + String xadd(K key, XAddArgs args, Map payload); + + /** + * Execute the command XAUTOCLAIM. + * Summary: Changes (or acquires) ownership of messages in a consumer group, as if the messages were delivered to + * the specified consumer. + *

+ * This command transfers ownership of pending stream entries that match the specified criteria. Conceptually, + * {@code XAUTOCLAIM} is equivalent to calling {@code XPENDING} and then {@code XCLAIM}, but provides a more + * straightforward way to deal with message delivery failures via {@code SCAN}-like semantics. + *

+ * Like {@code XCLAIM}, the command operates on the stream entries at {@code key} and in the context of the provided + * {@code group}. It transfers ownership to @{code consumer} of messages pending for more than {@code min-idle-time} + * milliseconds and having an equal or greater ID than {@code start}. + *

+ * Group: stream + * Requires Redis 6.2.0+ + *

+ * + * @param key key the key + * @param group string the consumer group + * @param consumer string the consumer id + * @param minIdleTime the min pending time of the message to claim + * @param start the min id of the message to claim + * @return A {@link io.smallrye.mutiny.Uni} emitting the claimed messages + */ + ClaimedMessages xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start); + + /** + * Execute the command XAUTOCLAIM. + * Summary: Changes (or acquires) ownership of messages in a consumer group, as if the messages were delivered to + * the specified consumer. + *

+ * This command transfers ownership of pending stream entries that match the specified criteria. Conceptually, + * {@code XAUTOCLAIM} is equivalent to calling {@code XPENDING} and then {@code XCLAIM}, but provides a more + * straightforward way to deal with message delivery failures via {@code SCAN}-like semantics. + *

+ * Like {@code XCLAIM}, the command operates on the stream entries at {@code key} and in the context of the provided + * {@code group}. It transfers ownership to @{code consumer} of messages pending for more than {@code min-idle-time} + * milliseconds and having an equal or greater ID than {@code start}. + *

+ * Group: stream + * Requires Redis 6.2.0+ + *

+ * + * @param key key the key + * @param group string the consumer group + * @param consumer string the consumer id + * @param minIdleTime the min pending time of the message to claim + * @param start the min id of the message to claim + * @param count the upper limit of the number of entries to claim, default is 100. + * @return A {@link io.smallrye.mutiny.Uni} emitting the claimed messages + */ + ClaimedMessages xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start, int count); + + /** + * Execute the command XAUTOCLAIM. + * Summary: Changes (or acquires) ownership of messages in a consumer group, as if the messages were delivered to + * the specified consumer. + *

+ * This command transfers ownership of pending stream entries that match the specified criteria. Conceptually, + * {@code XAUTOCLAIM} is equivalent to calling {@code XPENDING} and then {@code XCLAIM}, but provides a more + * straightforward way to deal with message delivery failures via {@code SCAN}-like semantics. + *

+ * Like {@code XCLAIM}, the command operates on the stream entries at {@code key} and in the context of the provided + * {@code group}. It transfers ownership to @{code consumer} of messages pending for more than {@code min-idle-time} + * milliseconds and having an equal or greater ID than {@code start}. + *

+ * Group: stream + * Requires Redis 6.2.0+ + *

+ * + * @param key key the key + * @param group string the consumer group + * @param consumer string the consumer id + * @param minIdleTime the min pending time of the message to claim + * @param start the min id of the message to claim + * @param count the upper limit of the number of entries to claim, default is 100. + * @param justId if {@code true} the returned structure would only contain the id of the messages and not the payloads + * @return A {@link io.smallrye.mutiny.Uni} emitting the claimed messages + */ + ClaimedMessages xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start, int count, + boolean justId); + + /** + * Execute the command XCLAIM. + * Summary: In the context of a stream consumer group, this command changes the ownership of a pending message, so + * that the new owner is the consumer specified as the command argument. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param group string the consumer group + * @param consumer string the consumer id + * @param minIdleTime the min pending time of the message to claim + * @param id the message ids to claim, must not be empty, must not contain {@code null} + * @return A {@link io.smallrye.mutiny.Uni} emitting the claimed messages + */ + List> xclaim(K key, String group, String consumer, Duration minIdleTime, String... id); + + /** + * Execute the command XCLAIM. + * Summary: In the context of a stream consumer group, this command changes the ownership of a pending message, so + * that the new owner is the consumer specified as the command argument. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param group string the consumer group + * @param consumer string the consumer id + * @param minIdleTime the min pending time of the message to claim + * @param args the extra command parameters + * @param id the message ids to claim, must not be empty, must not contain {@code null} + * @return A {@link io.smallrye.mutiny.Uni} emitting the claimed messages + */ + List> xclaim(K key, String group, String consumer, Duration minIdleTime, XClaimArgs args, + String... id); + + /** + * Execute the command XDEL. + * Summary: Removes the specified entries from a stream, and returns the number of entries deleted. This number may + * be less than the number of IDs passed to the command in the case where some of the specified IDs do not exist in + * the stream. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param id the message ids, must not be empty, must not contain {@code null} + * @return A {@link io.smallrye.mutiny.Uni} emitting the number of deleted messages + */ + int xdel(K key, String... id); + + /** + * Execute the command XGROUP CREATE. + * Summary: Create a new consumer group uniquely identified by {@code groupname} for the stream stored at {@code key} + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param from the last delivered entry in the stream from the new group's perspective. The special ID {@code $} + * is the ID of the last entry in the stream, but you can substitute it with any valid ID. + */ + void xgroupCreate(K key, String groupname, String from); + + /** + * Execute the command XGROUP CREATE. + * Summary: Create a new consumer group uniquely identified by {@code groupname} for the stream stored at {@code key} + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param from the last delivered entry in the stream from the new group's perspective. The special ID {@code $} + * is the ID of the last entry in the stream, but you can substitute it with any valid ID. + * @param args the extra command parameters + */ + void xgroupCreate(K key, String groupname, String from, XGroupCreateArgs args); + + /** + * Execute the command XGROUP CREATECONSUMER. + * Summary: Create a consumer named {@code consumername} in the consumer group {@code groupname} of the stream + * that's stored at {@code key}. + *

+ * Consumers are also created automatically whenever an operation, such as {@code XREADGROUP}, references a consumer + * that doesn't exist. + *

+ * Group: stream + * Requires Redis 6.2.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param consumername the consumer name + * @return A {@link io.smallrye.mutiny.Uni} emitting {@code true} if the consumer was created, {@code false} otherwise. + */ + boolean xgroupCreateConsumer(K key, String groupname, String consumername); + + /** + * Execute the command XGROUP DELCONSUMER. + * Summary: Deletes a consumer from the consumer group. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param consumername the consumer name + * @return A {@link io.smallrye.mutiny.Uni} emitting the number of pending messages that the consumer had before it + * was deleted + */ + long xgroupDelConsumer(K key, String groupname, String consumername); + + /** + * Execute the command XGROUP DESTROY. + * Summary: Completely destroys a consumer group. The consumer group will be destroyed even if there are active + * consumers, and pending messages, so make sure to call this command only when really needed. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @return A {@link io.smallrye.mutiny.Uni} emitting {@code true} if the consumer group was destroyed, + * {@code false} otherwise. + */ + boolean xgroupDestroy(K key, String groupname); + + /** + * Execute the command XGROUP SETID. + * Summary: Set the last delivered ID for a consumer group. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param from the last delivered entry in the stream from the new group's perspective. The special ID {@code $} + * is the ID of the last entry in the stream, but you can substitute it with any valid ID. + */ + void xgroupSetId(K key, String groupname, String from); + + /** + * Execute the command XGROUP SETID. + * Summary: Set the last delivered ID for a consumer group. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param from the last delivered entry in the stream from the new group's perspective. The special ID {@code $} + * is the ID of the last entry in the stream, but you can substitute it with any valid ID. + * @param args the extra command parameters + */ + void xgroupSetId(K key, String groupname, String from, XGroupSetIdArgs args); + + /** + * Execute the command XLEN. + * Summary: Returns the number of entries inside a stream. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @return A {@link io.smallrye.mutiny.Uni} emitting the number of messages in the stream + */ + long xlen(K key); + + /** + * Execute the command XRANGE. + * Summary: The command returns the stream entries matching a given range of IDs. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param range the range, must not be {@code null} + * @param count the max number of entries to return + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages from the given range. + */ + List> xrange(K key, StreamRange range, int count); + + /** + * Execute the command XRANGE. + * Summary: The command returns the stream entries matching a given range of IDs. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param range the range, must not be {@code null} + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages from the given range. + */ + List> xrange(K key, StreamRange range); + + /** + * Execute the command XREAD. + * Summary: Read data from one or multiple streams, only returning entries with an ID greater than the last received + * ID reported by the caller. This command has an option to block if items are not available, in a similar fashion + * to {@code BRPOP} or {@code BZPOPMIN} and others. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key of the stream + * @param id the last read id + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages. + */ + List> xread(K key, String id); + + /** + * Execute the command XREAD. + * Summary: Read data from one or multiple streams, only returning entries with an ID greater than the last received + * ID reported by the caller. This command has an option to block if items are not available, in a similar fashion + * to {@code BRPOP} or {@code BZPOPMIN} and others. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param lastIdsPerStream the map of key -> id indicating the last received id per stream to read + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages. + */ + List> xread(Map lastIdsPerStream); + + /** + * Execute the command XREAD. + * Summary: Read data from one or multiple streams, only returning entries with an ID greater than the last received + * ID reported by the caller. This command has an option to block if items are not available, in a similar fashion + * to {@code BRPOP} or {@code BZPOPMIN} and others. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key of the stream + * @param id the last read id + * @param args the extra parameter + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages. + */ + List> xread(K key, String id, XReadArgs args); + + /** + * Execute the command XREAD. + * Summary: Read data from one or multiple streams, only returning entries with an ID greater than the last received + * ID reported by the caller. This command has an option to block if items are not available, in a similar fashion + * to {@code BRPOP} or {@code BZPOPMIN} and others. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param lastIdsPerStream the map of key -> id indicating the last received id per stream to read + * @param args the extra parameter + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages. + */ + List> xread(Map lastIdsPerStream, XReadArgs args); + + /** + * Execute the command XREADGROUP. + * Summary: The {@code XREADGROUP} command is a special version of the {@code XREAD} command with support for + * consumer groups. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param group the group name + * @param consumer the consumer name + * @param key the stream key + * @param id the last read id + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages. + */ + List> xreadgroup(String group, String consumer, K key, String id); + + /** + * Execute the command XREADGROUP. + * Summary: The {@code XREADGROUP} command is a special version of the {@code XREAD} command with support for + * consumer groups. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param group the group name + * @param consumer the consumer name + * @param lastIdsPerStream the map of key -> id indicating the last received id per stream to read + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages. + */ + List> xreadgroup(String group, String consumer, Map lastIdsPerStream); + + /** + * Execute the command XREADGROUP. + * Summary: The {@code XREADGROUP} command is a special version of the {@code XREAD} command with support for + * consumer groups. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param group the group name + * @param consumer the consumer name + * @param key the stream key + * @param id the last read id + * @param args the extra parameter + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages. + */ + List> xreadgroup(String group, String consumer, K key, String id, XReadGroupArgs args); + + /** + * Execute the command XREADGROUP. + * Summary: The {@code XREADGROUP} command is a special version of the {@code XREAD} command with support for + * consumer groups. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param group the group name + * @param consumer the consumer name + * @param lastIdsPerStream the map of key -> id indicating the last received id per stream to read + * @param args the extra parameter + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages. + */ + List> xreadgroup(String group, String consumer, Map lastIdsPerStream, + XReadGroupArgs args); + + /** + * Execute the command XREVRANGE. + * Summary: This command is exactly like {@code XRANGE}, but with the notable difference of returning the entries + * in reverse order, and also taking the start-end range in reverse order: in {@code XREVRANGE} you need to state + * the end ID and later the start ID, and the command will produce all the element between (or exactly like) the + * two IDs, starting from the end side. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param range the range, must not be {@code null} + * @param count the max number of entries to return + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages from the given range. + */ + List> xrevrange(K key, StreamRange range, int count); + + /** + * Execute the command XRANGE. + * Summary: This command is exactly like {@code XRANGE}, but with the notable difference of returning the entries + * in reverse order, and also taking the start-end range in reverse order: in {@code XREVRANGE} you need to state + * the end ID and later the start ID, and the command will produce all the element between (or exactly like) the + * two IDs, starting from the end side. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param range the range, must not be {@code null} + * @return A {@link io.smallrye.mutiny.Uni} emitting a list containing the messages from the given range. + */ + List> xrevrange(K key, StreamRange range); + + /** + * Execute the command XTRIM. + * Summary: Trims the stream by evicting older entries (entries with lower IDs) if needed. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key the key + * @param threshold the threshold + * @return A {@link io.smallrye.mutiny.Uni} emitting the number of entries deleted from the stream + */ + long xtrim(K key, String threshold); + + /** + * Execute the command XTRIM. + * Summary: Trims the stream by evicting older entries (entries with lower IDs) if needed. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key the key + * @param args the extra parameters + * @return A {@link io.smallrye.mutiny.Uni} emitting the number of entries deleted from the stream + */ + long xtrim(K key, XTrimArgs args); +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamEntry.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamEntry.java new file mode 100644 index 0000000000000..5967d18a46474 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamEntry.java @@ -0,0 +1,14 @@ +package io.quarkus.redis.datasource.stream; + +public class StreamEntry { + + public final String id; + + public final T content; + + public StreamEntry(String id, T content) { + this.id = id; + this.content = content; + } + +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamMessage.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamMessage.java new file mode 100644 index 0000000000000..23349e2574ff4 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamMessage.java @@ -0,0 +1,44 @@ +package io.quarkus.redis.datasource.stream; + +import java.util.Map; + +/** + * Represents a message received from a stream + * + * @param the type of the key + * @param the field type for the payload + * @param the value type for the payload + */ +public class StreamMessage { + + private final K stream; + private final String id; + private final Map payload; + + public StreamMessage(K stream, String id, Map payload) { + this.stream = stream; + this.id = id; + this.payload = payload; + } + + /** + * @return the key of the stream from which the message has been received. + */ + public K key() { + return this.stream; + } + + /** + * @return the stream id, i.e. the id of the message in the stream. + */ + public String id() { + return this.id; + } + + /** + * @return the payload of the message + */ + public Map payload() { + return this.payload; + } +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamRange.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamRange.java new file mode 100644 index 0000000000000..1bf69911cef02 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamRange.java @@ -0,0 +1,34 @@ +package io.quarkus.redis.datasource.stream; + +import java.util.ArrayList; +import java.util.List; + +import io.quarkus.redis.datasource.RedisCommandExtraArguments; +import io.quarkus.redis.runtime.datasource.Validation; + +/** + * Represents a stream range. + */ +public class StreamRange implements RedisCommandExtraArguments { + + private final String lowerBound; + + private final String higherBound; + + public StreamRange(String lowerBound, String higherBound) { + this.lowerBound = Validation.notNullOrBlank(lowerBound, "lowerBound"); + this.higherBound = Validation.notNullOrBlank(higherBound, "higherBound"); + } + + public static StreamRange of(String lowerBound, String higherBound) { + return new StreamRange(lowerBound, higherBound); + } + + @Override + public List toArgs() { + List list = new ArrayList<>(); + list.add(lowerBound); + list.add(higherBound); + return list; + } +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/TransactionalStreamCommands.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/TransactionalStreamCommands.java new file mode 100644 index 0000000000000..a72b9f9a66074 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/TransactionalStreamCommands.java @@ -0,0 +1,554 @@ +package io.quarkus.redis.datasource.stream; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +import io.quarkus.redis.datasource.TransactionalRedisCommands; + +/** + * Allows executing commands manipulating streams. + * See the stream command list for further information about these + * commands. + *

+ *

+ * The messages are represented as {@code Map}. + * This API is intended to be used in a Redis transaction ({@code MULTI}), thus, all command methods return {@code void}. + * + * @param the type of the keys, often {@link String} + * @param the key type of the messages, generally {@link String} + * @param the value type of the messages + */ +public interface TransactionalStreamCommands extends TransactionalRedisCommands { + + /** + * Execute the command XACK. + * Summary: Marks a pending message as correctly processed, effectively removing it from the pending entries list + * of the consumer group. Return value of the command is the number of messages successfully acknowledged, that is, + * the IDs we were actually able to resolve in the PEL. + *

+ * The {@code XACK} command removes one or multiple messages from the Pending Entries List (PEL) of a stream consumer + * group. A message is pending, and as such stored inside the PEL, when it was delivered to some consumer, normally + * as a side effect of calling {@code XREADGROUP}, or when a consumer took ownership of a message + * calling {@code XCLAIM}. The pending message was delivered to some consumer but the server is yet not sure it was + * processed at least once. So new calls to {@code XREADGROUP} to grab the messages history for a consumer + * (for instance using an ID of 0), will return such message. Similarly, the pending message will be listed by the + * {@code XPENDING} command, that inspects the PEL. + *

+ * Once a consumer successfully processes a message, it should call {@code XACK} so that such message does not get + * processed again, and as a side effect, the PEL entry about this message is also purged, releasing memory from + * the Redis server. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key the key + * @param group the name of the consumer group + * @param ids the message ids to acknowledge + */ + void xack(K key, String group, String... ids); + + /** + * Execute the command XADD. + * Summary: Appends the specified stream entry to the stream at the specified key. If the key does not exist, as a + * side effect of running this command the key is created with a stream value. The creation of stream's key can be + * disabled with the {@code NOMKSTREAM} option. + *

+ * An entry is composed of a list of field-value pairs. The field-value pairs are stored in the same order they are + * given by the user. Commands that read the stream, such as {@code XRANGE} or {@code XREAD}, are guaranteed to + * return the fields and values exactly in the same order they were added by {@code XADD}. + *

+ * {@code XADD} is the only Redis command that can add data to a stream, but there are other commands, such as + * {@code XDEL} and {@code XTRIM}, that are able to remove data from a stream. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key the key + * @param payload the payload to write to the stream, must not be {@code null} + */ + void xadd(K key, Map payload); + + /** + * Execute the command XADD. + * Summary: Appends the specified stream entry to the stream at the specified key. If the key does not exist, as a + * side effect of running this command the key is created with a stream value. The creation of stream's key can be + * disabled with the {@code NOMKSTREAM} option. + *

+ * An entry is composed of a list of field-value pairs. The field-value pairs are stored in the same order they are + * given by the user. Commands that read the stream, such as {@code XRANGE} or {@code XREAD}, are guaranteed to + * return the fields and values exactly in the same order they were added by {@code XADD}. + *

+ * {@code XADD} is the only Redis command that can add data to a stream, but there are other commands, such as + * {@code XDEL} and {@code XTRIM}, that are able to remove data from a stream. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key the key + * @param args the extra parameters + * @param payload the payload to write to the stream, must not be {@code null} + */ + void xadd(K key, XAddArgs args, Map payload); + + /** + * Execute the command XAUTOCLAIM. + * Summary: Changes (or acquires) ownership of messages in a consumer group, as if the messages were delivered to + * the specified consumer. + *

+ * This command transfers ownership of pending stream entries that match the specified criteria. Conceptually, + * {@code XAUTOCLAIM} is equivalent to calling {@code XPENDING} and then {@code XCLAIM}, but provides a more + * straightforward way to deal with message delivery failures via {@code SCAN}-like semantics. + *

+ * Like {@code XCLAIM}, the command operates on the stream entries at {@code key} and in the context of the provided + * {@code group}. It transfers ownership to @{code consumer} of messages pending for more than {@code min-idle-time} + * milliseconds and having an equal or greater ID than {@code start}. + *

+ * Group: stream + * Requires Redis 6.2.0+ + *

+ * + * @param key key the key + * @param group string the consumer group + * @param consumer string the consumer id + * @param minIdleTime the min pending time of the message to claim + * @param start the min id of the message to claim + */ + void xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start); + + /** + * Execute the command XAUTOCLAIM. + * Summary: Changes (or acquires) ownership of messages in a consumer group, as if the messages were delivered to + * the specified consumer. + *

+ * This command transfers ownership of pending stream entries that match the specified criteria. Conceptually, + * {@code XAUTOCLAIM} is equivalent to calling {@code XPENDING} and then {@code XCLAIM}, but provides a more + * straightforward way to deal with message delivery failures via {@code SCAN}-like semantics. + *

+ * Like {@code XCLAIM}, the command operates on the stream entries at {@code key} and in the context of the provided + * {@code group}. It transfers ownership to @{code consumer} of messages pending for more than {@code min-idle-time} + * milliseconds and having an equal or greater ID than {@code start}. + *

+ * Group: stream + * Requires Redis 6.2.0+ + *

+ * + * @param key key the key + * @param group string the consumer group + * @param consumer string the consumer id + * @param minIdleTime the min pending time of the message to claim + * @param start the min id of the message to claim + * @param count the upper limit of the number of entries to claim, default is 100. + */ + void xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start, int count); + + /** + * Execute the command XAUTOCLAIM. + * Summary: Changes (or acquires) ownership of messages in a consumer group, as if the messages were delivered to + * the specified consumer. + *

+ * This command transfers ownership of pending stream entries that match the specified criteria. Conceptually, + * {@code XAUTOCLAIM} is equivalent to calling {@code XPENDING} and then {@code XCLAIM}, but provides a more + * straightforward way to deal with message delivery failures via {@code SCAN}-like semantics. + *

+ * Like {@code XCLAIM}, the command operates on the stream entries at {@code key} and in the context of the provided + * {@code group}. It transfers ownership to @{code consumer} of messages pending for more than {@code min-idle-time} + * milliseconds and having an equal or greater ID than {@code start}. + *

+ * Group: stream + * Requires Redis 6.2.0+ + *

+ * + * @param key key the key + * @param group string the consumer group + * @param consumer string the consumer id + * @param minIdleTime the min pending time of the message to claim + * @param start the min id of the message to claim + * @param count the upper limit of the number of entries to claim, default is 100. + * @param justId if {@code true} the returned structure would only contain the id of the messages and not the payloads + */ + void xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start, int count, boolean justId); + + /** + * Execute the command XCLAIM. + * Summary: In the context of a stream consumer group, this command changes the ownership of a pending message, so + * that the new owner is the consumer specified as the command argument. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param group string the consumer group + * @param consumer string the consumer id + * @param minIdleTime the min pending time of the message to claim + * @param id the message ids to claim, must not be empty, must not contain {@code null} + */ + void xclaim(K key, String group, String consumer, Duration minIdleTime, String... id); + + /** + * Execute the command XCLAIM. + * Summary: In the context of a stream consumer group, this command changes the ownership of a pending message, so + * that the new owner is the consumer specified as the command argument. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param group string the consumer group + * @param consumer string the consumer id + * @param minIdleTime the min pending time of the message to claim + * @param args the extra command parameters + * @param id the message ids to claim, must not be empty, must not contain {@code null} + */ + void xclaim(K key, String group, String consumer, Duration minIdleTime, XClaimArgs args, String... id); + + /** + * Execute the command XDEL. + * Summary: Removes the specified entries from a stream, and returns the number of entries deleted. This number may + * be less than the number of IDs passed to the command in the case where some of the specified IDs do not exist in + * the stream. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param id the message ids, must not be empty, must not contain {@code null} + */ + void xdel(K key, String... id); + + /** + * Execute the command XGROUP CREATE. + * Summary: Create a new consumer group uniquely identified by {@code groupname} for the stream stored at {@code key} + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param from the last delivered entry in the stream from the new group's perspective. The special ID {@code $} + * is the ID of the last entry in the stream, but you can substitute it with any valid ID. + */ + void xgroupCreate(K key, String groupname, String from); + + /** + * Execute the command XGROUP CREATE. + * Summary: Create a new consumer group uniquely identified by {@code groupname} for the stream stored at {@code key} + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param from the last delivered entry in the stream from the new group's perspective. The special ID {@code $} + * is the ID of the last entry in the stream, but you can substitute it with any valid ID. + * @param args the extra command parameters + */ + void xgroupCreate(K key, String groupname, String from, XGroupCreateArgs args); + + /** + * Execute the command XGROUP CREATECONSUMER. + * Summary: Create a consumer named {@code consumername} in the consumer group {@code groupname} of the stream + * that's stored at {@code key}. + *

+ * Consumers are also created automatically whenever an operation, such as {@code XREADGROUP}, references a consumer + * that doesn't exist. + *

+ * Group: stream + * Requires Redis 6.2.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param consumername the consumer name + */ + void xgroupCreateConsumer(K key, String groupname, String consumername); + + /** + * Execute the command XGROUP DELCONSUMER. + * Summary: Deletes a consumer from the consumer group. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param consumername the consumer name + */ + void xgroupDelConsumer(K key, String groupname, String consumername); + + /** + * Execute the command XGROUP DESTROY. + * Summary: Completely destroys a consumer group. The consumer group will be destroyed even if there are active + * consumers, and pending messages, so make sure to call this command only when really needed. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + */ + void xgroupDestroy(K key, String groupname); + + /** + * Execute the command XGROUP SETID. + * Summary: Set the last delivered ID for a consumer group. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param from the last delivered entry in the stream from the new group's perspective. The special ID {@code $} + * is the ID of the last entry in the stream, but you can substitute it with any valid ID. + */ + void xgroupSetId(K key, String groupname, String from); + + /** + * Execute the command XGROUP SETID. + * Summary: Set the last delivered ID for a consumer group. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param groupname the name of the group, must be unique, and not {@code null} + * @param from the last delivered entry in the stream from the new group's perspective. The special ID {@code $} + * is the ID of the last entry in the stream, but you can substitute it with any valid ID. + * @param args the extra command parameters + */ + void xgroupSetId(K key, String groupname, String from, XGroupSetIdArgs args); + + /** + * Execute the command XLEN. + * Summary: Returns the number of entries inside a stream. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + */ + void xlen(K key); + + /** + * Execute the command XRANGE. + * Summary: The command returns the stream entries matching a given range of IDs. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param range the range, must not be {@code null} + * @param count the max number of entries to return + */ + void xrange(K key, StreamRange range, int count); + + /** + * Execute the command XRANGE. + * Summary: The command returns the stream entries matching a given range of IDs. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param range the range, must not be {@code null} + */ + void xrange(K key, StreamRange range); + + /** + * Execute the command XREAD. + * Summary: Read data from one or multiple streams, only returning entries with an ID greater than the last received + * ID reported by the caller. This command has an option to block if items are not available, in a similar fashion + * to {@code BRPOP} or {@code BZPOPMIN} and others. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key of the stream + * @param id the last read id + */ + void xread(K key, String id); + + /** + * Execute the command XREAD. + * Summary: Read data from one or multiple streams, only returning entries with an ID greater than the last received + * ID reported by the caller. This command has an option to block if items are not available, in a similar fashion + * to {@code BRPOP} or {@code BZPOPMIN} and others. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param lastIdsPerStream the map of key -> id indicating the last received id per stream to read + */ + void xread(Map lastIdsPerStream); + + /** + * Execute the command XREAD. + * Summary: Read data from one or multiple streams, only returning entries with an ID greater than the last received + * ID reported by the caller. This command has an option to block if items are not available, in a similar fashion + * to {@code BRPOP} or {@code BZPOPMIN} and others. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key of the stream + * @param id the last read id + * @param args the extra parameter + */ + void xread(K key, String id, XReadArgs args); + + /** + * Execute the command XREAD. + * Summary: Read data from one or multiple streams, only returning entries with an ID greater than the last received + * ID reported by the caller. This command has an option to block if items are not available, in a similar fashion + * to {@code BRPOP} or {@code BZPOPMIN} and others. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param lastIdsPerStream the map of key -> id indicating the last received id per stream to read + * @param args the extra parameter + */ + void xread(Map lastIdsPerStream, XReadArgs args); + + /** + * Execute the command XREADGROUP. + * Summary: The {@code XREADGROUP} command is a special version of the {@code XREAD} command with support for + * consumer groups. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param group the group name + * @param consumer the consumer name + * @param key the stream key + * @param id the last read id + */ + void xreadgroup(String group, String consumer, K key, String id); + + /** + * Execute the command XREADGROUP. + * Summary: The {@code XREADGROUP} command is a special version of the {@code XREAD} command with support for + * consumer groups. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param group the group name + * @param consumer the consumer name + * @param lastIdsPerStream the map of key -> id indicating the last received id per stream to read + */ + void xreadgroup(String group, String consumer, Map lastIdsPerStream); + + /** + * Execute the command XREADGROUP. + * Summary: The {@code XREADGROUP} command is a special version of the {@code XREAD} command with support for + * consumer groups. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param group the group name + * @param consumer the consumer name + * @param key the stream key + * @param id the last read id + * @param args the extra parameter + */ + void xreadgroup(String group, String consumer, K key, String id, XReadGroupArgs args); + + /** + * Execute the command XREADGROUP. + * Summary: The {@code XREADGROUP} command is a special version of the {@code XREAD} command with support for + * consumer groups. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param group the group name + * @param consumer the consumer name + * @param lastIdsPerStream the map of key -> id indicating the last received id per stream to read + * @param args the extra parameter + */ + void xreadgroup(String group, String consumer, Map lastIdsPerStream, XReadGroupArgs args); + + /** + * Execute the command XREVRANGE. + * Summary: This command is exactly like {@code XRANGE}, but with the notable difference of returning the entries + * in reverse order, and also taking the start-end range in reverse order: in {@code XREVRANGE} you need to state + * the end ID and later the start ID, and the command will produce all the element between (or exactly like) the + * two IDs, starting from the end side. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param range the range, must not be {@code null} + * @param count the max number of entries to return + */ + void xrevrange(K key, StreamRange range, int count); + + /** + * Execute the command XRANGE. + * Summary: This command is exactly like {@code XRANGE}, but with the notable difference of returning the entries + * in reverse order, and also taking the start-end range in reverse order: in {@code XREVRANGE} you need to state + * the end ID and later the start ID, and the command will produce all the element between (or exactly like) the + * two IDs, starting from the end side. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key key the key + * @param range the range, must not be {@code null} + */ + void xrevrange(K key, StreamRange range); + + /** + * Execute the command XTRIM. + * Summary: Trims the stream by evicting older entries (entries with lower IDs) if needed. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key the key + * @param threshold the threshold + */ + void xtrim(K key, String threshold); + + /** + * Execute the command XTRIM. + * Summary: Trims the stream by evicting older entries (entries with lower IDs) if needed. + *

+ * Group: stream + * Requires Redis 5.0.0+ + *

+ * + * @param key the key + * @param args the extra parameters + */ + void xtrim(K key, XTrimArgs args); +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XAddArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XAddArgs.java new file mode 100644 index 0000000000000..196ebb8a09144 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XAddArgs.java @@ -0,0 +1,137 @@ +package io.quarkus.redis.datasource.stream; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import io.quarkus.redis.datasource.RedisCommandExtraArguments; + +/** + * The argument of the XADD command. + */ +public class XAddArgs implements RedisCommandExtraArguments { + + private String id; + + private long maxlen = -1; + + private boolean approximateTrimming; + + private boolean nomkstream; + + private String minid; + + private long limit = -1; + + /** + * Sets the stream id to identify a given entry inside a stream. + * If not set, the stream id is generated by the Redis server. + * + * @param id the id, must not be {@code null}, but be formed by two numbers separated by a {@code -}. In general, + * the first number is a timestamp. + * @return the current {@code XAddArgs} + */ + public XAddArgs id(String id) { + this.id = id; + return this; + } + + /** + * Sets the max length of the stream. + * When {@code XADD} is called with this parameter, the new entry is added to the stream, but if the max size is + * reached, the oldest entry is evicted. + * + * @param maxlen the max length of the stream, must be positive + * @return the current {@code XAddArgs} + */ + public XAddArgs maxlen(Long maxlen) { + this.maxlen = maxlen; + return this; + } + + /** + * When set, prefix the {@link #maxlen} with {@code ~} to enable the almost exact trimming. + * This is recommended when using {@link #maxlen(Long)}. + * + * @return the current {@code XAddArgs} + */ + public XAddArgs nearlyExactTrimming() { + this.approximateTrimming = true; + return this; + } + + /** + * Do not create a new stream if the stream does not exist yet. + * + * @return the current {@code XAddArgs} + */ + public XAddArgs nomkstream() { + this.nomkstream = true; + return this; + } + + /** + * Evicts entries from the stream having IDs lower to the specified one. + * + * @param minid the min id, must not be {@code null}, must be a valid stream id + * @return the current {@code XAddArgs} + */ + public XAddArgs minid(String minid) { + this.minid = minid; + return this; + } + + /** + * Sets the maximum entries that can get evicted. + * + * @param limit the limit, must be positive + * @return the current {@code XAddArgs} + */ + public XAddArgs limit(long limit) { + this.limit = limit; + return this; + } + + @Override + public List toArgs() { + List args = new ArrayList<>(); + if (nomkstream) { + args.add("NOMKSTREAM"); + } + + if (maxlen > 0) { + if (minid != null) { + throw new IllegalArgumentException("Cannot use `MAXLEN` and `MINID` together"); + } + + args.add("MAXLEN"); + if (approximateTrimming) { + args.add("~"); + } else { + args.add("="); + } + args.add(Long.toString(maxlen)); + } + + if (minid != null) { + args.add("MINID"); + if (approximateTrimming) { + args.add("~"); + } else { + args.add("="); + } + args.add(minid); + } + + if (limit > 0) { + if (!approximateTrimming) { + throw new IllegalArgumentException("Cannot set the eviction limit when using exact trimming"); + } + args.add("LIMIT"); + args.add(Long.toString(limit)); + } + + args.add(Objects.requireNonNullElse(id, "*")); + return args; + } +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XClaimArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XClaimArgs.java new file mode 100644 index 0000000000000..3538cc9c81687 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XClaimArgs.java @@ -0,0 +1,136 @@ +package io.quarkus.redis.datasource.stream; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import io.quarkus.redis.datasource.RedisCommandExtraArguments; + +/** + * The argument of the XCLAIM command. + */ +public class XClaimArgs implements RedisCommandExtraArguments { + + private Duration idle; + + private long time = -1; + + private int retryCount = -1; + + private boolean force; + + private boolean justId; + + private String lastId; + + /** + * Set the idle time (last time it was delivered) of the message. If {@code IDLE} is not specified, an {@code IDLE} + * of 0 is assumed, that is, the time count is reset because the message has now a new owner trying to process it. + * + * @param idle the idle duration, must not be {@code null} + * @return the current {@code XClaimArgs} + */ + public XClaimArgs idle(Duration idle) { + this.idle = idle; + return this; + } + + /** + * This is the same as {@code IDLE} but instead of a relative amount of milliseconds, it sets the idle time to a + * specific Unix time (in milliseconds). This is useful in order to rewrite the {@code AOF} file + * generating {@code XCLAIM} commands. + * + * @param time the timestamp + * @return the current {@code XClaimArgs} + */ + public XClaimArgs time(long time) { + this.time = time; + return this; + } + + /** + * Set the retry counter to the specified value. This counter is incremented every time a message is delivered again. + * Normally {@code XCLAIM} does not alter this counter, which is just served to clients when the {@code XPENDING} + * command is called: this way clients can detect anomalies, like messages that are never processed for some reason + * after a big number of delivery attempts. + * + * @param retryCount the retry count, must be positive + * @return the current {@code XClaimArgs} + */ + public XClaimArgs retryCount(int retryCount) { + this.retryCount = retryCount; + return this; + } + + /** + * Creates the pending message entry in the PEL even if certain specified IDs are not already in the PEL assigned + * to a different client. + * However, the message must exist in the stream, otherwise the IDs of non-existing messages are ignored. + * + * @return the current {@code XClaimArgs} + */ + public XClaimArgs force() { + this.force = true; + return this; + } + + /** + * In the returned structure, only set the IDs of messages successfully claimed, without returning the actual message. + * Using this option means the retry counter is not incremented. + * + * @return the current {@code XClaimArgs} + */ + public XClaimArgs justId() { + this.justId = true; + return this; + } + + /** + * Sets the last id of the message to claim. + * + * @param lastId the last id, must not be {@code null} + * @return the current {@code XClaimArgs} + */ + public XClaimArgs lastId(String lastId) { + this.lastId = lastId; + return this; + } + + @Override + public List toArgs() { + List args = new ArrayList<>(); + + if (idle != null) { + args.add("IDLE"); + args.add(Long.toString(idle.toMillis())); + if (time > 0) { + throw new IllegalStateException("Cannot combine `IDLE` and `TIME`"); + } + } + + if (time > 0) { + args.add("TIME"); + args.add(Long.toString(time)); + } + + if (retryCount > 0) { + args.add("RETRYCOUNT"); + args.add(Integer.toString(retryCount)); + } + + if (force) { + args.add("FORCE"); + } + + if (justId) { + args.add("JUSTID"); + } + + if (lastId != null) { + args.add("LASTID"); + args.add(lastId); + } + + return args; + } +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupCreateArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupCreateArgs.java new file mode 100644 index 0000000000000..db51b9b474364 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupCreateArgs.java @@ -0,0 +1,55 @@ +package io.quarkus.redis.datasource.stream; + +import java.util.ArrayList; +import java.util.List; + +import io.quarkus.redis.datasource.RedisCommandExtraArguments; + +/** + * Represents the extra argument of the + * Requires REdis 7.0.0+ + * + * @param id the arbitrary id + * @return the current {@code XGroupCreateArgs} + */ + public XGroupCreateArgs entriesRead(String id) { + this.entriesRead = id; + return this; + } + + @Override + public List toArgs() { + List args = new ArrayList<>(); + if (mkstream) { + args.add("MKSTREAM"); + } + if (entriesRead != null) { + args.add("ENTRIESREAD"); + args.add(entriesRead); + } + + return args; + } +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupSetIdArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupSetIdArgs.java new file mode 100644 index 0000000000000..f5a00766e58f4 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupSetIdArgs.java @@ -0,0 +1,41 @@ +package io.quarkus.redis.datasource.stream; + +import java.util.ArrayList; +import java.util.List; + +import io.quarkus.redis.datasource.RedisCommandExtraArguments; + +/** + * Represents the extra argument of the + * Requires REdis 7.0.0+ + * + * @param id the arbitrary id + * @return the current {@code XGroupCreateArgs} + */ + public XGroupSetIdArgs entriesRead(long id) { + this.entriesRead = id; + return this; + } + + @Override + public List toArgs() { + List args = new ArrayList<>(); + if (entriesRead > 0) { + args.add("ENTRIESREAD"); + args.add(Long.toString(entriesRead)); + } + + return args; + } +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XReadArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XReadArgs.java new file mode 100644 index 0000000000000..630b89b22f4ea --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XReadArgs.java @@ -0,0 +1,55 @@ +package io.quarkus.redis.datasource.stream; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import io.quarkus.redis.datasource.RedisCommandExtraArguments; + +/** + * Represents the extra parameter of the XTRIM command. + */ +public class XTrimArgs implements RedisCommandExtraArguments { + + private long maxlen = -1; + + private boolean approximateTrimming; + + private String minid; + + private long limit = -1; + + /** + * Sets the max length of the stream. + * + * @param maxlen the max length of the stream, must be positive + * @return the current {@code XAddArgs} + */ + public XTrimArgs maxlen(long maxlen) { + this.maxlen = maxlen; + return this; + } + + /** + * When set, prefix the {@link #maxlen} with {@code ~} to enable the almost exact trimming. + * This is recommended when using {@link #maxlen(long)}. + * + * @return the current {@code XAddArgs} + */ + public XTrimArgs nearlyExactTrimming() { + this.approximateTrimming = true; + return this; + } + + /** + * Evicts entries from the stream having IDs lower to the specified one. + * + * @param minid the min id, must not be {@code null}, must be a valid stream id + * @return the current {@code XAddArgs} + */ + public XTrimArgs minid(String minid) { + this.minid = minid; + return this; + } + + /** + * Sets the maximum entries that can get evicted. + * + * @param limit the limit, must be positive + * @return the current {@code XAddArgs} + */ + public XTrimArgs limit(long limit) { + this.limit = limit; + return this; + } + + @Override + public List toArgs() { + List args = new ArrayList<>(); + + if (maxlen > 0) { + if (minid != null) { + throw new IllegalArgumentException("Cannot use `MAXLEN` and `MINID` together"); + } + + args.add("MAXLEN"); + if (approximateTrimming) { + args.add("~"); + } else { + args.add("="); + } + args.add(Long.toString(maxlen)); + } + + if (minid != null) { + args.add("MINID"); + if (approximateTrimming) { + args.add("~"); + } else { + args.add("="); + } + args.add(minid); + } + + if (limit > 0) { + if (!approximateTrimming) { + throw new IllegalArgumentException("Cannot set the eviction limit when using exact trimming"); + } + args.add("LIMIT"); + args.add(Long.toString(limit)); + } + + return args; + } +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/transactions/ReactiveTransactionalRedisDataSource.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/transactions/ReactiveTransactionalRedisDataSource.java index 3afa07c073ada..580de0d4b112a 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/transactions/ReactiveTransactionalRedisDataSource.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/transactions/ReactiveTransactionalRedisDataSource.java @@ -15,6 +15,7 @@ import io.quarkus.redis.datasource.search.ReactiveTransactionalSearchCommands; import io.quarkus.redis.datasource.set.ReactiveTransactionalSetCommands; import io.quarkus.redis.datasource.sortedset.ReactiveTransactionalSortedSetCommands; +import io.quarkus.redis.datasource.stream.ReactiveTransactionalStreamCommands; import io.quarkus.redis.datasource.string.ReactiveTransactionalStringCommands; import io.quarkus.redis.datasource.timeseries.ReactiveTransactionalTimeSeriesCommands; import io.quarkus.redis.datasource.topk.ReactiveTransactionalTopKCommands; @@ -279,6 +280,34 @@ default ReactiveTransactionalBitMapCommands bitmap() { return bitmap(String.class); } + /** + * Gets the object to execute commands manipulating streams. + *

+ * + * @param redisKeyType the class of the keys + * @param typeOfField the class of the fields + * @param typeOfValue the class of the values + * @param the type of the redis key + * @param the type of the fields (map's keys) + * @param the type of the value + * @return the object to execute commands manipulating streams. + */ + ReactiveTransactionalStreamCommands stream(Class redisKeyType, Class typeOfField, + Class typeOfValue); + + /** + * Gets the object to execute commands manipulating stream.. + *

+ * This is a shortcut on {@code stream(String.class, String.class, V)} + * + * @param typeOfValue the class of the values + * @param the type of the value + * @return the object to execute commands manipulating streams. + */ + default ReactiveTransactionalStreamCommands stream(Class typeOfValue) { + return stream(String.class, String.class, typeOfValue); + } + /** * Gets the object to manipulate JSON values. * This group requires the RedisJSON module. diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/transactions/TransactionalRedisDataSource.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/transactions/TransactionalRedisDataSource.java index 69ddfa28b208a..948014527dc8f 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/transactions/TransactionalRedisDataSource.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/transactions/TransactionalRedisDataSource.java @@ -15,6 +15,7 @@ import io.quarkus.redis.datasource.search.TransactionalSearchCommands; import io.quarkus.redis.datasource.set.TransactionalSetCommands; import io.quarkus.redis.datasource.sortedset.TransactionalSortedSetCommands; +import io.quarkus.redis.datasource.stream.TransactionalStreamCommands; import io.quarkus.redis.datasource.string.TransactionalStringCommands; import io.quarkus.redis.datasource.timeseries.TransactionalTimeSeriesCommands; import io.quarkus.redis.datasource.topk.TransactionalTopKCommands; @@ -277,6 +278,34 @@ default TransactionalBitMapCommands bitmap() { return bitmap(String.class); } + /** + * Gets the object to execute commands manipulating streams. + *

+ * + * @param redisKeyType the class of the keys + * @param typeOfField the class of the fields + * @param typeOfValue the class of the values + * @param the type of the redis key + * @param the type of the fields (map's keys) + * @param the type of the value + * @return the object to execute commands manipulating streams. + */ + TransactionalStreamCommands stream(Class redisKeyType, Class typeOfField, + Class typeOfValue); + + /** + * Gets the object to execute commands manipulating stream.. + *

+ * This is a shortcut on {@code stream(String.class, String.class, V)} + * + * @param typeOfValue the class of the values + * @param the type of the value + * @return the object to execute commands manipulating streams. + */ + default TransactionalStreamCommands stream(Class typeOfValue) { + return stream(String.class, String.class, typeOfValue); + } + /** * Gets the object to manipulate JSON values. * This group requires the RedisJSON module. diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractStreamCommands.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractStreamCommands.java new file mode 100644 index 0000000000000..18083a00de470 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractStreamCommands.java @@ -0,0 +1,430 @@ +package io.quarkus.redis.runtime.datasource; + +import static io.quarkus.redis.runtime.datasource.Validation.notNullOrBlank; +import static io.quarkus.redis.runtime.datasource.Validation.notNullOrEmpty; +import static io.quarkus.redis.runtime.datasource.Validation.positive; +import static io.smallrye.mutiny.helpers.ParameterValidation.doesNotContainNull; +import static io.smallrye.mutiny.helpers.ParameterValidation.nonNull; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import io.quarkus.redis.datasource.stream.StreamRange; +import io.quarkus.redis.datasource.stream.XAddArgs; +import io.quarkus.redis.datasource.stream.XClaimArgs; +import io.quarkus.redis.datasource.stream.XGroupCreateArgs; +import io.quarkus.redis.datasource.stream.XGroupSetIdArgs; +import io.quarkus.redis.datasource.stream.XReadArgs; +import io.quarkus.redis.datasource.stream.XReadGroupArgs; +import io.quarkus.redis.datasource.stream.XTrimArgs; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.helpers.ParameterValidation; +import io.vertx.mutiny.redis.client.Command; +import io.vertx.mutiny.redis.client.Response; + +public class AbstractStreamCommands extends AbstractRedisCommands { + + AbstractStreamCommands(RedisCommandExecutor redis, Class k, Class m, Class v) { + super(redis, new Marshaller(k, m, v)); + } + + Uni _xack(K key, String group, String... ids) { + nonNull(key, "key"); + nonNull(group, "group"); + notNullOrEmpty(ids, "ids"); + doesNotContainNull(ids, "ids"); + + RedisCommand cmd = RedisCommand.of(Command.XACK) + .put(marshaller.encode(key)) + .put(group) + .putAll(ids); + return execute(cmd); + } + + Uni _xadd(K key, Map payload) { + return _xadd(key, new XAddArgs(), payload); + } + + Uni _xadd(K key, XAddArgs args, Map payload) { + nonNull(key, "key"); + nonNull(args, "args"); + nonNull(payload, "payload"); + RedisCommand cmd = RedisCommand.of(Command.XADD) + .put(marshaller.encode(key)) + .putArgs(args); + for (Map.Entry entry : payload.entrySet()) { + cmd.put(marshaller.encode(entry.getKey())); + cmd.putNullable(marshaller.encode(entry.getValue())); + } + return execute(cmd); + } + + Uni _xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start, int count) { + nonNull(key, "key"); + notNullOrBlank(group, "group"); + notNullOrBlank(consumer, "consumer"); + ParameterValidation.validate(minIdleTime, "minIdleTime"); + notNullOrBlank(start, "start"); + positive(count, "count"); + RedisCommand cmd = RedisCommand.of(Command.XAUTOCLAIM) + .put(marshaller.encode(key)) + .put(group).put(consumer).put(minIdleTime.toMillis()).put(start) + .put("COUNT").put(count); + + return execute(cmd); + } + + Uni _xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start) { + nonNull(key, "key"); + notNullOrBlank(group, "group"); + notNullOrBlank(consumer, "consumer"); + ParameterValidation.validate(minIdleTime, "minIdleTime"); + notNullOrBlank(start, "start"); + RedisCommand cmd = RedisCommand.of(Command.XAUTOCLAIM) + .put(marshaller.encode(key)) + .put(group).put(consumer).put(minIdleTime.toMillis()).put(start); + + return execute(cmd); + } + + Uni _xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start, int count, + boolean justId) { + nonNull(key, "key"); + notNullOrBlank(group, "group"); + notNullOrBlank(consumer, "consumer"); + ParameterValidation.validate(minIdleTime, "minIdleTime"); + notNullOrBlank(start, "start"); + positive(count, "count"); + RedisCommand cmd = RedisCommand.of(Command.XAUTOCLAIM) + .put(marshaller.encode(key)) + .put(group).put(consumer).put(minIdleTime.toMillis()).put(start); + if (count > 0) { + cmd.put("COUNT").put(count); + } + if (justId) { + cmd.put("JUSTID"); + } + return execute(cmd); + } + + Uni _xclaim(K key, String group, String consumer, Duration minIdleTime, String... id) { + nonNull(key, "key"); + notNullOrBlank(group, "group"); + notNullOrBlank(consumer, "consumer"); + ParameterValidation.validate(minIdleTime, "minIdleTime"); + notNullOrEmpty(id, "id"); + doesNotContainNull(id, "id"); + + RedisCommand cmd = RedisCommand.of(Command.XCLAIM) + .put(marshaller.encode(key)) + .put(group) + .put(consumer) + .put(Long.toString(minIdleTime.toMillis())) + .putAll(id); + return execute(cmd); + } + + Uni _xclaim(K key, String group, String consumer, Duration minIdleTime, XClaimArgs args, String... id) { + nonNull(key, "key"); + notNullOrBlank(group, "group"); + notNullOrBlank(consumer, "consumer"); + ParameterValidation.validate(minIdleTime, "minIdleTime"); + nonNull(args, "args"); + notNullOrEmpty(id, "id"); + doesNotContainNull(id, "id"); + + RedisCommand cmd = RedisCommand.of(Command.XCLAIM) + .put(marshaller.encode(key)) + .put(group) + .put(consumer) + .put(Long.toString(minIdleTime.toMillis())) + .putAll(id) + .putArgs(args); + return execute(cmd); + } + + Uni _xdel(K key, String... id) { + nonNull(key, "key"); + notNullOrEmpty(id, "id"); + doesNotContainNull(id, "id"); + + RedisCommand cmd = RedisCommand.of(Command.XDEL) + .put(marshaller.encode(key)) + .putAll(id); + return execute(cmd); + } + + Uni _xgroupCreate(K key, String groupname, String from) { + nonNull(key, "key"); + notNullOrBlank(groupname, "groupname"); + notNullOrBlank(from, "from"); + + RedisCommand cmd = RedisCommand.of(Command.XGROUP) + .put("CREATE") + .put(marshaller.encode(key)) + .put(groupname) + .put(from); + return execute(cmd); + } + + Uni _xgroupCreate(K key, String groupname, String from, XGroupCreateArgs args) { + nonNull(key, "key"); + notNullOrBlank(groupname, "groupname"); + notNullOrBlank(from, "from"); + nonNull(args, "args"); + + RedisCommand cmd = RedisCommand.of(Command.XGROUP) + .put("CREATE") + .put(marshaller.encode(key)) + .put(groupname) + .put(from) + .putArgs(args); + return execute(cmd); + } + + Uni _xgroupCreateConsumer(K key, String groupname, String consumername) { + nonNull(key, "key"); + notNullOrBlank(groupname, "groupname"); + notNullOrBlank(consumername, "consumername"); + RedisCommand cmd = RedisCommand.of(Command.XGROUP) + .put("CREATECONSUMER") + .put(marshaller.encode(key)) + .put(groupname) + .put(consumername); + return execute(cmd); + } + + Uni _xgroupDelConsumer(K key, String groupname, String consumername) { + nonNull(key, "key"); + notNullOrBlank(groupname, "groupname"); + notNullOrBlank(consumername, "consumername"); + RedisCommand cmd = RedisCommand.of(Command.XGROUP) + .put("DELCONSUMER") + .put(marshaller.encode(key)) + .put(groupname) + .put(consumername); + return execute(cmd); + } + + Uni _xgroupDestroy(K key, String groupname) { + nonNull(key, "key"); + notNullOrBlank(groupname, "groupname"); + + RedisCommand cmd = RedisCommand.of(Command.XGROUP) + .put("DESTROY") + .put(marshaller.encode(key)) + .put(groupname); + return execute(cmd); + } + + Uni _xgroupSetId(K key, String groupname, String from) { + nonNull(key, "key"); + notNullOrBlank(groupname, "groupname"); + notNullOrBlank(from, "from"); + + RedisCommand cmd = RedisCommand.of(Command.XGROUP) + .put("SETID") + .put(marshaller.encode(key)) + .put(groupname) + .put(from); + return execute(cmd); + } + + Uni _xgroupSetId(K key, String groupname, String from, XGroupSetIdArgs args) { + nonNull(key, "key"); + notNullOrBlank(groupname, "groupname"); + notNullOrBlank(from, "from"); + nonNull(args, "args"); + + RedisCommand cmd = RedisCommand.of(Command.XGROUP) + .put("SETID") + .put(marshaller.encode(key)) + .put(groupname) + .put(from) + .putArgs(args); + return execute(cmd); + } + + Uni _xlen(K key) { + nonNull(key, "key"); + RedisCommand cmd = RedisCommand.of(Command.XLEN) + .put(marshaller.encode(key)); + return execute(cmd); + } + + Uni _xrange(K key, StreamRange range, int count) { + nonNull(key, "key"); + nonNull(range, "range"); + positive(count, "count"); + RedisCommand cmd = RedisCommand.of(Command.XRANGE) + .put(marshaller.encode(key)) + .putArgs(range) + .put("COUNT").put(count); + return execute(cmd); + } + + Uni _xrange(K key, StreamRange range) { + nonNull(key, "key"); + nonNull(range, "range"); + RedisCommand cmd = RedisCommand.of(Command.XRANGE) + .put(marshaller.encode(key)) + .putArgs(range); + return execute(cmd); + } + + Uni _xread(K key, String id) { + nonNull(key, "key"); + notNullOrBlank(id, "id"); + RedisCommand cmd = RedisCommand.of(Command.XREAD) + .put("STREAMS") + .put(marshaller.encode(key)) + .put(id); + return execute(cmd); + } + + Uni _xread(Map lastIdsPerStream) { + nonNull(lastIdsPerStream, "lastIdsPerStream"); + RedisCommand cmd = RedisCommand.of(Command.XREAD) + .put("STREAMS"); + + writeStreamsAndIds(lastIdsPerStream, cmd); + + return execute(cmd); + } + + Uni _xread(K key, String id, XReadArgs args) { + nonNull(key, "key"); + notNullOrBlank(id, "id"); + nonNull(args, "args"); + RedisCommand cmd = RedisCommand.of(Command.XREAD) + .putArgs(args) + .put("STREAMS") + .put(marshaller.encode(key)) + .put(id); + return execute(cmd); + } + + Uni _xread(Map lastIdsPerStream, XReadArgs args) { + nonNull(args, "args"); + RedisCommand cmd = RedisCommand.of(Command.XREAD) + .putArgs(args) + .put("STREAMS"); + + writeStreamsAndIds(lastIdsPerStream, cmd); + + return execute(cmd); + } + + private void writeStreamsAndIds(Map lastIdsPerStream, RedisCommand cmd) { + List ids = new ArrayList<>(); + for (Map.Entry entry : lastIdsPerStream.entrySet()) { + cmd.put(marshaller.encode(entry.getKey())); + ids.add(entry.getValue()); + } + cmd.putAll(ids); + } + + Uni _xreadgroup(String group, String consumer, K key, String id) { + notNullOrBlank(group, "group"); + notNullOrBlank(consumer, "consumer"); + nonNull(key, "key"); + notNullOrBlank(id, "id"); + + RedisCommand cmd = RedisCommand.of(Command.XREADGROUP) + .put("GROUP") + .put(group) + .put(consumer) + .put("STREAMS") + .put(marshaller.encode(key)) + .put(id); + return execute(cmd); + } + + Uni _xreadgroup(String group, String consumer, Map lastIdsPerStream) { + notNullOrBlank(group, "group"); + notNullOrBlank(consumer, "consumer"); + nonNull(lastIdsPerStream, "lastIdsPerStream"); + + RedisCommand cmd = RedisCommand.of(Command.XREADGROUP) + .put("GROUP") + .put(group) + .put(consumer) + .put("STREAMS"); + + writeStreamsAndIds(lastIdsPerStream, cmd); + + return execute(cmd); + + } + + Uni _xreadgroup(String group, String consumer, K key, String id, XReadGroupArgs args) { + notNullOrBlank(group, "group"); + notNullOrBlank(consumer, "consumer"); + nonNull(key, "key"); + notNullOrBlank(id, "id"); + nonNull(args, "args"); + + RedisCommand cmd = RedisCommand.of(Command.XREADGROUP) + .put("GROUP") + .put(group) + .put(consumer) + .putArgs(args) + .put("STREAMS") + .put(marshaller.encode(key)) + .put(id); + return execute(cmd); + } + + Uni _xreadgroup(String group, String consumer, Map lastIdsPerStream, XReadGroupArgs args) { + notNullOrBlank(group, "group"); + notNullOrBlank(consumer, "consumer"); + nonNull(lastIdsPerStream, "lastIdsPerStream"); + nonNull(args, "args"); + + RedisCommand cmd = RedisCommand.of(Command.XREADGROUP) + .put("GROUP") + .put(group) + .put(consumer) + .putArgs(args) + .put("STREAMS"); + + writeStreamsAndIds(lastIdsPerStream, cmd); + + return execute(cmd); + } + + Uni _xrevrange(K key, StreamRange range, int count) { + nonNull(key, "key"); + nonNull(range, "range"); + positive(count, "count"); + + RedisCommand cmd = RedisCommand.of(Command.XREVRANGE) + .put(marshaller.encode(key)) + .putArgs(range) + .put("COUNT") + .put(count); + return execute(cmd); + } + + Uni _xrevrange(K key, StreamRange range) { + nonNull(key, "key"); + nonNull(range, "range"); + RedisCommand cmd = RedisCommand.of(Command.XREVRANGE) + .put(marshaller.encode(key)) + .putArgs(range); + return execute(cmd); + } + + Uni _xtrim(K key, XTrimArgs args) { + nonNull(key, "key"); + nonNull(args, "args"); + + RedisCommand cmd = RedisCommand.of(Command.XTRIM) + .put(marshaller.encode(key)) + .putArgs(args); + + return execute(cmd); + } +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingRedisDataSourceImpl.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingRedisDataSourceImpl.java index 36c4c420016d3..3a54d936bd2c2 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingRedisDataSourceImpl.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingRedisDataSourceImpl.java @@ -25,6 +25,7 @@ import io.quarkus.redis.datasource.search.SearchCommands; import io.quarkus.redis.datasource.set.SetCommands; import io.quarkus.redis.datasource.sortedset.SortedSetCommands; +import io.quarkus.redis.datasource.stream.StreamCommands; import io.quarkus.redis.datasource.string.StringCommands; import io.quarkus.redis.datasource.timeseries.TimeSeriesCommands; import io.quarkus.redis.datasource.topk.TopKCommands; @@ -230,6 +231,11 @@ public BitMapCommands bitmap(Class redisKeyType) { return new BlockingBitmapCommandsImpl<>(this, reactive.bitmap(redisKeyType), timeout); } + @Override + public StreamCommands stream(Class redisKeyType, Class fieldType, Class valueType) { + return new BlockingStreamCommandsImpl<>(this, reactive.stream(redisKeyType, fieldType, valueType), timeout); + } + @Override public JsonCommands json(Class redisKeyType) { return new BlockingJsonCommandsImpl<>(this, reactive.json(redisKeyType), timeout); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingStreamCommandsImpl.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingStreamCommandsImpl.java new file mode 100644 index 0000000000000..da8f62c8048cb --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingStreamCommandsImpl.java @@ -0,0 +1,188 @@ +package io.quarkus.redis.runtime.datasource; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.redis.datasource.stream.ClaimedMessages; +import io.quarkus.redis.datasource.stream.ReactiveStreamCommands; +import io.quarkus.redis.datasource.stream.StreamCommands; +import io.quarkus.redis.datasource.stream.StreamMessage; +import io.quarkus.redis.datasource.stream.StreamRange; +import io.quarkus.redis.datasource.stream.XAddArgs; +import io.quarkus.redis.datasource.stream.XClaimArgs; +import io.quarkus.redis.datasource.stream.XGroupCreateArgs; +import io.quarkus.redis.datasource.stream.XGroupSetIdArgs; +import io.quarkus.redis.datasource.stream.XReadArgs; +import io.quarkus.redis.datasource.stream.XReadGroupArgs; +import io.quarkus.redis.datasource.stream.XTrimArgs; + +public class BlockingStreamCommandsImpl extends AbstractRedisCommandGroup implements StreamCommands { + + private final ReactiveStreamCommands reactive; + + public BlockingStreamCommandsImpl(RedisDataSource ds, ReactiveStreamCommands reactive, Duration timeout) { + super(ds, timeout); + this.reactive = reactive; + } + + @Override + public int xack(K key, String group, String... ids) { + return reactive.xack(key, group, ids).await().atMost(timeout); + } + + @Override + public String xadd(K key, Map payload) { + return reactive.xadd(key, payload).await().atMost(timeout); + } + + @Override + public String xadd(K key, XAddArgs args, Map payload) { + return reactive.xadd(key, args, payload).await().atMost(timeout); + } + + @Override + public ClaimedMessages xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start, + int count) { + return reactive.xautoclaim(key, group, consumer, minIdleTime, start, count).await().atMost(timeout); + } + + @Override + public ClaimedMessages xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start) { + return reactive.xautoclaim(key, group, consumer, minIdleTime, start).await().atMost(timeout); + } + + @Override + public ClaimedMessages xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start, + int count, boolean justId) { + return reactive.xautoclaim(key, group, consumer, minIdleTime, start, count, justId).await().atMost(timeout); + } + + @Override + public List> xclaim(K key, String group, String consumer, Duration minIdleTime, String... id) { + return reactive.xclaim(key, group, consumer, minIdleTime, id).await().atMost(timeout); + } + + @Override + public List> xclaim(K key, String group, String consumer, Duration minIdleTime, XClaimArgs args, + String... id) { + return reactive.xclaim(key, group, consumer, minIdleTime, args, id).await().atMost(timeout); + } + + @Override + public int xdel(K key, String... id) { + return reactive.xdel(key, id).await().atMost(timeout); + } + + @Override + public void xgroupCreate(K key, String groupname, String from) { + reactive.xgroupCreate(key, groupname, from).await().atMost(timeout); + } + + @Override + public void xgroupCreate(K key, String groupname, String from, XGroupCreateArgs args) { + reactive.xgroupCreate(key, groupname, from, args).await().atMost(timeout); + } + + @Override + public boolean xgroupCreateConsumer(K key, String groupname, String consumername) { + return reactive.xgroupCreateConsumer(key, groupname, consumername).await().atMost(timeout); + } + + @Override + public long xgroupDelConsumer(K key, String groupname, String consumername) { + return reactive.xgroupDelConsumer(key, groupname, consumername).await().atMost(timeout); + } + + @Override + public boolean xgroupDestroy(K key, String groupname) { + return reactive.xgroupDestroy(key, groupname).await().atMost(timeout); + } + + @Override + public void xgroupSetId(K key, String groupname, String from) { + reactive.xgroupSetId(key, groupname, from).await().atMost(timeout); + } + + @Override + public void xgroupSetId(K key, String groupname, String from, XGroupSetIdArgs args) { + reactive.xgroupSetId(key, groupname, from, args).await().atMost(timeout); + } + + @Override + public long xlen(K key) { + return reactive.xlen(key).await().atMost(timeout); + } + + @Override + public List> xrange(K key, StreamRange range, int count) { + return reactive.xrange(key, range, count).await().atMost(timeout); + } + + @Override + public List> xrange(K key, StreamRange range) { + return reactive.xrange(key, range).await().atMost(timeout); + } + + @Override + public List> xread(K key, String id) { + return reactive.xread(key, id).await().atMost(timeout); + } + + @Override + public List> xread(Map lastIdsPerStream) { + return reactive.xread(lastIdsPerStream).await().atMost(timeout); + } + + @Override + public List> xread(K key, String id, XReadArgs args) { + return reactive.xread(key, id, args).await().atMost(timeout); + } + + @Override + public List> xread(Map lastIdsPerStream, XReadArgs args) { + return reactive.xread(lastIdsPerStream, args).await().atMost(timeout); + } + + @Override + public List> xreadgroup(String group, String consumer, K key, String id) { + return reactive.xreadgroup(group, consumer, key, id).await().atMost(timeout); + } + + @Override + public List> xreadgroup(String group, String consumer, Map lastIdsPerStream) { + return reactive.xreadgroup(group, consumer, lastIdsPerStream).await().atMost(timeout); + } + + @Override + public List> xreadgroup(String group, String consumer, K key, String id, XReadGroupArgs args) { + return reactive.xreadgroup(group, consumer, key, id, args).await().atMost(timeout); + } + + @Override + public List> xreadgroup(String group, String consumer, Map lastIdsPerStream, + XReadGroupArgs args) { + return reactive.xreadgroup(group, consumer, lastIdsPerStream, args).await().atMost(timeout); + } + + @Override + public List> xrevrange(K key, StreamRange range, int count) { + return reactive.xrevrange(key, range, count).await().atMost(timeout); + } + + @Override + public List> xrevrange(K key, StreamRange range) { + return reactive.xrevrange(key, range).await().atMost(timeout); + } + + @Override + public long xtrim(K key, String threshold) { + return reactive.xtrim(key, threshold).await().atMost(timeout); + } + + @Override + public long xtrim(K key, XTrimArgs args) { + return reactive.xtrim(key, args).await().atMost(timeout); + } +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingTransactionalRedisDataSourceImpl.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingTransactionalRedisDataSourceImpl.java index 82ef33163733b..81e3f8b6d27e2 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingTransactionalRedisDataSourceImpl.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingTransactionalRedisDataSourceImpl.java @@ -17,6 +17,7 @@ import io.quarkus.redis.datasource.search.TransactionalSearchCommands; import io.quarkus.redis.datasource.set.TransactionalSetCommands; import io.quarkus.redis.datasource.sortedset.TransactionalSortedSetCommands; +import io.quarkus.redis.datasource.stream.TransactionalStreamCommands; import io.quarkus.redis.datasource.string.TransactionalStringCommands; import io.quarkus.redis.datasource.timeseries.TransactionalTimeSeriesCommands; import io.quarkus.redis.datasource.topk.TransactionalTopKCommands; @@ -98,6 +99,13 @@ public TransactionalBitMapCommands bitmap(Class redisKeyType) { return new BlockingTransactionalBitMapCommandsImpl<>(this, reactive.bitmap(redisKeyType), timeout); } + @Override + public TransactionalStreamCommands stream(Class redisKeyType, Class typeOfField, + Class typeOfValue) { + return new BlockingTransactionalStreamCommandsImpl<>(this, reactive.stream(redisKeyType, typeOfField, typeOfValue), + timeout); + } + @Override public TransactionalJsonCommands json(Class redisKeyType) { return new BlockingTransactionalJsonCommandsImpl<>(this, reactive.json(redisKeyType), timeout); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingTransactionalStreamCommandsImpl.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingTransactionalStreamCommandsImpl.java new file mode 100644 index 0000000000000..2c4854c930447 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/BlockingTransactionalStreamCommandsImpl.java @@ -0,0 +1,184 @@ +package io.quarkus.redis.runtime.datasource; + +import java.time.Duration; +import java.util.Map; + +import io.quarkus.redis.datasource.stream.ReactiveTransactionalStreamCommands; +import io.quarkus.redis.datasource.stream.StreamRange; +import io.quarkus.redis.datasource.stream.TransactionalStreamCommands; +import io.quarkus.redis.datasource.stream.XAddArgs; +import io.quarkus.redis.datasource.stream.XClaimArgs; +import io.quarkus.redis.datasource.stream.XGroupCreateArgs; +import io.quarkus.redis.datasource.stream.XGroupSetIdArgs; +import io.quarkus.redis.datasource.stream.XReadArgs; +import io.quarkus.redis.datasource.stream.XReadGroupArgs; +import io.quarkus.redis.datasource.stream.XTrimArgs; +import io.quarkus.redis.datasource.transactions.TransactionalRedisDataSource; + +public class BlockingTransactionalStreamCommandsImpl extends AbstractTransactionalRedisCommandGroup + implements TransactionalStreamCommands { + + private final ReactiveTransactionalStreamCommands reactive; + + public BlockingTransactionalStreamCommandsImpl(TransactionalRedisDataSource ds, + ReactiveTransactionalStreamCommands reactive, Duration timeout) { + super(ds, timeout); + this.reactive = reactive; + } + + @Override + public void xack(K key, String group, String... ids) { + reactive.xack(key, group, ids).await().atMost(timeout); + } + + @Override + public void xadd(K key, Map payload) { + reactive.xadd(key, payload).await().atMost(timeout); + } + + @Override + public void xadd(K key, XAddArgs args, Map payload) { + reactive.xadd(key, args, payload).await().atMost(timeout); + } + + @Override + public void xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start) { + reactive.xautoclaim(key, group, consumer, minIdleTime, start).await().atMost(timeout); + } + + @Override + public void xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start, int count) { + reactive.xautoclaim(key, group, consumer, minIdleTime, start, count).await().atMost(timeout); + } + + @Override + public void xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start, int count, + boolean justId) { + reactive.xautoclaim(key, group, consumer, minIdleTime, start, count, justId).await().atMost(timeout); + } + + @Override + public void xclaim(K key, String group, String consumer, Duration minIdleTime, String... id) { + reactive.xclaim(key, group, consumer, minIdleTime, id).await().atMost(timeout); + } + + @Override + public void xclaim(K key, String group, String consumer, Duration minIdleTime, XClaimArgs args, String... id) { + reactive.xclaim(key, group, consumer, minIdleTime, args, id).await().atMost(timeout); + } + + @Override + public void xdel(K key, String... id) { + reactive.xdel(key, id).await().atMost(timeout); + } + + @Override + public void xgroupCreate(K key, String groupname, String from) { + reactive.xgroupCreate(key, groupname, from).await().atMost(timeout); + } + + @Override + public void xgroupCreate(K key, String groupname, String from, XGroupCreateArgs args) { + reactive.xgroupCreate(key, groupname, from, args).await().atMost(timeout); + } + + @Override + public void xgroupCreateConsumer(K key, String groupname, String consumername) { + reactive.xgroupCreateConsumer(key, groupname, consumername).await().atMost(timeout); + } + + @Override + public void xgroupDelConsumer(K key, String groupname, String consumername) { + reactive.xgroupDelConsumer(key, groupname, consumername).await().atMost(timeout); + } + + @Override + public void xgroupDestroy(K key, String groupname) { + reactive.xgroupDestroy(key, groupname).await().atMost(timeout); + } + + @Override + public void xgroupSetId(K key, String groupname, String from) { + reactive.xgroupSetId(key, groupname, from).await().atMost(timeout); + } + + @Override + public void xgroupSetId(K key, String groupname, String from, XGroupSetIdArgs args) { + reactive.xgroupSetId(key, groupname, from, args).await().atMost(timeout); + } + + @Override + public void xlen(K key) { + reactive.xlen(key).await().atMost(timeout); + } + + @Override + public void xrange(K key, StreamRange range, int count) { + reactive.xrange(key, range, count).await().atMost(timeout); + } + + @Override + public void xrange(K key, StreamRange range) { + reactive.xrange(key, range).await().atMost(timeout); + } + + @Override + public void xread(K key, String id) { + reactive.xread(key, id).await().atMost(timeout); + } + + @Override + public void xread(Map lastIdsPerStream) { + reactive.xread(lastIdsPerStream).await().atMost(timeout); + } + + @Override + public void xread(K key, String id, XReadArgs args) { + reactive.xread(key, id, args).await().atMost(timeout); + } + + @Override + public void xread(Map lastIdsPerStream, XReadArgs args) { + reactive.xread(lastIdsPerStream, args).await().atMost(timeout); + } + + @Override + public void xreadgroup(String group, String consumer, K key, String id) { + reactive.xreadgroup(group, consumer, key, id).await().atMost(timeout); + } + + @Override + public void xreadgroup(String group, String consumer, Map lastIdsPerStream) { + reactive.xreadgroup(group, consumer, lastIdsPerStream).await().atMost(timeout); + } + + @Override + public void xreadgroup(String group, String consumer, K key, String id, XReadGroupArgs args) { + reactive.xreadgroup(group, consumer, key, id, args).await().atMost(timeout); + } + + @Override + public void xreadgroup(String group, String consumer, Map lastIdsPerStream, XReadGroupArgs args) { + reactive.xreadgroup(group, consumer, lastIdsPerStream, args).await().atMost(timeout); + } + + @Override + public void xrevrange(K key, StreamRange range, int count) { + reactive.xrevrange(key, range, count).await().atMost(timeout); + } + + @Override + public void xrevrange(K key, StreamRange range) { + reactive.xrevrange(key, range).await().atMost(timeout); + } + + @Override + public void xtrim(K key, String threshold) { + reactive.xtrim(key, threshold).await().atMost(timeout); + } + + @Override + public void xtrim(K key, XTrimArgs args) { + reactive.xtrim(key, args).await().atMost(timeout); + } +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveRedisDataSourceImpl.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveRedisDataSourceImpl.java index 72f3a9e6a97ac..4819b478e4900 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveRedisDataSourceImpl.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveRedisDataSourceImpl.java @@ -26,6 +26,7 @@ import io.quarkus.redis.datasource.search.ReactiveSearchCommands; import io.quarkus.redis.datasource.set.ReactiveSetCommands; import io.quarkus.redis.datasource.sortedset.ReactiveSortedSetCommands; +import io.quarkus.redis.datasource.stream.ReactiveStreamCommands; import io.quarkus.redis.datasource.string.ReactiveStringCommands; import io.quarkus.redis.datasource.timeseries.ReactiveTimeSeriesCommands; import io.quarkus.redis.datasource.topk.ReactiveTopKCommands; @@ -286,6 +287,11 @@ public ReactiveBitMapCommands bitmap(Class redisKeyType) { return new ReactiveBitMapCommandsImpl<>(this, redisKeyType); } + @Override + public ReactiveStreamCommands stream(Class redisKeyType, Class fieldType, Class valueType) { + return new ReactiveStreamCommandsImpl<>(this, redisKeyType, fieldType, valueType); + } + @Override public ReactiveJsonCommands json(Class redisKeyType) { return new ReactiveJsonCommandsImpl<>(this, redisKeyType); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveStreamCommandsImpl.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveStreamCommandsImpl.java new file mode 100644 index 0000000000000..ef190787cad42 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveStreamCommandsImpl.java @@ -0,0 +1,330 @@ +package io.quarkus.redis.runtime.datasource; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.quarkus.redis.datasource.ReactiveRedisCommands; +import io.quarkus.redis.datasource.ReactiveRedisDataSource; +import io.quarkus.redis.datasource.stream.ClaimedMessages; +import io.quarkus.redis.datasource.stream.ReactiveStreamCommands; +import io.quarkus.redis.datasource.stream.StreamMessage; +import io.quarkus.redis.datasource.stream.StreamRange; +import io.quarkus.redis.datasource.stream.XAddArgs; +import io.quarkus.redis.datasource.stream.XClaimArgs; +import io.quarkus.redis.datasource.stream.XGroupCreateArgs; +import io.quarkus.redis.datasource.stream.XGroupSetIdArgs; +import io.quarkus.redis.datasource.stream.XReadArgs; +import io.quarkus.redis.datasource.stream.XReadGroupArgs; +import io.quarkus.redis.datasource.stream.XTrimArgs; +import io.smallrye.mutiny.Uni; +import io.vertx.mutiny.redis.client.Response; +import io.vertx.redis.client.ResponseType; + +public class ReactiveStreamCommandsImpl extends AbstractStreamCommands + implements ReactiveStreamCommands, ReactiveRedisCommands { + + private final ReactiveRedisDataSource reactive; + private final Class typeOfValue; + private final Class typeOfField; + private final Class typeOfKey; + + public ReactiveStreamCommandsImpl(ReactiveRedisDataSourceImpl redis, Class k, Class f, Class v) { + super(redis, k, f, v); + this.typeOfKey = k; + this.typeOfField = f; + this.typeOfValue = v; + this.reactive = redis; + } + + @Override + public ReactiveRedisDataSource getDataSource() { + return reactive; + } + + @Override + public Uni xack(K key, String group, String... ids) { + return super._xack(key, group, ids) + .map(Response::toInteger); + } + + @Override + public Uni xadd(K key, Map payload) { + return super._xadd(key, payload) + .map(ReactiveStreamCommandsImpl::getIdOrNull); + } + + protected static String getIdOrNull(Response r) { + if (r == null) { + return null; + } + return r.toString(); + } + + @Override + public Uni xadd(K key, XAddArgs args, Map payload) { + return super._xadd(key, args, payload) + .map(ReactiveStreamCommandsImpl::getIdOrNull); + } + + @Override + public Uni> xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start, + int count) { + return super._xautoclaim(key, group, consumer, minIdleTime, start, count) + .map(r -> decodeAsClaimedMessages(key, r)); + } + + protected ClaimedMessages decodeAsClaimedMessages(K key, Response r) { + if (r == null) { + return new ClaimedMessages<>(null, List.of()); + } + var id = r.get(0).toString(); // This is the stream id for the next auto-claim call + var l = r.get(1); + var list = decodeListOfMessages(key, l); + // ignore the third item - competing messages + + return new ClaimedMessages<>(id, list); + } + + protected List> decodeMessageListPrefixedByKey(Response r) { + // Each response is a _list_ of two elements where the first element is the key. + // The second element is the list of messages + if (r == null) { + return List.of(); + } + var actualKey = marshaller.decode(typeOfKey, r.get(0)); + var listOfMessages = r.get(1); + List> list = new ArrayList<>(); + for (int i = 0; i < listOfMessages.size(); i++) { + list.add(decodeMessageWithStreamId(actualKey, listOfMessages.get(i))); + } + return list; + } + + private StreamMessage decodeMessageWithStreamId(K key, Response response) { + + // the response is an array with two elements: + // 1. the stream id + // 2. the payload (another array) + + if (response == null) { + return null; + } + + if (response.type() == ResponseType.BULK) { + // JUSTID was used + return new StreamMessage<>(key, response.toString(), Map.of()); + } else { + var streamId = response.get(0).toString(); + var payload = response.get(1); + var content = decodeMessagePayload(payload); + return new StreamMessage<>(key, streamId, content); + } + } + + Map decodeMessagePayload(Response response) { + Map map = new HashMap<>(); + F current = null; + for (Response nested : response) { + if (current == null) { + current = marshaller.decode(typeOfField, nested); + } else { + map.put(current, marshaller.decode(typeOfValue, nested)); + current = null; + } + } + return map; + } + + @Override + public Uni> xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start) { + return super._xautoclaim(key, group, consumer, minIdleTime, start) + .map(r -> decodeAsClaimedMessages(key, r)); + } + + @Override + public Uni> xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start, + int count, boolean justId) { + return super._xautoclaim(key, group, consumer, minIdleTime, start, count, justId) + .map(r -> decodeAsClaimedMessages(key, r)); + } + + @Override + public Uni>> xclaim(K key, String group, String consumer, Duration minIdleTime, String... id) { + return super._xclaim(key, group, consumer, minIdleTime, id) + .map(r -> decodeListOfMessages(key, r)); + } + + @Override + public Uni>> xclaim(K key, String group, String consumer, Duration minIdleTime, XClaimArgs args, + String... id) { + return super._xclaim(key, group, consumer, minIdleTime, args, id) + .map(r -> decodeListOfMessages(key, r)); + } + + @Override + public Uni xdel(K key, String... id) { + return super._xdel(key, id) + .map(Response::toInteger); + } + + @Override + public Uni xgroupCreate(K key, String groupname, String from) { + return super._xgroupCreate(key, groupname, from) + .replaceWithVoid(); + } + + @Override + public Uni xgroupCreate(K key, String groupname, String from, XGroupCreateArgs args) { + return super._xgroupCreate(key, groupname, from, args) + .replaceWithVoid(); + } + + @Override + public Uni xgroupCreateConsumer(K key, String groupname, String consumername) { + return super._xgroupCreateConsumer(key, groupname, consumername) + .map(Response::toBoolean); + } + + @Override + public Uni xgroupDelConsumer(K key, String groupname, String consumername) { + return super._xgroupDelConsumer(key, groupname, consumername) + .map(Response::toLong); + } + + @Override + public Uni xgroupDestroy(K key, String groupname) { + return super._xgroupDestroy(key, groupname) + .map(Response::toBoolean); + } + + @Override + public Uni xgroupSetId(K key, String groupname, String from) { + return super._xgroupSetId(key, groupname, from) + .replaceWithVoid(); + } + + @Override + public Uni xgroupSetId(K key, String groupname, String from, XGroupSetIdArgs args) { + return super._xgroupSetId(key, groupname, from, args) + .replaceWithVoid(); + } + + @Override + public Uni xlen(K key) { + return super._xlen(key) + .map(Response::toLong); + } + + @Override + public Uni>> xrange(K key, StreamRange range, int count) { + return super._xrange(key, range, count) + .map(r -> decodeListOfMessages(key, r)); + } + + @Override + public Uni>> xrange(K key, StreamRange range) { + return super._xrange(key, range) + .map(r -> decodeListOfMessages(key, r)); + } + + protected List> decodeListOfMessages(K key, Response r) { + if (r == null) { + return List.of(); + } + // The response is a list. + // Each element is a list of two element (stream id, payload) + List> list = new ArrayList<>(); + for (Response response : r) { + list.add(decodeMessageWithStreamId(key, response)); + } + return list; + } + + @Override + public Uni>> xread(K key, String id) { + return xread(Map.of(key, id)); + } + + protected List> decodeAsListOfMessagesFromXRead(Response r) { + if (r == null) { + return List.of(); + } + // The response is a _map_ key -> list, in this case + List> list = new ArrayList<>(); + for (Response response : r) { + // Each response is a _list_ where the first element is the key. + // The other elements are a list of array (stream id, message) + list.addAll(decodeMessageListPrefixedByKey(response)); + } + return list; + } + + @Override + public Uni>> xread(Map lastIdsPerStream) { + return super._xread(lastIdsPerStream) + .map(this::decodeAsListOfMessagesFromXRead); + } + + @Override + public Uni>> xread(K key, String id, XReadArgs args) { + return xread(Map.of(key, id), args); + } + + @Override + public Uni>> xread(Map lastIdsPerStream, XReadArgs args) { + return super._xread(lastIdsPerStream, args) + .map(this::decodeAsListOfMessagesFromXRead); + } + + @Override + public Uni>> xreadgroup(String group, String consumer, K key, String id) { + return xreadgroup(group, consumer, Map.of(key, id)); + + } + + @Override + public Uni>> xreadgroup(String group, String consumer, Map lastIdsPerStream) { + return super._xreadgroup(group, consumer, lastIdsPerStream) + .map(this::decodeAsListOfMessagesFromXRead); + } + + @Override + public Uni>> xreadgroup(String group, String consumer, K key, String id, XReadGroupArgs args) { + return xreadgroup(group, consumer, Map.of(key, id), args); + + } + + @Override + public Uni>> xreadgroup(String group, String consumer, Map lastIdsPerStream, + XReadGroupArgs args) { + return super._xreadgroup(group, consumer, lastIdsPerStream, args) + .map(this::decodeAsListOfMessagesFromXRead); + } + + @Override + public Uni>> xrevrange(K key, StreamRange range, int count) { + return super._xrevrange(key, range, count) + .map(r -> decodeListOfMessages(key, r)); + } + + @Override + public Uni>> xrevrange(K key, StreamRange range) { + return super._xrevrange(key, range) + .map(r -> decodeListOfMessages(key, r)); + } + + @Override + public Uni xtrim(K key, String threshold) { + return super._xtrim(key, new XTrimArgs().minid(threshold)) + .map(Response::toLong); + } + + @Override + public Uni xtrim(K key, XTrimArgs args) { + return super._xtrim(key, args) + .map(Response::toLong); + } +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveTransactionalRedisDataSourceImpl.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveTransactionalRedisDataSourceImpl.java index 6e5185976b93c..df630d203eb35 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveTransactionalRedisDataSourceImpl.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveTransactionalRedisDataSourceImpl.java @@ -20,6 +20,7 @@ import io.quarkus.redis.datasource.search.ReactiveTransactionalSearchCommands; import io.quarkus.redis.datasource.set.ReactiveTransactionalSetCommands; import io.quarkus.redis.datasource.sortedset.ReactiveTransactionalSortedSetCommands; +import io.quarkus.redis.datasource.stream.ReactiveTransactionalStreamCommands; import io.quarkus.redis.datasource.string.ReactiveTransactionalStringCommands; import io.quarkus.redis.datasource.timeseries.ReactiveTransactionalTimeSeriesCommands; import io.quarkus.redis.datasource.topk.ReactiveTransactionalTopKCommands; @@ -106,6 +107,13 @@ public ReactiveTransactionalBitMapCommands bitmap(Class redisKeyType) (ReactiveBitMapCommandsImpl) this.reactive.bitmap(redisKeyType), tx); } + @Override + public ReactiveTransactionalStreamCommands stream(Class redisKeyType, Class typeOfField, + Class typeOfValue) { + return new ReactiveTransactionalStreamCommandsImpl<>(this, + (ReactiveStreamCommandsImpl) this.reactive.stream(redisKeyType, typeOfField, typeOfValue), tx); + } + @Override public ReactiveTransactionalJsonCommands json(Class redisKeyType) { return new ReactiveTransactionalJsonCommandsImpl<>(this, diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveTransactionalStreamCommandsImpl.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveTransactionalStreamCommandsImpl.java new file mode 100644 index 0000000000000..06d36b7ac1af2 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveTransactionalStreamCommandsImpl.java @@ -0,0 +1,222 @@ +package io.quarkus.redis.runtime.datasource; + +import java.time.Duration; +import java.util.Map; + +import io.quarkus.redis.datasource.stream.ReactiveTransactionalStreamCommands; +import io.quarkus.redis.datasource.stream.StreamRange; +import io.quarkus.redis.datasource.stream.XAddArgs; +import io.quarkus.redis.datasource.stream.XClaimArgs; +import io.quarkus.redis.datasource.stream.XGroupCreateArgs; +import io.quarkus.redis.datasource.stream.XGroupSetIdArgs; +import io.quarkus.redis.datasource.stream.XReadArgs; +import io.quarkus.redis.datasource.stream.XReadGroupArgs; +import io.quarkus.redis.datasource.stream.XTrimArgs; +import io.quarkus.redis.datasource.transactions.ReactiveTransactionalRedisDataSource; +import io.smallrye.mutiny.Uni; +import io.vertx.mutiny.redis.client.Response; + +public class ReactiveTransactionalStreamCommandsImpl extends AbstractTransactionalCommands + implements ReactiveTransactionalStreamCommands { + + private final ReactiveStreamCommandsImpl reactive; + + public ReactiveTransactionalStreamCommandsImpl(ReactiveTransactionalRedisDataSource ds, + ReactiveStreamCommandsImpl reactive, TransactionHolder tx) { + super(ds, tx); + this.reactive = reactive; + } + + @Override + public Uni xack(K key, String group, String... ids) { + this.tx.enqueue(Response::toInteger); + return this.reactive._xack(key, group, ids).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xadd(K key, Map payload) { + this.tx.enqueue(Response::toString); + return this.reactive._xadd(key, payload).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xadd(K key, XAddArgs args, Map payload) { + this.tx.enqueue(Response::toString); + return this.reactive._xadd(key, args, payload).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start) { + this.tx.enqueue(r -> reactive.decodeAsClaimedMessages(key, r)); + return this.reactive._xautoclaim(key, group, consumer, minIdleTime, start).invoke(this::queuedOrDiscard) + .replaceWithVoid(); + } + + @Override + public Uni xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start, int count) { + this.tx.enqueue(r -> reactive.decodeAsClaimedMessages(key, r)); + return this.reactive._xautoclaim(key, group, consumer, minIdleTime, start, count).invoke(this::queuedOrDiscard) + .replaceWithVoid(); + } + + @Override + public Uni xautoclaim(K key, String group, String consumer, Duration minIdleTime, String start, int count, + boolean justId) { + this.tx.enqueue(r -> reactive.decodeAsClaimedMessages(key, r)); + return this.reactive._xautoclaim(key, group, consumer, minIdleTime, start, count, justId).invoke(this::queuedOrDiscard) + .replaceWithVoid(); + } + + @Override + public Uni xclaim(K key, String group, String consumer, Duration minIdleTime, String... id) { + this.tx.enqueue(r -> reactive.decodeListOfMessages(key, r)); + return this.reactive._xclaim(key, group, consumer, minIdleTime, id).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xclaim(K key, String group, String consumer, Duration minIdleTime, XClaimArgs args, String... id) { + this.tx.enqueue(r -> reactive.decodeListOfMessages(key, r)); + return this.reactive._xclaim(key, group, consumer, minIdleTime, args, id).invoke(this::queuedOrDiscard) + .replaceWithVoid(); + } + + @Override + public Uni xdel(K key, String... id) { + this.tx.enqueue(Response::toInteger); + return this.reactive._xdel(key, id).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xgroupCreate(K key, String groupname, String from) { + this.tx.enqueue(r -> null); + return this.reactive._xgroupCreate(key, groupname, from).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xgroupCreate(K key, String groupname, String from, XGroupCreateArgs args) { + this.tx.enqueue(r -> null); + return this.reactive._xgroupCreate(key, groupname, from, args).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xgroupCreateConsumer(K key, String groupname, String consumername) { + this.tx.enqueue(Response::toBoolean); + return this.reactive._xgroupCreateConsumer(key, groupname, consumername).invoke(this::queuedOrDiscard) + .replaceWithVoid(); + } + + @Override + public Uni xgroupDelConsumer(K key, String groupname, String consumername) { + this.tx.enqueue(Response::toLong); + return this.reactive._xgroupDelConsumer(key, groupname, consumername).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xgroupDestroy(K key, String groupname) { + this.tx.enqueue(Response::toBoolean); + return this.reactive._xgroupDestroy(key, groupname).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xgroupSetId(K key, String groupname, String from) { + this.tx.enqueue(r -> null); + return this.reactive._xgroupSetId(key, groupname, from).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xgroupSetId(K key, String groupname, String from, XGroupSetIdArgs args) { + this.tx.enqueue(r -> null); + return this.reactive._xgroupSetId(key, groupname, from, args).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xlen(K key) { + this.tx.enqueue(Response::toLong); + return this.reactive._xlen(key).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xrange(K key, StreamRange range, int count) { + this.tx.enqueue(r -> reactive.decodeListOfMessages(key, r)); + return this.reactive._xrange(key, range, count).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xrange(K key, StreamRange range) { + this.tx.enqueue(r -> reactive.decodeListOfMessages(key, r)); + return this.reactive._xrange(key, range).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xread(K key, String id) { + this.tx.enqueue(r -> reactive.decodeAsListOfMessagesFromXRead(r)); + return this.reactive._xread(key, id).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xread(Map lastIdsPerStream) { + this.tx.enqueue(r -> reactive.decodeAsListOfMessagesFromXRead(r)); + return this.reactive._xread(lastIdsPerStream).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xread(K key, String id, XReadArgs args) { + this.tx.enqueue(r -> reactive.decodeAsListOfMessagesFromXRead(r)); + return this.reactive._xread(key, id, args).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xread(Map lastIdsPerStream, XReadArgs args) { + this.tx.enqueue(r -> reactive.decodeAsListOfMessagesFromXRead(r)); + return this.reactive._xread(lastIdsPerStream, args).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xreadgroup(String group, String consumer, K key, String id) { + this.tx.enqueue(r -> reactive.decodeAsListOfMessagesFromXRead(r)); + return this.reactive._xreadgroup(group, consumer, key, id).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xreadgroup(String group, String consumer, Map lastIdsPerStream) { + this.tx.enqueue(r -> reactive.decodeAsListOfMessagesFromXRead(r)); + return this.reactive._xreadgroup(group, consumer, lastIdsPerStream).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xreadgroup(String group, String consumer, K key, String id, XReadGroupArgs args) { + this.tx.enqueue(r -> reactive.decodeAsListOfMessagesFromXRead(r)); + return this.reactive._xreadgroup(group, consumer, key, id, args).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xreadgroup(String group, String consumer, Map lastIdsPerStream, XReadGroupArgs args) { + this.tx.enqueue(r -> reactive.decodeAsListOfMessagesFromXRead(r)); + return this.reactive._xreadgroup(group, consumer, lastIdsPerStream, args).invoke(this::queuedOrDiscard) + .replaceWithVoid(); + } + + @Override + public Uni xrevrange(K key, StreamRange range, int count) { + this.tx.enqueue(r -> reactive.decodeListOfMessages(key, r)); + return this.reactive._xrevrange(key, range, count).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xrevrange(K key, StreamRange range) { + this.tx.enqueue(r -> reactive.decodeListOfMessages(key, r)); + return this.reactive._xrevrange(key, range).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xtrim(K key, String threshold) { + this.tx.enqueue(Response::toLong); + return this.reactive._xtrim(key, new XTrimArgs().minid(threshold)).invoke(this::queuedOrDiscard).replaceWithVoid(); + } + + @Override + public Uni xtrim(K key, XTrimArgs args) { + this.tx.enqueue(Response::toLong); + return this.reactive._xtrim(key, args).invoke(this::queuedOrDiscard).replaceWithVoid(); + } +} diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/StreamCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/StreamCommandsTest.java new file mode 100644 index 0000000000000..d1b2c5ac2ba5e --- /dev/null +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/StreamCommandsTest.java @@ -0,0 +1,628 @@ +package io.quarkus.redis.datasource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.entry; +import static org.awaitility.Awaitility.await; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.quarkus.redis.datasource.stream.StreamCommands; +import io.quarkus.redis.datasource.stream.StreamMessage; +import io.quarkus.redis.datasource.stream.StreamRange; +import io.quarkus.redis.datasource.stream.XAddArgs; +import io.quarkus.redis.datasource.stream.XClaimArgs; +import io.quarkus.redis.datasource.stream.XGroupCreateArgs; +import io.quarkus.redis.datasource.stream.XGroupSetIdArgs; +import io.quarkus.redis.datasource.stream.XReadArgs; +import io.quarkus.redis.datasource.stream.XReadGroupArgs; +import io.quarkus.redis.datasource.stream.XTrimArgs; +import io.quarkus.redis.runtime.datasource.BlockingRedisDataSourceImpl; + +@RequiresCommand("xadd") +public class StreamCommandsTest extends DatasourceTestBase { + + private RedisDataSource ds; + + private StreamCommands stream; + + @BeforeEach + void initialize() { + ds = new BlockingRedisDataSourceImpl(vertx, redis, api, Duration.ofSeconds(1)); + stream = ds.stream(Integer.class); + } + + @AfterEach + void clear() { + ds.flushall(); + } + + @Test + void getDataSource() { + assertThat(ds).isEqualTo(stream.getDataSource()); + } + + @Test + void xreadTest() { + stream.xadd("my-stream", Map.of("duration", 1532, "event-id", 5, "user-id", 77788)); + stream.xadd("my-stream", Map.of("duration", 1533, "event-id", 6, "user-id", 77788)); + stream.xadd("my-stream", Map.of("duration", 1534, "event-id", 7, "user-id", 77788)); + + List> messages = stream.xread("my-stream", "0-0"); + assertThat(messages).hasSize(3) + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo("my-stream"); + assertThat(m.id()).isNotEmpty().contains("-"); + assertThat(m.payload()).contains(entry("user-id", 77788)).containsKey("event-id").containsKey("duration"); + }); + } + + @Test + void xAdd() { + assertThat(stream.xadd("mystream", Map.of("sensor-id", 1234, "temperature", 19))) + .isNotBlank().contains("-"); + + long now = System.currentTimeMillis(); + assertThat(stream.xadd("mystream", new XAddArgs().id(now + 1000 + "-0"), + Map.of("sensor-id", 1234, "temperature", 19))).isEqualTo(now + 1000 + "-0"); + + for (int i = 0; i < 10; i++) { + assertThat(stream.xadd("my-second-stream", new XAddArgs().maxlen(5L), + Map.of("sensor-id", 1234, "temperature", 19))).isNotBlank(); + } + assertThat(stream.xlen("my-second-stream")).isEqualTo(5); + + for (int i = 0; i < 10; i++) { + assertThat(stream.xadd("my-third-stream", new XAddArgs().minid("12345-0").nearlyExactTrimming() + .limit(3).id("12346-" + i), + Map.of("sensor-id", 1234, "temperature", 19))).isNotBlank(); + } + assertThat(stream.xlen("my-third-stream")).isEqualTo(10); + assertThat(stream.xadd("another", new XAddArgs().nomkstream(), Map.of("foo", 12))).isNull(); + } + + @Test + void xLen() { + assertThat(stream.xlen("missing")).isEqualTo(0); + assertThat(stream.xadd("mystream", Map.of("sensor-id", 1234, "temperature", 19))) + .isNotBlank().contains("-"); + assertThat(stream.xlen("mystream")).isEqualTo(1); + } + + @Test + void xRangeAndxRevRange() { + List ids = new ArrayList<>(); + Map payload = Map.of("sensor-id", 1234, "temperature", 19); + for (int i = 0; i < 3; i++) { + ids.add(stream.xadd(key, payload)); + } + assertThat(stream.xlen(key)).isEqualTo(3); + + assertThat(stream.xrange(key, StreamRange.of("-", "+"))) + .hasSize(3) + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key); + assertThat(m.id()).isNotBlank(); + assertThat(ids).contains(m.id()); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + }); + + assertThat(stream.xrange(key, StreamRange.of(ids.get(1), ids.get(2)))) + .hasSize(2) + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key); + assertThat(m.id()).isNotBlank(); + assertThat(ids).contains(m.id()); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + }); + + assertThat(stream.xrange(key, StreamRange.of("-", "+"), 2)) + .hasSize(2) + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key); + assertThat(m.id()).isNotBlank(); + assertThat(ids).contains(m.id()); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + }); + + assertThat(stream.xrevrange(key, StreamRange.of("+", "-"))) + .hasSize(3) + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key); + assertThat(m.id()).isNotBlank(); + assertThat(ids).contains(m.id()); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + }); + + assertThat(stream.xrevrange(key, StreamRange.of(ids.get(2), ids.get(1)))) + .hasSize(2) + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key); + assertThat(m.id()).isNotBlank(); + assertThat(ids).contains(m.id()); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + }); + + assertThat(stream.xrevrange(key, StreamRange.of("+", "-"), 2)) + .hasSize(2) + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key); + assertThat(m.id()).isNotBlank(); + assertThat(ids).contains(m.id()); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + }); + + } + + @Test + void xReadWithAndWithoutCount() { + List ids1 = new ArrayList<>(); + List ids2 = new ArrayList<>(); + String key2 = key + "2"; + Map payload = Map.of("sensor-id", 1234, "temperature", 19); + for (int i = 0; i < 3; i++) { + ids1.add(stream.xadd(key, payload)); + ids2.add(stream.xadd(key2, payload)); + } + assertThat(stream.xlen(key)).isEqualTo(3); + assertThat(stream.xlen(key2)).isEqualTo(3); + + assertThat(stream.xread(key, "0", new XReadArgs().count(2))) + .hasSize(2) + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key); + assertThat(ids1).contains(m.id()); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + }); + + assertThat(stream.xread(key2, "0")) + .hasSize(3) + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key2); + assertThat(ids2).contains(m.id()); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + }); + } + + @Test + void xReadMultipleStreams() { + List ids = new ArrayList<>(); + String key2 = key + "2"; + Map payload = Map.of("sensor-id", 1234, "temperature", 19); + for (int i = 0; i < 3; i++) { + ids.add(stream.xadd(key, payload)); + ids.add(stream.xadd(key2, payload)); + } + assertThat(stream.xlen(key)).isEqualTo(3); + assertThat(stream.xlen(key2)).isEqualTo(3); + + assertThat(stream.xread(Map.of(key2, "0", key, "0"))) + .hasSize(6) + .allSatisfy(m -> { + assertThat(m.key().equals(key) || m.key().equals(key2)).isTrue(); + assertThat(ids).contains(m.id()); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + }); + + assertThat(stream.xread(Map.of(key2, "0", key, "0"), new XReadArgs().count(2))) + .hasSize(4) // the count is per stream + .allSatisfy(m -> { + assertThat(m.key().equals(key) || m.key().equals(key2)).isTrue(); + assertThat(ids).contains(m.id()); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + }); + } + + @Test + void xReadBlocking() throws InterruptedException { + Map payload = Map.of("sensor-id", 1234, "temperature", 19); + + CountDownLatch latch = new CountDownLatch(1); + new Thread(() -> { + assertThat(stream.xread(key, "$", new XReadArgs().block(Duration.ofSeconds(10)))) + .isNotEmpty() + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + }); + latch.countDown(); + }).start(); + + await() + .pollDelay(10, TimeUnit.MILLISECONDS) + .until(() -> { + stream.xadd(key, payload); + return latch.getCount() == 0; + }); + + } + + @Test + void xReadBlockingMultipleStreams() { + String key2 = key + "2"; + Map payload = Map.of("sensor-id", 1234, "temperature", 19); + + CountDownLatch latch = new CountDownLatch(1); + new Thread(() -> { + assertThat(stream.xread(Map.of(key, "$", key2, "$"), new XReadArgs().block(Duration.ofSeconds(10)))) + .isNotEmpty() + .allSatisfy(m -> { + assertThat(m.key().equals(key) || m.key().equals(key2)).isTrue(); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + }); + latch.countDown(); + }).start(); + + await() + .pollDelay(10, TimeUnit.MILLISECONDS) + .until(() -> { + stream.xadd(key2, payload); + stream.xadd(key, payload); + return latch.getCount() == 0; + }); + } + + @Test + void consumerGroupTests() { + String g1 = "my-group"; + stream.xgroupCreate(key, g1, "$", new XGroupCreateArgs().mkstream()); + String g2 = "my-group-2"; + stream.xgroupCreate(key, g2, "$"); + String g3 = "my-group-3"; + String key2 = key + "2"; + stream.xgroupCreate(key, g3, "$"); + stream.xgroupCreate(key2, g3, "$", new XGroupCreateArgs().mkstream()); + + Map payload = Map.of("sensor-id", 1234, "temperature", 19); + for (int i = 0; i < 5; i++) { + stream.xadd(key, payload); + stream.xadd(key2, payload); + } + + assertThat(stream.xreadgroup(g1, "c1", key, ">", new XReadGroupArgs().count(1))) + .hasSize(1) + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + }); + + assertThat(stream.xreadgroup(g1, "c2", key, ">")) + .hasSize(4) + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + assertThat(stream.xack(m.key(), g1, m.id())).isEqualTo(1); + }); + + assertThat(stream.xreadgroup(g2, "c2", key, ">")) + .hasSize(5) + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + assertThat(stream.xack(m.key(), g2, m.id())).isEqualTo(1); + }); + + assertThat(stream.xreadgroup(g2, "c2", key, ">")) + .hasSize(0); + + assertThat(stream.xreadgroup(g3, "c1", Map.of(key, ">", key2, ">"), new XReadGroupArgs().count(1))) + .hasSize(2); // 1 per stream + assertThat(stream.xreadgroup(g3, "c1", Map.of(key, ">", key2, ">"), + new XReadGroupArgs().block(Duration.ofSeconds(1)).noack())) + .hasSize(8); + assertThat(stream.xreadgroup(g3, "c1", Map.of(key, ">", key2, ">"))) + .hasSize(0); + } + + @Test + void consumerGroupTestsBlocking() { + String g1 = "my-group"; + stream.xgroupCreate(key, g1, "$", new XGroupCreateArgs().mkstream()); + String g3 = "my-group-3"; + String key2 = key + "2"; + stream.xgroupCreate(key, g3, "$"); + stream.xgroupCreate(key2, g3, "$", new XGroupCreateArgs().mkstream()); + + Map payload = Map.of("sensor-id", 1234, "temperature", 19); + + CountDownLatch latch = new CountDownLatch(1); + new Thread(() -> { + assertThat(stream.xreadgroup(g1, "c1", key, ">", new XReadGroupArgs().block(Duration.ofSeconds(10)).count(1))) + .isNotEmpty() + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + }); + latch.countDown(); + }).start(); + + stream.xadd(key, payload); + + await() + .pollDelay(10, TimeUnit.MILLISECONDS) + .until(() -> { + stream.xadd(key, payload); + return latch.getCount() == 0; + }); + CountDownLatch latch2 = new CountDownLatch(1); + + new Thread(() -> { + assertThat(stream.xreadgroup(g3, "c1", Map.of(key, ">", key2, ">"), + new XReadGroupArgs().block(Duration.ofSeconds(10)))) + .isNotEmpty(); + latch2.countDown(); + }).start(); + + stream.xadd(key2, payload); + + await() + .pollDelay(10, TimeUnit.MILLISECONDS) + .until(() -> { + stream.xadd(key2, payload); + return latch.getCount() == 0; + }); + } + + @Test + void xClaim() throws InterruptedException { + String g1 = "my-group"; + stream.xgroupCreate(key, g1, "$", new XGroupCreateArgs().mkstream()); + + Map payload = Map.of("sensor-id", 1234, "temperature", 19); + for (int i = 0; i < 5; i++) { + stream.xadd(key, payload); + } + + List pending = new ArrayList<>(); + assertThat(stream.xreadgroup(g1, "c1", key, ">", new XReadGroupArgs().count(2))) + .hasSize(2) + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + // Do not ack + pending.add(m.id()); + }); + + List read = new ArrayList<>(); + assertThat(stream.xreadgroup(g1, "c2", key, ">", new XReadGroupArgs().count(2))) + .hasSize(2) + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + read.add(m.id()); + }); + + assertThat(stream.xack(key, g1, read.toArray(new String[0]))).isEqualTo(2); + + // Make sure that the message are pending for a bit of time before claiming the ownership + Thread.sleep(5); + + assertThat(stream.xclaim(key, g1, "c2", Duration.ofMillis(1), pending.toArray(new String[0]))) + .hasSize(2) + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + stream.xack(key, g1, m.id()); + }); + + assertThat(stream.xreadgroup(g1, "c1", key, ">")).hasSize(1); + assertThat(stream.xreadgroup(g1, "c2", key, ">")).hasSize(0); + } + + @Test + void xClaimWithArgs() throws InterruptedException { + String g1 = "my-group"; + stream.xgroupCreate(key, g1, "$", new XGroupCreateArgs().mkstream()); + + Map payload = Map.of("sensor-id", 1234, "temperature", 19); + for (int i = 0; i < 5; i++) { + stream.xadd(key, payload); + } + + List pending = new ArrayList<>(); + assertThat(stream.xreadgroup(g1, "c1", key, ">", new XReadGroupArgs().count(2))) + .hasSize(2) + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + // Do not ack + pending.add(m.id()); + }); + + List read = new ArrayList<>(); + assertThat(stream.xreadgroup(g1, "c2", key, ">", new XReadGroupArgs().count(2))) + .hasSize(2) + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + read.add(m.id()); + }); + + assertThat(stream.xack(key, g1, read.toArray(new String[0]))).isEqualTo(2); + + // Make sure that the message are pending for a bit of time before claiming the ownership + Thread.sleep(5); + + assertThat(stream.xclaim(key, g1, "c2", Duration.ofMillis(1), new XClaimArgs() + .force().retryCount(5).idle(Duration.ofMillis(1)).justId(), pending.toArray(new String[0]))) + .hasSize(2) + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key); + assertThat(m.payload()).isEmpty(); // Justid + stream.xack(key, g1, m.id()); + }); + + assertThat(stream.xreadgroup(g1, "c1", key, ">")).hasSize(1); + assertThat(stream.xreadgroup(g1, "c2", key, ">")).hasSize(0); + } + + @Test + void xAutoClaim() throws InterruptedException { + String g1 = "my-group"; + stream.xgroupCreate(key, g1, "$", new XGroupCreateArgs().mkstream()); + + Map payload = Map.of("sensor-id", 1234, "temperature", 19); + for (int i = 0; i < 10; i++) { + stream.xadd(key, payload); + } + + assertThat(stream.xreadgroup(g1, "c1", key, ">", new XReadGroupArgs().count(4))) + .hasSize(4) + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + // Do not ack + }); + + List read = new ArrayList<>(); + assertThat(stream.xreadgroup(g1, "c2", key, ">", new XReadGroupArgs().count(2))) + .hasSize(2) + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + read.add(m.id()); + }); + + assertThat(stream.xack(key, g1, read.toArray(new String[0]))).isEqualTo(2); + + // Make sure that the message are pending for a bit of time before claiming the ownership + Thread.sleep(5); + + var claimed = stream.xautoclaim(key, g1, "c2", Duration.ofMillis(1), "0", 1); + assertThat(claimed.getMessages()) + .hasSize(1) + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + stream.xack(key, g1, m.id()); + }); + + claimed = stream.xautoclaim(key, g1, "c2", Duration.ofMillis(1), "0", 2, true); + assertThat(claimed.getMessages()) + .hasSize(2) + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key); + assertThat(m.payload()).isEmpty(); + stream.xack(key, g1, m.id()); + }); + + claimed = stream.xautoclaim(key, g1, "c2", Duration.ofMillis(1), claimed.getId()); + assertThat(claimed.getMessages()) + .hasSize(1) + .allSatisfy(m -> { + assertThat(m.key()).isEqualTo(key); + assertThat(m.payload()).containsExactlyInAnyOrderEntriesOf(payload); + stream.xack(key, g1, m.id()); + }); + } + + @Test + void xTrim() { + Map payload = Map.of("sensor-id", 1234, "temperature", 19); + for (int i = 0; i < 100; i++) { + stream.xadd(key, payload); + } + + var l = stream.xtrim(key, new XTrimArgs().maxlen(50)); + + assertThat(l).isEqualTo(50); + assertThat(stream.xlen(key)).isEqualTo(50); + + var list = stream.xrange(key, StreamRange.of("-", "+")); + var id = list.get(10).id(); + l = stream.xtrim(key, id); + + assertThat(l).isEqualTo(10); + assertThat(stream.xlen(key)).isEqualTo(40); + + id = list.get(20).id(); + l = stream.xtrim(key, new XTrimArgs().minid(id)); + + assertThat(l).isEqualTo(10); + assertThat(stream.xlen(key)).isEqualTo(30); + } + + @Test + void xDel() { + Map payload = Map.of("sensor-id", 1234, "temperature", 19); + for (int i = 0; i < 100; i++) { + stream.xadd(key, payload); + } + + var list = stream.xrange(key, StreamRange.of("-", "+")); + + assertThat(stream.xdel(key, list.get(0).id(), list.get(3).id(), "12345-01")).isEqualTo(2); + assertThat(stream.xlen(key)).isEqualTo(98); + } + + @Test + void xGroupCreateAndDeleteConsumer() { + Map payload = Map.of("sensor-id", 1234, "temperature", 19); + for (int i = 0; i < 100; i++) { + stream.xadd(key, payload); + } + + stream.xgroupCreate(key, "g1", "0"); + assertThat(stream.xgroupCreateConsumer(key, "g1", "c1")).isTrue(); + assertThat(stream.xgroupCreateConsumer(key, "g1", "c2")).isTrue(); + assertThat(stream.xgroupCreateConsumer(key, "g1", "c1")).isFalse(); + + assertThatThrownBy(() -> stream.xgroupCreateConsumer(key, "missing", "c3")) + .hasMessageContaining("missing"); + + assertThat(stream.xgroupDelConsumer(key, "g1", "c1")).isEqualTo(0); + + assertThat(stream.xreadgroup("g1", "c2", key, ">", new XReadGroupArgs().count(10))).hasSize(10); + + assertThat(stream.xgroupDelConsumer(key, "g1", "c2")).isEqualTo(10); + + assertThat(stream.xgroupDestroy(key, "g1")).isTrue(); + + } + + @Test + void xGroupSetId() { + Map payload = Map.of("sensor-id", 1234, "temperature", 19); + List ids = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + ids.add(stream.xadd(key, payload)); + } + + stream.xgroupCreate(key, "g1", "0"); + + assertThat(stream.xreadgroup("g1", "c2", key, ">", new XReadGroupArgs().count(10))).hasSize(10); + + stream.xgroupSetId(key, "g1", ids.get(50)); + + assertThat(stream.xreadgroup("g1", "c2", key, ">")).hasSize(49); + + } + + @Test + void xGroupSetIdWithArgs() { + Map payload = Map.of("sensor-id", 1234, "temperature", 19); + List ids = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + ids.add(stream.xadd(key, payload)); + } + + stream.xgroupCreate(key, "g1", "0"); + + assertThat(stream.xreadgroup("g1", "c2", key, ">", new XReadGroupArgs().count(10))).hasSize(10); + + stream.xgroupSetId(key, "g1", ids.get(50), new XGroupSetIdArgs().entriesRead(1234)); + + assertThat(stream.xreadgroup("g1", "c2", key, ">")).hasSize(49); + + } + +} diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalStreamCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalStreamCommandsTest.java new file mode 100644 index 0000000000000..d02ad9187a699 --- /dev/null +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalStreamCommandsTest.java @@ -0,0 +1,85 @@ +package io.quarkus.redis.datasource; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.quarkus.redis.datasource.stream.StreamMessage; +import io.quarkus.redis.datasource.stream.TransactionalStreamCommands; +import io.quarkus.redis.datasource.stream.XAddArgs; +import io.quarkus.redis.datasource.transactions.TransactionResult; +import io.quarkus.redis.runtime.datasource.BlockingRedisDataSourceImpl; +import io.quarkus.redis.runtime.datasource.ReactiveRedisDataSourceImpl; + +@SuppressWarnings("unchecked") +@RequiresRedis6OrHigher +public class TransactionalStreamCommandsTest extends DatasourceTestBase { + + private RedisDataSource blocking; + private ReactiveRedisDataSource reactive; + + public static final Map payload = Map.of("message", "hello"); + + @BeforeEach + void initialize() { + blocking = new BlockingRedisDataSourceImpl(vertx, redis, api, Duration.ofSeconds(60)); + reactive = new ReactiveRedisDataSourceImpl(vertx, redis, api); + } + + @AfterEach + public void clear() { + blocking.flushall(); + } + + @Test + public void streamBlocking() { + TransactionResult result = blocking.withTransaction(tx -> { + TransactionalStreamCommands stream = tx.stream(String.class); + assertThat(stream.getDataSource()).isEqualTo(tx); + + stream.xadd(key, payload); + stream.xadd(key, new XAddArgs().nomkstream(), payload); + + stream.xread(key, "0"); // 3 -> 2 messages + stream.xgroupCreate(key, "g1", "0"); + stream.xreadgroup("g1", "c1", key, ">"); + + }); + assertThat(result.size()).isEqualTo(5); + assertThat(result.discarded()).isFalse(); + assertThat((String) result.get(0)).isNotBlank(); + assertThat((String) result.get(1)).isNotBlank(); + + assertThat((List>) result.get(2)).hasSize(2); + assertThat((List>) result.get(4)).hasSize(2); + } + + @Test + public void streamReactive() { + TransactionResult result = reactive.withTransaction(tx -> { + var stream = tx.stream(String.class); + assertThat(stream.getDataSource()).isEqualTo(tx); + + return stream.xadd(key, payload) + .chain((x) -> stream.xadd(key, new XAddArgs().nomkstream(), payload)) + .chain(x -> stream.xread(key, "0")) + .chain(x -> stream.xgroupCreate(key, "g1", "0")) + .chain(x -> stream.xreadgroup("g1", "c1", key, ">")); + + }).await().indefinitely(); + assertThat(result.size()).isEqualTo(5); + assertThat(result.discarded()).isFalse(); + assertThat((String) result.get(0)).isNotBlank(); + assertThat((String) result.get(1)).isNotBlank(); + + assertThat((List>) result.get(2)).hasSize(2); + assertThat((List>) result.get(4)).hasSize(2); + } + +} diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/generator/RedisApiGenerator.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/generator/RedisApiGenerator.java index 8e25972cf9154..b093157d4712c 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/generator/RedisApiGenerator.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/generator/RedisApiGenerator.java @@ -39,7 +39,7 @@ import io.quarkus.redis.datasource.RedisCommands; import io.quarkus.redis.datasource.RedisDataSource; import io.quarkus.redis.datasource.TransactionalRedisCommands; -import io.quarkus.redis.datasource.timeseries.ReactiveTimeSeriesCommands; +import io.quarkus.redis.datasource.stream.ReactiveStreamCommands; import io.quarkus.redis.datasource.transactions.ReactiveTransactionalRedisDataSource; import io.quarkus.redis.datasource.transactions.TransactionalRedisDataSource; import io.quarkus.redis.runtime.datasource.AbstractRedisCommandGroup; @@ -63,8 +63,8 @@ public class RedisApiGenerator { public static void main(String[] args) throws FileNotFoundException { // PARAMETERS - String reactiveApi = ReactiveTimeSeriesCommands.class.getName(); - String prefix = "ts"; + String reactiveApi = ReactiveStreamCommands.class.getName(); + String prefix = "x"; // --------- File out = new File("extensions/redis-client/runtime/target/generation");