From b0dace09c8bbb5b55f4472ec8f19999c78daee01 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Tue, 5 Mar 2024 17:11:37 -0800 Subject: [PATCH] Add `CONFIG REWRITE` and `CONFIG RESETSTAT`. (#1013) * Add `CONFIG REWRITE` and `CONFIG RESETSTAT`. (#95) --------- Signed-off-by: Yury-Fridlyand Co-authored-by: SanHalacogluImproving --- .../src/main/java/glide/api/RedisClient.java | 14 +++ .../java/glide/api/RedisClusterClient.java | 26 ++++++ .../ServerManagementClusterCommands.java | 66 +++++++++++++- .../commands/ServerManagementCommands.java | 31 ++++++- .../glide/api/models/BaseTransaction.java | 27 ++++++ .../test/java/glide/api/RedisClientTest.java | 42 +++++++++ .../glide/api/RedisClusterClientTest.java | 89 +++++++++++++++++++ .../glide/api/models/TransactionTests.java | 8 ++ .../src/test/java/glide/TestUtilities.java | 12 +++ .../java/glide/TransactionTestUtilities.java | 3 + .../test/java/glide/cluster/CommandTests.java | 31 +++++++ .../java/glide/standalone/CommandTests.java | 25 ++++++ 12 files changed, 372 insertions(+), 2 deletions(-) diff --git a/java/client/src/main/java/glide/api/RedisClient.java b/java/client/src/main/java/glide/api/RedisClient.java index 11911f9bf5..5d106bf131 100644 --- a/java/client/src/main/java/glide/api/RedisClient.java +++ b/java/client/src/main/java/glide/api/RedisClient.java @@ -3,6 +3,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; import static redis_request.RedisRequestOuterClass.RequestType.ClientId; +import static redis_request.RedisRequestOuterClass.RequestType.ConfigResetStat; +import static redis_request.RedisRequestOuterClass.RequestType.ConfigRewrite; import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.Ping; @@ -88,4 +90,16 @@ public CompletableFuture clientGetName() { return commandManager.submitNewCommand( ClientGetName, new String[0], this::handleStringOrNullResponse); } + + @Override + public CompletableFuture configRewrite() { + return commandManager.submitNewCommand( + ConfigRewrite, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture configResetStat() { + return commandManager.submitNewCommand( + ConfigResetStat, new String[0], this::handleStringResponse); + } } diff --git a/java/client/src/main/java/glide/api/RedisClusterClient.java b/java/client/src/main/java/glide/api/RedisClusterClient.java index fb8c060687..6947555ecc 100644 --- a/java/client/src/main/java/glide/api/RedisClusterClient.java +++ b/java/client/src/main/java/glide/api/RedisClusterClient.java @@ -3,6 +3,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; import static redis_request.RedisRequestOuterClass.RequestType.ClientId; +import static redis_request.RedisRequestOuterClass.RequestType.ConfigResetStat; +import static redis_request.RedisRequestOuterClass.RequestType.ConfigRewrite; import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.Ping; @@ -181,4 +183,28 @@ public CompletableFuture> clientGetName(@NonNull Route rout ? ClusterValue.of(handleStringOrNullResponse(response)) : ClusterValue.of(handleMapResponse(response))); } + + @Override + public CompletableFuture configRewrite() { + return commandManager.submitNewCommand( + ConfigRewrite, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture configRewrite(@NonNull Route route) { + return commandManager.submitNewCommand( + ConfigRewrite, new String[0], route, this::handleStringResponse); + } + + @Override + public CompletableFuture configResetStat() { + return commandManager.submitNewCommand( + ConfigResetStat, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture configResetStat(@NonNull Route route) { + return commandManager.submitNewCommand( + ConfigResetStat, new String[0], route, this::handleStringResponse); + } } diff --git a/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java b/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java index f511cc81fb..33d3c3a4ed 100644 --- a/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java @@ -8,7 +8,7 @@ import java.util.concurrent.CompletableFuture; /** - * Server Management Commands interface. + * Server Management Commands interface for cluster client. * * @see Server Management Commands */ @@ -71,4 +71,68 @@ public interface ServerManagementClusterCommands { * value is the information of the sections requested for the node. */ CompletableFuture> info(InfoOptions options, Route route); + + /** + * Rewrites the configuration file with the current configuration.
+ * The command will be routed automatically to all nodes. + * + * @see redis.io for details. + * @return OK when the configuration was rewritten properly, otherwise an error is + * thrown. + * @example + *
{@code
+     * String response = client.configRewrite().get();
+     * assert response.equals("OK")
+     * }
+ */ + CompletableFuture configRewrite(); + + /** + * Rewrites the configuration file with the current configuration. + * + * @see redis.io for details. + * @param route Routing configuration for the command. Client will route the command to the nodes + * defined. + * @return OK when the configuration was rewritten properly, otherwise an error is + * thrown. + * @example + *
{@code
+     * String response = client.configRewrite(ALL_PRIMARIES).get();
+     * assert response.equals("OK")
+     * }
+ */ + CompletableFuture configRewrite(Route route); + + /** + * Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands.
+ * The command will be routed automatically to all nodes. + * + * @see redis.io for details. + * @return OK to confirm that the statistics were successfully reset. + * @example + *
{@code
+     * String response = client.configResetStat().get();
+     * assert response.equals("OK")
+     * }
+ */ + CompletableFuture configResetStat(); + + /** + * Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands. + * + * @see redis.io for details. + * @param route Routing configuration for the command. Client will route the command to the nodes + * defined. + * @return OK to confirm that the statistics were successfully reset. + * @example + *
{@code
+     * String response = client.configResetStat(ALL_PRIMARIES).get();
+     * assert response.equals("OK")
+     * }
+ */ + CompletableFuture configResetStat(Route route); } diff --git a/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java b/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java index 5dd94f93e9..3f5cc514b5 100644 --- a/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java +++ b/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java @@ -6,7 +6,7 @@ import java.util.concurrent.CompletableFuture; /** - * Server Management Commands interface. + * Server Management Commands interface for standalone client. * * @see Server Management Commands */ @@ -40,4 +40,33 @@ public interface ServerManagementCommands { * @return A simple OK response. */ CompletableFuture select(long index); + + /** + * Rewrites the configuration file with the current configuration. + * + * @see redis.io for details. + * @return OK when the configuration was rewritten properly, otherwise an error is + * thrown. + * @example + *
{@code
+     * String response = client.configRewrite().get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture configRewrite(); + + /** + * Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands. + * + * @see redis.io for details. + * @return OK to confirm that the statistics were successfully reset. + * @example + *
{@code
+     * String response = client.configResetStat().get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture configResetStat(); } 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 6eed1f4ce1..d51fcd1a16 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -4,6 +4,8 @@ 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.ConfigResetStat; +import static redis_request.RedisRequestOuterClass.RequestType.ConfigRewrite; import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.Decr; import static redis_request.RedisRequestOuterClass.RequestType.DecrBy; @@ -991,6 +993,31 @@ public T clientGetName() { return getThis(); } + /** + * Rewrites the configuration file with the current configuration. + * + * @see redis.io for details. + * @return OK is returned when the configuration was rewritten properly. Otherwise, + * the transaction fails with an error. + */ + public T configRewrite() { + protobufTransaction.addCommands(buildCommand(ConfigRewrite)); + return getThis(); + } + + /** + * Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands. + * + * @see redis.io for details. + * @return OK to confirm that the statistics were successfully reset. + */ + public T configResetStat() { + protobufTransaction.addCommands(buildCommand(ConfigResetStat)); + 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 c56d90e30f..2d69888112 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -14,6 +14,8 @@ 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.ConfigResetStat; +import static redis_request.RedisRequestOuterClass.RequestType.ConfigRewrite; import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.Decr; import static redis_request.RedisRequestOuterClass.RequestType.DecrBy; @@ -1326,4 +1328,44 @@ public void clientGetName_returns_success() { assertEquals(testResponse, response); assertEquals("TEST", response.get()); } + + @SneakyThrows + @Test + public void configRewrite_returns_success() { + // setup + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ConfigRewrite), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.configRewrite(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void configResetStat_returns_success() { + // setup + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ConfigResetStat), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.configResetStat(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } } diff --git a/java/client/src/test/java/glide/api/RedisClusterClientTest.java b/java/client/src/test/java/glide/api/RedisClusterClientTest.java index 616f3440c7..e478e11917 100644 --- a/java/client/src/test/java/glide/api/RedisClusterClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClusterClientTest.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api; +import static glide.api.BaseClient.OK; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute.ALL_NODES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute.ALL_PRIMARIES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute.RANDOM; @@ -13,6 +14,8 @@ 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.ConfigResetStat; +import static redis_request.RedisRequestOuterClass.RequestType.ConfigRewrite; import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.Ping; @@ -441,4 +444,90 @@ public void clientGetName_with_multi_node_route_returns_success() { var value = client.clientGetName(ALL_NODES).get(); assertEquals(data, value.getMultiValue()); } + + @SneakyThrows + @Test + public void configRewrite_without_route_returns_success() { + // setup + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ConfigRewrite), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.configRewrite(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void configRewrite_with_route_returns_success() { + // setup + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(OK); + + Route route = ALL_NODES; + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(ConfigRewrite), eq(new String[0]), eq(route), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.configRewrite(route); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void configResetStat_without_route_returns_success() { + // setup + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ConfigResetStat), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.configResetStat(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void configResetStat_with_route_returns_success() { + // setup + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(OK); + + Route route = ALL_NODES; + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(ConfigResetStat), eq(new String[0]), eq(route), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.configResetStat(route); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } } 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 8a8660f900..2e60763ac3 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -5,6 +5,8 @@ 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.ConfigResetStat; +import static redis_request.RedisRequestOuterClass.RequestType.ConfigRewrite; import static redis_request.RedisRequestOuterClass.RequestType.Decr; import static redis_request.RedisRequestOuterClass.RequestType.DecrBy; import static redis_request.RedisRequestOuterClass.RequestType.Del; @@ -277,6 +279,12 @@ public void transaction_builds_protobuf_request(BaseTransaction transaction) transaction.clientGetName(); results.add(Pair.of(ClientGetName, ArgsArray.newBuilder().build())); + transaction.configRewrite(); + results.add(Pair.of(ConfigRewrite, ArgsArray.newBuilder().build())); + + transaction.configResetStat(); + results.add(Pair.of(ConfigResetStat, 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/TestUtilities.java b/java/integTest/src/test/java/glide/TestUtilities.java index 49570a117e..8c18e8b98f 100644 --- a/java/integTest/src/test/java/glide/TestUtilities.java +++ b/java/integTest/src/test/java/glide/TestUtilities.java @@ -1,11 +1,23 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide; +import static org.junit.jupiter.api.Assertions.fail; + import glide.api.models.ClusterValue; import lombok.experimental.UtilityClass; @UtilityClass public class TestUtilities { + /** Extract integer parameter value from INFO command output */ + public static int getValueFromInfo(String data, String value) { + for (var line : data.split("\r\n")) { + if (line.contains(value)) { + return Integer.parseInt(line.split(":")[1]); + } + } + fail(); + return 0; + } /** Extract first value from {@link ClusterValue} assuming it contains a multi-value. */ public static T getFirstEntryFromMultiValue(ClusterValue data) { diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index 3524d7e26d..94f54998bf 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -79,6 +79,8 @@ public static BaseTransaction transactionTest(BaseTransaction baseTransact baseTransaction.scard(key7); baseTransaction.smembers(key7); + baseTransaction.configResetStat(); + return baseTransaction; } @@ -122,6 +124,7 @@ public static Object[] transactionTestResult() { 1L, 1L, Set.of("baz"), + OK }; } } diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index 4c9be4f1ca..6af733d8dc 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -4,6 +4,8 @@ import static glide.TestConfiguration.CLUSTER_PORTS; import static glide.TestConfiguration.REDIS_VERSION; import static glide.TestUtilities.getFirstEntryFromMultiValue; +import static glide.TestUtilities.getValueFromInfo; +import static glide.api.BaseClient.OK; 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; @@ -11,12 +13,14 @@ import static glide.api.models.commands.InfoOptions.Section.EVERYTHING; import static glide.api.models.commands.InfoOptions.Section.MEMORY; import static glide.api.models.commands.InfoOptions.Section.REPLICATION; +import static glide.api.models.commands.InfoOptions.Section.STATS; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute.ALL_NODES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute.ALL_PRIMARIES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute.RANDOM; import static glide.api.models.configuration.RequestRoutingConfiguration.SlotType.PRIMARY; import static org.junit.jupiter.api.Assertions.assertEquals; 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 glide.api.RedisClusterClient; @@ -25,7 +29,9 @@ import glide.api.models.configuration.NodeAddress; import glide.api.models.configuration.RedisClusterClientConfiguration; import glide.api.models.configuration.RequestRoutingConfiguration.SlotKeyRoute; +import glide.api.models.exceptions.RequestException; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import lombok.SneakyThrows; import org.junit.jupiter.api.AfterAll; @@ -335,4 +341,29 @@ public void clientGetName_with_multi_node_route() { assertEquals("clientGetName_with_multi_node_route", getFirstEntryFromMultiValue(name)); } + + @Test + @SneakyThrows + public void config_reset_stat() { + var data = clusterClient.info(InfoOptions.builder().section(STATS).build()).get(); + String firstNodeInfo = getFirstEntryFromMultiValue(data); + int value_before = getValueFromInfo(firstNodeInfo, "total_net_input_bytes"); + + var result = clusterClient.configResetStat().get(); + assertEquals(OK, result); + + data = clusterClient.info(InfoOptions.builder().section(STATS).build()).get(); + firstNodeInfo = getFirstEntryFromMultiValue(data); + int value_after = getValueFromInfo(firstNodeInfo, "total_net_input_bytes"); + assertTrue(value_after < value_before); + } + + @Test + @SneakyThrows + public void config_rewrite_non_existent_config_file() { + // The setup for the Integration Tests server does not include a configuration file for Redis. + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> clusterClient.configRewrite().get()); + assertTrue(executionException.getCause() instanceof RequestException); + } } diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index 86382214bb..5ded6e00ce 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -3,11 +3,13 @@ import static glide.TestConfiguration.REDIS_VERSION; import static glide.TestConfiguration.STANDALONE_PORTS; +import static glide.TestUtilities.getValueFromInfo; import static glide.api.BaseClient.OK; import static glide.api.models.commands.InfoOptions.Section.CLUSTER; import static glide.api.models.commands.InfoOptions.Section.CPU; import static glide.api.models.commands.InfoOptions.Section.EVERYTHING; import static glide.api.models.commands.InfoOptions.Section.MEMORY; +import static glide.api.models.commands.InfoOptions.Section.STATS; import static glide.cluster.CommandTests.DEFAULT_INFO_SECTIONS; import static glide.cluster.CommandTests.EVERYTHING_INFO_SECTIONS; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -160,4 +162,27 @@ public void clientGetName() { assertEquals("clientGetName", name); } + + @Test + @SneakyThrows + public void config_reset_stat() { + String data = regularClient.info(InfoOptions.builder().section(STATS).build()).get(); + int value_before = getValueFromInfo(data, "total_net_input_bytes"); + + var result = regularClient.configResetStat().get(); + assertEquals(OK, result); + + data = regularClient.info(InfoOptions.builder().section(STATS).build()).get(); + int value_after = getValueFromInfo(data, "total_net_input_bytes"); + assertTrue(value_after < value_before); + } + + @Test + @SneakyThrows + public void config_rewrite_non_existent_config_file() { + // The setup for the Integration Tests server does not include a configuration file for Redis. + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> regularClient.configRewrite().get()); + assertTrue(executionException.getCause() instanceof RequestException); + } }