Skip to content

Commit

Permalink
Java: Add ZMPOP command. (#1442)
Browse files Browse the repository at this point in the history
* Add `ZMPOP` command. (#293)

Signed-off-by: Yury-Fridlyand <yury.fridlyand@improving.com>

* Address PR comments.

Signed-off-by: Yury-Fridlyand <yury.fridlyand@improving.com>

---------

Signed-off-by: Yury-Fridlyand <yury.fridlyand@improving.com>
  • Loading branch information
Yury-Fridlyand authored May 23, 2024
1 parent c7a47a8 commit 9a3fcc5
Show file tree
Hide file tree
Showing 9 changed files with 256 additions and 29 deletions.
20 changes: 20 additions & 0 deletions java/client/src/main/java/glide/api/BaseClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
import static redis_request.RedisRequestOuterClass.RequestType.ZInterCard;
import static redis_request.RedisRequestOuterClass.RequestType.ZInterStore;
import static redis_request.RedisRequestOuterClass.RequestType.ZLexCount;
import static redis_request.RedisRequestOuterClass.RequestType.ZMPop;
import static redis_request.RedisRequestOuterClass.RequestType.ZMScore;
import static redis_request.RedisRequestOuterClass.RequestType.ZPopMax;
import static redis_request.RedisRequestOuterClass.RequestType.ZPopMin;
Expand Down Expand Up @@ -1234,6 +1235,25 @@ public CompletableFuture<Map<String, Double>> zrangeWithScores(
return zrangeWithScores(key, rangeQuery, false);
}

@Override
public CompletableFuture<Object[]> zmpop(@NonNull String[] keys, @NonNull ScoreFilter modifier) {
String[] arguments =
concatenateArrays(
new String[] {Integer.toString(keys.length)}, keys, new String[] {modifier.toString()});
return commandManager.submitNewCommand(ZMPop, arguments, this::handleArrayOrNullResponse);
}

@Override
public CompletableFuture<Object[]> zmpop(
@NonNull String[] keys, @NonNull ScoreFilter modifier, long count) {
String[] arguments =
concatenateArrays(
new String[] {Integer.toString(keys.length)},
keys,
new String[] {modifier.toString(), COUNT_REDIS_API, Long.toString(count)});
return commandManager.submitNewCommand(ZMPop, arguments, this::handleArrayOrNullResponse);
}

@Override
public CompletableFuture<Object[]> bzmpop(
@NonNull String[] keys, @NonNull ScoreFilter modifier, double timeout) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -963,23 +963,68 @@ CompletableFuture<Long> zinterstore(
*/
CompletableFuture<Long> zinterstore(String destination, KeysOrWeightedKeys keysOrWeightedKeys);

// TODO add @link to ZMPOP when implemented
/**
* Pops a member-score pair from the first non-empty sorted set, with the given <code>keys</code>
* being checked in the order they are provided.
*
* @apiNote When in cluster mode, all <code>keys</code> must map to the same hash slot.
* @since Redis 7.0 and above.
* @see <a href="https://redis.io/commands/zmpop/">redis.io</a> for more details.
* @param keys The keys of the sorted sets.
* @param modifier The element pop criteria - either {@link ScoreFilter#MIN} or {@link
* ScoreFilter#MAX} to pop the member with the lowest/highest score accordingly.
* @return A two-element <code>array</code> containing the key name of the set from which the
* element was popped, and a member-score <code>Map</code> of the popped element.<br>
* If no member could be popped, returns <code>null</code>.
* @example
* <pre>{@code
* Object[] result = client.zmpop(new String[] { "zSet1", "zSet2" }, MAX).get();
* Map<String, Double> data = (Map<String, Double>)result[1];
* String element = data.keySet().toArray(String[]::new)[0];
* System.out.printf("Popped '%s' with score %d from '%s'%n", element, data.get(element), result[0]);
* }</pre>
*/
CompletableFuture<Object[]> zmpop(String[] keys, ScoreFilter modifier);

/**
* Pops multiple member-score pairs from the first non-empty sorted set, with the given <code>keys
* </code> being checked in the order they are provided.
*
* @apiNote When in cluster mode, all <code>keys</code> must map to the same hash slot.
* @since Redis 7.0 and above.
* @see <a href="https://redis.io/commands/zmpop/">redis.io</a> for more details.
* @param keys The keys of the sorted sets.
* @param modifier The element pop criteria - either {@link ScoreFilter#MIN} or {@link
* ScoreFilter#MAX} to pop members with the lowest/highest scores accordingly.
* @param count The number of elements to pop.
* @return A two-element <code>array</code> containing the key name of the set from which elements
* were popped, and a member-score <code>Map</code> of the popped elements.<br>
* If no member could be popped, returns <code>null</code>.
* @example
* <pre>{@code
* Object[] result = client.zmpop(new String[] { "zSet1", "zSet2" }, MAX, 2).get();
* Map<String, Double> data = (Map<String, Double>)result[1];
* for (Map.Entry<String, Double> entry : data.entrySet()) {
* System.out.printf("Popped '%s' with score %d from '%s'%n", entry.getKey(), entry.getValue(), result[0]);
* }
* }</pre>
*/
CompletableFuture<Object[]> zmpop(String[] keys, ScoreFilter modifier, long count);

/**
* Blocks the connection until it pops and returns a member-score pair from the first non-empty
* sorted set, with the given <code>keys</code> being checked in the order they are provided.<br>
* To pop more than one element use {@link #bzmpop(String[], ScoreFilter, double, long)}.<br>
* <code>BZMPOP</code> is the blocking variant of <code>ZMPOP</code>.
* <code>BZMPOP</code> is the blocking variant of {@link #zmpop(String[], ScoreFilter)}.
*
* @apiNote
* <ol>
* <li>When in cluster mode, all <code>keys</code> must map to the same <code>hash slot
* </code>.
* <li>When in cluster mode, all <code>keys</code> must map to the same hash slot.
* <li><code>BZMPOP</code> is a client blocking command, see <a
* href="https://github.com/aws/glide-for-redis/wiki/General-Concepts#blocking-commands">Blocking
* Commands</a> for more details and best practices.
* </ol>
*
* @since Redis 7.0 and above
* @since Redis 7.0 and above.
* @see <a href="https://redis.io/commands/bzmpop/">redis.io</a> for more details.
* @param keys The keys of the sorted sets.
* @param modifier The element pop criteria - either {@link ScoreFilter#MIN} or {@link
Expand All @@ -999,23 +1044,21 @@ CompletableFuture<Long> zinterstore(
*/
CompletableFuture<Object[]> bzmpop(String[] keys, ScoreFilter modifier, double timeout);

// TODO add @link to ZMPOP when implemented
/**
* Blocks the connection until it pops and returns multiple member-score pairs from the first
* non-empty sorted set, with the given <code>keys</code> being checked in the order they are
* provided.<br>
* <code>BZMPOP</code> is the blocking variant of <code>ZMPOP</code>.
* <code>BZMPOP</code> is the blocking variant of {@link #zmpop(String[], ScoreFilter, long)}.
*
* @apiNote
* <ol>
* <li>When in cluster mode, all <code>keys</code> must map to the same <code>hash slot
* </code>.
* <li>When in cluster mode, all <code>keys</code> must map to the same hash slot.
* <li><code>BZMPOP</code> is a client blocking command, see <a
* href="https://github.com/aws/glide-for-redis/wiki/General-Concepts#blocking-commands">Blocking
* Commands</a> for more details and best practices.
* </ol>
*
* @since Redis 7.0 and above
* @since Redis 7.0 and above.
* @see <a href="https://redis.io/commands/bzmpop/">redis.io</a> for more details.
* @param keys The keys of the sorted sets.
* @param modifier The element pop criteria - either {@link ScoreFilter#MIN} or {@link
Expand Down
63 changes: 56 additions & 7 deletions java/client/src/main/java/glide/api/models/BaseTransaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
import static redis_request.RedisRequestOuterClass.RequestType.ZInterCard;
import static redis_request.RedisRequestOuterClass.RequestType.ZInterStore;
import static redis_request.RedisRequestOuterClass.RequestType.ZLexCount;
import static redis_request.RedisRequestOuterClass.RequestType.ZMPop;
import static redis_request.RedisRequestOuterClass.RequestType.ZMScore;
import static redis_request.RedisRequestOuterClass.RequestType.ZPopMax;
import static redis_request.RedisRequestOuterClass.RequestType.ZPopMin;
Expand Down Expand Up @@ -2862,14 +2863,63 @@ public T zrangeWithScores(@NonNull String key, @NonNull ScoredRangeQuery rangeQu
return zrangeWithScores(key, rangeQuery, false);
}

// TODO add @link to ZMPOP when implemented
/**
* Pops a member-score pair from the first non-empty sorted set, with the given <code>keys</code>
* being checked in the order they are provided.
*
* @since Redis 7.0 and above.
* @see <a href="https://redis.io/commands/zmpop/">redis.io</a> for more details.
* @param keys The keys of the sorted sets.
* @param modifier The element pop criteria - either {@link ScoreFilter#MIN} or {@link
* ScoreFilter#MAX} to pop the member with the lowest/highest score accordingly.
* @return Command Response - A two-element <code>array</code> containing the key name of the set
* from which the element was popped, and a member-score <code>Map</code> of the popped
* element.<br>
* If no member could be popped, returns <code>null</code>.
*/
public T zmpop(@NonNull String[] keys, @NonNull ScoreFilter modifier) {
ArgsArray commandArgs =
buildArgs(
concatenateArrays(
new String[] {Integer.toString(keys.length)},
keys,
new String[] {modifier.toString()}));
protobufTransaction.addCommands(buildCommand(ZMPop, commandArgs));
return getThis();
}

/**
* Pops multiple member-score pairs from the first non-empty sorted set, with the given <code>keys
* </code> being checked in the order they are provided.
*
* @since Redis 7.0 and above.
* @see <a href="https://redis.io/commands/zmpop/">redis.io</a> for more details.
* @param keys The keys of the sorted sets.
* @param modifier The element pop criteria - either {@link ScoreFilter#MIN} or {@link
* ScoreFilter#MAX} to pop members with the lowest/highest scores accordingly.
* @param count The number of elements to pop.
* @return Command Response - A two-element <code>array</code> containing the key name of the set
* from which elements were popped, and a member-score <code>Map</code> of the popped
* elements.<br>
* If no member could be popped, returns <code>null</code>.
*/
public T zmpop(@NonNull String[] keys, @NonNull ScoreFilter modifier, long count) {
ArgsArray commandArgs =
buildArgs(
concatenateArrays(
new String[] {Integer.toString(keys.length)},
keys,
new String[] {modifier.toString(), COUNT_REDIS_API, Long.toString(count)}));
protobufTransaction.addCommands(buildCommand(ZMPop, commandArgs));
return getThis();
}

/**
* Blocks the connection until it pops and returns a member-score pair from the first non-empty
* sorted set, with the given <code>keys</code> being checked in the order they are provided.<br>
* To pop more than one element use {@link #bzmpop(String[], ScoreFilter, double, long)}.<br>
* <code>BZMPOP</code> is the blocking variant of <code>ZMPOP</code>.
* <code>BZMPOP</code> is the blocking variant of {@link #zmpop(String[], ScoreFilter)}.
*
* @since Redis 7.0 and above
* @since Redis 7.0 and above.
* @see <a href="https://redis.io/commands/bzmpop/">redis.io</a> for more details.
* @apiNote <code>BZMPOP</code> is a client blocking command, see <a
* href="https://github.com/aws/glide-for-redis/wiki/General-Concepts#blocking-commands">Blocking
Expand All @@ -2895,14 +2945,13 @@ public T bzmpop(@NonNull String[] keys, @NonNull ScoreFilter modifier, double ti
return getThis();
}

// TODO add @link to ZMPOP when implemented
/**
* Blocks the connection until it pops and returns multiple member-score pairs from the first
* non-empty sorted set, with the given <code>keys</code> being checked in the order they are
* provided.<br>
* <code>BZMPOP</code> is the blocking variant of <code>ZMPOP</code>.<br>
* <code>BZMPOP</code> is the blocking variant of {@link #zmpop(String[], ScoreFilter, long)}.
*
* @since Redis 7.0 and above
* @since Redis 7.0 and above.
* @see <a href="https://redis.io/commands/bzmpop/">redis.io</a> for more details.
* @apiNote <code>BZMPOP</code> is a client blocking command, see <a
* href="https://github.com/aws/glide-for-redis/wiki/General-Concepts#blocking-commands">Blocking
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@

import glide.api.commands.SortedSetBaseCommands;

// TODO add @link to ZMPOP when implemented
/**
* Mandatory option for {@link SortedSetBaseCommands#bzmpop(String[], ScoreFilter, double)} and for
* {@link SortedSetBaseCommands#bzmpop(String[], ScoreFilter, double, long)}. Defines which elements
* to pop from the sorted set.
* Mandatory option for {@link SortedSetBaseCommands#bzmpop} and for {@link
* SortedSetBaseCommands#zmpop}.<br>
* Defines which elements to pop from the sorted set.
*/
public enum ScoreFilter {
/** Pop elements with the lowest scores. */
Expand Down
52 changes: 52 additions & 0 deletions java/client/src/test/java/glide/api/RedisClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@
import static redis_request.RedisRequestOuterClass.RequestType.ZInterCard;
import static redis_request.RedisRequestOuterClass.RequestType.ZInterStore;
import static redis_request.RedisRequestOuterClass.RequestType.ZLexCount;
import static redis_request.RedisRequestOuterClass.RequestType.ZMPop;
import static redis_request.RedisRequestOuterClass.RequestType.ZMScore;
import static redis_request.RedisRequestOuterClass.RequestType.ZPopMax;
import static redis_request.RedisRequestOuterClass.RequestType.ZPopMin;
Expand Down Expand Up @@ -2126,6 +2127,57 @@ public void zaddIncr_withOptions_returns_success() {
assertEquals(value, payload);
}

@SneakyThrows
@Test
public void zmpop_returns_success() {
// setup
String[] keys = new String[] {"key1", "key2"};
ScoreFilter modifier = MAX;
String[] arguments = {"2", "key1", "key2", "MAX"};
Object[] value = new Object[] {"key1", "elem"};

CompletableFuture<Object[]> testResponse = new CompletableFuture<>();
testResponse.complete(value);

// match on protobuf request
when(commandManager.<Object[]>submitNewCommand(eq(ZMPop), eq(arguments), any()))
.thenReturn(testResponse);

// exercise
CompletableFuture<Object[]> response = service.zmpop(keys, modifier);
Object[] payload = response.get();

// verify
assertEquals(testResponse, response);
assertArrayEquals(value, payload);
}

@SneakyThrows
@Test
public void zmpop_with_count_returns_success() {
// setup
String[] keys = new String[] {"key1", "key2"};
ScoreFilter modifier = MAX;
long count = 42;
String[] arguments = {"2", "key1", "key2", "MAX", "COUNT", "42"};
Object[] value = new Object[] {"key1", "elem"};

CompletableFuture<Object[]> testResponse = new CompletableFuture<>();
testResponse.complete(value);

// match on protobuf request
when(commandManager.<Object[]>submitNewCommand(eq(ZMPop), eq(arguments), any()))
.thenReturn(testResponse);

// exercise
CompletableFuture<Object[]> response = service.zmpop(keys, modifier, count);
Object[] payload = response.get();

// verify
assertEquals(testResponse, response);
assertArrayEquals(value, payload);
}

@SneakyThrows
@Test
public void bzmpop_returns_success() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
import static redis_request.RedisRequestOuterClass.RequestType.ZInterCard;
import static redis_request.RedisRequestOuterClass.RequestType.ZInterStore;
import static redis_request.RedisRequestOuterClass.RequestType.ZLexCount;
import static redis_request.RedisRequestOuterClass.RequestType.ZMPop;
import static redis_request.RedisRequestOuterClass.RequestType.ZMScore;
import static redis_request.RedisRequestOuterClass.RequestType.ZPopMax;
import static redis_request.RedisRequestOuterClass.RequestType.ZPopMin;
Expand Down Expand Up @@ -481,6 +482,10 @@ public void transaction_builds_protobuf_request(BaseTransaction<?> transaction)
transaction.zdiffstore("destKey", new String[] {"key1", "key2"});
results.add(Pair.of(ZDiffStore, buildArgs("destKey", "2", "key1", "key2")));

transaction.zmpop(new String[] {"key1", "key2"}, MAX).zmpop(new String[] {"key"}, MIN, 42);
results.add(Pair.of(ZMPop, buildArgs("2", "key1", "key2", "MAX")));
results.add(Pair.of(ZMPop, buildArgs("1", "key", "MIN", "COUNT", "42")));

transaction
.bzmpop(new String[] {"key1", "key2"}, MAX, .1)
.bzmpop(new String[] {"key"}, MIN, .1, 42);
Expand Down
51 changes: 51 additions & 0 deletions java/integTest/src/test/java/glide/SharedCommandTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -2687,6 +2687,57 @@ public void zinterstore(BaseClient client) {
assertTrue(executionException.getCause() instanceof RequestException);
}

@SneakyThrows
@ParameterizedTest(autoCloseArguments = false)
@MethodSource("getClients")
public void zmpop(BaseClient client) {
assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7");
String key1 = "{zmpop}-1-" + UUID.randomUUID();
String key2 = "{zmpop}-2-" + UUID.randomUUID();
String key3 = "{zmpop}-3-" + UUID.randomUUID();

assertEquals(2, client.zadd(key1, Map.of("a1", 1., "b1", 2.)).get());
assertEquals(2, client.zadd(key2, Map.of("a2", .1, "b2", .2)).get());

assertArrayEquals(
new Object[] {key1, Map.of("b1", 2.)}, client.zmpop(new String[] {key1, key2}, MAX).get());
assertArrayEquals(
new Object[] {key2, Map.of("b2", .2, "a2", .1)},
client.zmpop(new String[] {key2, key1}, MAX, 10).get());

// nothing popped out
assertNull(client.zmpop(new String[] {key3}, MIN).get());
assertNull(client.zmpop(new String[] {key3}, MIN, 1).get());

// Key exists, but it is not a sorted set
assertEquals(OK, client.set(key3, "value").get());
ExecutionException executionException =
assertThrows(ExecutionException.class, () -> client.zmpop(new String[] {key3}, MAX).get());
assertInstanceOf(RequestException.class, executionException.getCause());
executionException =
assertThrows(
ExecutionException.class, () -> client.zmpop(new String[] {key3}, MAX, 1).get());
assertInstanceOf(RequestException.class, executionException.getCause());

// incorrect argument
executionException =
assertThrows(
ExecutionException.class, () -> client.zmpop(new String[] {key1}, MAX, 0).get());
assertInstanceOf(RequestException.class, executionException.getCause());
executionException =
assertThrows(ExecutionException.class, () -> client.zmpop(new String[0], MAX).get());
assertInstanceOf(RequestException.class, executionException.getCause());

// check that order of entries in the response is preserved
var entries = new LinkedHashMap<String, Double>();
for (int i = 0; i < 10; i++) {
// a => 1., b => 2. etc
entries.put("" + ('a' + i), (double) i);
}
assertEquals(10, client.zadd(key2, entries).get());
assertEquals(entries, client.zmpop(new String[] {key2}, MIN, 10).get()[1]);
}

@SneakyThrows
@ParameterizedTest(autoCloseArguments = false)
@MethodSource("getClients")
Expand Down
Loading

0 comments on commit 9a3fcc5

Please sign in to comment.