diff --git a/java/client/src/main/java/glide/api/RedisClusterClient.java b/java/client/src/main/java/glide/api/RedisClusterClient.java index 400e407d85..9282b90667 100644 --- a/java/client/src/main/java/glide/api/RedisClusterClient.java +++ b/java/client/src/main/java/glide/api/RedisClusterClient.java @@ -29,11 +29,13 @@ import static redis_request.RedisRequestOuterClass.RequestType.Lolwut; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.Time; +import static redis_request.RedisRequestOuterClass.RequestType.UnWatch; import glide.api.commands.ConnectionManagementClusterCommands; import glide.api.commands.GenericClusterCommands; import glide.api.commands.ScriptingAndFunctionsClusterCommands; import glide.api.commands.ServerManagementClusterCommands; +import glide.api.commands.TransactionsBaseClusterCommands; import glide.api.models.ClusterTransaction; import glide.api.models.ClusterValue; import glide.api.models.commands.FlushMode; @@ -59,7 +61,8 @@ public class RedisClusterClient extends BaseClient implements ConnectionManagementClusterCommands, GenericClusterCommands, ServerManagementClusterCommands, - ScriptingAndFunctionsClusterCommands { + ScriptingAndFunctionsClusterCommands, + TransactionsBaseClusterCommands { protected RedisClusterClient(ConnectionManager connectionManager, CommandManager commandManager) { super(connectionManager, commandManager); @@ -575,4 +578,16 @@ public CompletableFuture> fcall( ? ClusterValue.ofSingleValue(handleObjectOrNullResponse(response)) : ClusterValue.ofMultiValue(handleMapResponse(response))); } + + @Override + public CompletableFuture> unwatch(@NonNull Route route) { + return commandManager.submitNewCommand( + UnWatch, + new String[0], + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.of(handleStringResponse(response)) + : ClusterValue.of(handleMapResponse(response))); + } } diff --git a/java/client/src/main/java/glide/api/commands/TransactionsBaseClusterCommands.java b/java/client/src/main/java/glide/api/commands/TransactionsBaseClusterCommands.java new file mode 100644 index 0000000000..d4b3c9c2a2 --- /dev/null +++ b/java/client/src/main/java/glide/api/commands/TransactionsBaseClusterCommands.java @@ -0,0 +1,29 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.commands; + +import glide.api.models.ClusterValue; +import glide.api.models.configuration.RequestRoutingConfiguration.Route; +import java.util.concurrent.CompletableFuture; + +/** + * Supports commands for the "Transactions Commands" group for cluster clients. + * + * @see Transactions Commands + */ +public interface TransactionsBaseClusterCommands { + /** + * Flushes all the previously watched keys for a transaction. Executing a transaction will + * automatically flush all previously watched keys. + * + * @see redis.io for details. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return The string OK. + * @example + *
{@code
+     * client.watch(new String[] {"sampleKey"});
+     * client.unwatch(ALL_PRIMARIES); // Flushes "sampleKey" from watched keys for all primary nodes.
+     * }
+ */ + CompletableFuture> unwatch(Route route); +} diff --git a/java/client/src/test/java/glide/api/RedisClusterClientTest.java b/java/client/src/test/java/glide/api/RedisClusterClientTest.java index 15ff0a59fd..ab7adcb1fa 100644 --- a/java/client/src/test/java/glide/api/RedisClusterClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClusterClientTest.java @@ -37,6 +37,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.Lolwut; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.Time; +import static redis_request.RedisRequestOuterClass.RequestType.UnWatch; import glide.api.models.ClusterTransaction; import glide.api.models.ClusterValue; @@ -1450,6 +1451,27 @@ public void functionDelete_with_route_returns_success() { assertEquals(OK, payload); } + @SneakyThrows + @Test + public void unwatch_with_route_returns_success() { + // setup + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(ClusterValue.ofSingleValue(OK)); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(UnWatch), eq(new String[0]), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.unwatch(RANDOM); + String payload = response.get().getSingleValue(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + @SneakyThrows @Test public void fcall_without_keys_and_without_args_returns_success() { diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index dde3ab4eb6..323cee1f78 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -31,7 +31,6 @@ import glide.api.RedisClient; import glide.api.RedisClusterClient; import glide.api.models.Script; -import glide.api.models.Transaction; import glide.api.models.commands.ConditionalChange; import glide.api.models.commands.ExpireOptions; import glide.api.models.commands.ListDirection; diff --git a/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java b/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java index 958460b1fb..23e54c482e 100644 --- a/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java +++ b/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java @@ -4,10 +4,13 @@ import static glide.TestConfiguration.REDIS_VERSION; import static glide.TestUtilities.assertDeepEquals; import static glide.api.BaseClient.OK; +import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_PRIMARIES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleSingleNodeRoute.RANDOM; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -20,10 +23,12 @@ import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; import glide.api.models.configuration.RequestRoutingConfiguration.SlotIdRoute; import glide.api.models.configuration.RequestRoutingConfiguration.SlotType; +import glide.api.models.exceptions.RequestException; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ExecutionException; import lombok.SneakyThrows; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -176,4 +181,84 @@ public void zrank_zrevrank_withscores() { assertArrayEquals(new Object[] {0L, 1.0}, (Object[]) result[1]); assertArrayEquals(new Object[] {2L, 1.0}, (Object[]) result[2]); } + + @Test + @SneakyThrows + public void watch() { + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String key3 = "{key}-3" + UUID.randomUUID(); + String key4 = "{key}-4" + UUID.randomUUID(); + String foobarString = "foobar"; + String helloString = "hello"; + String[] keys = new String[] {key1, key2, key3}; + ClusterTransaction setFoobarTransaction = new ClusterTransaction(); + ClusterTransaction setHelloTransaction = new ClusterTransaction(); + String[] expectedExecResponse = new String[] {OK, OK, OK}; + + // Returns null when a watched key is modified before it is executed in a transaction command. + // Transaction commands are not performed. + assertEquals(OK, clusterClient.watch(keys).get()); + assertEquals(OK, clusterClient.set(key2, helloString).get()); + setFoobarTransaction.set(key1, foobarString).set(key2, foobarString).set(key3, foobarString); + assertEquals(null, clusterClient.exec(setFoobarTransaction).get()); + assertEquals(null, clusterClient.get(key1).get()); + assertEquals(helloString, clusterClient.get(key2).get()); + assertEquals(null, clusterClient.get(key3).get()); + + // Transaction executes command successfully with a read command on the watch key before + // transaction is executed. + assertEquals(OK, clusterClient.watch(keys).get()); + assertEquals(helloString, clusterClient.get(key2).get()); + assertArrayEquals(expectedExecResponse, clusterClient.exec(setFoobarTransaction).get()); + assertEquals(foobarString, clusterClient.get(key1).get()); + assertEquals(foobarString, clusterClient.get(key2).get()); + assertEquals(foobarString, clusterClient.get(key3).get()); + + // Transaction executes command successfully with unmodified watched keys + assertEquals(OK, clusterClient.watch(keys).get()); + assertArrayEquals(expectedExecResponse, clusterClient.exec(setFoobarTransaction).get()); + assertEquals(foobarString, clusterClient.get(key1).get()); + assertEquals(foobarString, clusterClient.get(key2).get()); + assertEquals(foobarString, clusterClient.get(key3).get()); + + // Transaction executes command successfully with a modified watched key but is not in the + // transaction. + assertEquals(OK, clusterClient.watch(new String[] {key4}).get()); + setHelloTransaction.set(key1, helloString).set(key2, helloString).set(key3, helloString); + assertArrayEquals(expectedExecResponse, clusterClient.exec(setHelloTransaction).get()); + assertEquals(helloString, clusterClient.get(key1).get()); + assertEquals(helloString, clusterClient.get(key2).get()); + assertEquals(helloString, clusterClient.get(key3).get()); + + // WATCH can not have an empty String array parameter + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> clusterClient.watch(new String[] {}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @Test + @SneakyThrows + public void unwatch() { + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String foobarString = "foobar"; + String helloString = "hello"; + String[] keys = new String[] {key1, key2}; + ClusterTransaction setFoobarTransaction = new ClusterTransaction(); + String[] expectedExecResponse = new String[] {OK, OK}; + + // UNWATCH returns OK when there no watched keys + assertEquals(OK, clusterClient.unwatch().get()); + + // Transaction executes successfully after modifying a watched key then calling UNWATCH + assertEquals(OK, clusterClient.watch(keys).get()); + assertEquals(OK, clusterClient.set(key2, helloString).get()); + Map multiPayload = clusterClient.unwatch(ALL_PRIMARIES).get().getMultiValue(); + multiPayload.forEach((key, value) -> assertEquals(OK, value)); + setFoobarTransaction.set(key1, foobarString).set(key2, foobarString); + assertArrayEquals(expectedExecResponse, clusterClient.exec(setFoobarTransaction).get()); + assertEquals(foobarString, clusterClient.get(key1).get()); + assertEquals(foobarString, clusterClient.get(key2).get()); + } } diff --git a/java/integTest/src/test/java/glide/standalone/TransactionTests.java b/java/integTest/src/test/java/glide/standalone/TransactionTests.java index a0adf6c8e0..854e79c9f5 100644 --- a/java/integTest/src/test/java/glide/standalone/TransactionTests.java +++ b/java/integTest/src/test/java/glide/standalone/TransactionTests.java @@ -7,7 +7,9 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -18,10 +20,12 @@ import glide.api.models.commands.InfoOptions; import glide.api.models.configuration.NodeAddress; import glide.api.models.configuration.RedisClientConfiguration; +import glide.api.models.exceptions.RequestException; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ExecutionException; import lombok.SneakyThrows; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -264,4 +268,83 @@ public void copy() { Object[] result = client.exec(transaction).get(); assertArrayEquals(expectedResult, result); } + + @Test + @SneakyThrows + public void watch() { + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String key3 = "{key}-3" + UUID.randomUUID(); + String key4 = "{key}-4" + UUID.randomUUID(); + String foobarString = "foobar"; + String helloString = "hello"; + String[] keys = new String[] {key1, key2, key3}; + Transaction setFoobarTransaction = new Transaction(); + Transaction setHelloTransaction = new Transaction(); + String[] expectedExecResponse = new String[] {OK, OK, OK}; + + // Returns null when a watched key is modified before it is executed in a transaction command. + // Transaction commands are not performed. + assertEquals(OK, client.watch(keys).get()); + assertEquals(OK, client.set(key2, helloString).get()); + setFoobarTransaction.set(key1, foobarString).set(key2, foobarString).set(key3, foobarString); + assertEquals(null, client.exec(setFoobarTransaction).get()); + assertEquals(null, client.get(key1).get()); + assertEquals(helloString, client.get(key2).get()); + assertEquals(null, client.get(key3).get()); + + // Transaction executes command successfully with a read command on the watch key before + // transaction is executed. + assertEquals(OK, client.watch(keys).get()); + assertEquals(helloString, client.get(key2).get()); + assertArrayEquals(expectedExecResponse, client.exec(setFoobarTransaction).get()); + assertEquals(foobarString, client.get(key1).get()); + assertEquals(foobarString, client.get(key2).get()); + assertEquals(foobarString, client.get(key3).get()); + + // Transaction executes command successfully with unmodified watched keys + assertEquals(OK, client.watch(keys).get()); + assertArrayEquals(expectedExecResponse, client.exec(setFoobarTransaction).get()); + assertEquals(foobarString, client.get(key1).get()); + assertEquals(foobarString, client.get(key2).get()); + assertEquals(foobarString, client.get(key3).get()); + + // Transaction executes command successfully with a modified watched key but is not in the + // transaction. + assertEquals(OK, client.watch(new String[] {key4}).get()); + setHelloTransaction.set(key1, helloString).set(key2, helloString).set(key3, helloString); + assertArrayEquals(expectedExecResponse, client.exec(setHelloTransaction).get()); + assertEquals(helloString, client.get(key1).get()); + assertEquals(helloString, client.get(key2).get()); + assertEquals(helloString, client.get(key3).get()); + + // WATCH can not have an empty String array parameter + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.watch(new String[] {}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @Test + @SneakyThrows + public void unwatch() { + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String foobarString = "foobar"; + String helloString = "hello"; + String[] keys = new String[] {key1, key2}; + Transaction setFoobarTransaction = new Transaction(); + String[] expectedExecResponse = new String[] {OK, OK}; + + // UNWATCH returns OK when there no watched keys + assertEquals(OK, client.unwatch().get()); + + // Transaction executes successfully after modifying a watched key then calling UNWATCH + assertEquals(OK, client.watch(keys).get()); + assertEquals(OK, client.set(key2, helloString).get()); + assertEquals(OK, client.unwatch().get()); + setFoobarTransaction.set(key1, foobarString).set(key2, foobarString); + assertArrayEquals(expectedExecResponse, client.exec(setFoobarTransaction).get()); + assertEquals(foobarString, client.get(key1).get()); + assertEquals(foobarString, client.get(key2).get()); + } }