diff --git a/glide-core/src/client/value_conversion.rs b/glide-core/src/client/value_conversion.rs index 22bd4f7c46..7d007141cd 100644 --- a/glide-core/src/client/value_conversion.rs +++ b/glide-core/src/client/value_conversion.rs @@ -700,7 +700,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 8b629ec619..3d493dab8f 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -212,6 +212,7 @@ enum RequestType { SInterCard = 175; XRevRange = 176; Copy = 178; + MSetNX = 179; } message Command { diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index 176a461fe0..f3b29b6f12 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -182,6 +182,7 @@ pub enum RequestType { SInterCard = 175, XRevRange = 176, Copy = 178, + MSetNX = 179, } fn get_two_word_command(first: &str, second: &str) -> Cmd { @@ -367,6 +368,7 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::Copy => RequestType::Copy, ProtobufRequestType::Sort => RequestType::Sort, ProtobufRequestType::XRevRange => RequestType::XRevRange, + ProtobufRequestType::MSetNX => RequestType::MSetNX, } } } @@ -548,6 +550,7 @@ impl RequestType { RequestType::Copy => Some(cmd("COPY")), RequestType::Sort => Some(cmd("SORT")), RequestType::XRevRange => Some(cmd("XREVRANGE")), + 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 fc7fb6889b..739cdded9d 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; @@ -1786,4 +1787,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..23496355ca 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,23 @@ public interface StringBaseCommands { */ CompletableFuture mset(Map keyValueMap); + /** + * Sets multiple keys to values if the key does not exist. The operation is atomic, and if one or + * more keys already exist, the entire operation fails. + * + * @apiNote When in cluster mode, all keys in keyValueMap must map to the same hash + * slot. + * @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();
+     * assert 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 98a89acce9..c399e332d4 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; @@ -456,6 +457,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 825290341f..879e7a4368 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; @@ -1056,6 +1057,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 04d4b8f702..107242e1ba 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; @@ -277,6 +278,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 90c874351a..323cee1f78 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -5133,4 +5133,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 51d5037ae3..9c432901ec 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}) }; } diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index c19f708647..289009fd26 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -749,7 +749,8 @@ public static Stream callCrossSlotCommandsWhichShouldFail() { "sintercard", "7.0.0", clusterClient.sintercard(new String[] {"abc", "def"}, 1)), Arguments.of( "xread", null, clusterClient.xread(Map.of("abc", "stream1", "zxy", "stream2"))), - Arguments.of("copy", "6.2.0", clusterClient.copy("abc", "def", true))); + Arguments.of("copy", "6.2.0", clusterClient.copy("abc", "def", true)), + Arguments.of("msetnx", null, clusterClient.msetnx(Map.of("abc", "def", "ghi", "jkl")))); } @SneakyThrows