Skip to content

fix(tests): prevent HashedWheelTimer leak in integration tests#99

Open
dkasimovskiy wants to merge 4 commits into
masterfrom
fix/hashedwheeltimer-leak-in-integration-tests
Open

fix(tests): prevent HashedWheelTimer leak in integration tests#99
dkasimovskiy wants to merge 4 commits into
masterfrom
fix/hashedwheeltimer-leak-in-integration-tests

Conversation

@dkasimovskiy

@dkasimovskiy dkasimovskiy commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Fixes a HashedWheelTimer leak that integration tests caused on every CI run, then deduplicates the per-class shutdown hook that stops the leaked timer.

Background

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: HashedWheelTimer.release() was not called before it's garbage-collected warning for every test class on every CI run.

Changes

  • Promote timerService and factory to static and register a JVM shutdown hook per test base class that calls timerService.stop() and cancels any pending timeouts at JVM exit. A shutdown hook is used instead of @AfterAll because the static fields live on the per-declaring-class BaseTest, so @AfterAll in one subclass would stop the timer before later subclasses run.

  • Refactor IProtoClientTest.testTimeoutCancel to use a local HashedWheelTimer and ConnectionFactory so the cancellation behaviour is still exercised without mutating the shared static timer that other tests now rely on.

  • In IProtoClientWatchersTest.checkTTVersion, fall back to the actual container version when the TARANTOOL_VERSION environment variable is unset, instead of throwing NullPointerException (CI sets the env var; local runs do not).

  • Extract the four identical inline shutdown-hook blocks into a single TimerShutdownHook helper. A copy of the helper lives in each of the four modules' own test sources — no cross-module test-jar dependency.

  • Revert the dead version == null ternary branch introduced alongside the env-var fallback (the null branch returned a value identical to the non-null branch and never changed the assertion outcome).

  • Rename testTimeoutCancel to testLocalTimerHasNoPending and inline the redundant localFactory local, so the test name describes what it actually does after the static-timer refactor.

Diff shape

9 files changed, 153 insertions(+), 8 deletions(-)

No production code touched. Test sources only.

I haven't forgotten about:

  • Tests
  • Changelog
  • Documentation
    • JavaDoc was written
  • Commit messages comply with the guideline
  • Cleanup the code for review. See checklist

Related issues:
Refines #98

dkasimovskiy and others added 3 commits June 17, 2026 17:28
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.
…sions

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 <noreply@anthropic.com>
The previous commit added <type>test-jar</type> 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 <noreply@anthropic.com>
@dkasimovskiy dkasimovskiy changed the title refactor(tests): dedupe HashedWheelTimer shutdown hook and fix regressions refactor(tests): dedupe HashedWheelTimer shutdown hook across modules Jun 18, 2026
@dkasimovskiy dkasimovskiy changed the title refactor(tests): dedupe HashedWheelTimer shutdown hook across modules fix(tests): prevent HashedWheelTimer leak in integration tests Jun 18, 2026
@dkasimovskiy dkasimovskiy assigned Alex-pvl and unassigned Alex-pvl Jun 18, 2026
@dkasimovskiy dkasimovskiy requested a review from Alex-pvl June 19, 2026 09:37
private TimerShutdownHook() {}

public static void register(Supplier<Timer> timerRef, String name) {
Runtime.getRuntime()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Runtime.getRuntime()
Runtime.getRuntime().addShutdownHook(new Thread(timer::stop, name));

stop() не возвращает null, а cancel() не работает на остановленном таймере

public final class TimerShutdownHook {
private TimerShutdownHook() {}

public static void register(Supplier<Timer> timerRef, String name) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supplier избыточен

Suggested change
public static void register(Supplier<Timer> timerRef, String name) {
public static void register(Timer timer, String name) {

…direction

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<Timer> 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 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants