Version
5.1.6, and every 5.x I tested down to 5.0.12. Not present in 4.5.21 or 4.5.27.
Context
vertx.createNetClient() returns a client that is never released, even after close() has completed. Each call leaves one NetClientImpl reachable for as long as the Vert.x instance lives.
VertxImpl.createNetClient registers the internal client on the close future of its owner:
public NetClient createNetClient(NetClientOptions options) {
NetClientImpl netClient = new NetClientBuilder(this, options).build();
CloseFuture fut = resolveCloseFuture();
fut.add(netClient);
return new CleanableNetClient(CleanerProvider.INSTANCE.get(), netClient);
}
CloseFuture.add puts it in children, a plain HashMap, and NetClientImpl is not a NestedCloseable, so add never sets an owner and the unregisterFromOwner() path in cascadeClose() never applies to it.
Closing the client reaches channelGroup.shutdown(...), which takes the connections down but leaves the map entry in place. Nothing ever calls CloseFuture.remove(netClient).
4.5.x registered a dedicated CloseFuture per client instead, which took the weakHooks branch of add — a WeakHashMap, plus fut.future().onComplete(ar -> remove(fut)) for auto-removal. Both safety nets went away in 5.x.
There is no workaround through the public API: the object registered is NetClientImpl while
createNetClient returns a CleanableNetClient wrapper, so ContextInternal.removeCloseHook(client)` does not match — the same obstacle described in #6268.
This looks like the NetClient counterpart of #6268, fixed for httpClientBuilder() in 5.1.6. The histogram in that report already showed 10 000 leaked NetClientImpl alongside the HTTP clients. The direct createNetClient path still leaks on 5.1.6.
Steps to reproduce
import io.vertx.core.Vertx;
import io.vertx.core.net.NetClient;
public class Repro {
public static void main(String[] args) throws Exception {
Vertx vertx = Vertx.vertx();
for (int i = 0; i < 10_000; i++) {
NetClient client = vertx.createNetClient();
client.close().toCompletionStage().toCompletableFuture().join();
}
System.gc();
Thread.sleep(2_000);
System.gc();
System.out.println("pid " + ProcessHandle.current().pid());
Thread.sleep(120_000);
}
}
$ jcmd <pid> GC.class_histogram | grep NetClientImpl
8: 10000 720000 io.vertx.core.net.impl.tcp.NetClientImpl
11: 10000 640000 io.vertx.core.net.impl.tcp.NetClientImpl$1
The same loop on 4.5.21 and 4.5.27 leaves nothing behind. Connecting the client before closing it changes nothing — the leak is in create + close alone. The loop above runs on the main thread, so the owner is the Vert.x instance's own close future and nothing releases the clients before the JVM exits.
Measured on JDK 21.0.5 (Temurin), macOS 15.
Do you have a reproducer?
No response
Version
5.1.6, and every 5.x I tested down to 5.0.12. Not present in 4.5.21 or 4.5.27.
Context
vertx.createNetClient()returns a client that is never released, even afterclose()has completed. Each call leaves oneNetClientImplreachable for as long as the Vert.x instance lives.VertxImpl.createNetClientregisters the internal client on the close future of its owner:CloseFuture.addputs it inchildren, a plainHashMap, andNetClientImplis not aNestedCloseable, soaddnever sets anownerand theunregisterFromOwner()path incascadeClose()never applies to it.Closing the client reaches
channelGroup.shutdown(...), which takes the connections down but leaves the map entry in place. Nothing ever callsCloseFuture.remove(netClient).4.5.x registered a dedicated
CloseFutureper client instead, which took theweakHooksbranch ofadd— aWeakHashMap, plusfut.future().onComplete(ar -> remove(fut))for auto-removal. Both safety nets went away in 5.x.There is no workaround through the public API: the object registered is
NetClientImplwhilecreateNetClientreturns aCleanableNetClientwrapper, so ContextInternal.removeCloseHook(client)` does not match — the same obstacle described in #6268.This looks like the
NetClientcounterpart of #6268, fixed forhttpClientBuilder()in 5.1.6. The histogram in that report already showed 10 000 leakedNetClientImplalongside the HTTP clients. The directcreateNetClientpath still leaks on 5.1.6.Steps to reproduce
The same loop on 4.5.21 and 4.5.27 leaves nothing behind. Connecting the client before closing it changes nothing — the leak is in create + close alone. The loop above runs on the main thread, so the owner is the Vert.x instance's own close future and nothing releases the clients before the JVM exits.
Measured on JDK 21.0.5 (Temurin), macOS 15.
Do you have a reproducer?
No response