From f2d5c4d18432478e14cc077f18af2df5d3d179bc Mon Sep 17 00:00:00 2001 From: Dmitry Kasimovskiy Date: Thu, 11 Jun 2026 16:37:24 +0300 Subject: [PATCH 1/4] fix(tests): prevent HashedWheelTimer leak in integration tests The integration test base classes in tarantool-core, tarantool-schema, tarantool-jackson-mapping, and tarantool-client created a static HashedWheelTimer that was never released, producing a Netty leak warning ('LEAK: HashedWheelTimer.release() was not called before it's garbage-collected') for every test class on every CI run. - Promote timerService and factory to static (shared across tests in a class for efficiency) and register a JVM shutdown hook that calls timerService.stop() and cancels any pending timeouts at JVM exit. A shutdown hook is used instead of @AfterAll because the static fields in BaseTest are per-declaring-class and would otherwise be stopped after the first subclass, breaking subsequent test classes. - IProtoClientTest.testTimeoutCancel was reassigning the shared static timer; refactored to use a local HashedWheelTimer and ConnectionFactory so the cancellation behavior is still exercised without affecting other tests. - IProtoClientWatchersTest.checkTTVersion was throwing NPE when the TARANTOOL_VERSION environment variable was not set (the case for local runs; CI sets it). Fall back to the actual container version when the env var is unset. --- .../TarantoolCrudClientWithRetryTest.java | 20 +++++++++++++++ .../tarantool/core/integration/BaseTest.java | 25 +++++++++++++++++-- .../core/integration/IProtoClientTest.java | 11 +++++--- .../integration/IProtoClientWatchersTest.java | 3 ++- .../mapping/integration/BaseTest.java | 21 ++++++++++++++++ .../TarantoolSchemaFetcherTest.java | 20 +++++++++++++++ 6 files changed, 93 insertions(+), 7 deletions(-) diff --git a/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientWithRetryTest.java b/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientWithRetryTest.java index eb814a3..6d3a93b 100644 --- a/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientWithRetryTest.java +++ b/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientWithRetryTest.java @@ -7,6 +7,7 @@ import java.time.Duration; import java.util.Collections; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.TimeUnit; @@ -39,6 +40,25 @@ @Testcontainers public class TarantoolCrudClientWithRetryTest { + private static final boolean SHUTDOWN_HOOK_REGISTERED = registerShutdownHook(); + + private static boolean registerShutdownHook() { + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + Timer t = timerService; + if (t != null) { + Set pending = t.stop(); + if (pending != null && !pending.isEmpty()) { + pending.forEach(io.netty.util.Timeout::cancel); + } + } + }, + "tarantool-crud-client-retry-test-timer-shutdown")); + return true; + } + public class OperationsRepeater { private final Supplier> operation; diff --git a/tarantool-core/src/test/java/io/tarantool/core/integration/BaseTest.java b/tarantool-core/src/test/java/io/tarantool/core/integration/BaseTest.java index 3a54dea..1697131 100644 --- a/tarantool-core/src/test/java/io/tarantool/core/integration/BaseTest.java +++ b/tarantool-core/src/test/java/io/tarantool/core/integration/BaseTest.java @@ -11,6 +11,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelOption; @@ -18,6 +19,7 @@ import io.netty.channel.nio.NioIoHandler; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.util.HashedWheelTimer; +import io.netty.util.Timeout; import io.netty.util.Timer; import org.msgpack.core.MessagePack; import org.msgpack.core.MessageUnpacker; @@ -32,6 +34,25 @@ public abstract class BaseTest { + private static final boolean SHUTDOWN_HOOK_REGISTERED = registerShutdownHook(); + + private static boolean registerShutdownHook() { + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + Timer t = timerService; + if (t != null) { + Set pending = t.stop(); + if (pending != null && !pending.isEmpty()) { + pending.forEach(Timeout::cancel); + } + } + }, + "base-test-timer-shutdown")); + return true; + } + protected static final Bootstrap bootstrap = new Bootstrap() .group(new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory())) @@ -58,9 +79,9 @@ public abstract class BaseTest { } }; - protected Timer timerService = new HashedWheelTimer(); + protected static Timer timerService = new HashedWheelTimer(); - protected ConnectionFactory factory = new ConnectionFactory(bootstrap, timerService); + protected static ConnectionFactory factory = new ConnectionFactory(bootstrap, timerService); protected static ArrayValue decodeTuple(IProtoClient client, ArrayValue arrayValue) throws IOException { diff --git a/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientTest.java b/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientTest.java index 212d7fc..3f06d7d 100644 --- a/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientTest.java +++ b/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientTest.java @@ -68,6 +68,7 @@ import io.tarantool.core.IProtoClient; import io.tarantool.core.IProtoClientImpl; import io.tarantool.core.IProtoFeature; +import io.tarantool.core.connection.ConnectionFactory; import io.tarantool.core.exceptions.BoxError; import io.tarantool.core.exceptions.BoxErrorStackItem; import io.tarantool.core.exceptions.ClientException; @@ -1515,16 +1516,18 @@ public void testPing() throws Exception { @Test public void testTimeoutCancel() throws Exception { - IProtoClient client = createClientAndConnect(address, true); + io.netty.util.Timer localTimer = new HashedWheelTimer(); + ConnectionFactory localFactory = new ConnectionFactory(bootstrap, localTimer); + IProtoClient client = + new IProtoClientImpl(localFactory, localTimer, DEFAULT_WATCHER_OPTS, null, null, true); + client.connect(address, 3_000).get(); client.authorize(API_USER, CREDS.get(API_USER)).join(); IProtoMessage message = client.ping().get(); checkMessageHeader(message, IPROTO_OK, 4); assertEquals(0, message.getBody().map().size()); - Set timers = timerService.stop(); + Set timers = localTimer.stop(); assertTrue(timers.isEmpty()); - - timerService = new HashedWheelTimer(); } @Test diff --git a/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientWatchersTest.java b/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientWatchersTest.java index 4c64282..8f4c43f 100644 --- a/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientWatchersTest.java +++ b/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientWatchersTest.java @@ -207,6 +207,7 @@ private void testWatcherRecoveryAfterReconnectOnContainer( private void checkTTVersion(TarantoolContainer tt, String version) throws Exception { List result = TarantoolContainerClientHelper.executeCommandDecoded(tt, "return _TARANTOOL"); String ttVersion = (String) result.get(0); - assertTrue(ttVersion.startsWith(version.split("-")[0])); + String expectedPrefix = version == null ? ttVersion.split("-")[0] : version.split("-")[0]; + assertTrue(ttVersion.startsWith(expectedPrefix)); } } diff --git a/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/BaseTest.java b/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/BaseTest.java index 79ed68a..188c39f 100644 --- a/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/BaseTest.java +++ b/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/BaseTest.java @@ -7,6 +7,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Set; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelOption; @@ -14,12 +15,32 @@ import io.netty.channel.nio.NioIoHandler; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.util.HashedWheelTimer; +import io.netty.util.Timeout; import io.netty.util.Timer; import io.tarantool.core.connection.ConnectionFactory; public abstract class BaseTest { + private static final boolean SHUTDOWN_HOOK_REGISTERED = registerShutdownHook(); + + private static boolean registerShutdownHook() { + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + Timer t = timerService; + if (t != null) { + Set pending = t.stop(); + if (pending != null && !pending.isEmpty()) { + pending.forEach(Timeout::cancel); + } + } + }, + "jackson-mapping-base-test-timer-shutdown")); + return true; + } + protected static final String API_USER = "api_user"; protected static final Map CREDS = diff --git a/tarantool-schema/src/test/java/io/tarantool/client/integration/TarantoolSchemaFetcherTest.java b/tarantool-schema/src/test/java/io/tarantool/client/integration/TarantoolSchemaFetcherTest.java index 9c45259..81a2644 100644 --- a/tarantool-schema/src/test/java/io/tarantool/client/integration/TarantoolSchemaFetcherTest.java +++ b/tarantool-schema/src/test/java/io/tarantool/client/integration/TarantoolSchemaFetcherTest.java @@ -13,6 +13,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; import io.netty.bootstrap.Bootstrap; @@ -47,6 +48,25 @@ @Timeout(value = 5) public class TarantoolSchemaFetcherTest { + private static final boolean SHUTDOWN_HOOK_REGISTERED = registerShutdownHook(); + + private static boolean registerShutdownHook() { + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + Timer t = timerService; + if (t != null) { + Set pending = t.stop(); + if (pending != null && !pending.isEmpty()) { + pending.forEach(io.netty.util.Timeout::cancel); + } + } + }, + "tarantool-schema-fetcher-test-timer-shutdown")); + return true; + } + private static final String API_USER = "api_user"; private static final Map CREDS = From eaca16c33fa7288fd6572504bd3c966a2c5fa293 Mon Sep 17 00:00:00 2001 From: Dmitry Kasimovskiy Date: Wed, 17 Jun 2026 18:46:07 +0300 Subject: [PATCH 2/4] refactor(tests): dedupe HashedWheelTimer shutdown hook and fix regressions Extract the four identical inline shutdown hooks (one per test class) into a single TimerShutdownHook helper. Two copies of the helper live in tarantool-core and tarantool-schema test sources; client and jackson reuse the core copy via a new test-jar dependency on tarantool-core. schema does not depend on core so the helper is duplicated there rather than introducing a new cross-module dependency. Also revert a dead `version == null` ternary branch in IProtoClientWatchersTest.checkTTVersion (the null branch returned a value that never changed the assertion outcome) and rename testTimeoutCancel to testLocalTimerHasNoPending while inlining the redundant localFactory variable, so the test name describes what it actually does after the static-timer refactor in f2d5c4d. Co-Authored-By: Claude Opus 4.7 --- tarantool-client/pom.xml | 7 +++++ .../TarantoolCrudClientWithRetryTest.java | 26 ++++------------ .../tarantool/core/integration/BaseTest.java | 25 +++------------- .../core/integration/IProtoClientTest.java | 17 +++++++---- .../integration/IProtoClientWatchersTest.java | 3 +- .../core/integration/TimerShutdownHook.java | 30 +++++++++++++++++++ tarantool-jackson-mapping/pom.xml | 7 +++++ .../mapping/integration/BaseTest.java | 26 ++++------------ .../TarantoolSchemaFetcherTest.java | 24 +++------------ .../client/integration/TimerShutdownHook.java | 30 +++++++++++++++++++ 10 files changed, 105 insertions(+), 90 deletions(-) create mode 100644 tarantool-core/src/test/java/io/tarantool/core/integration/TimerShutdownHook.java create mode 100644 tarantool-schema/src/test/java/io/tarantool/client/integration/TimerShutdownHook.java diff --git a/tarantool-client/pom.xml b/tarantool-client/pom.xml index c3200f4..cbc27b1 100644 --- a/tarantool-client/pom.xml +++ b/tarantool-client/pom.xml @@ -25,6 +25,13 @@ io.tarantool tarantool-core + + io.tarantool + tarantool-core + ${project.version} + test-jar + test + io.tarantool tarantool-jackson-mapping diff --git a/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientWithRetryTest.java b/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientWithRetryTest.java index 6d3a93b..81f86a4 100644 --- a/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientWithRetryTest.java +++ b/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientWithRetryTest.java @@ -7,7 +7,6 @@ import java.time.Duration; import java.util.Collections; -import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.TimeUnit; @@ -34,31 +33,13 @@ import io.tarantool.client.crud.TarantoolCrudSpace; import io.tarantool.client.factory.TarantoolFactory; import io.tarantool.core.exceptions.BoxError; +import io.tarantool.core.integration.TimerShutdownHook; import io.tarantool.mapping.Tuple; @Timeout(value = 5) @Testcontainers public class TarantoolCrudClientWithRetryTest { - private static final boolean SHUTDOWN_HOOK_REGISTERED = registerShutdownHook(); - - private static boolean registerShutdownHook() { - Runtime.getRuntime() - .addShutdownHook( - new Thread( - () -> { - Timer t = timerService; - if (t != null) { - Set pending = t.stop(); - if (pending != null && !pending.isEmpty()) { - pending.forEach(io.netty.util.Timeout::cancel); - } - } - }, - "tarantool-crud-client-retry-test-timer-shutdown")); - return true; - } - public class OperationsRepeater { private final Supplier> operation; @@ -119,6 +100,11 @@ private void execute() { private static final Person personInstance = new Person(1, true, "Roman"); private static final Timer timerService = new HashedWheelTimer(); + static { + TimerShutdownHook.register( + () -> timerService, "tarantool-crud-client-retry-test-timer-shutdown"); + } + @BeforeAll public static void setUp() throws Exception { if (isCartridgeAvailable()) { diff --git a/tarantool-core/src/test/java/io/tarantool/core/integration/BaseTest.java b/tarantool-core/src/test/java/io/tarantool/core/integration/BaseTest.java index 1697131..47a5df4 100644 --- a/tarantool-core/src/test/java/io/tarantool/core/integration/BaseTest.java +++ b/tarantool-core/src/test/java/io/tarantool/core/integration/BaseTest.java @@ -11,7 +11,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelOption; @@ -19,7 +18,6 @@ import io.netty.channel.nio.NioIoHandler; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.util.HashedWheelTimer; -import io.netty.util.Timeout; import io.netty.util.Timer; import org.msgpack.core.MessagePack; import org.msgpack.core.MessageUnpacker; @@ -34,25 +32,6 @@ public abstract class BaseTest { - private static final boolean SHUTDOWN_HOOK_REGISTERED = registerShutdownHook(); - - private static boolean registerShutdownHook() { - Runtime.getRuntime() - .addShutdownHook( - new Thread( - () -> { - Timer t = timerService; - if (t != null) { - Set pending = t.stop(); - if (pending != null && !pending.isEmpty()) { - pending.forEach(Timeout::cancel); - } - } - }, - "base-test-timer-shutdown")); - return true; - } - protected static final Bootstrap bootstrap = new Bootstrap() .group(new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory())) @@ -83,6 +62,10 @@ private static boolean registerShutdownHook() { protected static ConnectionFactory factory = new ConnectionFactory(bootstrap, timerService); + static { + TimerShutdownHook.register(() -> timerService, "base-test-timer-shutdown"); + } + protected static ArrayValue decodeTuple(IProtoClient client, ArrayValue arrayValue) throws IOException { if (client.hasTupleExtension()) { diff --git a/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientTest.java b/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientTest.java index 3f06d7d..f6ca247 100644 --- a/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientTest.java +++ b/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientTest.java @@ -28,6 +28,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import io.netty.util.HashedWheelTimer; +import io.netty.util.Timer; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -1515,19 +1516,23 @@ public void testPing() throws Exception { } @Test - public void testTimeoutCancel() throws Exception { - io.netty.util.Timer localTimer = new HashedWheelTimer(); - ConnectionFactory localFactory = new ConnectionFactory(bootstrap, localTimer); + public void testLocalTimerHasNoPending() throws Exception { + Timer localTimer = new HashedWheelTimer(); IProtoClient client = - new IProtoClientImpl(localFactory, localTimer, DEFAULT_WATCHER_OPTS, null, null, true); + new IProtoClientImpl( + new ConnectionFactory(bootstrap, localTimer), + localTimer, + DEFAULT_WATCHER_OPTS, + null, + null, + true); client.connect(address, 3_000).get(); client.authorize(API_USER, CREDS.get(API_USER)).join(); IProtoMessage message = client.ping().get(); checkMessageHeader(message, IPROTO_OK, 4); assertEquals(0, message.getBody().map().size()); - Set timers = localTimer.stop(); - assertTrue(timers.isEmpty()); + assertTrue(localTimer.stop().isEmpty()); } @Test diff --git a/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientWatchersTest.java b/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientWatchersTest.java index 8f4c43f..4c64282 100644 --- a/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientWatchersTest.java +++ b/tarantool-core/src/test/java/io/tarantool/core/integration/IProtoClientWatchersTest.java @@ -207,7 +207,6 @@ private void testWatcherRecoveryAfterReconnectOnContainer( private void checkTTVersion(TarantoolContainer tt, String version) throws Exception { List result = TarantoolContainerClientHelper.executeCommandDecoded(tt, "return _TARANTOOL"); String ttVersion = (String) result.get(0); - String expectedPrefix = version == null ? ttVersion.split("-")[0] : version.split("-")[0]; - assertTrue(ttVersion.startsWith(expectedPrefix)); + assertTrue(ttVersion.startsWith(version.split("-")[0])); } } diff --git a/tarantool-core/src/test/java/io/tarantool/core/integration/TimerShutdownHook.java b/tarantool-core/src/test/java/io/tarantool/core/integration/TimerShutdownHook.java new file mode 100644 index 0000000..f3ea3d6 --- /dev/null +++ b/tarantool-core/src/test/java/io/tarantool/core/integration/TimerShutdownHook.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026 VK DIGITAL TECHNOLOGIES LIMITED LIABILITY COMPANY + * All Rights Reserved. + */ + +package io.tarantool.core.integration; + +import java.util.Set; +import java.util.function.Supplier; + +import io.netty.util.Timeout; +import io.netty.util.Timer; + +public final class TimerShutdownHook { + private TimerShutdownHook() {} + + public static void register(Supplier timerRef, String name) { + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + Timer t = timerRef.get(); + if (t != null) { + Set pending = t.stop(); + if (pending != null) pending.forEach(Timeout::cancel); + } + }, + name)); + } +} diff --git a/tarantool-jackson-mapping/pom.xml b/tarantool-jackson-mapping/pom.xml index 7835145..1fe4dc3 100644 --- a/tarantool-jackson-mapping/pom.xml +++ b/tarantool-jackson-mapping/pom.xml @@ -25,6 +25,13 @@ io.tarantool tarantool-core + + io.tarantool + tarantool-core + ${project.version} + test-jar + test + org.msgpack jackson-dataformat-msgpack diff --git a/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/BaseTest.java b/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/BaseTest.java index 188c39f..ae55212 100644 --- a/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/BaseTest.java +++ b/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/BaseTest.java @@ -7,7 +7,6 @@ import java.util.HashMap; import java.util.Map; -import java.util.Set; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelOption; @@ -15,32 +14,13 @@ import io.netty.channel.nio.NioIoHandler; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.util.HashedWheelTimer; -import io.netty.util.Timeout; import io.netty.util.Timer; import io.tarantool.core.connection.ConnectionFactory; +import io.tarantool.core.integration.TimerShutdownHook; public abstract class BaseTest { - private static final boolean SHUTDOWN_HOOK_REGISTERED = registerShutdownHook(); - - private static boolean registerShutdownHook() { - Runtime.getRuntime() - .addShutdownHook( - new Thread( - () -> { - Timer t = timerService; - if (t != null) { - Set pending = t.stop(); - if (pending != null && !pending.isEmpty()) { - pending.forEach(Timeout::cancel); - } - } - }, - "jackson-mapping-base-test-timer-shutdown")); - return true; - } - protected static final String API_USER = "api_user"; protected static final Map CREDS = @@ -68,4 +48,8 @@ private static boolean registerShutdownHook() { .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000); protected static final Timer timerService = new HashedWheelTimer(); protected static final ConnectionFactory factory = new ConnectionFactory(bootstrap, timerService); + + static { + TimerShutdownHook.register(() -> timerService, "jackson-mapping-base-test-timer-shutdown"); + } } diff --git a/tarantool-schema/src/test/java/io/tarantool/client/integration/TarantoolSchemaFetcherTest.java b/tarantool-schema/src/test/java/io/tarantool/client/integration/TarantoolSchemaFetcherTest.java index 81a2644..abb4c07 100644 --- a/tarantool-schema/src/test/java/io/tarantool/client/integration/TarantoolSchemaFetcherTest.java +++ b/tarantool-schema/src/test/java/io/tarantool/client/integration/TarantoolSchemaFetcherTest.java @@ -13,7 +13,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; import io.netty.bootstrap.Bootstrap; @@ -48,25 +47,6 @@ @Timeout(value = 5) public class TarantoolSchemaFetcherTest { - private static final boolean SHUTDOWN_HOOK_REGISTERED = registerShutdownHook(); - - private static boolean registerShutdownHook() { - Runtime.getRuntime() - .addShutdownHook( - new Thread( - () -> { - Timer t = timerService; - if (t != null) { - Set pending = t.stop(); - if (pending != null && !pending.isEmpty()) { - pending.forEach(io.netty.util.Timeout::cancel); - } - } - }, - "tarantool-schema-fetcher-test-timer-shutdown")); - return true; - } - private static final String API_USER = "api_user"; private static final Map CREDS = @@ -95,6 +75,10 @@ private static boolean registerShutdownHook() { private static final Timer timerService = new HashedWheelTimer(); private static final ConnectionFactory factory = new ConnectionFactory(bootstrap, timerService); + static { + TimerShutdownHook.register(() -> timerService, "tarantool-schema-fetcher-test-timer-shutdown"); + } + private static TarantoolContainer tt; private Long spacePersonId; diff --git a/tarantool-schema/src/test/java/io/tarantool/client/integration/TimerShutdownHook.java b/tarantool-schema/src/test/java/io/tarantool/client/integration/TimerShutdownHook.java new file mode 100644 index 0000000..155bed5 --- /dev/null +++ b/tarantool-schema/src/test/java/io/tarantool/client/integration/TimerShutdownHook.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026 VK DIGITAL TECHNOLOGIES LIMITED LIABILITY COMPANY + * All Rights Reserved. + */ + +package io.tarantool.client.integration; + +import java.util.Set; +import java.util.function.Supplier; + +import io.netty.util.Timeout; +import io.netty.util.Timer; + +public final class TimerShutdownHook { + private TimerShutdownHook() {} + + public static void register(Supplier timerRef, String name) { + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + Timer t = timerRef.get(); + if (t != null) { + Set pending = t.stop(); + if (pending != null) pending.forEach(Timeout::cancel); + } + }, + name)); + } +} From b6b7f4a57b0e892b448f4122cd29d4486807a3b8 Mon Sep 17 00:00:00 2001 From: Dmitry Kasimovskiy Date: Wed, 17 Jun 2026 19:03:28 +0300 Subject: [PATCH 3/4] fix(tests): drop cross-module test-jar dep, copy helper locally The previous commit added test-jar dependencies on tarantool-core in client and jackson-mapping pom.xml. The maven-jar-plugin only produces the test-jar artifact during the `package` phase, which `mvn test` (used by CI's unit profile) does not reach. Downstream modules therefore fail with "Could not find artifact ... in central-portal-snapshots". Replace the cross-module test-jar deps with a copy of TimerShutdownHook in each of the four modules' own test sources (the same pattern already used for tarantool-schema in the previous commit). This matches the plan's "Option A" recommendation that was originally only applied to schema. Co-Authored-By: Claude Opus 4.7 --- tarantool-client/pom.xml | 7 ----- .../TarantoolCrudClientWithRetryTest.java | 1 - .../client/integration/TimerShutdownHook.java | 30 +++++++++++++++++++ tarantool-jackson-mapping/pom.xml | 7 ----- .../mapping/integration/BaseTest.java | 1 - .../integration/TimerShutdownHook.java | 30 +++++++++++++++++++ 6 files changed, 60 insertions(+), 16 deletions(-) create mode 100644 tarantool-client/src/test/java/io/tarantool/client/integration/TimerShutdownHook.java create mode 100644 tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/TimerShutdownHook.java diff --git a/tarantool-client/pom.xml b/tarantool-client/pom.xml index cbc27b1..c3200f4 100644 --- a/tarantool-client/pom.xml +++ b/tarantool-client/pom.xml @@ -25,13 +25,6 @@ io.tarantool tarantool-core - - io.tarantool - tarantool-core - ${project.version} - test-jar - test - io.tarantool tarantool-jackson-mapping diff --git a/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientWithRetryTest.java b/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientWithRetryTest.java index 81f86a4..ac2f6b9 100644 --- a/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientWithRetryTest.java +++ b/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientWithRetryTest.java @@ -33,7 +33,6 @@ import io.tarantool.client.crud.TarantoolCrudSpace; import io.tarantool.client.factory.TarantoolFactory; import io.tarantool.core.exceptions.BoxError; -import io.tarantool.core.integration.TimerShutdownHook; import io.tarantool.mapping.Tuple; @Timeout(value = 5) diff --git a/tarantool-client/src/test/java/io/tarantool/client/integration/TimerShutdownHook.java b/tarantool-client/src/test/java/io/tarantool/client/integration/TimerShutdownHook.java new file mode 100644 index 0000000..155bed5 --- /dev/null +++ b/tarantool-client/src/test/java/io/tarantool/client/integration/TimerShutdownHook.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026 VK DIGITAL TECHNOLOGIES LIMITED LIABILITY COMPANY + * All Rights Reserved. + */ + +package io.tarantool.client.integration; + +import java.util.Set; +import java.util.function.Supplier; + +import io.netty.util.Timeout; +import io.netty.util.Timer; + +public final class TimerShutdownHook { + private TimerShutdownHook() {} + + public static void register(Supplier timerRef, String name) { + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + Timer t = timerRef.get(); + if (t != null) { + Set pending = t.stop(); + if (pending != null) pending.forEach(Timeout::cancel); + } + }, + name)); + } +} diff --git a/tarantool-jackson-mapping/pom.xml b/tarantool-jackson-mapping/pom.xml index 1fe4dc3..7835145 100644 --- a/tarantool-jackson-mapping/pom.xml +++ b/tarantool-jackson-mapping/pom.xml @@ -25,13 +25,6 @@ io.tarantool tarantool-core - - io.tarantool - tarantool-core - ${project.version} - test-jar - test - org.msgpack jackson-dataformat-msgpack diff --git a/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/BaseTest.java b/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/BaseTest.java index ae55212..c1b654e 100644 --- a/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/BaseTest.java +++ b/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/BaseTest.java @@ -17,7 +17,6 @@ import io.netty.util.Timer; import io.tarantool.core.connection.ConnectionFactory; -import io.tarantool.core.integration.TimerShutdownHook; public abstract class BaseTest { diff --git a/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/TimerShutdownHook.java b/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/TimerShutdownHook.java new file mode 100644 index 0000000..a8270e8 --- /dev/null +++ b/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/TimerShutdownHook.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026 VK DIGITAL TECHNOLOGIES LIMITED LIABILITY COMPANY + * All Rights Reserved. + */ + +package io.tarantool.mapping.integration; + +import java.util.Set; +import java.util.function.Supplier; + +import io.netty.util.Timeout; +import io.netty.util.Timer; + +public final class TimerShutdownHook { + private TimerShutdownHook() {} + + public static void register(Supplier timerRef, String name) { + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + Timer t = timerRef.get(); + if (t != null) { + Set pending = t.stop(); + if (pending != null) pending.forEach(Timeout::cancel); + } + }, + name)); + } +} From 5355bc17363aac5824d57580ccbb162c35649d38 Mon Sep 17 00:00:00 2001 From: Dmitry Kasimovskiy Date: Fri, 19 Jun 2026 15:08:39 +0300 Subject: [PATCH 4/4] refactor(tests): simplify TimerShutdownHook body and drop Supplier indirection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per PR #99 review feedback: Timer.stop() already cancels all pending tasks (per Netty 4.2 javadoc), so the explicit pending.forEach(cancel) pass in the shutdown hook body is dead work. After stop() returns the worker thread is dead, so cancel() on the returned handles is a no-op, and stop() never returns null. Also drop the Supplier wrapper at every call site — the timer reference is a static field, stable for the JVM lifetime. Body collapses to one line per module: Runtime.getRuntime().addShutdownHook(new Thread(timer::stop, name)); Net diff: -56 LOC across 8 files. box-integration profile passes (Tarantool 2.11.8). Co-Authored-By: Claude Opus 4.7 --- .../TarantoolCrudClientWithRetryTest.java | 3 +-- .../client/integration/TimerShutdownHook.java | 18 ++---------------- .../tarantool/core/integration/BaseTest.java | 2 +- .../core/integration/TimerShutdownHook.java | 18 ++---------------- .../mapping/integration/BaseTest.java | 2 +- .../mapping/integration/TimerShutdownHook.java | 18 ++---------------- .../TarantoolSchemaFetcherTest.java | 2 +- .../client/integration/TimerShutdownHook.java | 18 ++---------------- 8 files changed, 12 insertions(+), 69 deletions(-) diff --git a/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientWithRetryTest.java b/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientWithRetryTest.java index ac2f6b9..29709e4 100644 --- a/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientWithRetryTest.java +++ b/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientWithRetryTest.java @@ -100,8 +100,7 @@ private void execute() { private static final Timer timerService = new HashedWheelTimer(); static { - TimerShutdownHook.register( - () -> timerService, "tarantool-crud-client-retry-test-timer-shutdown"); + TimerShutdownHook.register(timerService, "tarantool-crud-client-retry-test-timer-shutdown"); } @BeforeAll diff --git a/tarantool-client/src/test/java/io/tarantool/client/integration/TimerShutdownHook.java b/tarantool-client/src/test/java/io/tarantool/client/integration/TimerShutdownHook.java index 155bed5..6cb4da5 100644 --- a/tarantool-client/src/test/java/io/tarantool/client/integration/TimerShutdownHook.java +++ b/tarantool-client/src/test/java/io/tarantool/client/integration/TimerShutdownHook.java @@ -5,26 +5,12 @@ package io.tarantool.client.integration; -import java.util.Set; -import java.util.function.Supplier; - -import io.netty.util.Timeout; import io.netty.util.Timer; public final class TimerShutdownHook { private TimerShutdownHook() {} - public static void register(Supplier timerRef, String name) { - Runtime.getRuntime() - .addShutdownHook( - new Thread( - () -> { - Timer t = timerRef.get(); - if (t != null) { - Set pending = t.stop(); - if (pending != null) pending.forEach(Timeout::cancel); - } - }, - name)); + public static void register(Timer timer, String name) { + Runtime.getRuntime().addShutdownHook(new Thread(timer::stop, name)); } } diff --git a/tarantool-core/src/test/java/io/tarantool/core/integration/BaseTest.java b/tarantool-core/src/test/java/io/tarantool/core/integration/BaseTest.java index 47a5df4..c4778f2 100644 --- a/tarantool-core/src/test/java/io/tarantool/core/integration/BaseTest.java +++ b/tarantool-core/src/test/java/io/tarantool/core/integration/BaseTest.java @@ -63,7 +63,7 @@ public abstract class BaseTest { protected static ConnectionFactory factory = new ConnectionFactory(bootstrap, timerService); static { - TimerShutdownHook.register(() -> timerService, "base-test-timer-shutdown"); + TimerShutdownHook.register(timerService, "base-test-timer-shutdown"); } protected static ArrayValue decodeTuple(IProtoClient client, ArrayValue arrayValue) diff --git a/tarantool-core/src/test/java/io/tarantool/core/integration/TimerShutdownHook.java b/tarantool-core/src/test/java/io/tarantool/core/integration/TimerShutdownHook.java index f3ea3d6..1e1c8dd 100644 --- a/tarantool-core/src/test/java/io/tarantool/core/integration/TimerShutdownHook.java +++ b/tarantool-core/src/test/java/io/tarantool/core/integration/TimerShutdownHook.java @@ -5,26 +5,12 @@ package io.tarantool.core.integration; -import java.util.Set; -import java.util.function.Supplier; - -import io.netty.util.Timeout; import io.netty.util.Timer; public final class TimerShutdownHook { private TimerShutdownHook() {} - public static void register(Supplier timerRef, String name) { - Runtime.getRuntime() - .addShutdownHook( - new Thread( - () -> { - Timer t = timerRef.get(); - if (t != null) { - Set pending = t.stop(); - if (pending != null) pending.forEach(Timeout::cancel); - } - }, - name)); + public static void register(Timer timer, String name) { + Runtime.getRuntime().addShutdownHook(new Thread(timer::stop, name)); } } diff --git a/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/BaseTest.java b/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/BaseTest.java index c1b654e..7ec6c26 100644 --- a/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/BaseTest.java +++ b/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/BaseTest.java @@ -49,6 +49,6 @@ public abstract class BaseTest { protected static final ConnectionFactory factory = new ConnectionFactory(bootstrap, timerService); static { - TimerShutdownHook.register(() -> timerService, "jackson-mapping-base-test-timer-shutdown"); + TimerShutdownHook.register(timerService, "jackson-mapping-base-test-timer-shutdown"); } } diff --git a/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/TimerShutdownHook.java b/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/TimerShutdownHook.java index a8270e8..275519d 100644 --- a/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/TimerShutdownHook.java +++ b/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/integration/TimerShutdownHook.java @@ -5,26 +5,12 @@ package io.tarantool.mapping.integration; -import java.util.Set; -import java.util.function.Supplier; - -import io.netty.util.Timeout; import io.netty.util.Timer; public final class TimerShutdownHook { private TimerShutdownHook() {} - public static void register(Supplier timerRef, String name) { - Runtime.getRuntime() - .addShutdownHook( - new Thread( - () -> { - Timer t = timerRef.get(); - if (t != null) { - Set pending = t.stop(); - if (pending != null) pending.forEach(Timeout::cancel); - } - }, - name)); + public static void register(Timer timer, String name) { + Runtime.getRuntime().addShutdownHook(new Thread(timer::stop, name)); } } diff --git a/tarantool-schema/src/test/java/io/tarantool/client/integration/TarantoolSchemaFetcherTest.java b/tarantool-schema/src/test/java/io/tarantool/client/integration/TarantoolSchemaFetcherTest.java index abb4c07..55d1877 100644 --- a/tarantool-schema/src/test/java/io/tarantool/client/integration/TarantoolSchemaFetcherTest.java +++ b/tarantool-schema/src/test/java/io/tarantool/client/integration/TarantoolSchemaFetcherTest.java @@ -76,7 +76,7 @@ public class TarantoolSchemaFetcherTest { private static final ConnectionFactory factory = new ConnectionFactory(bootstrap, timerService); static { - TimerShutdownHook.register(() -> timerService, "tarantool-schema-fetcher-test-timer-shutdown"); + TimerShutdownHook.register(timerService, "tarantool-schema-fetcher-test-timer-shutdown"); } private static TarantoolContainer tt; diff --git a/tarantool-schema/src/test/java/io/tarantool/client/integration/TimerShutdownHook.java b/tarantool-schema/src/test/java/io/tarantool/client/integration/TimerShutdownHook.java index 155bed5..6cb4da5 100644 --- a/tarantool-schema/src/test/java/io/tarantool/client/integration/TimerShutdownHook.java +++ b/tarantool-schema/src/test/java/io/tarantool/client/integration/TimerShutdownHook.java @@ -5,26 +5,12 @@ package io.tarantool.client.integration; -import java.util.Set; -import java.util.function.Supplier; - -import io.netty.util.Timeout; import io.netty.util.Timer; public final class TimerShutdownHook { private TimerShutdownHook() {} - public static void register(Supplier timerRef, String name) { - Runtime.getRuntime() - .addShutdownHook( - new Thread( - () -> { - Timer t = timerRef.get(); - if (t != null) { - Set pending = t.stop(); - if (pending != null) pending.forEach(Timeout::cancel); - } - }, - name)); + public static void register(Timer timer, String name) { + Runtime.getRuntime().addShutdownHook(new Thread(timer::stop, name)); } }