diff --git a/glide-core/src/client/value_conversion.rs b/glide-core/src/client/value_conversion.rs index 347fa11a54..efe24ec66c 100644 --- a/glide-core/src/client/value_conversion.rs +++ b/glide-core/src/client/value_conversion.rs @@ -698,7 +698,7 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { }), b"INCRBYFLOAT" | b"HINCRBYFLOAT" | b"ZINCRBY" => Some(ExpectedReturnType::Double), b"HEXISTS" | b"HSETNX" | b"EXPIRE" | b"EXPIREAT" | b"PEXPIRE" | b"PEXPIREAT" - | b"SISMEMBER" | b"PERSIST" | b"SMOVE" | b"RENAMENX" | b"MOVE" | b"COPY" => { + | b"SISMEMBER" | b"PERSIST" | b"SMOVE" | b"RENAMENX" | b"MOVE" | b"COPY" | b"MSETNX" => { Some(ExpectedReturnType::Boolean) } b"SMISMEMBER" => Some(ExpectedReturnType::ArrayOfBools), diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index 7a8f3a2746..161e1b2ec5 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -211,6 +211,7 @@ enum RequestType { Move = 174; SInterCard = 175; Copy = 178; + MSetNX = 179; } message Command { diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index 8e20449c1d..556885cc26 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -181,6 +181,7 @@ pub enum RequestType { Move = 174, SInterCard = 175, Copy = 178, + MSetNX = 179, } fn get_two_word_command(first: &str, second: &str) -> Cmd { @@ -365,6 +366,7 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::SInterCard => RequestType::SInterCard, ProtobufRequestType::Copy => RequestType::Copy, ProtobufRequestType::Sort => RequestType::Sort, + ProtobufRequestType::MSetNX => RequestType::MSetNX, } } } @@ -545,6 +547,7 @@ impl RequestType { RequestType::SInterCard => Some(cmd("SINTERCARD")), RequestType::Copy => Some(cmd("COPY")), RequestType::Sort => Some(cmd("SORT")), + RequestType::MSetNX => Some(cmd("MSETNX")), } } } diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index db038b9373..8deae71b9a 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -73,6 +73,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.LTrim; import static redis_request.RedisRequestOuterClass.RequestType.MGet; import static redis_request.RedisRequestOuterClass.RequestType.MSet; +import static redis_request.RedisRequestOuterClass.RequestType.MSetNX; import static redis_request.RedisRequestOuterClass.RequestType.ObjectEncoding; import static redis_request.RedisRequestOuterClass.RequestType.ObjectFreq; import static redis_request.RedisRequestOuterClass.RequestType.ObjectIdleTime; @@ -1765,4 +1766,10 @@ public CompletableFuture copy(@NonNull String source, @NonNull String d String[] arguments = new String[] {source, destination}; return commandManager.submitNewCommand(Copy, arguments, this::handleBooleanResponse); } + + @Override + public CompletableFuture msetnx(@NonNull Map keyValueMap) { + String[] args = convertMapToKeyValueStringArray(keyValueMap); + return commandManager.submitNewCommand(MSetNX, args, this::handleBooleanResponse); + } } diff --git a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java index 45c46a1e90..d753602818 100644 --- a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java @@ -156,6 +156,25 @@ public interface StringBaseCommands { */ CompletableFuture mset(Map keyValueMap); + /** + * Sets multiple keys to multiple values in a single operation. Performs not operation at all even + * if just a single key already exists. + * + * @apiNote When in cluster mode, the command may route to multiple nodes when keys in + * keyValueMap map to different hash slots. + * @see redis.io for details. + * @param keyValueMap A key-value map consisting of keys and their respective values to set. + * @return true if all keys were set, false if no key was set. + * @example + *
{@code
+     * Boolean result = client.msetnx(Map.of("key1", "value1", "key2", "value2"}).get();
+     * assertTrue(result);
+     * Boolean result = client.msetnx(Map.of("key1", "value1", "key2", "value2"}).get();
+     * assertFalse(result);
+     * }
+ */ + CompletableFuture msetnx(Map keyValueMap); + /** * Increments the number stored at key by one. If key does not exist, it * is set to 0 before performing the operation. diff --git a/java/client/src/main/java/glide/api/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java index e61e5a146c..d063f8c279 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -96,6 +96,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.Lolwut; import static redis_request.RedisRequestOuterClass.RequestType.MGet; import static redis_request.RedisRequestOuterClass.RequestType.MSet; +import static redis_request.RedisRequestOuterClass.RequestType.MSetNX; import static redis_request.RedisRequestOuterClass.RequestType.ObjectEncoding; import static redis_request.RedisRequestOuterClass.RequestType.ObjectFreq; import static redis_request.RedisRequestOuterClass.RequestType.ObjectIdleTime; @@ -455,6 +456,23 @@ public T mset(@NonNull Map keyValueMap) { return getThis(); } + /** + * Sets multiple keys to multiple values in a single operation. Performs no operation at all even + * if just a single key already exists. + * + * @see redis.io for details. + * @param keyValueMap A key-value map consisting of keys and their respective values to set. + * @return Command Response - true if all keys were set, false if no key + * was set. + */ + public T msetnx(@NonNull Map keyValueMap) { + String[] args = convertMapToKeyValueStringArray(keyValueMap); + ArgsArray commandArgs = buildArgs(args); + + protobufTransaction.addCommands(buildCommand(MSetNX, commandArgs)); + return getThis(); + } + /** * Increments the number stored at key by one. If key does not exist, it * is set to 0 before performing the operation. diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 195940e30d..29eeb8dadf 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -128,6 +128,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.Lolwut; import static redis_request.RedisRequestOuterClass.RequestType.MGet; import static redis_request.RedisRequestOuterClass.RequestType.MSet; +import static redis_request.RedisRequestOuterClass.RequestType.MSetNX; import static redis_request.RedisRequestOuterClass.RequestType.Move; import static redis_request.RedisRequestOuterClass.RequestType.ObjectEncoding; import static redis_request.RedisRequestOuterClass.RequestType.ObjectFreq; @@ -1055,6 +1056,32 @@ public void mset_returns_success() { assertEquals(OK, payload); } + @SneakyThrows + @Test + public void msetnx_returns_success() { + // setup + Map keyValueMap = new LinkedHashMap<>(); + keyValueMap.put("key1", "value1"); + keyValueMap.put("key2", "value2"); + String[] args = {"key1", "value1", "key2", "value2"}; + Boolean value = true; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(MSetNX), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.msetnx(keyValueMap); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void incr_returns_success() { diff --git a/java/client/src/test/java/glide/api/models/TransactionTests.java b/java/client/src/test/java/glide/api/models/TransactionTests.java index 2454e210d7..bb960855a6 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -109,6 +109,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.Lolwut; import static redis_request.RedisRequestOuterClass.RequestType.MGet; import static redis_request.RedisRequestOuterClass.RequestType.MSet; +import static redis_request.RedisRequestOuterClass.RequestType.MSetNX; import static redis_request.RedisRequestOuterClass.RequestType.ObjectEncoding; import static redis_request.RedisRequestOuterClass.RequestType.ObjectFreq; import static redis_request.RedisRequestOuterClass.RequestType.ObjectIdleTime; @@ -276,6 +277,9 @@ public void transaction_builds_protobuf_request(BaseTransaction transaction) transaction.mset(Map.of("key", "value")); results.add(Pair.of(MSet, buildArgs("key", "value"))); + transaction.msetnx(Map.of("key", "value")); + results.add(Pair.of(MSetNX, buildArgs("key", "value"))); + transaction.mget(new String[] {"key"}); results.add(Pair.of(MGet, buildArgs("key"))); diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 49a35ed4c2..71ca2a0244 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -5081,4 +5081,28 @@ public void copy(BaseClient client) { assertTrue(client.copy(source, destination, true).get()); assertEquals("two", client.get(destination).get()); } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void msetnx(BaseClient client) { + // keys are from different slots + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String key3 = "{key}-3" + UUID.randomUUID(); + String nonExisting = UUID.randomUUID().toString(); + String value = UUID.randomUUID().toString(); + Map keyValueMap1 = Map.of(key1, value, key2, value); + Map keyValueMap2 = Map.of(key2, value, key3, value); + + // all keys are empty, successfully set + assertTrue(client.msetnx(keyValueMap1).get()); + assertArrayEquals( + new String[] {value, value, null}, + client.mget(new String[] {key1, key2, nonExisting}).get()); + + // one of the keys is already set, nothing gets set + assertFalse(client.msetnx(keyValueMap2).get()); + assertNull(client.get(key3).get()); + } } diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index a6abc8ce87..7a0863c267 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -199,6 +199,8 @@ private static Object[] stringCommands(BaseTransaction transaction) { String stringKey1 = "{StringKey}-1-" + UUID.randomUUID(); String stringKey2 = "{StringKey}-2-" + UUID.randomUUID(); String stringKey3 = "{StringKey}-3-" + UUID.randomUUID(); + String stringKey4 = "{StringKey}-4-" + UUID.randomUUID(); + String stringKey5 = "{StringKey}-5-" + UUID.randomUUID(); transaction .set(stringKey1, value1) @@ -215,7 +217,12 @@ private static Object[] stringCommands(BaseTransaction transaction) { .decrBy(stringKey3, 2) .incrByFloat(stringKey3, 0.5) .setrange(stringKey3, 0, "GLIDE") - .getrange(stringKey3, 0, 5); + .getrange(stringKey3, 0, 5) + .msetnx(Map.of(stringKey4, "foo", stringKey5, "bar")) + .mget(new String[] {stringKey4, stringKey5}) + .del(new String[] {stringKey5}) + .msetnx(Map.of(stringKey4, "foo", stringKey5, "bar")) + .mget(new String[] {stringKey4, stringKey5}); return new Object[] { OK, // set(stringKey1, value1) @@ -232,7 +239,12 @@ private static Object[] stringCommands(BaseTransaction transaction) { 0L, // decrBy(stringKey3, 2) 0.5, // incrByFloat(stringKey3, 0.5) 5L, // setrange(stringKey3, 0, "GLIDE") - "GLIDE" // getrange(stringKey3, 0, 5) + "GLIDE", // getrange(stringKey3, 0, 5) + true, // msetnx(Map.of(stringKey4, "foo", stringKey5, "bar")) + new String[] {"foo", "bar"}, // mget({stringKey4, stringKey5}) + 1L, // del(stringKey5) + false, // msetnx(Map.of(stringKey4, "foo", stringKey5, "bar")) + new String[] {"foo", null}, // mget({stringKey4, stringKey5}) }; }