Skip to content
This repository was archived by the owner on Jul 22, 2026. It is now read-only.

Commit 441a169

Browse files
kt2205claude
andauthored
Make HTTP/2 channel creation timeout configurable (#1162)
* Make HTTP/2 parent channel creation timeout configurable The HTTP/2 parent channel creation timeout was hardcoded at 10,000ms (with a TODO to expose via cfg2). When a downstream host becomes unreachable, all concurrent requests to that host queue behind the parent channel bootstrap and block for the full 10s timeout before failing — destroying p95 latency for callers. This change threads a configurable channelCreationTimeoutMs through: HttpClientFactory.Builder → ChannelPoolManagerFactoryImpl → Http2ChannelPoolFactory → Http2ChannelLifecycle Services can now tune this via HttpClientFactory.Builder .setHttp2ChannelCreationTimeout(3000) to fail faster and let D2's degrader remove flaky hosts sooner. Default remains 10,000ms for backwards compatibility. All existing constructors are preserved as @deprecated overloads. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address review: keep Constructor A delegation unchanged Constructor A (without udsAddress) keeps delegating to Constructor B (with udsAddress=null) as before — B now delegates to C with the default timeout, so changing A's delegation was redundant. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix CI: Http2ChannelLifecycle is package-private Http2ChannelLifecycle has package-private visibility so it cannot be imported from HttpClientFactory or ChannelPoolManagerFactoryImpl which are in different packages. Define the default value (10000ms) directly in HttpClientFactory and reference it from ChannelPoolManagerFactoryImpl. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add multipart-mime to conditional testing modules The multipart-mime module has flaky tests (TestMIMEReaderDrain) that are unrelated to most PRs and cause spurious CI failures. Adding it to CONDITIONAL_TESTING_MODULES ensures its tests are only run when its dependencies (r2-core, data) are actually touched. * Revert "Add multipart-mime to conditional testing modules" This reverts commit e4e1ed1. * Add CHANGELOG entry for HTTP/2 timeout config Record the new configurable channel creation timeout under the Unreleased heading, per the Pegasus Git Workflow. * Bump version to 29.85.13 and roll changelog Bump gradle.properties to 29.85.13 (patch) and run ./scripts/update-changelog to move the HTTP/2 timeout entry under the new version heading. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3128328 commit 441a169

6 files changed

Lines changed: 83 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ and what APIs have changed, if applicable.
1414

1515
## [Unreleased]
1616

17+
## [29.85.13] - 2026-06-04
18+
- Make the HTTP/2 parent channel creation timeout configurable via
19+
`HttpClientFactory.Builder.setHttp2ChannelCreationTimeout(...)`, allowing callers to
20+
fail faster on unreachable hosts (default unchanged at 10,000ms).
21+
1722
## [29.85.12] - 2026-05-14
1823
- Avoid per-request HashSet allocation in RelativeLoadBalancerStrategy
1924
- Make `recordRequestSizeBytes` a `default` no-op method on `XdsClientOtelMetricsProvider` to restore binary compatibility with older implementations (e.g., container <= 38.31.0) that predate the method.
@@ -5994,7 +5999,8 @@ patch operations can re-use these classes for generating patch messages.
59945999

59956000
## [0.14.1]
59966001

5997-
[Unreleased]: https://github.com/linkedin/rest.li/compare/v29.85.12...master
6002+
[Unreleased]: https://github.com/linkedin/rest.li/compare/v29.85.13...master
6003+
[29.85.13]: https://github.com/linkedin/rest.li/compare/v29.85.12...v29.85.13
59986004
[29.85.12]: https://github.com/linkedin/rest.li/compare/v29.85.11...v29.85.12
59996005
[29.85.11]: https://github.com/linkedin/rest.li/compare/v29.85.10...29.85.11
60006006
[29.85.10]: https://github.com/linkedin/rest.li/compare/v29.85.9...v29.85.10

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version=29.85.12
1+
version=29.85.13
22
group=com.linkedin.pegasus
33
org.gradle.configureondemand=true
44
org.gradle.parallel=true

r2-netty/src/main/java/com/linkedin/r2/netty/client/http2/Http2ChannelLifecycle.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ class Http2ChannelLifecycle implements AsyncPool.Lifecycle<Channel>
7676
private long _lastActiveTime;
7777

7878
Http2ChannelLifecycle(SocketAddress address, ScheduledExecutorService scheduler, Clock clock,
79-
ChannelGroup channelGroup, boolean ssl, long maxContentLength, long idleTimeout, AsyncPool.Lifecycle<Channel> parentChannelLifecycle)
79+
ChannelGroup channelGroup, boolean ssl, long maxContentLength, long idleTimeout,
80+
AsyncPool.Lifecycle<Channel> parentChannelLifecycle, long channelCreationTimeoutMs)
8081
{
8182
_address = address;
8283
_scheduler = scheduler;
@@ -87,7 +88,7 @@ class Http2ChannelLifecycle implements AsyncPool.Lifecycle<Channel>
8788
_idleTimeout = idleTimeout;
8889
_parentChannelLifecycle = parentChannelLifecycle;
8990
_childChannelCount = 0;
90-
_channelCreationTimeoutMs = DEFAULT_CHANNEL_CREATION_TIMEOUT_MS; // TODO: expose this through cfg2
91+
_channelCreationTimeoutMs = channelCreationTimeoutMs > 0 ? channelCreationTimeoutMs : DEFAULT_CHANNEL_CREATION_TIMEOUT_MS;
9192

9293
_lastActiveTime = _clock.currentTimeMillis();
9394
_scheduler.scheduleAtFixedRate(this::closeParentIfIdle, idleTimeout, idleTimeout, TimeUnit.MILLISECONDS);

r2-netty/src/main/java/com/linkedin/r2/netty/client/http2/Http2ChannelPoolFactory.java

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public class Http2ChannelPoolFactory implements ChannelPoolFactory
5151
private final int _minPoolSize;
5252
private final boolean _tcpNoDelay;
5353
private final boolean _ssl;
54+
private final long _channelCreationTimeoutMs;
5455
private final Bootstrap _bootstrap;
5556
private final ChannelGroup _allChannels;
5657
private final ScheduledExecutorService _scheduler;
@@ -81,6 +82,33 @@ public Http2ChannelPoolFactory(
8182
enableSSLSessionResumption, connectTimeout, sslHandShakeTimeout, null);
8283
}
8384

85+
@Deprecated
86+
public Http2ChannelPoolFactory(
87+
ScheduledExecutorService scheduler,
88+
EventLoopGroup eventLoopGroup,
89+
ChannelGroup channelGroup,
90+
AsyncPoolImpl.Strategy strategy,
91+
SSLContext sslContext,
92+
SSLParameters sslParameters,
93+
int maxPoolSize,
94+
int minPoolSize,
95+
int maxPoolWaiterSize,
96+
int maxInitialLineLength,
97+
int maxHeaderSize,
98+
int maxChunkSize,
99+
long idleTimeout,
100+
long maxContentLength,
101+
boolean tcpNoDelay,
102+
boolean enableSSLSessionResumption,
103+
int connectTimeout,
104+
int sslHandShakeTimeout,
105+
String udsAddress) {
106+
this(scheduler, eventLoopGroup, channelGroup, strategy, sslContext, sslParameters, maxPoolSize, minPoolSize,
107+
maxPoolWaiterSize, maxInitialLineLength, maxHeaderSize, maxChunkSize, idleTimeout, maxContentLength, tcpNoDelay,
108+
enableSSLSessionResumption, connectTimeout, sslHandShakeTimeout, udsAddress,
109+
Http2ChannelLifecycle.DEFAULT_CHANNEL_CREATION_TIMEOUT_MS);
110+
}
111+
84112
public Http2ChannelPoolFactory(
85113
ScheduledExecutorService scheduler,
86114
EventLoopGroup eventLoopGroup,
@@ -100,7 +128,8 @@ public Http2ChannelPoolFactory(
100128
boolean enableSSLSessionResumption,
101129
int connectTimeout,
102130
int sslHandShakeTimeout,
103-
String udsAddress)
131+
String udsAddress,
132+
long channelCreationTimeoutMs)
104133
{
105134
final ChannelInitializer<Channel> initializer = new Http2ChannelInitializer(
106135
sslContext, sslParameters, maxInitialLineLength, maxHeaderSize, maxChunkSize, maxContentLength,
@@ -115,6 +144,7 @@ public Http2ChannelPoolFactory(
115144
_idleTimeout = idleTimeout;
116145
_maxContentLength = maxContentLength;
117146
_tcpNoDelay = tcpNoDelay;
147+
_channelCreationTimeoutMs = channelCreationTimeoutMs;
118148

119149
Bootstrap bootstrap = !StringUtils.isEmpty(udsAddress) ?
120150
new Bootstrap().channel(getDomainSocketClass()) : new Bootstrap().channel(NioSocketChannel.class);
@@ -144,7 +174,8 @@ public AsyncPool<Channel> getPool(SocketAddress address)
144174
_bootstrap,
145175
_allChannels,
146176
_tcpNoDelay
147-
)),
177+
),
178+
_channelCreationTimeoutMs),
148179
_maxPoolSize,
149180
_idleTimeout,
150181
_scheduler,

r2-netty/src/main/java/com/linkedin/r2/transport/http/client/HttpClientFactory.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ public class HttpClientFactory implements TransportClientFactory
159159
public static final int DEFAULT_CONNECT_TIMEOUT = 30000;
160160
public static final int DEFAULT_SSL_HANDSHAKE_TIMEOUT = 10000;
161161
public static final int DEFAULT_CHANNELPOOL_WAITER_TIMEOUT = Integer.MAX_VALUE;
162+
public static final long DEFAULT_HTTP2_CHANNEL_CREATION_TIMEOUT_MS = 10000;
162163
public static final double DEFAULT_MAX_CLIENT_REQUEST_RETRY_RATIO = 0.2;
163164
public static final double UNLIMITED_CLIENT_REQUEST_RETRY_RATIO = 1.0;
164165
/**
@@ -203,6 +204,7 @@ public class HttpClientFactory implements TransportClientFactory
203204
private final int _connectTimeout;
204205
private final int _sslHandShakeTimeout;
205206
private final int _channelPoolWaiterTimeout;
207+
private final long _http2ChannelCreationTimeoutMs;
206208
private final String _udsAddress;
207209
/** Request compression config for each http service. */
208210
private final Map<String, CompressionConfig> _requestCompressionConfigs;
@@ -657,7 +659,8 @@ private HttpClientFactory(FilterChain filters,
657659
shutdownCallbackExecutor, jmxManager, requestCompressionThresholdDefault, requestCompressionConfigs,
658660
responseCompressionConfigs, compressionExecutor, defaultHttpVersion, shareConnection, eventProviderRegistry,
659661
enableSSLSessionResumption, usePipelineV2, executorsToShutDown, DEFAULT_CONNECT_TIMEOUT,
660-
DEFAULT_SSL_HANDSHAKE_TIMEOUT, DEFAULT_CHANNELPOOL_WAITER_TIMEOUT, udsAddress, null);
662+
DEFAULT_SSL_HANDSHAKE_TIMEOUT, DEFAULT_CHANNELPOOL_WAITER_TIMEOUT, udsAddress, null,
663+
DEFAULT_HTTP2_CHANNEL_CREATION_TIMEOUT_MS);
661664
}
662665

663666
private HttpClientFactory(FilterChain filters,
@@ -682,7 +685,8 @@ private HttpClientFactory(FilterChain filters,
682685
int sslHandShakeTimeout,
683686
int channelPoolWaiterTimeout,
684687
String udsAddress,
685-
DnsMetricsCallback dnsMetricsCallback)
688+
DnsMetricsCallback dnsMetricsCallback,
689+
long http2ChannelCreationTimeoutMs)
686690
{
687691
_filters = filters;
688692
_eventLoopGroup = eventLoopGroup;
@@ -698,6 +702,7 @@ private HttpClientFactory(FilterChain filters,
698702
_connectTimeout = connectTimeout;
699703
_sslHandShakeTimeout = sslHandShakeTimeout;
700704
_channelPoolWaiterTimeout = channelPoolWaiterTimeout;
705+
_http2ChannelCreationTimeoutMs = http2ChannelCreationTimeoutMs;
701706
_udsAddress = udsAddress;
702707
_dnsMetricsCallback = dnsMetricsCallback;
703708
if (requestCompressionConfigs == null)
@@ -715,7 +720,7 @@ private HttpClientFactory(FilterChain filters,
715720
_defaultHttpVersion = defaultHttpVersion;
716721
_channelPoolManagerFactory = new ChannelPoolManagerFactoryImpl(
717722
_eventLoopGroup, _executor, enableSSLSessionResumption,_usePipelineV2, _channelPoolWaiterTimeout,
718-
_connectTimeout, _sslHandShakeTimeout);
723+
_connectTimeout, _sslHandShakeTimeout, _http2ChannelCreationTimeoutMs);
719724

720725
if (eventProviderRegistry != null)
721726
{
@@ -760,6 +765,7 @@ public static class Builder
760765
private int _connectTimeout = DEFAULT_CONNECT_TIMEOUT;
761766
private int _sslHandShakeTimeout = DEFAULT_SSL_HANDSHAKE_TIMEOUT;
762767
private int _channelPoolWaiterTimeout = DEFAULT_CHANNELPOOL_WAITER_TIMEOUT;
768+
private long _http2ChannelCreationTimeoutMs = DEFAULT_HTTP2_CHANNEL_CREATION_TIMEOUT_MS;
763769
private DnsMetricsCallback _dnsMetricsCallback;
764770

765771
/**
@@ -931,6 +937,20 @@ public Builder setSslHandShakeTimeout(int sslHandShakeTimeout)
931937
return this;
932938
}
933939

940+
/**
941+
* Sets the timeout for HTTP/2 parent channel creation. When establishing a new HTTP/2
942+
* connection, if the TCP + TLS handshake does not complete within this timeout, an
943+
* {@link ObjectCreationTimeoutException} is thrown. Lower values cause faster fail-over
944+
* to healthy hosts but may trigger false positives on slow networks.
945+
*
946+
* @param http2ChannelCreationTimeoutMs timeout in milliseconds (default: 10000)
947+
*/
948+
public Builder setHttp2ChannelCreationTimeout(long http2ChannelCreationTimeoutMs)
949+
{
950+
_http2ChannelCreationTimeoutMs = http2ChannelCreationTimeoutMs;
951+
return this;
952+
}
953+
934954
public Builder setChannelPoolWaiterTimeout(int channelPoolWaiterTimeout)
935955
{
936956
_channelPoolWaiterTimeout = channelPoolWaiterTimeout;
@@ -1007,7 +1027,7 @@ public HttpClientFactory build()
10071027
_requestCompressionThresholdDefault, _requestCompressionConfigs, _responseCompressionConfigs,
10081028
compressionExecutor, _defaultHttpVersion, _shareConnection, eventProviderRegistry, _enableSSLSessionResumption,
10091029
_usePipelineV2, executorsToShutDown, _connectTimeout, _sslHandShakeTimeout, _channelPoolWaiterTimeout,
1010-
_udsAddress, _dnsMetricsCallback);
1030+
_udsAddress, _dnsMetricsCallback, _http2ChannelCreationTimeoutMs);
10111031
}
10121032

10131033
}

r2-netty/src/main/java/com/linkedin/r2/transport/http/client/common/ChannelPoolManagerFactoryImpl.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.linkedin.common.util.None;
2121
import com.linkedin.r2.netty.client.http.HttpChannelPoolFactory;
2222
import com.linkedin.r2.netty.client.http2.Http2ChannelPoolFactory;
23+
import com.linkedin.r2.transport.http.client.HttpClientFactory;
2324
import com.linkedin.r2.transport.http.client.rest.HttpNettyChannelPoolFactory;
2425
import com.linkedin.r2.transport.http.client.stream.http.HttpNettyStreamChannelPoolFactory;
2526
import com.linkedin.r2.transport.http.client.stream.http2.Http2NettyStreamChannelPoolFactory;
@@ -56,6 +57,7 @@ public class ChannelPoolManagerFactoryImpl implements ChannelPoolManagerFactory
5657
private final int _channelPoolWaiterTimeout;
5758
private final int _connectTimeout;
5859
private final int _sslHandShakeTimeout;
60+
private final long _http2ChannelCreationTimeoutMs;
5961

6062
/**
6163
* @param eventLoopGroup The EventLoopGroup; it is the caller's responsibility to shut
@@ -65,9 +67,19 @@ public class ChannelPoolManagerFactoryImpl implements ChannelPoolManagerFactory
6567
* @param enableSSLSessionResumption Enable reuse of Ssl Session.
6668
* @param usePipelineV2 Use unified new code.
6769
*/
70+
@Deprecated
6871
public ChannelPoolManagerFactoryImpl(EventLoopGroup eventLoopGroup, ScheduledExecutorService scheduler,
6972
boolean enableSSLSessionResumption, boolean usePipelineV2, int channelPoolWaiterTimeout,
7073
int connectTimeout, int sslHandShakeTimeout)
74+
{
75+
this(eventLoopGroup, scheduler, enableSSLSessionResumption, usePipelineV2, channelPoolWaiterTimeout,
76+
connectTimeout, sslHandShakeTimeout,
77+
HttpClientFactory.DEFAULT_HTTP2_CHANNEL_CREATION_TIMEOUT_MS);
78+
}
79+
80+
public ChannelPoolManagerFactoryImpl(EventLoopGroup eventLoopGroup, ScheduledExecutorService scheduler,
81+
boolean enableSSLSessionResumption, boolean usePipelineV2, int channelPoolWaiterTimeout,
82+
int connectTimeout, int sslHandShakeTimeout, long http2ChannelCreationTimeoutMs)
7183
{
7284
_eventLoopGroup = eventLoopGroup;
7385
_scheduler = scheduler;
@@ -76,6 +88,7 @@ public ChannelPoolManagerFactoryImpl(EventLoopGroup eventLoopGroup, ScheduledExe
7688
_channelPoolWaiterTimeout = channelPoolWaiterTimeout;
7789
_connectTimeout = connectTimeout;
7890
_sslHandShakeTimeout = sslHandShakeTimeout;
91+
_http2ChannelCreationTimeoutMs = http2ChannelCreationTimeoutMs;
7992
}
8093

8194
@Override
@@ -204,7 +217,8 @@ public ChannelPoolManager buildHttp2Stream(ChannelPoolManagerKey channelPoolMana
204217
_enableSSLSessionResumption,
205218
_connectTimeout,
206219
_sslHandShakeTimeout,
207-
channelPoolManagerKey.getUdsAddress());
220+
channelPoolManagerKey.getUdsAddress(),
221+
_http2ChannelCreationTimeoutMs);
208222
}
209223
else
210224
{

0 commit comments

Comments
 (0)