From c5f847a1b138d38b93a66ccbed95f34e1390802b Mon Sep 17 00:00:00 2001 From: yawkat Date: Fri, 17 Oct 2025 14:56:36 +0200 Subject: [PATCH 1/4] New leak detection for tests Netty 4.2.7 introduced a new leak detection mechanism for tests that is more reliable and has lower overhead. https://github.com/micronaut-projects/micronaut-test/pull/1291 introduces a micronaut-test module that uses this new mechanism. However, the new detector is more strict about resource lifetimes, so tests need various small adjustments: - Contexts must be closed at the end of the test - Event loops must be shut down - Tests must use only threads created in those tests (no Finalizers, no reactive boundedElastic) - SSL contexts must be released immediately There is also one minor leak fix in this PR, related to h2c support, in Http2ServerHandler. Draft because this requires the new micronaut-test module. --- .../http/server/netty/NettyResponseLifecycle.java | 3 ++- .../http/server/netty/handler/Http2ServerHandler.java | 4 +++- .../server/netty/binding/JsonBodyBindingSpec.groovy | 9 ++++++++- .../netty/context/ServerRequestContextSpec.groovy | 2 +- .../http/server/netty/fuzzing/FuzzyInputSpec.groovy | 11 ++--------- .../netty/handler/accesslog/AccessLogSpec.groovy | 7 +++++++ .../micronaut/http/server/netty/http2/H2cSpec.groovy | 8 ++++++-- .../server/netty/http2/Http2ServerPushSpec.groovy | 3 +++ .../http/server/netty/http2/Http3Spec.groovy | 4 ++++ .../resources/Http2StaticResourceCacheSpec.groovy | 2 ++ .../server/netty/websocket/ChatServerWebSocket.java | 3 +++ .../http/server/netty/fuzzing/FlagAppender.java | 5 ----- .../http/server/netty/fuzzing/FuzzyInputTest.java | 2 -- 13 files changed, 41 insertions(+), 22 deletions(-) diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyResponseLifecycle.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyResponseLifecycle.java index c42d848dff4..5264caf6229 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyResponseLifecycle.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyResponseLifecycle.java @@ -36,6 +36,7 @@ import io.micronaut.http.server.ResponseLifecycle; import io.netty.buffer.ByteBufAllocator; import io.netty.handler.codec.http.HttpContent; +import io.netty.util.LeakPresenceDetector; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -97,7 +98,7 @@ private NettyByteBodyFactory byteBodyFactory() { } private static class NettyConcatenatingSubscriber extends ConcatenatingSubscriber implements BufferConsumer { - static final Separators JSON_NETTY = Separators.jsonSeparators(NettyReadBufferFactory.of(ByteBufAllocator.DEFAULT)); + static final Separators JSON_NETTY = LeakPresenceDetector.staticInitializer(() -> Separators.jsonSeparators(NettyReadBufferFactory.of(ByteBufAllocator.DEFAULT))); private final EventLoopFlow flow; diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/Http2ServerHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/Http2ServerHandler.java index 8cb35d0b491..8fa8c1a667d 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/Http2ServerHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/Http2ServerHandler.java @@ -257,8 +257,8 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc if (idle.state() == IdleState.ALL_IDLE) { ctx.close(); } + super.userEventTriggered(ctx, evt); } - super.userEventTriggered(ctx, evt); } /** @@ -276,6 +276,8 @@ public void handleFakeRequest(io.netty.handler.codec.http2.Http2Stream onStream, stream.onHeadersRead(fhr, empty); if (!empty) { stream.onDataRead(fhr.content(), true); + } else { + fhr.content().release(); } } } diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/JsonBodyBindingSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/JsonBodyBindingSpec.groovy index f321c96c29e..e53cda84e1b 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/JsonBodyBindingSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/JsonBodyBindingSpec.groovy @@ -19,12 +19,16 @@ import io.micronaut.http.hateoas.JsonError import io.micronaut.http.hateoas.Link import io.micronaut.http.server.netty.AbstractMicronautSpec import io.micronaut.json.JsonSyntaxException +import io.micronaut.scheduling.TaskExecutors +import jakarta.inject.Inject +import jakarta.inject.Named import org.reactivestreams.Publisher import reactor.core.publisher.Flux import reactor.core.scheduler.Schedulers import spock.lang.Issue import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executor class JsonBodyBindingSpec extends AbstractMicronautSpec { @@ -388,6 +392,9 @@ class JsonBodyBindingSpec extends AbstractMicronautSpec { @Controller(value = "/json", produces = io.micronaut.http.MediaType.APPLICATION_JSON) @Requires(property = "test.controller", value = "JsonController") static class JsonController { + @Inject + @Named(TaskExecutors.BLOCKING) + Executor blocking @Post("/params") String params(String name, int age) { @@ -472,7 +479,7 @@ class JsonBodyBindingSpec extends AbstractMicronautSpec { @Post("/publisher-object") Publisher publisherObject(@Body Publisher publisher) { return Flux.from(publisher) - .subscribeOn(Schedulers.boundedElastic()) + .subscribeOn(Schedulers.fromExecutor(blocking)) .map({ Foo foo -> foo.toString() }) diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/context/ServerRequestContextSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/context/ServerRequestContextSpec.groovy index 64fc8ecfdbf..067fbbc16fb 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/context/ServerRequestContextSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/context/ServerRequestContextSpec.groovy @@ -128,7 +128,7 @@ class ServerRequestContextSpec extends Specification { Mono.fromCallable({ -> def request = ServerRequestContext.currentRequest().orElseThrow { -> new RuntimeException("no request") } request.uri.toString() - }).subscribeOn(Schedulers.boundedElastic()) + }).subscribeOn(Schedulers.fromExecutor(executorService)) } @Get("/reactor-context") diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/fuzzing/FuzzyInputSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/fuzzing/FuzzyInputSpec.groovy index e2fffca3d97..64b27f0b5a4 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/fuzzing/FuzzyInputSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/fuzzing/FuzzyInputSpec.groovy @@ -4,8 +4,8 @@ import io.micronaut.context.ApplicationContext import io.micronaut.context.BeanProvider import io.micronaut.context.annotation.Replaces import io.micronaut.context.annotation.Requires -import io.micronaut.http.annotation.Body import io.micronaut.core.annotation.Nullable +import io.micronaut.http.annotation.Body import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.Post @@ -13,7 +13,6 @@ import io.micronaut.http.netty.channel.EventLoopGroupConfiguration import io.micronaut.http.netty.channel.EventLoopGroupRegistry import io.micronaut.http.server.HttpServerConfiguration import io.micronaut.http.server.netty.NettyHttpServer -import io.micronaut.http.tck.netty.TestLeakDetector import io.micronaut.http.server.util.DefaultHttpHostResolver import io.micronaut.runtime.server.EmbeddedServer import io.netty.bootstrap.Bootstrap @@ -36,8 +35,6 @@ class FuzzyInputSpec extends Specification { def 'http1 cleartext buffer leaks'() { given: - TestLeakDetector.startTracking("") - ApplicationContext ctx = ApplicationContext.run([ 'spec.name': 'FuzzyInputSpec', "micronaut.server.port": "-1", @@ -67,7 +64,7 @@ class FuzzyInputSpec extends Specification { channel.closeFuture().sync() then: - TestLeakDetector.stopTrackingAndReportLeaks() + noExceptionThrown() cleanup: embeddedServer.stop() @@ -82,7 +79,6 @@ class FuzzyInputSpec extends Specification { def 'http1 cleartext embedded channel'() { given: FlagAppender.clear() - TestLeakDetector.startTracking("") ApplicationContext ctx = ApplicationContext.run([ 'spec.name': 'FuzzyInputSpec', @@ -107,7 +103,6 @@ class FuzzyInputSpec extends Specification { then: embeddedChannel.checkException() - TestLeakDetector.stopTrackingAndReportLeaks() FlagAppender.checkTriggered() where: @@ -131,7 +126,6 @@ class FuzzyInputSpec extends Specification { def 'http2 cleartext embedded channel'() { given: FlagAppender.clear() - TestLeakDetector.startTracking("") ApplicationContext ctx = ApplicationContext.run([ 'spec.name': 'FuzzyInputSpec', @@ -157,7 +151,6 @@ class FuzzyInputSpec extends Specification { then: embeddedChannel.checkException() - TestLeakDetector.stopTrackingAndReportLeaks() FlagAppender.checkTriggered() where: diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/handler/accesslog/AccessLogSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/handler/accesslog/AccessLogSpec.groovy index e11fad6d0a3..5abcf3ed94f 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/handler/accesslog/AccessLogSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/handler/accesslog/AccessLogSpec.groovy @@ -50,6 +50,7 @@ import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler import io.netty.handler.ssl.SslContextBuilder import io.netty.handler.ssl.SupportedCipherSuiteFilter import io.netty.handler.ssl.util.InsecureTrustManagerFactory +import io.netty.util.ReferenceCountUtil import jakarta.inject.Singleton import org.slf4j.LoggerFactory import reactor.core.publisher.Mono @@ -129,6 +130,7 @@ class AccessLogSpec extends Specification { server.close() channel.close() bootstrap.config().group().shutdownGracefully() + ctx.close() } @Issue('https://github.com/micronaut-projects/micronaut-core/issues/6782') @@ -195,6 +197,7 @@ class AccessLogSpec extends Specification { server.close() channel.close() bootstrap.config().group().shutdownGracefully() + ctx.close() } @Issue('https://github.com/micronaut-projects/micronaut-core/issues/6782') @@ -387,6 +390,8 @@ class AccessLogSpec extends Specification { server.close() channel.close() bootstrap.config().group().shutdownGracefully() + ctx.close() + ReferenceCountUtil.release(ctx) } @Issue('https://github.com/micronaut-projects/micronaut-core/issues/6782') @@ -485,6 +490,7 @@ class AccessLogSpec extends Specification { server.close() channel.close() bootstrap.config().group().shutdownGracefully() + ctx.close() } @Issue('https://github.com/micronaut-projects/micronaut-core/issues/6782') @@ -581,6 +587,7 @@ class AccessLogSpec extends Specification { server.close() channel.close() bootstrap.config().group().shutdownGracefully() + ctx.close() } @Issue('https://github.com/micronaut-projects/micronaut-core/issues/6782') diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/H2cSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/H2cSpec.groovy index a7b96010b3e..7707b9b3473 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/H2cSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/H2cSpec.groovy @@ -67,9 +67,10 @@ class H2cSpec extends Specification { private CompletableFuture requestUpgrade(DefaultFullHttpRequest initialRequest) { def responseFuture = new CompletableFuture() + def group = new NioEventLoopGroup(1) def bootstrap = new Bootstrap() .remoteAddress(embeddedServer.host, embeddedServer.port) - .group(new NioEventLoopGroup()) + .group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer() { @Override @@ -120,7 +121,7 @@ class H2cSpec extends Specification { channel.writeAndFlush(initialRequest) channel.read() - return responseFuture + return responseFuture.whenComplete((r, e) -> group.shutdownGracefully()) } void 'test using direct netty http2 client'() { @@ -129,6 +130,9 @@ class H2cSpec extends Specification { expect: responseFuture.get(10, TimeUnit.SECONDS) != null + + cleanup: + ReferenceCountUtil.release(responseFuture.getNow(null)) } void 'test using micronaut http client: retrieve'() { diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/Http2ServerPushSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/Http2ServerPushSpec.groovy index 575342e3d3a..f429aaa51df 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/Http2ServerPushSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/Http2ServerPushSpec.groovy @@ -42,6 +42,7 @@ import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler import io.netty.handler.ssl.SslContextBuilder import io.netty.handler.ssl.SupportedCipherSuiteFilter import io.netty.handler.ssl.util.InsecureTrustManagerFactory +import io.netty.util.ReferenceCountUtil import jakarta.inject.Inject import org.intellij.lang.annotations.Language import spock.lang.Specification @@ -196,6 +197,8 @@ class Http2ServerPushSpec extends Specification { completion.get() channel.closeFuture().await() + + ReferenceCountUtil.release(sslContext) } class FrameListener extends Http2FrameListenerDecorator { diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/Http3Spec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/Http3Spec.groovy index 9ec5575b5ec..e994469b02d 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/Http3Spec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/Http3Spec.groovy @@ -66,6 +66,10 @@ class Http3Spec extends Specification { def resp = client.toBlocking().exchange("/h3/stream", String) then: resp.body() == '["foo","bar"]' + + cleanup: + server.close() + ctx.close() } @Controller diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/resources/Http2StaticResourceCacheSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/resources/Http2StaticResourceCacheSpec.groovy index b8cf8e85897..2a2c95d35fc 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/resources/Http2StaticResourceCacheSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/resources/Http2StaticResourceCacheSpec.groovy @@ -29,6 +29,7 @@ import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler import io.netty.handler.ssl.SslContextBuilder import io.netty.handler.ssl.SupportedCipherSuiteFilter import io.netty.handler.ssl.util.InsecureTrustManagerFactory +import io.netty.util.ReferenceCountUtil import spock.lang.Specification import java.time.Instant @@ -120,5 +121,6 @@ class Http2StaticResourceCacheSpec extends Specification { tempFile.delete() channel.close() embeddedServer.close() + ReferenceCountUtil.release(sslContext) } } diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/ChatServerWebSocket.java b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/ChatServerWebSocket.java index 6f6ff72074d..68dee67bd84 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/ChatServerWebSocket.java +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/websocket/ChatServerWebSocket.java @@ -18,6 +18,8 @@ //tag::clazz[] import io.micronaut.context.annotation.Requires; import io.micronaut.http.context.ServerRequestContext; +import io.micronaut.scheduling.TaskExecutors; +import io.micronaut.scheduling.annotation.ExecuteOn; import io.micronaut.websocket.WebSocketBroadcaster; import io.micronaut.websocket.WebSocketSession; import io.micronaut.websocket.annotation.OnClose; @@ -29,6 +31,7 @@ @Requires(property = "spec.name", value = "SimpleTextWebSocketSpec") @ServerWebSocket("/chat/{topic}/{username}") // <1> +@ExecuteOn(TaskExecutors.BLOCKING) public class ChatServerWebSocket { private WebSocketBroadcaster broadcaster; private String subProtocol; diff --git a/http-server-netty/src/test/java/io/micronaut/http/server/netty/fuzzing/FlagAppender.java b/http-server-netty/src/test/java/io/micronaut/http/server/netty/fuzzing/FlagAppender.java index 533b6e6f0e3..76e78a47f88 100644 --- a/http-server-netty/src/test/java/io/micronaut/http/server/netty/fuzzing/FlagAppender.java +++ b/http-server-netty/src/test/java/io/micronaut/http/server/netty/fuzzing/FlagAppender.java @@ -2,7 +2,6 @@ import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.AppenderBase; -import io.micronaut.http.tck.netty.TestLeakDetector; public class FlagAppender extends AppenderBase { private static volatile boolean triggered = false; @@ -20,10 +19,6 @@ public static void checkTriggered() { @Override protected void append(ILoggingEvent eventObject) { - if (eventObject.getLoggerName().equals(TestLeakDetector.class.getName())) { - // ignore 'Canary leak detection failed.' messages - return; - } triggered = true; } } diff --git a/http-server-netty/src/test/java/io/micronaut/http/server/netty/fuzzing/FuzzyInputTest.java b/http-server-netty/src/test/java/io/micronaut/http/server/netty/fuzzing/FuzzyInputTest.java index 0ffd8c6a885..8d40076d6e1 100644 --- a/http-server-netty/src/test/java/io/micronaut/http/server/netty/fuzzing/FuzzyInputTest.java +++ b/http-server-netty/src/test/java/io/micronaut/http/server/netty/fuzzing/FuzzyInputTest.java @@ -8,7 +8,6 @@ import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Post; import io.micronaut.http.server.netty.NettyHttpServer; -import io.micronaut.http.tck.netty.TestLeakDetector; import io.micronaut.runtime.server.EmbeddedServer; import io.netty.buffer.ByteBufUtil; import io.netty.channel.embedded.EmbeddedChannel; @@ -27,7 +26,6 @@ void setup() { @AfterEach void tearDown() throws Exception { FlagAppender.checkTriggered(); - TestLeakDetector.reportStillOpen(); } @Test From c1d3420c09c15ee46037ca9780e45609645cb50f Mon Sep 17 00:00:00 2001 From: yawkat Date: Tue, 21 Oct 2025 12:00:05 +0200 Subject: [PATCH 2/4] move to micronaut-test module --- buffer-netty/build.gradle.kts | 2 +- gradle/libs.versions.toml | 3 +- http-server-netty/build.gradle.kts | 4 +- .../netty/BufferLeakDetectionExtension.java | 23 -- ...amework.runtime.extension.IGlobalExtension | 1 - http-tck/build.gradle.kts | 1 + .../tck/netty/LeakDetectionExtension.java | 54 ----- .../http/tck/netty/TestLeakDetector.java | 197 ------------------ .../org.junit.jupiter.api.extension.Extension | 1 - .../tck/jdk/tests/JdkHttpMethodTests.java | 1 + .../tck/netty/tests/NettyHttpMethodTests.java | 1 + .../netty/tests/JdkHttpServerTestSuite.java | 2 + 12 files changed, 11 insertions(+), 279 deletions(-) delete mode 100644 http-server-netty/src/test/groovy/io/micronaut/http/server/netty/BufferLeakDetectionExtension.java delete mode 100644 http-server-netty/src/test/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension delete mode 100644 http-tck/src/main/java/io/micronaut/http/tck/netty/LeakDetectionExtension.java delete mode 100644 http-tck/src/main/java/io/micronaut/http/tck/netty/TestLeakDetector.java delete mode 100644 http-tck/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension diff --git a/buffer-netty/build.gradle.kts b/buffer-netty/build.gradle.kts index fbd1d9d4071..0826a51eb8e 100644 --- a/buffer-netty/build.gradle.kts +++ b/buffer-netty/build.gradle.kts @@ -10,7 +10,7 @@ dependencies { annotationProcessor(projects.micronautInjectJava) - testRuntimeOnly(projects.micronautHttpTck) // leak detection module + testRuntimeOnly(libs.micronaut.test.netty.leak) } tasks { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b04f6cebf10..5edb8bb40d3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,7 +43,7 @@ micronaut-build-plugins="7.6.4" micronaut-groovy = "4.7.0" micronaut-session = "4.6.0" micronaut-sql = "5.3.0" -micronaut-test = "4.6.2" +micronaut-test = "4.10.1" micronaut-validation = "4.9.0" micronaut-rxjava2 = "2.7.0" micronaut-rxjava3 = "3.7.0" @@ -264,6 +264,7 @@ micronaut-test-junit5 = { module = "io.micronaut.test:micronaut-test-junit5", ve micronaut-test-kotest5 = { module = "io.micronaut.test:micronaut-test-kotest5", version.ref = "micronaut-test" } micronaut-test-spock = { module = "io.micronaut.test:micronaut-test-spock", version.ref = "micronaut-test" } micronaut-test-type-pollution = { module = "io.micronaut.test:micronaut-test-type-pollution", version.ref = "micronaut-test" } +micronaut-test-netty-leak = { module = "io.micronaut.test:micronaut-test-netty-leak", version.ref = "micronaut-test" } micronaut-sql-jdbc = { module = "io.micronaut.sql:micronaut-jdbc", version.ref = "micronaut-sql" } micronaut-sql-jdbc-tomcat = { module = "io.micronaut.sql:micronaut-jdbc-tomcat", version.ref = "micronaut-sql" } diff --git a/http-server-netty/build.gradle.kts b/http-server-netty/build.gradle.kts index 54522e7acd4..a2cbf1270dd 100644 --- a/http-server-netty/build.gradle.kts +++ b/http-server-netty/build.gradle.kts @@ -16,6 +16,7 @@ tasks { systemProperty("io.netty.leakDetection.level", "paranoid") systemProperty("io.netty.leakDetection.targetRecords", "100") systemProperty("junit.jupiter.extensions.autodetection.enabled", "true") + systemProperty("io.netty.util.LeakPresenceDetector.trackCreationStack", "true") jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") maxHeapSize = "1G" } @@ -65,7 +66,6 @@ dependencies { testImplementation(libs.bcpkix) testImplementation(libs.managed.netty.pkitesting) testImplementation(projects.micronautJacksonDatabind) - testImplementation(projects.micronautHttpTck) // Add Micronaut Jackson XML after v4 Migration // testImplementation(libs.managed.micronaut.xml) { // exclude module:'micronaut-inject' @@ -125,12 +125,14 @@ dependencies { exclude(group = "io.micronaut") } testImplementation(libs.junit.jupiter.api) + testImplementation(libs.micronaut.test.netty.leak) } tasks.withType().configureEach { forkEvery = 100 maxParallelForks = 4 useJUnitPlatform() + systemProperty("junit.jupiter.extensions.autodetection.enabled", "true") } //tasks.withType(Test).configureEach { diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/BufferLeakDetectionExtension.java b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/BufferLeakDetectionExtension.java deleted file mode 100644 index a74c2b6bea9..00000000000 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/BufferLeakDetectionExtension.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.micronaut.http.server.netty; - -import io.micronaut.http.tck.netty.TestLeakDetector; -import org.spockframework.runtime.extension.IGlobalExtension; -import org.spockframework.runtime.model.SpecInfo; - -public class BufferLeakDetectionExtension implements IGlobalExtension { - static { - TestLeakDetector.init(); - } - - @Override - public void visitSpec(SpecInfo spec) { - spec.addSetupInterceptor(invocation -> { - TestLeakDetector.startTracking(invocation.getFeature().getName()); - invocation.proceed(); - }); - spec.addCleanupInterceptor(invocation -> { - invocation.proceed(); - TestLeakDetector.stopTrackingAndReportLeaks(); - }); - } -} diff --git a/http-server-netty/src/test/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension b/http-server-netty/src/test/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension deleted file mode 100644 index e59faadc26a..00000000000 --- a/http-server-netty/src/test/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension +++ /dev/null @@ -1 +0,0 @@ -io.micronaut.http.server.netty.BufferLeakDetectionExtension diff --git a/http-tck/build.gradle.kts b/http-tck/build.gradle.kts index caf56fdfedc..c2e3fb952ac 100644 --- a/http-tck/build.gradle.kts +++ b/http-tck/build.gradle.kts @@ -21,4 +21,5 @@ dependencies { api(libs.junit.jupiter.api) api(libs.junit.jupiter.params) api(libs.managed.reactor) + implementation(libs.micronaut.test.netty.leak) } diff --git a/http-tck/src/main/java/io/micronaut/http/tck/netty/LeakDetectionExtension.java b/http-tck/src/main/java/io/micronaut/http/tck/netty/LeakDetectionExtension.java deleted file mode 100644 index 18094504e35..00000000000 --- a/http-tck/src/main/java/io/micronaut/http/tck/netty/LeakDetectionExtension.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2017-2025 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.tck.netty; - -import io.netty.util.ResourceLeakDetector; -import org.junit.jupiter.api.extension.AfterEachCallback; -import org.junit.jupiter.api.extension.BeforeEachCallback; -import org.junit.jupiter.api.extension.ExtensionContext; - -public final class LeakDetectionExtension implements BeforeEachCallback, AfterEachCallback { - private static final boolean NETTY_AVAILABLE; - - static { - boolean available; - try { - //noinspection ResultOfMethodCallIgnored - ResourceLeakDetector.getLevel(); - available = true; - } catch (NoClassDefFoundError e) { - available = false; - } - NETTY_AVAILABLE = available; - if (NETTY_AVAILABLE) { - TestLeakDetector.init(); - } - } - - @Override - public void beforeEach(ExtensionContext context) throws Exception { - if (NETTY_AVAILABLE) { - TestLeakDetector.startTracking(context.getDisplayName()); - } - } - - @Override - public void afterEach(ExtensionContext context) throws Exception { - if (NETTY_AVAILABLE) { - TestLeakDetector.stopTrackingAndReportLeaks(); - } - } -} diff --git a/http-tck/src/main/java/io/micronaut/http/tck/netty/TestLeakDetector.java b/http-tck/src/main/java/io/micronaut/http/tck/netty/TestLeakDetector.java deleted file mode 100644 index f50cbefcf94..00000000000 --- a/http-tck/src/main/java/io/micronaut/http/tck/netty/TestLeakDetector.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright 2017-2025 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.tck.netty; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.util.ResourceLeakDetector; -import io.netty.util.ResourceLeakDetectorFactory; -import io.netty.util.ResourceLeakTracker; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.reflect.Field; -import java.util.List; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.CopyOnWriteArrayList; - -/** - * Utility for testing for netty buffer leaks. - * - * @since 4.8.19 - * @author Jonas Konrad - */ -public final class TestLeakDetector { - private static final Logger LOG = LoggerFactory.getLogger(TestLeakDetector.class); - private static final String BASE_CANARY_STRING = "canary-" + UUID.randomUUID() + "-"; - - private static final List> ALL_DETECTORS = new CopyOnWriteArrayList<>(); - - private static volatile boolean leakDetected; - private static volatile boolean canaryDetected; - private static volatile String canaryString; - - private static volatile long sink; - - static { - System.setProperty("io.netty.leakDetection.level", "paranoid"); // this prevents vertx from resetting it - ResourceLeakDetectorFactory.setResourceLeakDetectorFactory(new ResourceLeakDetectorFactory() { - @Override - public ResourceLeakDetector newResourceLeakDetector(Class resource, int samplingInterval, long maxActive) { - return new TestResourceLeakDetector<>(resource, samplingInterval); - } - - @Override - public ResourceLeakDetector newResourceLeakDetector(Class resource, int samplingInterval) { - return new TestResourceLeakDetector<>(resource, samplingInterval); - } - }); - ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID); - } - - private TestLeakDetector() { - } - - /** - * Initialize the leak detector. - */ - public static void init() { - // run static initializer - } - - /** - * Start tracking leaks. - * - * @param testName The current test name - */ - public static void startTracking(String testName) { - ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID); - - triggerGc(); - - leakDetected = false; - - LOG.debug("Starting resource leak tracking"); - } - - /** - * Stop tracking leaks. - * - * @throws RuntimeException If there was a leak since the last {@link #startTracking(String)} - */ - public static void stopTrackingAndReportLeaks() { - triggerGc(); - - if (leakDetected) { - throw new RuntimeException("Detected a resource leak. Please check logs"); - } else { - LOG.debug("No resource leak detected"); - } - } - - private static void leakCanary() { - ByteBuf resource = ByteBufAllocator.DEFAULT.directBuffer(); - resource.touch(canaryString); - } - - private static void triggerGc() { - // timeout of last resort for the loop below. use nanoTime because it's monotonic - long startTime = System.nanoTime(); - - // need to randomize this every time, since ResourceLeakDetector will deduplicate leaks - canaryString = BASE_CANARY_STRING + UUID.randomUUID(); - canaryDetected = false; - - leakCanary(); - - do { - if (System.nanoTime() - startTime > 30_000_000_000L) { - LOG.warn("Canary leak detection failed."); - break; - } - - // Trigger GC. - System.gc(); - - // trigger detectors – ref queue collection is only done on track() - //noinspection rawtypes - for (ResourceLeakDetector detector : ALL_DETECTORS) { - Object obj = new Object(); - //noinspection unchecked - ResourceLeakTracker track = detector.track(obj); - if (track == null) { - throw new RuntimeException("getLevel: " + ResourceLeakDetector.getLevel() + " detector: " + detector); - } - track.close(obj); - } - - // Give the GC something to work on. - for (int i = 0; i < 1000; i++) { - sink = System.identityHashCode(new byte[10000]); - } - } while (!canaryDetected && !Thread.interrupted()); - } - - public static void reportStillOpen() throws Exception { - triggerGc(); - Field allLeaksField = ResourceLeakDetector.class.getDeclaredField("allLeaks"); - allLeaksField.trySetAccessible(); - for (ResourceLeakDetector detector : ALL_DETECTORS) { - Set allLeaks = (Set) allLeaksField.get(detector); - for (Object leak : allLeaks) { - String stack = leak.toString(); - if (!stack.contains("")) { - throw new IllegalStateException("Still open: " + stack); - } - } - } - } - - private static final class TestResourceLeakDetector extends ResourceLeakDetector { - public TestResourceLeakDetector(Class resourceType, int samplingInterval) { - super(resourceType, samplingInterval); - ALL_DETECTORS.add(this); - } - - @Override - protected boolean needReport() { - return true; - } - - @Override - protected void reportTracedLeak(String resourceType, String records) { - String canary = canaryString; - if (canary != null && records.contains(canary)) { - canaryDetected = true; - return; - } - if (records.contains(BASE_CANARY_STRING)) { - // probably a canary from another run that ran into a timeout, drop - return; - } - - leakDetected = true; - super.reportTracedLeak(resourceType, records); - } - - @Override - protected void reportUntracedLeak(String resourceType) { - leakDetected = true; - super.reportUntracedLeak(resourceType); - } - } -} diff --git a/http-tck/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/http-tck/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension deleted file mode 100644 index b2b16910a77..00000000000 --- a/http-tck/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension +++ /dev/null @@ -1 +0,0 @@ -io.micronaut.http.tck.netty.LeakDetectionExtension diff --git a/test-suite-http-client-tck-jdk/src/test/java/io/micronaut/http/client/tck/jdk/tests/JdkHttpMethodTests.java b/test-suite-http-client-tck-jdk/src/test/java/io/micronaut/http/client/tck/jdk/tests/JdkHttpMethodTests.java index e9840fb3939..e9897ffa4c4 100644 --- a/test-suite-http-client-tck-jdk/src/test/java/io/micronaut/http/client/tck/jdk/tests/JdkHttpMethodTests.java +++ b/test-suite-http-client-tck-jdk/src/test/java/io/micronaut/http/client/tck/jdk/tests/JdkHttpMethodTests.java @@ -14,6 +14,7 @@ @SelectPackages("io.micronaut.http.client.tck.tests") @SuiteDisplayName("HTTP Client TCK for the HTTP Client Implementation based on Java HTTP Client") @ConfigurationParameter(key = ClientDisabledCondition.HTTP_CLIENT_CONFIGURATION, value = ClientDisabledCondition.JDK) +@ConfigurationParameter(key = "junit.jupiter.extensions.autodetection.enabled", value = "true") @SuppressWarnings("java:S2187") // This runs a suite of tests, but has no tests of its own @ExcludeClassNamePatterns({ "io.micronaut.http.client.tck.tests.ContinueTest", // Unsupported body type errors diff --git a/test-suite-http-client-tck-netty/src/test/java/io/micronaut/http/client/tck/netty/tests/NettyHttpMethodTests.java b/test-suite-http-client-tck-netty/src/test/java/io/micronaut/http/client/tck/netty/tests/NettyHttpMethodTests.java index c568dc84398..c0a4d4c378a 100644 --- a/test-suite-http-client-tck-netty/src/test/java/io/micronaut/http/client/tck/netty/tests/NettyHttpMethodTests.java +++ b/test-suite-http-client-tck-netty/src/test/java/io/micronaut/http/client/tck/netty/tests/NettyHttpMethodTests.java @@ -10,6 +10,7 @@ @SelectPackages("io.micronaut.http.client.tck.tests") @SuiteDisplayName("HTTP Client TCK for the HTTP Client Implementation based on Netty") @ConfigurationParameter(key = ClientDisabledCondition.HTTP_CLIENT_CONFIGURATION, value = ClientDisabledCondition.NETTY) +@ConfigurationParameter(key = "junit.jupiter.extensions.autodetection.enabled", value = "true") @SuppressWarnings("java:S2187") // This runs a suite of tests, but has no tests of its own public class NettyHttpMethodTests { } diff --git a/test-suite-http-server-tck-jdk/src/test/java/io/micronaut/http/server/tck/netty/tests/JdkHttpServerTestSuite.java b/test-suite-http-server-tck-jdk/src/test/java/io/micronaut/http/server/tck/netty/tests/JdkHttpServerTestSuite.java index d41ee9a30ff..347e149b02c 100644 --- a/test-suite-http-server-tck-jdk/src/test/java/io/micronaut/http/server/tck/netty/tests/JdkHttpServerTestSuite.java +++ b/test-suite-http-server-tck-jdk/src/test/java/io/micronaut/http/server/tck/netty/tests/JdkHttpServerTestSuite.java @@ -1,5 +1,6 @@ package io.micronaut.http.server.tck.netty.tests; +import org.junit.platform.suite.api.ConfigurationParameter; import org.junit.platform.suite.api.ExcludeClassNamePatterns; import org.junit.platform.suite.api.ExcludeTags; import org.junit.platform.suite.api.SelectPackages; @@ -15,5 +16,6 @@ "io.micronaut.http.server.tck.tests.forms.UploadTest" // multipart }) @ExcludeTags("multipart") // Multipart not supported by HttpClient +@ConfigurationParameter(key = "junit.jupiter.extensions.autodetection.enabled", value = "true") public class JdkHttpServerTestSuite { } From 00ec32f6ab89ae46e75f02226d95a54dd67be691 Mon Sep 17 00:00:00 2001 From: yawkat Date: Wed, 5 Nov 2025 12:02:53 +0100 Subject: [PATCH 3/4] fix test --- http-server-netty/build.gradle.kts | 1 - .../handler/accesslog/AccessLogSpec.groovy | 35 ++++++++++++------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/http-server-netty/build.gradle.kts b/http-server-netty/build.gradle.kts index a2cbf1270dd..baed8d91369 100644 --- a/http-server-netty/build.gradle.kts +++ b/http-server-netty/build.gradle.kts @@ -16,7 +16,6 @@ tasks { systemProperty("io.netty.leakDetection.level", "paranoid") systemProperty("io.netty.leakDetection.targetRecords", "100") systemProperty("junit.jupiter.extensions.autodetection.enabled", "true") - systemProperty("io.netty.util.LeakPresenceDetector.trackCreationStack", "true") jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") maxHeapSize = "1G" } diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/handler/accesslog/AccessLogSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/handler/accesslog/AccessLogSpec.groovy index 5abcf3ed94f..f77f33c6252 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/handler/accesslog/AccessLogSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/handler/accesslog/AccessLogSpec.groovy @@ -78,8 +78,10 @@ class AccessLogSpec extends Specification { server.start() def responses = new CopyOnWriteArrayList() + + def group = new NioEventLoopGroup(1) Bootstrap bootstrap = new Bootstrap() - .group(new NioEventLoopGroup(1)) + .group(group) .channel(NioSocketChannel) .option(ChannelOption.AUTO_READ, true) .handler(new ChannelInitializer() { @@ -129,8 +131,8 @@ class AccessLogSpec extends Specification { responses*.content().forEach(ByteBuf::release) server.close() channel.close() - bootstrap.config().group().shutdownGracefully() ctx.close() + group.shutdownGracefully() } @Issue('https://github.com/micronaut-projects/micronaut-core/issues/6782') @@ -146,8 +148,10 @@ class AccessLogSpec extends Specification { server.start() def responses = new CopyOnWriteArrayList() + + def group = new NioEventLoopGroup(1) Bootstrap bootstrap = new Bootstrap() - .group(new NioEventLoopGroup(1)) + .group(group) .channel(NioSocketChannel) .option(ChannelOption.AUTO_READ, true) .handler(new ChannelInitializer() { @@ -196,8 +200,8 @@ class AccessLogSpec extends Specification { responses*.content().forEach(ByteBuf::release) server.close() channel.close() - bootstrap.config().group().shutdownGracefully() ctx.close() + group.shutdownGracefully() } @Issue('https://github.com/micronaut-projects/micronaut-core/issues/6782') @@ -212,8 +216,10 @@ class AccessLogSpec extends Specification { server.start() def responses = new CopyOnWriteArrayList() + + def group = new NioEventLoopGroup(1) Bootstrap bootstrap = new Bootstrap() - .group(new NioEventLoopGroup(1)) + .group(group) .channel(NioSocketChannel) .option(ChannelOption.AUTO_READ, true) .handler(new ChannelInitializer() { @@ -275,7 +281,7 @@ class AccessLogSpec extends Specification { responses*.content().forEach(ByteBuf::release) server.close() channel.close() - bootstrap.config().group().shutdownGracefully() + group.shutdownGracefully() } @Issue('https://github.com/micronaut-projects/micronaut-core/issues/6782') @@ -317,8 +323,10 @@ class AccessLogSpec extends Specification { ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, ApplicationProtocolNames.HTTP_2)) .build() + + def group = new NioEventLoopGroup(1) Bootstrap bootstrap = new Bootstrap() - .group(new NioEventLoopGroup(1)) + .group(group) .channel(NioSocketChannel) .option(ChannelOption.AUTO_READ, true) .handler(new ChannelInitializer() { @@ -389,9 +397,9 @@ class AccessLogSpec extends Specification { responses*.content().forEach(ByteBuf::release) server.close() channel.close() - bootstrap.config().group().shutdownGracefully() ctx.close() ReferenceCountUtil.release(ctx) + group.shutdownGracefully() } @Issue('https://github.com/micronaut-projects/micronaut-core/issues/6782') @@ -420,8 +428,9 @@ class AccessLogSpec extends Specification { request4.headers().add(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), ':https') def responses = new CopyOnWriteArrayList() + def group = new NioEventLoopGroup(1) Bootstrap bootstrap = new Bootstrap() - .group(new NioEventLoopGroup(1)) + .group(group) .channel(NioSocketChannel) .option(ChannelOption.AUTO_READ, true) .handler(new ChannelInitializer() { @@ -489,8 +498,8 @@ class AccessLogSpec extends Specification { responses*.content().forEach(ByteBuf::release) server.close() channel.close() - bootstrap.config().group().shutdownGracefully() ctx.close() + group.shutdownGracefully() } @Issue('https://github.com/micronaut-projects/micronaut-core/issues/6782') @@ -520,8 +529,10 @@ class AccessLogSpec extends Specification { request4.headers().add(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), ':https') def responses = new CopyOnWriteArrayList() + + def group = new NioEventLoopGroup(1) Bootstrap bootstrap = new Bootstrap() - .group(new NioEventLoopGroup(1)) + .group(group) .channel(NioSocketChannel) .option(ChannelOption.AUTO_READ, true) .handler(new ChannelInitializer() { @@ -586,8 +597,8 @@ class AccessLogSpec extends Specification { responses*.content().forEach(ByteBuf::release) server.close() channel.close() - bootstrap.config().group().shutdownGracefully() ctx.close() + group.shutdownGracefully() } @Issue('https://github.com/micronaut-projects/micronaut-core/issues/6782') From 3ead1f419fe192d28d06fafd08bb25ca4b221be9 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Wed, 5 Nov 2025 14:19:31 +0100 Subject: [PATCH 4/4] Update gradle/libs.versions.toml Co-authored-by: Sergio del Amo --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5edb8bb40d3..5fd05271541 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,7 +43,7 @@ micronaut-build-plugins="7.6.4" micronaut-groovy = "4.7.0" micronaut-session = "4.6.0" micronaut-sql = "5.3.0" -micronaut-test = "4.10.1" +micronaut-test = "4.10.2" micronaut-validation = "4.9.0" micronaut-rxjava2 = "2.7.0" micronaut-rxjava3 = "3.7.0"