From 850fc100234fd4972d25f3f5bac340bf72472bc7 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 13 Jun 2024 16:25:06 -0700 Subject: [PATCH] Java: Add `FUNCTION KILL` command. (#1560) * Java: Add `FUNCTION KILL` command. (#328) * Add `FUNCTION KILL` command. * Frantic and rampant code clean up. * More tests for the god of the tests! * Address PR comments. * Typo fix. * Fixing the tests s02ep06. * Fixing the tests s03ep01. * Address PR comments. Signed-off-by: Yury-Fridlyand Co-authored-by: aaron-congo --- glide-core/src/protobuf/redis_request.proto | 1 + glide-core/src/request_type.rs | 3 + .../src/main/java/glide/api/RedisClient.java | 6 + .../java/glide/api/RedisClusterClient.java | 12 + .../ScriptingAndFunctionsClusterCommands.java | 33 ++ .../ScriptingAndFunctionsCommands.java | 15 + .../test/java/glide/api/RedisClientTest.java | 22 ++ .../glide/api/RedisClusterClientTest.java | 43 +++ .../src/test/java/glide/TestUtilities.java | 32 ++ .../test/java/glide/cluster/CommandTests.java | 313 +++++++++++++++++- .../java/glide/standalone/CommandTests.java | 144 +++++++- .../glide/standalone/TransactionTests.java | 12 +- 12 files changed, 610 insertions(+), 26 deletions(-) diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index e633efefb5..9882ca78ca 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -200,6 +200,7 @@ enum RequestType { BLMPop = 158; XLen = 159; Sort = 160; + FunctionKill = 161; LSet = 165; XDel = 166; XRange = 167; diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index 216991da39..e0230b5847 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -170,6 +170,7 @@ pub enum RequestType { BLMPop = 158, XLen = 159, Sort = 160, + FunctionKill = 161, LSet = 165, XDel = 166, XRange = 167, @@ -357,6 +358,7 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::ExpireTime => RequestType::ExpireTime, ProtobufRequestType::PExpireTime => RequestType::PExpireTime, ProtobufRequestType::XLen => RequestType::XLen, + ProtobufRequestType::FunctionKill => RequestType::FunctionKill, ProtobufRequestType::LSet => RequestType::LSet, ProtobufRequestType::XDel => RequestType::XDel, ProtobufRequestType::XRange => RequestType::XRange, @@ -541,6 +543,7 @@ impl RequestType { RequestType::ExpireTime => Some(cmd("EXPIRETIME")), RequestType::PExpireTime => Some(cmd("PEXPIRETIME")), RequestType::XLen => Some(cmd("XLEN")), + RequestType::FunctionKill => Some(get_two_word_command("FUNCTION", "KILL")), RequestType::LSet => Some(cmd("LSET")), RequestType::XDel => Some(cmd("XDEL")), RequestType::XRange => Some(cmd("XRANGE")), diff --git a/java/client/src/main/java/glide/api/RedisClient.java b/java/client/src/main/java/glide/api/RedisClient.java index 4f675f7e11..4afb9b03e8 100644 --- a/java/client/src/main/java/glide/api/RedisClient.java +++ b/java/client/src/main/java/glide/api/RedisClient.java @@ -20,6 +20,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; import static redis_request.RedisRequestOuterClass.RequestType.FunctionFlush; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionKill; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; import static redis_request.RedisRequestOuterClass.RequestType.Info; @@ -277,4 +278,9 @@ public CompletableFuture copy( } return commandManager.submitNewCommand(Copy, arguments, this::handleBooleanResponse); } + + @Override + public CompletableFuture functionKill() { + return commandManager.submitNewCommand(FunctionKill, 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 400e407d85..4520c15164 100644 --- a/java/client/src/main/java/glide/api/RedisClusterClient.java +++ b/java/client/src/main/java/glide/api/RedisClusterClient.java @@ -22,6 +22,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; import static redis_request.RedisRequestOuterClass.RequestType.FunctionFlush; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionKill; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; import static redis_request.RedisRequestOuterClass.RequestType.Info; @@ -575,4 +576,15 @@ public CompletableFuture> fcall( ? ClusterValue.ofSingleValue(handleObjectOrNullResponse(response)) : ClusterValue.ofMultiValue(handleMapResponse(response))); } + + @Override + public CompletableFuture functionKill() { + return commandManager.submitNewCommand(FunctionKill, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture functionKill(@NonNull Route route) { + return commandManager.submitNewCommand( + FunctionKill, new String[0], route, this::handleStringResponse); + } } diff --git a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java index bb9eabf156..52d9119cbe 100644 --- a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java @@ -339,4 +339,37 @@ CompletableFuture[]>> functionList( * } */ CompletableFuture> fcall(String function, String[] arguments, Route route); + + /** + * Kills a function that is currently executing.
+ * FUNCTION KILL terminates read-only functions only.
+ * The command will be routed to all primary nodes. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @return OK if function is terminated. Otherwise, throws an error. + * @example + *
{@code
+     * String response = client.functionKill().get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionKill(); + + /** + * Kills a function that is currently executing.
+ * FUNCTION KILL terminates read-only functions only. + * + * @since Redis 7.0 and above. + * @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 OK if function is terminated. Otherwise, throws an error. + * @example + *
{@code
+     * String response = client.functionKill(RANDOM).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionKill(Route route); } diff --git a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java index 2815e4a2a3..77102e10e4 100644 --- a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java +++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java @@ -142,4 +142,19 @@ public interface ScriptingAndFunctionsCommands { * } */ CompletableFuture fcall(String function); + + /** + * Kills a function that is currently executing.
+ * FUNCTION KILL terminates read-only functions only. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @return OK if function is terminated. Otherwise, throws an error. + * @example + *
{@code
+     * String response = client.functionKill().get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionKill(); } diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index e130235677..9337a906f5 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -86,6 +86,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; import static redis_request.RedisRequestOuterClass.RequestType.FunctionFlush; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionKill; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; import static redis_request.RedisRequestOuterClass.RequestType.GeoAdd; @@ -5228,6 +5229,27 @@ public void fcall_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void functionKill_returns_success() { + // setup + String[] args = new String[0]; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionKill), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionKill(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + @SneakyThrows @Test public void bitcount_returns_success() { diff --git a/java/client/src/test/java/glide/api/RedisClusterClientTest.java b/java/client/src/test/java/glide/api/RedisClusterClientTest.java index 15ff0a59fd..46cf670800 100644 --- a/java/client/src/test/java/glide/api/RedisClusterClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClusterClientTest.java @@ -30,6 +30,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; import static redis_request.RedisRequestOuterClass.RequestType.FunctionFlush; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionKill; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; import static redis_request.RedisRequestOuterClass.RequestType.Info; @@ -1543,4 +1544,46 @@ public void fcall_without_keys_and_with_route_returns_success() { assertEquals(testResponse, response); assertEquals(value, payload); } + + @SneakyThrows + @Test + public void functionKill_returns_success() { + // setup + String[] args = new String[0]; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionKill), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionKill(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionKill_with_route_returns_success() { + // setup + String[] args = new String[0]; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionKill), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionKill(RANDOM); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } } diff --git a/java/integTest/src/test/java/glide/TestUtilities.java b/java/integTest/src/test/java/glide/TestUtilities.java index 9032f7f048..73b52bb3dc 100644 --- a/java/integTest/src/test/java/glide/TestUtilities.java +++ b/java/integTest/src/test/java/glide/TestUtilities.java @@ -181,4 +181,36 @@ public static String generateLuaLibCode( } return code.toString(); } + + /** + * Create a lua lib with a RO function which runs an endless loop up to timeout sec.
+ * Execution takes at least 5 sec regardless of the timeout configured.
+ * If readOnly is false, function sets a dummy value to the first key + * given. + */ + public static String createLuaLibWithLongRunningFunction( + String libName, String funcName, int timeout, boolean readOnly) { + String code = + "#!lua name=$libName\n" + + "local function $libName_$funcName(keys, args)\n" + + " local started = tonumber(redis.pcall('time')[1])\n" + // fun fact - redis does no writes if 'no-writes' flag is set + + " redis.pcall('set', keys[1], 42)\n" + + " while (true) do\n" + + " local now = tonumber(redis.pcall('time')[1])\n" + + " if now > started + $timeout then\n" + + " return 'Timed out $timeout sec'\n" + + " end\n" + + " end\n" + + " return 'OK'\n" + + "end\n" + + "redis.register_function{\n" + + "function_name='$funcName',\n" + + "callback=$libName_$funcName,\n" + + (readOnly ? "flags={ 'no-writes' }\n" : "") + + "}"; + return code.replace("$timeout", Integer.toString(timeout)) + .replace("$funcName", funcName) + .replace("$libName", libName); + } } diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index 1ef67f178d..2ff5664c1a 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -1,10 +1,11 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.cluster; -import static glide.TestConfiguration.CLUSTER_PORTS; import static glide.TestConfiguration.REDIS_VERSION; import static glide.TestUtilities.assertDeepEquals; import static glide.TestUtilities.checkFunctionListResponse; +import static glide.TestUtilities.commonClusterClientConfig; +import static glide.TestUtilities.createLuaLibWithLongRunningFunction; import static glide.TestUtilities.generateLuaLibCode; import static glide.TestUtilities.getFirstEntryFromMultiValue; import static glide.TestUtilities.getValueFromInfo; @@ -47,8 +48,6 @@ import glide.api.models.commands.RangeOptions.RangeByIndex; import glide.api.models.commands.WeightAggregateOptions.KeyArray; import glide.api.models.commands.bitmap.BitwiseOperation; -import glide.api.models.configuration.NodeAddress; -import glide.api.models.configuration.RedisClusterClientConfiguration; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; import glide.api.models.configuration.RequestRoutingConfiguration.SlotKeyRoute; @@ -131,11 +130,7 @@ public class CommandTests { @SneakyThrows public static void init() { clusterClient = - RedisClusterClient.CreateClient( - RedisClusterClientConfiguration.builder() - .address(NodeAddress.builder().port(CLUSTER_PORTS[0]).build()) - .requestTimeout(5000) - .build()) + RedisClusterClient.CreateClient(commonClusterClientConfig().requestTimeout(7000).build()) .get(); } @@ -1099,4 +1094,306 @@ public void fcall_readonly_function() { assertEquals(OK, clusterClient.functionDelete(libName).get()); } + + @Test + @SneakyThrows + public void functionStats_and_functionKill_without_route() { + assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); + + String libName = "functionStats_and_functionKill_without_route"; + String funcName = "deadlock_without_route"; + String code = createLuaLibWithLongRunningFunction(libName, funcName, 15, true); + String error = ""; + + try { + // nothing to kill + var exception = + assertThrows(ExecutionException.class, () -> clusterClient.functionKill().get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("notbusy")); + + // load the lib + assertEquals(libName, clusterClient.functionLoad(code, true).get()); + + try (var testClient = + RedisClusterClient.CreateClient(commonClusterClientConfig().requestTimeout(7000).build()) + .get()) { + // call the function without await + // Using a random primary node route, otherwise FCALL can go to a replica. + // FKILL and FSTATS go to primary nodes if no route given, test fails in such case. + Route route = new SlotKeyRoute(UUID.randomUUID().toString(), PRIMARY); + var promise = testClient.fcall(funcName, route); + + int timeout = 5200; // ms + while (timeout > 0) { + var stats = clusterClient.customCommand(new String[] {"FUNCTION", "STATS"}).get(); + boolean found = false; + for (var response : stats.getMultiValue().values()) { + if (((Map) response).get("running_script") != null) { + found = true; + break; + } + } + if (found) { + break; + } + Thread.sleep(100); + timeout -= 100; + } + if (timeout == 0) { + error += "Can't find a running function."; + } + + assertEquals(OK, clusterClient.functionKill().get()); + + exception = + assertThrows(ExecutionException.class, () -> clusterClient.functionKill().get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("notbusy")); + + exception = assertThrows(ExecutionException.class, promise::get); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().contains("Script killed by user")); + } + } finally { + // If function wasn't killed, and it didn't time out - it blocks the server and cause rest + // test to fail. + try { + clusterClient.functionKill().get(); + // should throw `notbusy` error, because the function should be killed before + error += "Function should be killed before."; + } catch (Exception ignored) { + } + } + + assertEquals(OK, clusterClient.functionDelete(libName).get()); + + assertTrue(error.isEmpty(), "Something went wrong during the test"); + } + + @ParameterizedTest(name = "single node route = {0}") + @ValueSource(booleans = {true, false}) + @SneakyThrows + public void functionStats_and_functionKill_with_route(boolean singleNodeRoute) { + assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); + + String libName = "functionStats_and_functionKill_with_route_" + singleNodeRoute; + String funcName = "deadlock_with_route_" + singleNodeRoute; + String code = createLuaLibWithLongRunningFunction(libName, funcName, 15, true); + Route route = + singleNodeRoute ? new SlotKeyRoute(UUID.randomUUID().toString(), PRIMARY) : ALL_PRIMARIES; + String error = ""; + + try { + // nothing to kill + var exception = + assertThrows(ExecutionException.class, () -> clusterClient.functionKill(route).get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("notbusy")); + + // load the lib + assertEquals(libName, clusterClient.functionLoad(code, true, route).get()); + + try (var testClient = + RedisClusterClient.CreateClient(commonClusterClientConfig().requestTimeout(7000).build()) + .get()) { + // call the function without await + var promise = testClient.fcall(funcName, route); + + int timeout = 5200; // ms + while (timeout > 0) { + var stats = clusterClient.customCommand(new String[] {"FUNCTION", "STATS"}, route).get(); + if (singleNodeRoute) { + var response = stats.getSingleValue(); + if (((Map) response).get("running_script") != null) { + break; + } + } else { + boolean found = false; + for (var response : stats.getMultiValue().values()) { + if (((Map) response).get("running_script") != null) { + found = true; + break; + } + } + if (found) { + break; + } + } + Thread.sleep(100); + timeout -= 100; + } + if (timeout == 0) { + error += "Can't find a running function."; + } + + // redis kills a function with 5 sec delay + assertEquals(OK, clusterClient.functionKill(route).get()); + Thread.sleep(404); + + exception = + assertThrows(ExecutionException.class, () -> clusterClient.functionKill(route).get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("notbusy")); + + exception = assertThrows(ExecutionException.class, promise::get); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().contains("Script killed by user")); + } + } finally { + // If function wasn't killed, and it didn't time out - it blocks the server and cause rest + // test to fail. + try { + clusterClient.functionKill(route).get(); + // should throw `notbusy` error, because the function should be killed before + error += "Function should be killed before."; + } catch (Exception ignored) { + } + } + + assertEquals(OK, clusterClient.functionDelete(libName, route).get()); + + assertTrue(error.isEmpty(), "Something went wrong during the test"); + } + + @Test + @SneakyThrows + public void functionStats_and_functionKill_with_key_based_route() { + assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); + + String libName = "functionStats_and_functionKill_with_key_based_route"; + String funcName = "deadlock_with_key_based_route"; + String key = libName; + String code = createLuaLibWithLongRunningFunction(libName, funcName, 15, true); + Route route = new SlotKeyRoute(key, PRIMARY); + String error = ""; + + try { + // nothing to kill + var exception = + assertThrows(ExecutionException.class, () -> clusterClient.functionKill(route).get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("notbusy")); + + // load the lib + assertEquals(libName, clusterClient.functionLoad(code, true, route).get()); + + try (var testClient = + RedisClusterClient.CreateClient(commonClusterClientConfig().requestTimeout(7000).build()) + .get()) { + // call the function without await + var promise = testClient.fcall(funcName, new String[] {key}, new String[0]); + + int timeout = 5200; // ms + while (timeout > 0) { + var stats = clusterClient.customCommand(new String[] {"FUNCTION", "STATS"}, route).get(); + var response = stats.getSingleValue(); + if (((Map) response).get("running_script") != null) { + break; + } + Thread.sleep(100); + timeout -= 100; + } + if (timeout == 0) { + error += "Can't find a running function."; + } + + // redis kills a function with 5 sec delay + assertEquals(OK, clusterClient.functionKill(route).get()); + + exception = + assertThrows(ExecutionException.class, () -> clusterClient.functionKill(route).get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("notbusy")); + + exception = assertThrows(ExecutionException.class, promise::get); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().contains("Script killed by user")); + } + } finally { + // If function wasn't killed, and it didn't time out - it blocks the server and cause rest + // test to fail. + try { + clusterClient.functionKill(route).get(); + // should throw `notbusy` error, because the function should be killed before + error += "Function should be killed before."; + } catch (Exception ignored) { + } + } + + assertEquals(OK, clusterClient.functionDelete(libName, route).get()); + + assertTrue(error.isEmpty(), "Something went wrong during the test"); + } + + @Test + @SneakyThrows + public void functionStats_and_functionKill_write_function() { + assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); + + String libName = "functionStats_and_functionKill_write_function"; + String funcName = "deadlock_write_function_with_key_based_route"; + String key = libName; + String code = createLuaLibWithLongRunningFunction(libName, funcName, 6, false); + Route route = new SlotKeyRoute(key, PRIMARY); + String error = ""; + + try { + // nothing to kill + var exception = + assertThrows(ExecutionException.class, () -> clusterClient.functionKill(route).get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("notbusy")); + + // load the lib + assertEquals(libName, clusterClient.functionLoad(code, true, route).get()); + + try (var testClient = + RedisClusterClient.CreateClient(commonClusterClientConfig().requestTimeout(7000).build()) + .get()) { + // call the function without await + var promise = testClient.fcall(funcName, new String[] {key}, new String[0]); + + int timeout = 5200; // ms + while (timeout > 0) { + var stats = clusterClient.customCommand(new String[] {"FUNCTION", "STATS"}, route).get(); + var response = stats.getSingleValue(); + if (((Map) response).get("running_script") != null) { + break; + } + Thread.sleep(100); + timeout -= 100; + } + if (timeout == 0) { + error += "Can't find a running function."; + } + + // redis kills a function with 5 sec delay + exception = + assertThrows(ExecutionException.class, () -> clusterClient.functionKill(route).get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("unkillable")); + + assertEquals("Timed out 6 sec", promise.get()); + + exception = + assertThrows(ExecutionException.class, () -> clusterClient.functionKill(route).get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("notbusy")); + } + } finally { + // If function wasn't killed, and it didn't time out - it blocks the server and cause rest + // test to fail. + try { + clusterClient.functionKill(route).get(); + // should throw `notbusy` error, because the function should be killed before + error += "Function should finish prior to the test end."; + } catch (Exception ignored) { + } + } + + assertEquals(OK, clusterClient.functionDelete(libName, route).get()); + + assertTrue(error.isEmpty(), "Something went wrong during the test"); + } } diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index 1ec5835a89..432be4e89d 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -2,8 +2,9 @@ package glide.standalone; import static glide.TestConfiguration.REDIS_VERSION; -import static glide.TestConfiguration.STANDALONE_PORTS; import static glide.TestUtilities.checkFunctionListResponse; +import static glide.TestUtilities.commonClientConfig; +import static glide.TestUtilities.createLuaLibWithLongRunningFunction; import static glide.TestUtilities.generateLuaLibCode; import static glide.TestUtilities.getValueFromInfo; import static glide.TestUtilities.parseInfoResponseToMap; @@ -29,8 +30,6 @@ import glide.api.RedisClient; 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; @@ -57,11 +56,7 @@ public class CommandTests { @SneakyThrows public static void init() { regularClient = - RedisClient.CreateClient( - RedisClientConfiguration.builder() - .address(NodeAddress.builder().port(STANDALONE_PORTS[0]).build()) - .build()) - .get(); + RedisClient.CreateClient(commonClientConfig().requestTimeout(7000).build()).get(); } @AfterAll @@ -518,4 +513,137 @@ public void copy() { regularClient.select(0).get(); } } + + @Test + @SneakyThrows + public void functionStats_and_functionKill() { + assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); + + String libName = "functionStats_and_functionKill"; + String funcName = "deadlock"; + String code = createLuaLibWithLongRunningFunction(libName, funcName, 15, true); + String error = ""; + + try { + // nothing to kill + var exception = + assertThrows(ExecutionException.class, () -> regularClient.functionKill().get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("notbusy")); + + // load the lib + assertEquals(libName, regularClient.functionLoad(code, true).get()); + + try (var testClient = + RedisClient.CreateClient(commonClientConfig().requestTimeout(7000).build()).get()) { + // call the function without await + var promise = testClient.fcall(funcName); + + int timeout = 5200; // ms + while (timeout > 0) { + var response = regularClient.customCommand(new String[] {"FUNCTION", "STATS"}).get(); + if (((Map) response).get("running_script") != null) { + break; + } + Thread.sleep(100); + timeout -= 100; + } + if (timeout == 0) { + error += "Can't find a running function."; + } + + // redis kills a function with 5 sec delay + assertEquals(OK, regularClient.functionKill().get()); + + exception = + assertThrows(ExecutionException.class, () -> regularClient.functionKill().get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("notbusy")); + + exception = assertThrows(ExecutionException.class, promise::get); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().contains("Script killed by user")); + } + } finally { + // If function wasn't killed, and it didn't time out - it blocks the server and cause rest + // test to fail. + try { + regularClient.functionKill().get(); + // should throw `notbusy` error, because the function should be killed before + error += "Function should be killed before."; + } catch (Exception ignored) { + } + } + + assertEquals(OK, regularClient.functionDelete(libName).get()); + + assertTrue(error.isEmpty(), "Something went wrong during the test"); + } + + @Test + @SneakyThrows + public void functionStats_and_functionKill_write_function() { + assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); + + String libName = "functionStats_and_functionKill_write_function"; + String funcName = "deadlock_write_function"; + String code = createLuaLibWithLongRunningFunction(libName, funcName, 6, false); + String error = ""; + + try { + // nothing to kill + var exception = + assertThrows(ExecutionException.class, () -> regularClient.functionKill().get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("notbusy")); + + // load the lib + assertEquals(libName, regularClient.functionLoad(code, true).get()); + + try (var testClient = + RedisClient.CreateClient(commonClientConfig().requestTimeout(7000).build()).get()) { + // call the function without await + var promise = testClient.fcall(funcName, new String[] {libName}, new String[0]); + + int timeout = 5200; // ms + while (timeout > 0) { + var response = regularClient.customCommand(new String[] {"FUNCTION", "STATS"}).get(); + if (((Map) response).get("running_script") != null) { + break; + } + Thread.sleep(100); + timeout -= 100; + } + if (timeout == 0) { + error += "Can't find a running function."; + } + + // can't kill a write function + exception = + assertThrows(ExecutionException.class, () -> regularClient.functionKill().get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("unkillable")); + + assertEquals("Timed out 6 sec", promise.get()); + + exception = + assertThrows(ExecutionException.class, () -> regularClient.functionKill().get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("notbusy")); + } + } finally { + // If function wasn't killed, and it didn't time out - it blocks the server and cause rest + // test to fail. + try { + regularClient.functionKill().get(); + // should throw `notbusy` error, because the function should be killed before + error += "Function should finish prior to the test end."; + } catch (Exception ignored) { + } + } + + assertEquals(OK, regularClient.functionDelete(libName).get()); + + assertTrue(error.isEmpty(), "Something went wrong during the test"); + } } diff --git a/java/integTest/src/test/java/glide/standalone/TransactionTests.java b/java/integTest/src/test/java/glide/standalone/TransactionTests.java index a0adf6c8e0..5ca4019d6c 100644 --- a/java/integTest/src/test/java/glide/standalone/TransactionTests.java +++ b/java/integTest/src/test/java/glide/standalone/TransactionTests.java @@ -3,6 +3,7 @@ import static glide.TestConfiguration.REDIS_VERSION; import static glide.TestUtilities.assertDeepEquals; +import static glide.TestUtilities.commonClientConfig; import static glide.api.BaseClient.OK; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -11,13 +12,10 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; -import glide.TestConfiguration; import glide.TransactionTestUtilities.TransactionBuilder; import glide.api.RedisClient; import glide.api.models.Transaction; import glide.api.models.commands.InfoOptions; -import glide.api.models.configuration.NodeAddress; -import glide.api.models.configuration.RedisClientConfiguration; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Map; @@ -38,13 +36,7 @@ public class TransactionTests { @BeforeAll @SneakyThrows public static void init() { - client = - RedisClient.CreateClient( - RedisClientConfiguration.builder() - .address( - NodeAddress.builder().port(TestConfiguration.STANDALONE_PORTS[0]).build()) - .build()) - .get(); + client = RedisClient.CreateClient(commonClientConfig().requestTimeout(7000).build()).get(); } @AfterAll