2020import io .netty .channel .Channel ;
2121import io .netty .channel .ChannelFactory ;
2222import io .netty .channel .ChannelHandler ;
23+ import io .netty .channel .ChannelHandlerContext ;
2324import io .netty .channel .ChannelInboundHandlerAdapter ;
2425import io .netty .channel .ChannelInitializer ;
2526import io .netty .channel .ChannelOption ;
4142import io .netty .handler .codec .http2 .Http2FrameCodecBuilder ;
4243import io .netty .handler .codec .http2 .Http2MultiplexHandler ;
4344import io .netty .handler .codec .http2 .Http2Settings ;
45+ import io .netty .handler .codec .http2 .Http2GoAwayFrame ;
4446import io .netty .handler .codec .http2 .Http2SettingsFrame ;
4547import io .netty .handler .codec .http2 .Http2StreamChannel ;
4648import io .netty .handler .logging .LogLevel ;
4749import io .netty .handler .logging .LoggingHandler ;
48- import io .netty .handler .timeout .IdleStateHandler ;
4950import io .netty .handler .proxy .ProxyHandler ;
5051import io .netty .handler .proxy .Socks4ProxyHandler ;
5152import io .netty .handler .proxy .Socks5ProxyHandler ;
5253import io .netty .handler .ssl .SslHandler ;
5354import io .netty .handler .stream .ChunkedWriteHandler ;
55+ import io .netty .handler .timeout .IdleStateHandler ;
5456import io .netty .resolver .NameResolver ;
5557import io .netty .util .Timer ;
5658import io .netty .util .concurrent .DefaultThreadFactory ;
8991import java .net .InetSocketAddress ;
9092import java .util .Map ;
9193import java .util .Map .Entry ;
94+ import java .util .concurrent .ConcurrentHashMap ;
9295import java .util .concurrent .ThreadFactory ;
9396import java .util .concurrent .TimeUnit ;
9497import java .util .function .Function ;
@@ -122,6 +125,7 @@ public class ChannelManager {
122125
123126 private final ChannelPool channelPool ;
124127 private final ChannelGroup openChannels ;
128+ private final ConcurrentHashMap <Object , Channel > http2Connections = new ConcurrentHashMap <>();
125129
126130 private AsyncHttpClientHandler wsHandler ;
127131 private Http2Handler http2Handler ;
@@ -338,6 +342,59 @@ public final void tryToOfferChannelToPool(Channel channel, AsyncHandler<?> async
338342 }
339343 }
340344
345+ /**
346+ * Registers an HTTP/2 connection in the registry for the given partition key.
347+ * The connection stays in the registry (not the regular pool) to allow multiplexing —
348+ * multiple requests can share the same connection concurrently.
349+ */
350+ public void registerHttp2Connection (Object partitionKey , Channel channel ) {
351+ Http2ConnectionState state = channel .attr (Http2ConnectionState .HTTP2_STATE_KEY ).get ();
352+ if (state != null ) {
353+ state .setPartitionKey (partitionKey );
354+ }
355+ http2Connections .put (partitionKey , channel );
356+ // Auto-remove from registry when the connection closes
357+ channel .closeFuture ().addListener (future -> removeHttp2Connection (partitionKey , channel ));
358+ }
359+
360+ /**
361+ * Removes an HTTP/2 connection from the registry, but only if it's the currently registered
362+ * connection for that partition key (avoids removing a replacement connection).
363+ */
364+ public void removeHttp2Connection (Object partitionKey , Channel channel ) {
365+ http2Connections .remove (partitionKey , channel );
366+ }
367+
368+ /**
369+ * Returns an active, non-draining HTTP/2 connection for the given partition key, or {@code null}.
370+ * Unlike the regular pool, this does NOT remove the connection — it remains available for
371+ * concurrent multiplexed requests.
372+ */
373+ public Channel pollHttp2Connection (Object partitionKey ) {
374+ Channel channel = http2Connections .get (partitionKey );
375+ if (channel == null ) {
376+ return null ;
377+ }
378+ if (!channel .isActive ()) {
379+ http2Connections .remove (partitionKey , channel );
380+ return null ;
381+ }
382+ Http2ConnectionState state = channel .attr (Http2ConnectionState .HTTP2_STATE_KEY ).get ();
383+ if (state != null && state .isDraining ()) {
384+ return null ;
385+ }
386+ return channel ;
387+ }
388+
389+ /**
390+ * Polls for an HTTP/2 connection by URI/virtualHost/proxy, using the same partition key logic
391+ * as the regular pool. Returns the connection without removing it from the registry.
392+ */
393+ public Channel pollHttp2 (Uri uri , String virtualHost , ProxyServer proxy , ChannelPoolPartitioning connectionPoolPartitioning ) {
394+ Object partitionKey = connectionPoolPartitioning .getPartitionKey (uri , virtualHost , proxy );
395+ return pollHttp2Connection (partitionKey );
396+ }
397+
341398 public Channel poll (Uri uri , String virtualHost , ProxyServer proxy , ChannelPoolPartitioning connectionPoolPartitioning ) {
342399 Object partitionKey = connectionPoolPartitioning .getPartitionKey (uri , virtualHost , proxy );
343400 return channelPool .poll (partitionKey );
@@ -348,6 +405,7 @@ public void removeAll(Channel connection) {
348405 }
349406
350407 private void doClose () {
408+ http2Connections .clear ();
351409 ChannelGroupFuture groupFuture = openChannels .close ();
352410 channelPool .destroy ();
353411 groupFuture .addListener (future -> sslEngineFactory .destroy ());
@@ -644,7 +702,7 @@ protected void initChannel(Channel ch) {
644702 // Install SETTINGS listener to update MAX_CONCURRENT_STREAMS from server
645703 pipeline .addLast ("http2-settings-listener" , new ChannelInboundHandlerAdapter () {
646704 @ Override
647- public void channelRead (io . netty . channel . ChannelHandlerContext ctx , Object msg ) throws Exception {
705+ public void channelRead (ChannelHandlerContext ctx , Object msg ) throws Exception {
648706 if (msg instanceof Http2SettingsFrame ) {
649707 Http2SettingsFrame settingsFrame = (Http2SettingsFrame ) msg ;
650708 Long maxStreams = settingsFrame .settings ().maxConcurrentStreams ();
@@ -659,10 +717,38 @@ public void channelRead(io.netty.channel.ChannelHandlerContext ctx, Object msg)
659717 }
660718 });
661719
720+ // Install GOAWAY handler on the parent channel to mark the connection as draining
721+ // and remove it from the HTTP/2 registry. GOAWAY is a connection-level frame that
722+ // arrives on the parent channel, not on stream child channels.
723+ pipeline .addLast ("http2-goaway-listener" , new ChannelInboundHandlerAdapter () {
724+ @ Override
725+ public void channelRead (ChannelHandlerContext ctx , Object msg ) throws Exception {
726+ if (msg instanceof Http2GoAwayFrame ) {
727+ Http2GoAwayFrame goAwayFrame = (Http2GoAwayFrame ) msg ;
728+ int lastStreamId = goAwayFrame .lastStreamId ();
729+ Http2ConnectionState connState = ctx .channel ().attr (Http2ConnectionState .HTTP2_STATE_KEY ).get ();
730+ if (connState != null ) {
731+ connState .setDraining (lastStreamId );
732+ Object pk = connState .getPartitionKey ();
733+ if (pk != null ) {
734+ removeHttp2Connection (pk , ctx .channel ());
735+ }
736+ }
737+ LOGGER .debug ("HTTP/2 GOAWAY received on {}, lastStreamId={}, errorCode={}" ,
738+ ctx .channel (), lastStreamId , goAwayFrame .errorCode ());
739+ // Close the connection when no more active streams
740+ if (connState != null && connState .getActiveStreams () <= 0 ) {
741+ closeChannel (ctx .channel ());
742+ }
743+ }
744+ ctx .fireChannelRead (msg );
745+ }
746+ });
747+
662748 // Install PING handler for keepalive if configured
663749 long pingIntervalMs = config .getHttp2PingInterval ().toMillis ();
664750 if (pingIntervalMs > 0 ) {
665- pipeline .addLast ("http2-idle-state" , new IdleStateHandler (0 , 0 , pingIntervalMs , java . util . concurrent . TimeUnit .MILLISECONDS ));
751+ pipeline .addLast ("http2-idle-state" , new IdleStateHandler (0 , 0 , pingIntervalMs , TimeUnit .MILLISECONDS ));
666752 pipeline .addLast ("http2-ping" , new Http2PingHandler ());
667753 }
668754 }
@@ -732,4 +818,4 @@ public boolean isOpen() {
732818 public boolean isHttp2CleartextEnabled () {
733819 return config .isHttp2Enabled () && config .isHttp2CleartextEnabled ();
734820 }
735- }
821+ }
0 commit comments