From 5a28edd3ae5169e89d4bb35052835289ff07ff25 Mon Sep 17 00:00:00 2001 From: Pavel Ptashyts <49400901+pavel-ptashyts@users.noreply.github.com> Date: Mon, 3 Aug 2026 14:49:03 +0200 Subject: [PATCH 1/2] Guard debug logs in pollPooledChannel SLF4J's Logger has overloads for one and two arguments only, so the four log statements in pollPooledChannel bind to debug(String, Object...) and build their varargs Object[3] at the call site regardless of whether debug is enabled. pollPooledChannel runs on every request, and its two pooled-channel branches are the steady-state keep-alive path, so this allocated an array per served request in production configurations that log at INFO or above. Wrap the calls in isDebugEnabled(), matching what sendRequestWithOpenChannel already does for its own three-argument statement a few lines up. Behaviour when debug logging is enabled is unchanged. Claude Code on behalf of Pavel Ptashyts Co-Authored-By: Claude Opus 5 --- .../netty/request/NettyRequestSender.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java index fc7956c4d..e78988488 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java @@ -1380,12 +1380,17 @@ private Channel pollPooledChannel(NettyResponseFuture future, Request request if (!uri.isWebSocket()) { Channel h2Channel = channelManager.pollHttp2Connection(override); if (h2Channel != null) { - LOGGER.debug("Using HTTP/2 multiplexed Channel '{}' for '{}' to '{}'", h2Channel, request.getMethod(), uri); + // SLF4J has no three-argument overload, so every log statement in this method binds to + // debug(String, Object...) and allocates its varargs array at the call site whatever the + // level. This is the steady-state connection-reuse path, so guard them all explicitly. + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Using HTTP/2 multiplexed Channel '{}' for '{}' to '{}'", h2Channel, request.getMethod(), uri); + } return h2Channel; } } Channel channel = channelManager.poll(override); - if (channel != null) { + if (channel != null && LOGGER.isDebugEnabled()) { LOGGER.debug("Using pooled Channel '{}' for '{}' to '{}'", channel, request.getMethod(), uri); } return channel; @@ -1406,14 +1411,16 @@ private Channel pollPooledChannel(NettyResponseFuture future, Request request if (!uri.isWebSocket()) { Channel h2Channel = channelManager.pollHttp2Connection(partitionKey); if (h2Channel != null) { - LOGGER.debug("Using HTTP/2 multiplexed Channel '{}' for '{}' to '{}'", h2Channel, request.getMethod(), uri); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Using HTTP/2 multiplexed Channel '{}' for '{}' to '{}'", h2Channel, request.getMethod(), uri); + } return h2Channel; } } final Channel channel = channelManager.poll(partitionKey); - if (channel != null) { + if (channel != null && LOGGER.isDebugEnabled()) { LOGGER.debug("Using pooled Channel '{}' for '{}' to '{}'", channel, request.getMethod(), uri); } return channel; From 7dfcf1916c792f13fce1f77787ed5c842706ed42 Mon Sep 17 00:00:00 2001 From: Pavel Ptashyts <49400901+pavel-ptashyts@users.noreply.github.com> Date: Tue, 4 Aug 2026 08:07:57 +0200 Subject: [PATCH 2/2] Move the varargs note to the top of the method Review feedback on #2300. The note explaining why all four debug statements are guarded sat in the deepest branch of pollPooledChannel, where a reader following the path most requests take never passes it. Move it to the top of the method, next to the values the statements log. It also claimed "every log statement in this method", which is wrong: the onConnectionPoolAttempt failure at the top binds to error(String, Throwable) and allocates nothing. Say debug statement. Claude Code on behalf of Pavel Ptashyts Co-Authored-By: Claude Opus 5 --- .../asynchttpclient/netty/request/NettyRequestSender.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java index e78988488..a6601d57d 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java @@ -1373,6 +1373,10 @@ private Channel pollPooledChannel(NettyResponseFuture future, Request request Uri uri = request.getUri(); String virtualHost = request.getVirtualHost(); + // SLF4J has no three-argument overload, so every debug statement below binds to + // debug(String, Object...) and allocates its varargs array at the call site whatever the level. This + // is the steady-state connection-reuse path, so they are all guarded explicitly. + // Round-robin mode: poll with the IP-aware key so reuse stays pinned to the chosen IP (both the // HTTP/2 registry and the HTTP/1.1 pool). Object override = future != null ? future.getPartitionKeyOverride() : null; @@ -1380,9 +1384,6 @@ private Channel pollPooledChannel(NettyResponseFuture future, Request request if (!uri.isWebSocket()) { Channel h2Channel = channelManager.pollHttp2Connection(override); if (h2Channel != null) { - // SLF4J has no three-argument overload, so every log statement in this method binds to - // debug(String, Object...) and allocates its varargs array at the call site whatever the - // level. This is the steady-state connection-reuse path, so guard them all explicitly. if (LOGGER.isDebugEnabled()) { LOGGER.debug("Using HTTP/2 multiplexed Channel '{}' for '{}' to '{}'", h2Channel, request.getMethod(), uri); }