Skip to content

Commit 44efeaa

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 16a9df9 commit 44efeaa

13 files changed

+1836
-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,30 +44,64 @@ 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;
5059
private final Timeout socketTimeout;
5160
private final TimeValue validateAfterInactivity;
5261
private final TimeValue timeToLive;
5362

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

6187
ConnectionConfig(
6288
final Timeout connectTimeout,
6389
final Timeout socketTimeout,
6490
final TimeValue validateAfterInactivity,
65-
final TimeValue timeToLive) {
91+
final TimeValue timeToLive,
92+
final boolean staggeredConnectEnabled,
93+
final TimeValue happyEyeballsAttemptDelay,
94+
final TimeValue happyEyeballsOtherFamilyDelay,
95+
final ProtocolFamilyPreference protocolFamilyPreference) {
6696
super();
6797
this.connectTimeout = connectTimeout;
6898
this.socketTimeout = socketTimeout;
6999
this.validateAfterInactivity = validateAfterInactivity;
70100
this.timeToLive = timeToLive;
101+
this.staggeredConnectEnabled = staggeredConnectEnabled;
102+
this.happyEyeballsAttemptDelay = happyEyeballsAttemptDelay != null ? happyEyeballsAttemptDelay : DEFAULT_HE_ATTEMPT_DELAY;
103+
this.happyEyeballsOtherFamilyDelay = happyEyeballsOtherFamilyDelay != null ? happyEyeballsOtherFamilyDelay : DEFAULT_HE_OTHER_FAMILY_DELAY;
104+
this.protocolFamilyPreference = protocolFamilyPreference != null ? protocolFamilyPreference : ProtocolFamilyPreference.INTERLEAVE;
71105
}
72106

73107
/**
@@ -98,6 +132,46 @@ public TimeValue getTimeToLive() {
98132
return timeToLive;
99133
}
100134

135+
/**
136+
* Whether staggered (Happy Eyeballs–style) connection attempts are enabled.
137+
*
138+
* @see Builder#setStaggeredConnectEnabled(boolean)
139+
* @since 5.6
140+
*/
141+
public boolean isStaggeredConnectEnabled() {
142+
return staggeredConnectEnabled;
143+
}
144+
145+
/**
146+
* Delay between subsequent staggered connection attempts.
147+
*
148+
* @see Builder#setHappyEyeballsAttemptDelay(TimeValue)
149+
* @since 5.6
150+
*/
151+
public TimeValue getHappyEyeballsAttemptDelay() {
152+
return happyEyeballsAttemptDelay;
153+
}
154+
155+
/**
156+
* Initial delay before launching the first address of the other protocol family.
157+
*
158+
* @see Builder#setHappyEyeballsOtherFamilyDelay(TimeValue)
159+
* @since 5.6
160+
*/
161+
public TimeValue getHappyEyeballsOtherFamilyDelay() {
162+
return happyEyeballsOtherFamilyDelay;
163+
}
164+
165+
/**
166+
* Protocol family preference controlling address selection and ordering.
167+
*
168+
* @see Builder#setProtocolFamilyPreference(ProtocolFamilyPreference)
169+
* @since 5.6
170+
*/
171+
public ProtocolFamilyPreference getProtocolFamilyPreference() {
172+
return protocolFamilyPreference;
173+
}
174+
101175
@Override
102176
protected ConnectionConfig clone() throws CloneNotSupportedException {
103177
return (ConnectionConfig) super.clone();
@@ -111,6 +185,10 @@ public String toString() {
111185
builder.append(", socketTimeout=").append(socketTimeout);
112186
builder.append(", validateAfterInactivity=").append(validateAfterInactivity);
113187
builder.append(", timeToLive=").append(timeToLive);
188+
builder.append(", staggeredConnectEnabled=").append(staggeredConnectEnabled);
189+
builder.append(", happyEyeballsAttemptDelay=").append(happyEyeballsAttemptDelay);
190+
builder.append(", happyEyeballsOtherFamilyDelay=").append(happyEyeballsOtherFamilyDelay);
191+
builder.append(", protocolFamilyPreference=").append(protocolFamilyPreference);
114192
builder.append("]");
115193
return builder.toString();
116194
}
@@ -124,7 +202,11 @@ public static ConnectionConfig.Builder copy(final ConnectionConfig config) {
124202
.setConnectTimeout(config.getConnectTimeout())
125203
.setSocketTimeout(config.getSocketTimeout())
126204
.setValidateAfterInactivity(config.getValidateAfterInactivity())
127-
.setTimeToLive(config.getTimeToLive());
205+
.setTimeToLive(config.getTimeToLive())
206+
.setStaggeredConnectEnabled(config.isStaggeredConnectEnabled())
207+
.setHappyEyeballsAttemptDelay(config.getHappyEyeballsAttemptDelay())
208+
.setHappyEyeballsOtherFamilyDelay(config.getHappyEyeballsOtherFamilyDelay())
209+
.setProtocolFamilyPreference(config.getProtocolFamilyPreference());
128210
}
129211

130212
public static class Builder {
@@ -134,6 +216,12 @@ public static class Builder {
134216
private TimeValue validateAfterInactivity;
135217
private TimeValue timeToLive;
136218

219+
// New fields (defaults)
220+
private boolean staggeredConnectEnabled = false; // disabled by default
221+
private TimeValue happyEyeballsAttemptDelay = DEFAULT_HE_ATTEMPT_DELAY;
222+
private TimeValue happyEyeballsOtherFamilyDelay = DEFAULT_HE_OTHER_FAMILY_DELAY;
223+
private ProtocolFamilyPreference protocolFamilyPreference = ProtocolFamilyPreference.INTERLEAVE;
224+
137225
Builder() {
138226
super();
139227
this.connectTimeout = DEFAULT_CONNECT_TIMEOUT;
@@ -232,12 +320,62 @@ public Builder setTimeToLive(final long timeToLive, final TimeUnit timeUnit) {
232320
return this;
233321
}
234322

323+
/**
324+
* Enables or disables staggered (Happy Eyeballs–style) connection attempts.
325+
*
326+
* @since 5.6
327+
* @return this instance.
328+
*/
329+
public Builder setStaggeredConnectEnabled(final boolean enabled) {
330+
this.staggeredConnectEnabled = enabled;
331+
return this;
332+
}
333+
334+
/**
335+
* Sets the delay between staggered connection attempts.
336+
*
337+
* @since 5.6
338+
* @return this instance.
339+
*/
340+
public Builder setHappyEyeballsAttemptDelay(final TimeValue delay) {
341+
this.happyEyeballsAttemptDelay = delay;
342+
return this;
343+
}
344+
345+
/**
346+
* Sets the initial delay before launching the first address of the other
347+
* protocol family (IPv6 vs IPv4) when interleaving attempts.
348+
*
349+
* @since 5.6
350+
* @return this instance.
351+
*/
352+
public Builder setHappyEyeballsOtherFamilyDelay(final TimeValue delay) {
353+
this.happyEyeballsOtherFamilyDelay = delay;
354+
return this;
355+
}
356+
357+
/**
358+
* Sets the protocol family preference that guides address selection and ordering.
359+
*
360+
* @since 5.6
361+
* @return this instance.
362+
*/
363+
public Builder setProtocolFamilyPreference(final ProtocolFamilyPreference preference) {
364+
this.protocolFamilyPreference = preference;
365+
return this;
366+
}
367+
235368
public ConnectionConfig build() {
236369
return new ConnectionConfig(
237370
connectTimeout != null ? connectTimeout : DEFAULT_CONNECT_TIMEOUT,
238371
socketTimeout,
239372
validateAfterInactivity,
240-
timeToLive);
373+
timeToLive,
374+
staggeredConnectEnabled,
375+
happyEyeballsAttemptDelay != null ? happyEyeballsAttemptDelay : DEFAULT_HE_ATTEMPT_DELAY,
376+
happyEyeballsOtherFamilyDelay != null ? happyEyeballsOtherFamilyDelay : DEFAULT_HE_OTHER_FAMILY_DELAY,
377+
protocolFamilyPreference != null ? protocolFamilyPreference : ProtocolFamilyPreference.INTERLEAVE
378+
);
241379
}
242380

243381
}
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: 5 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

0 commit comments

Comments
 (0)