diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 784cf6d6e9..72d0785360 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -79,6 +79,7 @@ public abstract class BaseClient HashCommands, ListBaseCommands, SetCommands { + /** Redis simple string response with "OK" */ public static final String OK = ConstantResponse.OK.toString(); @@ -214,7 +215,7 @@ protected Object[] handleArrayOrNullResponse(Response response) throws RedisExce /** * @param response A Protobuf response * @return A map of String to V - * @param Value type could be even map too + * @param Value type */ @SuppressWarnings("unchecked") // raw Map cast to Map protected Map handleMapResponse(Response response) throws RedisException { diff --git a/java/client/src/main/java/glide/api/RedisClient.java b/java/client/src/main/java/glide/api/RedisClient.java index 58844630a9..11911f9bf5 100644 --- a/java/client/src/main/java/glide/api/RedisClient.java +++ b/java/client/src/main/java/glide/api/RedisClient.java @@ -1,6 +1,8 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api; +import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; +import static redis_request.RedisRequestOuterClass.RequestType.ClientId; import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.Ping; @@ -75,4 +77,15 @@ public CompletableFuture select(long index) { return commandManager.submitNewCommand( Select, new String[] {Long.toString(index)}, this::handleStringResponse); } + + @Override + public CompletableFuture clientId() { + return commandManager.submitNewCommand(ClientId, new String[0], this::handleLongResponse); + } + + @Override + public CompletableFuture clientGetName() { + return commandManager.submitNewCommand( + ClientGetName, new String[0], this::handleStringOrNullResponse); + } } diff --git a/java/client/src/main/java/glide/api/RedisClusterClient.java b/java/client/src/main/java/glide/api/RedisClusterClient.java index 6b693ed1db..fb8c060687 100644 --- a/java/client/src/main/java/glide/api/RedisClusterClient.java +++ b/java/client/src/main/java/glide/api/RedisClusterClient.java @@ -1,6 +1,8 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api; +import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; +import static redis_request.RedisRequestOuterClass.RequestType.ClientId; import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.Ping; @@ -144,4 +146,39 @@ public CompletableFuture> info( ? ClusterValue.of(handleStringResponse(response)) : ClusterValue.of(handleMapResponse(response))); } + + @Override + public CompletableFuture clientId() { + return commandManager.submitNewCommand(ClientId, new String[0], this::handleLongResponse); + } + + @Override + public CompletableFuture> clientId(@NonNull Route route) { + return commandManager.submitNewCommand( + ClientId, + new String[0], + route, + response -> + route.isSingleNodeRoute() + ? ClusterValue.of(handleLongResponse(response)) + : ClusterValue.of(handleMapResponse(response))); + } + + @Override + public CompletableFuture clientGetName() { + return commandManager.submitNewCommand( + ClientGetName, new String[0], this::handleStringOrNullResponse); + } + + @Override + public CompletableFuture> clientGetName(@NonNull Route route) { + return commandManager.submitNewCommand( + ClientGetName, + new String[0], + route, + response -> + route.isSingleNodeRoute() + ? ClusterValue.of(handleStringOrNullResponse(response)) + : ClusterValue.of(handleMapResponse(response))); + } } diff --git a/java/client/src/main/java/glide/api/commands/ConnectionManagementClusterCommands.java b/java/client/src/main/java/glide/api/commands/ConnectionManagementClusterCommands.java index 5620052dfe..49774f0c09 100644 --- a/java/client/src/main/java/glide/api/commands/ConnectionManagementClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/ConnectionManagementClusterCommands.java @@ -1,13 +1,14 @@ /** 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; /** - * Connection Management Commands interface. + * Connection Management Commands interface for cluster client. * - * @see: Connection Management Commands + * @see Connection Management Commands */ public interface ConnectionManagementClusterCommands { @@ -48,4 +49,74 @@ public interface ConnectionManagementClusterCommands { * @return String with a copy of the argument message. */ CompletableFuture ping(String message, Route route); + + /** + * Gets the current connection id.
+ * The command will be routed to a random node. + * + * @see redis.io for details. + * @return The id of the client. + * @example + *
{@code
+     * long id = client.clientId().get();
+     * assert id > 0
+     * }
+ */ + CompletableFuture clientId(); + + /** + * Gets the current connection id. + * + * @see redis.io for details. + * @param route Routing configuration for the command. Client will route the command to the nodes + * defined. + * @return A {@link ClusterValue} which holds a single value if single node route is used or a + * dictionary where each address is the key and its corresponding node response is the value. + * The value is the id of the client on that node. + * @example + *
{@code
+     * long id = client.clientId(new SlotIdRoute(...)).get().getSingleValue();
+     * assert id > 0;
+     *
+     * Map idPerNode = client.clientId(ALL_NODES).get().getMultiValue();
+     * assert idPerNode.get("") > 0;
+     * 
+ */ + CompletableFuture> clientId(Route route); + + /** + * Gets the name of the current connection.
+ * The command will be routed a random node. + * + * @see redis.io for details. + * @return The name of the client connection as a string if a name is set, or null if + * no name is assigned. + * @example + *
{@code
+     * String clientName = client.clientGetName().get();
+     * assert clientName != null;
+     * }
+ */ + CompletableFuture clientGetName(); + + /** + * Gets the name of the current connection. + * + * @see redis.io for details. + * @param route Routing configuration for the command. Client will route the command to the nodes + * defined. + * @return A {@link ClusterValue} which holds a single value if single node route is used or a + * dictionary where each address is the key and its corresponding node response is the value. + * The value is the name of the client connection as a string if a name is set, or null if no + * name is assigned. + * @example + *
{@code
+     * String clientName = client.clientGetName(new SlotIdRoute(...)).get().getSingleValue();
+     * assert clientName != null;
+     *
+     * Map clientNamePerNode = client.clientGetName(ALL_NODES).get().getMultiValue();
+     * assert clientNamePerNode.get("") != null
+     * }
+ */ + CompletableFuture> clientGetName(Route route); } diff --git a/java/client/src/main/java/glide/api/commands/ConnectionManagementCommands.java b/java/client/src/main/java/glide/api/commands/ConnectionManagementCommands.java index a38cff926e..f1840921c6 100644 --- a/java/client/src/main/java/glide/api/commands/ConnectionManagementCommands.java +++ b/java/client/src/main/java/glide/api/commands/ConnectionManagementCommands.java @@ -4,9 +4,9 @@ import java.util.concurrent.CompletableFuture; /** - * Connection Management Commands interface. + * Connection Management Commands interface for standalone client. * - * @see: Connection Management Commands + * @see Connection Management Commands */ public interface ConnectionManagementCommands { @@ -26,4 +26,31 @@ public interface ConnectionManagementCommands { * @return String with a copy of the argument message. */ CompletableFuture ping(String message); + + /** + * Gets the current connection id. + * + * @see redis.io for details. + * @return The id of the client. + * @example + *
{@code
+     * Long id = client.clientId().get();
+     * assert id > 0;
+     * }
+ */ + CompletableFuture clientId(); + + /** + * Gets the name of the current connection. + * + * @see redis.io for details. + * @return The name of the client connection as a string if a name is set, or null if + * no name is assigned. + * @example + *
{@code
+     * String clientName = client.clientGetName().get();
+     * assert clientName != null;
+     * }
+ */ + CompletableFuture clientGetName(); } 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 42a8ab222b..6eed1f4ce1 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -2,6 +2,8 @@ package glide.api.models; import static glide.utils.ArrayTransformUtils.convertMapToArgArray; +import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; +import static redis_request.RedisRequestOuterClass.RequestType.ClientId; import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.Decr; import static redis_request.RedisRequestOuterClass.RequestType.DecrBy; @@ -966,6 +968,29 @@ public T ttl(@NonNull String key) { return getThis(); } + /** + * Get the current connection id. + * + * @see redis.io for details. + * @return Command response - The id of the client. + */ + public T clientId() { + protobufTransaction.addCommands(buildCommand(ClientId)); + return getThis(); + } + + /** + * Get the name of the current connection. + * + * @see redis.io for details. + * @return Command response - The name of the client connection as a string if a name is set, or + * null if no name is assigned. + */ + public T clientGetName() { + protobufTransaction.addCommands(buildCommand(ClientGetName)); + return getThis(); + } + /** Build protobuf {@link Command} object for given command and arguments. */ protected Command buildCommand(RequestType requestType) { return buildCommand(requestType, buildArgs()); diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index b8f89ac6d9..c56d90e30f 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -12,6 +12,8 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; +import static redis_request.RedisRequestOuterClass.RequestType.ClientId; import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.Decr; import static redis_request.RedisRequestOuterClass.RequestType.DecrBy; @@ -1286,4 +1288,42 @@ public void scard_returns_success() { assertEquals(testResponse, response); assertEquals(value, payload); } + + @SneakyThrows + @Test + public void clientId_returns_success() { + // setup + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(42L); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ClientId), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.clientId(); + + // verify + assertEquals(testResponse, response); + assertEquals(42L, response.get()); + } + + @SneakyThrows + @Test + public void clientGetName_returns_success() { + // setup + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn("TEST"); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ClientGetName), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.clientGetName(); + + // verify + assertEquals(testResponse, response); + assertEquals("TEST", response.get()); + } } diff --git a/java/client/src/test/java/glide/api/RedisClusterClientTest.java b/java/client/src/test/java/glide/api/RedisClusterClientTest.java index 341cd40025..616f3440c7 100644 --- a/java/client/src/test/java/glide/api/RedisClusterClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClusterClientTest.java @@ -11,6 +11,8 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; +import static redis_request.RedisRequestOuterClass.RequestType.ClientId; import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.Ping; @@ -118,7 +120,6 @@ public TestClient(CommandManager commandManager, Object objectToReturn) { object = objectToReturn; } - @SuppressWarnings("unchecked") @Override protected T handleRedisResponse(Class classType, boolean isNullable, Response response) { return (T) object; @@ -356,4 +357,88 @@ public void info_with_options_and_multi_node_route_returns_multi_value() { assertAll( () -> assertTrue(value.hasMultiData()), () -> assertEquals(data, value.getMultiValue())); } + + @SneakyThrows + @Test + public void clientId_returns_success() { + // setup + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(42L); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ClientId), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.clientId(); + + // verify + assertEquals(testResponse, response); + assertEquals(42L, response.get()); + } + + @Test + @SneakyThrows + public void clientId_with_multi_node_route_returns_success() { + var commandManager = new TestCommandManager(null); + + var data = Map.of("n1", 42L); + var client = new TestClient(commandManager, data); + + var value = client.clientId(ALL_NODES).get(); + assertEquals(data, value.getMultiValue()); + } + + @Test + @SneakyThrows + public void clientId_with_single_node_route_returns_success() { + var commandManager = new TestCommandManager(null); + + var client = new TestClient(commandManager, 42L); + + var value = client.clientId(RANDOM).get(); + assertEquals(42, value.getSingleValue()); + } + + @SneakyThrows + @Test + public void clientGetName_returns_success() { + // setup + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn("TEST"); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ClientGetName), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.clientGetName(); + + // verify + assertEquals(testResponse, response); + assertEquals("TEST", response.get()); + } + + @Test + @SneakyThrows + public void clientGetName_with_single_node_route_returns_success() { + var commandManager = new TestCommandManager(null); + + var client = new TestClient(commandManager, "TEST"); + + var value = client.clientGetName(RANDOM).get(); + assertEquals("TEST", value.getSingleValue()); + } + + @Test + @SneakyThrows + public void clientGetName_with_multi_node_route_returns_success() { + var commandManager = new TestCommandManager(null); + + var data = Map.of("n1", "TEST"); + var client = new TestClient(commandManager, data); + + var value = client.clientGetName(ALL_NODES).get(); + assertEquals(data, value.getMultiValue()); + } } 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 89a507fbb7..8a8660f900 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -3,6 +3,8 @@ import static glide.api.models.commands.SetOptions.RETURN_OLD_VALUE; import static org.junit.jupiter.api.Assertions.assertEquals; +import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; +import static redis_request.RedisRequestOuterClass.RequestType.ClientId; import static redis_request.RedisRequestOuterClass.RequestType.Decr; import static redis_request.RedisRequestOuterClass.RequestType.DecrBy; import static redis_request.RedisRequestOuterClass.RequestType.Del; @@ -269,6 +271,12 @@ public void transaction_builds_protobuf_request(BaseTransaction transaction) transaction.ttl("key"); results.add(Pair.of(TTL, ArgsArray.newBuilder().addArgs("key").build())); + transaction.clientId(); + results.add(Pair.of(ClientId, ArgsArray.newBuilder().build())); + + transaction.clientGetName(); + results.add(Pair.of(ClientGetName, ArgsArray.newBuilder().build())); + var protobufTransaction = transaction.getProtobufTransaction().build(); for (int idx = 0; idx < protobufTransaction.getCommandsCount(); idx++) { diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 26acf92eda..e8742fc103 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -296,7 +296,7 @@ public void set_missing_value_and_returnOldValue_is_null(BaseClient client) { assertEquals(OK, ok); SetOptions options = SetOptions.builder().returnOldValue(true).build(); - String data = client.set("another", ANOTHER_VALUE, options).get(); + String data = client.set(UUID.randomUUID().toString(), ANOTHER_VALUE, options).get(); assertNull(data); } diff --git a/java/integTest/src/test/java/glide/TestUtilities.java b/java/integTest/src/test/java/glide/TestUtilities.java new file mode 100644 index 0000000000..49570a117e --- /dev/null +++ b/java/integTest/src/test/java/glide/TestUtilities.java @@ -0,0 +1,14 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide; + +import glide.api.models.ClusterValue; +import lombok.experimental.UtilityClass; + +@UtilityClass +public class TestUtilities { + + /** Extract first value from {@link ClusterValue} assuming it contains a multi-value. */ + public static T getFirstEntryFromMultiValue(ClusterValue data) { + return data.getMultiValue().get(data.getMultiValue().keySet().toArray(String[]::new)[0]); + } +} diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index 408cabb8a6..4c9be4f1ca 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -3,6 +3,7 @@ import static glide.TestConfiguration.CLUSTER_PORTS; import static glide.TestConfiguration.REDIS_VERSION; +import static glide.TestUtilities.getFirstEntryFromMultiValue; import static glide.api.models.commands.InfoOptions.Section.CLIENTS; import static glide.api.models.commands.InfoOptions.Section.CLUSTER; import static glide.api.models.commands.InfoOptions.Section.COMMANDSTATS; @@ -274,4 +275,64 @@ public void info_with_multi_node_route_and_options() { } } } + + @Test + @SneakyThrows + public void clientId() { + var id = clusterClient.clientId().get(); + assertTrue(id > 0); + } + + @Test + @SneakyThrows + public void clientId_with_single_node_route() { + var data = clusterClient.clientId(RANDOM).get(); + assertTrue(data.getSingleValue() > 0L); + } + + @Test + @SneakyThrows + public void clientId_with_multi_node_route() { + var data = clusterClient.clientId(ALL_NODES).get(); + data.getMultiValue().values().forEach(id -> assertTrue(id > 0)); + } + + @Test + @SneakyThrows + public void clientGetName() { + // TODO replace with the corresponding command once implemented + clusterClient.customCommand(new String[] {"client", "setname", "clientGetName"}).get(); + + var name = clusterClient.clientGetName().get(); + + assertEquals("clientGetName", name); + } + + @Test + @SneakyThrows + public void clientGetName_with_single_node_route() { + // TODO replace with the corresponding command once implemented + clusterClient + .customCommand( + new String[] {"client", "setname", "clientGetName_with_single_node_route"}, ALL_NODES) + .get(); + + var name = clusterClient.clientGetName(RANDOM).get(); + + assertEquals("clientGetName_with_single_node_route", name.getSingleValue()); + } + + @Test + @SneakyThrows + public void clientGetName_with_multi_node_route() { + // TODO replace with the corresponding command once implemented + clusterClient + .customCommand( + new String[] {"client", "setname", "clientGetName_with_multi_node_route"}, ALL_NODES) + .get(); + + var name = clusterClient.clientGetName(ALL_NODES).get(); + + assertEquals("clientGetName_with_multi_node_route", getFirstEntryFromMultiValue(name)); + } } diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index 747c6e152d..86382214bb 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -142,4 +142,22 @@ public void select_test_gives_error() { assertThrows(ExecutionException.class, () -> regularClient.select(-1).get()); assertTrue(e.getCause() instanceof RequestException); } + + @Test + @SneakyThrows + public void clientId() { + var id = regularClient.clientId().get(); + assertTrue(id > 0); + } + + @Test + @SneakyThrows + public void clientGetName() { + // TODO replace with the corresponding command once implemented + regularClient.customCommand(new String[] {"client", "setname", "clientGetName"}).get(); + + var name = regularClient.clientGetName().get(); + + assertEquals("clientGetName", name); + } }