Skip to content

Commit 33d9263

Browse files
fitekoneaarturobernalg
authored andcommitted
Destination address ordering + Happy Eyeballs v2 (concurrent multihome dialing).
Add Rfc6724AddressSelectingDnsResolver; refactor MultihomeIOSessionRequester; wire via ConnectionConfig. New AsyncClientHappyEyeballs example (URI args, System.out trace) and JUnit tests; Java 8 compatible.
1 parent 068db5d commit 33d9263

13 files changed

+1872
-1270
lines changed

httpclient5/src/main/java/org/apache/hc/client5/http/Rfc6724AddressSelectingDnsResolver.java

Lines changed: 602 additions & 0 deletions
Large diffs are not rendered by default.

httpclient5/src/main/java/org/apache/hc/client5/http/config/ConnectionConfig.java

Lines changed: 142 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ public class ConnectionConfig implements Cloneable {
4444

4545
private static final Timeout DEFAULT_CONNECT_TIMEOUT = Timeout.ofMinutes(3);
4646

47+
/**
48+
* @since 5.6
49+
*/
50+
private static final TimeValue DEFAULT_HE_ATTEMPT_DELAY = TimeValue.ofMilliseconds(250);
51+
/**
52+
* @since 5.6
53+
*/
54+
private static final TimeValue DEFAULT_HE_OTHER_FAMILY_DELAY = TimeValue.ofMilliseconds(50);
55+
4756
public static final ConnectionConfig DEFAULT = new Builder().build();
4857

4958
private final Timeout connectTimeout;
@@ -52,25 +61,50 @@ public class ConnectionConfig implements Cloneable {
5261
private final TimeValue validateAfterInactivity;
5362
private final TimeValue timeToLive;
5463

64+
/**
65+
* @since 5.6
66+
*/
67+
private final boolean staggeredConnectEnabled;
68+
/**
69+
* @since 5.6
70+
*/
71+
private final TimeValue happyEyeballsAttemptDelay;
72+
/**
73+
* @since 5.6
74+
*/
75+
private final TimeValue happyEyeballsOtherFamilyDelay;
76+
/**
77+
* @since 5.6
78+
*/
79+
private final ProtocolFamilyPreference protocolFamilyPreference;
80+
5581
/**
5682
* Intended for CDI compatibility
5783
*/
5884
protected ConnectionConfig() {
59-
this(DEFAULT_CONNECT_TIMEOUT, null, null, null, null);
85+
this(DEFAULT_CONNECT_TIMEOUT, null, null, null, null, false, DEFAULT_HE_ATTEMPT_DELAY, DEFAULT_HE_OTHER_FAMILY_DELAY, ProtocolFamilyPreference.INTERLEAVE);
6086
}
6187

6288
ConnectionConfig(
6389
final Timeout connectTimeout,
6490
final Timeout socketTimeout,
6591
final Timeout idleTimeout,
6692
final TimeValue validateAfterInactivity,
67-
final TimeValue timeToLive) {
93+
final TimeValue timeToLive,
94+
final boolean staggeredConnectEnabled,
95+
final TimeValue happyEyeballsAttemptDelay,
96+
final TimeValue happyEyeballsOtherFamilyDelay,
97+
final ProtocolFamilyPreference protocolFamilyPreference) {
6898
super();
6999
this.connectTimeout = connectTimeout;
70100
this.socketTimeout = socketTimeout;
71101
this.idleTimeout = idleTimeout;
72102
this.validateAfterInactivity = validateAfterInactivity;
73103
this.timeToLive = timeToLive;
104+
this.staggeredConnectEnabled = staggeredConnectEnabled;
105+
this.happyEyeballsAttemptDelay = happyEyeballsAttemptDelay != null ? happyEyeballsAttemptDelay : DEFAULT_HE_ATTEMPT_DELAY;
106+
this.happyEyeballsOtherFamilyDelay = happyEyeballsOtherFamilyDelay != null ? happyEyeballsOtherFamilyDelay : DEFAULT_HE_OTHER_FAMILY_DELAY;
107+
this.protocolFamilyPreference = protocolFamilyPreference != null ? protocolFamilyPreference : ProtocolFamilyPreference.INTERLEAVE;
74108
}
75109

76110
/**
@@ -108,6 +142,46 @@ public TimeValue getTimeToLive() {
108142
return timeToLive;
109143
}
110144

145+
/**
146+
* Whether staggered (Happy Eyeballs–style) connection attempts are enabled.
147+
*
148+
* @see Builder#setStaggeredConnectEnabled(boolean)
149+
* @since 5.6
150+
*/
151+
public boolean isStaggeredConnectEnabled() {
152+
return staggeredConnectEnabled;
153+
}
154+
155+
/**
156+
* Delay between subsequent staggered connection attempts.
157+
*
158+
* @see Builder#setHappyEyeballsAttemptDelay(TimeValue)
159+
* @since 5.6
160+
*/
161+
public TimeValue getHappyEyeballsAttemptDelay() {
162+
return happyEyeballsAttemptDelay;
163+
}
164+
165+
/**
166+
* Initial delay before launching the first address of the other protocol family.
167+
*
168+
* @see Builder#setHappyEyeballsOtherFamilyDelay(TimeValue)
169+
* @since 5.6
170+
*/
171+
public TimeValue getHappyEyeballsOtherFamilyDelay() {
172+
return happyEyeballsOtherFamilyDelay;
173+
}
174+
175+
/**
176+
* Protocol family preference controlling address selection and ordering.
177+
*
178+
* @see Builder#setProtocolFamilyPreference(ProtocolFamilyPreference)
179+
* @since 5.6
180+
*/
181+
public ProtocolFamilyPreference getProtocolFamilyPreference() {
182+
return protocolFamilyPreference;
183+
}
184+
111185
@Override
112186
protected ConnectionConfig clone() throws CloneNotSupportedException {
113187
return (ConnectionConfig) super.clone();
@@ -122,6 +196,10 @@ public String toString() {
122196
builder.append(", idleTimeout=").append(idleTimeout);
123197
builder.append(", validateAfterInactivity=").append(validateAfterInactivity);
124198
builder.append(", timeToLive=").append(timeToLive);
199+
builder.append(", staggeredConnectEnabled=").append(staggeredConnectEnabled);
200+
builder.append(", happyEyeballsAttemptDelay=").append(happyEyeballsAttemptDelay);
201+
builder.append(", happyEyeballsOtherFamilyDelay=").append(happyEyeballsOtherFamilyDelay);
202+
builder.append(", protocolFamilyPreference=").append(protocolFamilyPreference);
125203
builder.append("]");
126204
return builder.toString();
127205
}
@@ -135,7 +213,11 @@ public static ConnectionConfig.Builder copy(final ConnectionConfig config) {
135213
.setConnectTimeout(config.getConnectTimeout())
136214
.setSocketTimeout(config.getSocketTimeout())
137215
.setValidateAfterInactivity(config.getValidateAfterInactivity())
138-
.setTimeToLive(config.getTimeToLive());
216+
.setTimeToLive(config.getTimeToLive())
217+
.setStaggeredConnectEnabled(config.isStaggeredConnectEnabled())
218+
.setHappyEyeballsAttemptDelay(config.getHappyEyeballsAttemptDelay())
219+
.setHappyEyeballsOtherFamilyDelay(config.getHappyEyeballsOtherFamilyDelay())
220+
.setProtocolFamilyPreference(config.getProtocolFamilyPreference());
139221
}
140222

141223
public static class Builder {
@@ -146,6 +228,12 @@ public static class Builder {
146228
private TimeValue validateAfterInactivity;
147229
private TimeValue timeToLive;
148230

231+
// New fields (defaults)
232+
private boolean staggeredConnectEnabled = false; // disabled by default
233+
private TimeValue happyEyeballsAttemptDelay = DEFAULT_HE_ATTEMPT_DELAY;
234+
private TimeValue happyEyeballsOtherFamilyDelay = DEFAULT_HE_OTHER_FAMILY_DELAY;
235+
private ProtocolFamilyPreference protocolFamilyPreference = ProtocolFamilyPreference.INTERLEAVE;
236+
149237
Builder() {
150238
super();
151239
this.connectTimeout = DEFAULT_CONNECT_TIMEOUT;
@@ -281,13 +369,63 @@ public Builder setTimeToLive(final long timeToLive, final TimeUnit timeUnit) {
281369
return this;
282370
}
283371

372+
/**
373+
* Enables or disables staggered (Happy Eyeballs–style) connection attempts.
374+
*
375+
* @since 5.6
376+
* @return this instance.
377+
*/
378+
public Builder setStaggeredConnectEnabled(final boolean enabled) {
379+
this.staggeredConnectEnabled = enabled;
380+
return this;
381+
}
382+
383+
/**
384+
* Sets the delay between staggered connection attempts.
385+
*
386+
* @since 5.6
387+
* @return this instance.
388+
*/
389+
public Builder setHappyEyeballsAttemptDelay(final TimeValue delay) {
390+
this.happyEyeballsAttemptDelay = delay;
391+
return this;
392+
}
393+
394+
/**
395+
* Sets the initial delay before launching the first address of the other
396+
* protocol family (IPv6 vs IPv4) when interleaving attempts.
397+
*
398+
* @since 5.6
399+
* @return this instance.
400+
*/
401+
public Builder setHappyEyeballsOtherFamilyDelay(final TimeValue delay) {
402+
this.happyEyeballsOtherFamilyDelay = delay;
403+
return this;
404+
}
405+
406+
/**
407+
* Sets the protocol family preference that guides address selection and ordering.
408+
*
409+
* @since 5.6
410+
* @return this instance.
411+
*/
412+
public Builder setProtocolFamilyPreference(final ProtocolFamilyPreference preference) {
413+
this.protocolFamilyPreference = preference;
414+
return this;
415+
}
416+
284417
public ConnectionConfig build() {
285418
return new ConnectionConfig(
286419
connectTimeout != null ? connectTimeout : DEFAULT_CONNECT_TIMEOUT,
287420
socketTimeout,
288421
idleTimeout,
289422
validateAfterInactivity,
290-
timeToLive);
423+
timeToLive,
424+
staggeredConnectEnabled,
425+
happyEyeballsAttemptDelay != null ? happyEyeballsAttemptDelay : DEFAULT_HE_ATTEMPT_DELAY,
426+
happyEyeballsOtherFamilyDelay != null ? happyEyeballsOtherFamilyDelay : DEFAULT_HE_OTHER_FAMILY_DELAY,
427+
protocolFamilyPreference != null ? protocolFamilyPreference : ProtocolFamilyPreference.INTERLEAVE
428+
);
291429
}
292430

293431
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
package org.apache.hc.client5.http.config;
28+
29+
/**
30+
* Protocol family preference for outbound connections.
31+
*
32+
* <p>Used by connection initiation code to filter or order destination
33+
* addresses and, when enabled, to interleave families during staggered attempts.
34+
*
35+
* @since 5.6
36+
*/
37+
public enum ProtocolFamilyPreference {
38+
/** Keep families as returned (or RFC 6724 ordered). */
39+
DEFAULT,
40+
/**
41+
* Prefer IPv4 addresses but allow IPv6 as a fallback.
42+
*/
43+
PREFER_IPV4,
44+
45+
/**
46+
* Prefer IPv6 addresses but allow IPv4 as a fallback.
47+
*/
48+
PREFER_IPV6,
49+
50+
/**
51+
* Use only IPv4 addresses.
52+
*/
53+
IPV4_ONLY,
54+
55+
/**
56+
* Use only IPv6 addresses.
57+
*/
58+
IPV6_ONLY,
59+
60+
/**
61+
* Interleave address families (v6, then v4, then v6, …) when multiple
62+
* addresses are available. When staggered connects are enabled, the first
63+
* address of the other family is delayed by a small offset.
64+
*/
65+
INTERLEAVE
66+
}
67+

httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/DefaultAsyncClientConnectionOperator.java

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.apache.hc.client5.http.DnsResolver;
3838
import org.apache.hc.client5.http.SchemePortResolver;
3939
import org.apache.hc.client5.http.UnsupportedSchemeException;
40+
import org.apache.hc.client5.http.config.ConnectionConfig;
4041
import org.apache.hc.client5.http.config.TlsConfig;
4142
import org.apache.hc.client5.http.impl.ConnPoolSupport;
4243
import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
@@ -71,21 +72,14 @@ public class DefaultAsyncClientConnectionOperator implements AsyncClientConnecti
7172
private final MultihomeIOSessionRequester sessionRequester;
7273
private final Lookup<TlsStrategy> tlsStrategyLookup;
7374

74-
/**
75-
* Constructs a new {@code DefaultAsyncClientConnectionOperator}.
76-
*
77-
* <p><strong>Note:</strong> this class is marked {@code @Internal}; rely on it
78-
* only if you are prepared for incompatible changes in a future major
79-
* release. Typical client code should use the high-level builders in
80-
* {@code HttpAsyncClients} instead.</p>
81-
*/
82-
protected DefaultAsyncClientConnectionOperator(
75+
DefaultAsyncClientConnectionOperator(
8376
final Lookup<TlsStrategy> tlsStrategyLookup,
8477
final SchemePortResolver schemePortResolver,
85-
final DnsResolver dnsResolver) {
78+
final DnsResolver dnsResolver,
79+
final ConnectionConfig defaultConnectionConfig) {
8680
this.tlsStrategyLookup = Args.notNull(tlsStrategyLookup, "TLS strategy lookup");
8781
this.schemePortResolver = schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE;
88-
this.sessionRequester = new MultihomeIOSessionRequester(dnsResolver);
82+
this.sessionRequester = new MultihomeIOSessionRequester(dnsResolver, defaultConnectionConfig);
8983
}
9084

9185
@Override
@@ -279,4 +273,8 @@ protected void onBeforeTlsHandshake(final HttpContext httpContext, final HttpHos
279273
protected void onAfterTlsHandshake(final HttpContext httpContext, final HttpHost endpointHost) {
280274
}
281275

276+
public void shutdown() {
277+
sessionRequester.shutdown();
278+
}
279+
282280
}

0 commit comments

Comments
 (0)