Skip to content

Commit f8b92f3

Browse files
committed
RFC6724 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 30bae84 commit f8b92f3

File tree

2 files changed

+187
-119
lines changed

2 files changed

+187
-119
lines changed

httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientHappyEyeballs.java

Lines changed: 182 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
*/
2727
package org.apache.hc.client5.http.examples;
2828

29+
import java.net.URI;
30+
import java.net.URISyntaxException;
31+
import java.util.ArrayList;
32+
import java.util.List;
2933
import java.util.concurrent.Future;
3034

3135
import org.apache.hc.client5.http.Rfc6724AddressSelectingDnsResolver;
@@ -37,6 +41,7 @@
3741
import org.apache.hc.client5.http.async.methods.SimpleResponseConsumer;
3842
import org.apache.hc.client5.http.config.ConnectionConfig;
3943
import org.apache.hc.client5.http.config.ProtocolFamilyPreference;
44+
import org.apache.hc.client5.http.config.RequestConfig;
4045
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
4146
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
4247
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
@@ -49,134 +54,114 @@
4954
import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
5055
import org.apache.hc.core5.io.CloseMode;
5156
import org.apache.hc.core5.util.TimeValue;
57+
import org.apache.hc.core5.util.Timeout;
5258

5359
/**
54-
* <h2>Example: RFC 6724 DNS ordering + Happy Eyeballs (with logs)</h2>
60+
* <h2>Example: RFC 6724 DNS ordering + Happy Eyeballs (with console output)</h2>
5561
*
56-
* <p>This example shows how to:
62+
* <p>This example shows how to:</p>
5763
* <ul>
5864
* <li>Wrap the system DNS resolver with {@link org.apache.hc.client5.http.Rfc6724AddressSelectingDnsResolver}
59-
* to apply <b>RFC 6724</b> destination address selection (v6/v4 ordering).</li>
65+
* to apply <b>RFC 6724</b> destination address selection (IPv6/IPv4 ordering).</li>
6066
* <li>Use {@link org.apache.hc.client5.http.config.ConnectionConfig} to enable <b>Happy Eyeballs v2</b> pacing
6167
* and set a <b>protocol family preference</b> (e.g., {@code IPV4_ONLY}, {@code IPV6_ONLY}, {@code PREFER_IPV6},
62-
* {@code INTERLEAVE}).</li>
63-
* <li>Turn on DEBUG logs that make the resolver’s ordering and the connection layer’s decisions observable.</li>
68+
* {@code PREFER_IPV4}, {@code INTERLEAVE}).</li>
69+
* <li>Control the connect timeout so demos don’t stall on slow/broken networks.</li>
6470
* </ul>
65-
* </p>
6671
*
67-
* <p><b>Logging categories</b> (enable at DEBUG):</p>
68-
* <ul>
69-
* <li>{@code org.apache.hc.client5.http.Rfc6724AddressSelectingDnsResolver} — resolver decisions
70-
* (delegate results, RFC 6724 attributes, output order, family bias).</li>
71-
* <li>{@code org.apache.hc.client5.http.impl.nio.MultihomeIOSessionRequester} — Happy Eyeballs scheduling and the winning attempt.</li>
72-
* <li>(optional) {@code org.apache.hc.client5.http.impl.nio}, {@code org.apache.hc.client5.http.impl.async},
73-
* {@code org.apache.hc.core5} — connection pool, protocol exec, TLS, and I/O transport.</li>
74-
* </ul>
75-
*
76-
* <p><b>Log4j2 quick config</b> (trimmed):</p>
77-
* <pre>{@code
78-
* <Configuration status="WARN">
79-
* <Appenders>
80-
* <Console name="Console" target="SYSTEM_OUT">
81-
* <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss,SSS} %-5level [%t][%c] %msg%n"/>
82-
* </Console>
83-
* </Appenders>
84-
* <Loggers>
85-
* <Logger name="org.apache.hc.client5.http.Rfc6724AddressSelectingDnsResolver" level="debug" additivity="false">
86-
* <AppenderRef ref="Console"/>
87-
* </Logger>
88-
* <Logger name="org.apache.hc.client5.http.impl.nio.MultihomeIOSessionRequester" level="debug" additivity="false">
89-
* <AppenderRef ref="Console"/>
90-
* </Logger>
91-
* <!-- Optional plumbing -->
92-
* <Logger name="org.apache.hc.client5.http.impl.nio" level="debug"/>
93-
* <Logger name="org.apache.hc.client5.http.impl.async" level="debug"/>
94-
* <Logger name="org.apache.hc.core5" level="debug"/>
95-
* <Root level="info"><AppenderRef ref="Console"/></Root>
96-
* </Loggers>
97-
* </Configuration>
98-
* }</pre>
72+
* <h3>How to run with the example runner</h3>
73+
* <pre>
74+
* # Default (no args): hits http://ipv6-test.com/ and https://ipv6-test.com/
75+
* ./run-example.sh AsyncClientHappyEyeballs
9976
*
100-
* <p><b>What to expect in the logs</b> (trimmed, dual-stack target):</p>
77+
* # Pass one URI (runner supports command-line args)
78+
* ./run-example.sh AsyncClientHappyEyeballs http://neverssl.com/
10179
*
102-
* <p><u>A) {@code IPV4_ONLY}</u> — v6 is present but is filtered out early</p>
103-
* <pre>{@code
104-
* ... Rfc6724Resolver resolving host 'ipv6-test.com' via delegate org.apache.hc.client5.http.SystemDefaultDnsResolver
105-
* ... Rfc6724Resolver familyPreference=IPV4_ONLY
106-
* ... delegate returned 2 addresses for 'ipv6-test.com': [IPv4(51.75.78.103), IPv6]
107-
* ... after family filter IPV4_ONLY -> 1 candidate(s): [IPv4(51.75.78.103)]
108-
* ... RFC6724 output order: [IPv4(51.75.78.103)]
109-
* ... final ordered list for 'ipv6-test.com': [IPv4(51.75.78.103)]
110-
* ... using Happy Eyeballs: attemptDelay=250ms, otherFamilyDelay=50ms, pref=IPV4_ONLY
111-
* ... scheduling connect to IPv4 in 0 ms
112-
* ... winner: connected to ipv6-test.com/51.75.78.103:80
113-
* }</pre>
80+
* # Pass multiple URIs
81+
* ./run-example.sh AsyncClientHappyEyeballs http://neverssl.com/ https://example.org/
11482
*
115-
* <p><u>B) {@code PREFER_IPV6}</u> — v6 attempted first, v4 shortly after</p>
116-
* <pre>{@code
117-
* ... familyPreference=PREFER_IPV6
118-
* ... delegate returned 2 addresses: [IPv6(2001:db8::1234), IPv4(203.0.113.10)]
119-
* ... RFC6724 inferred source addresses: [IPv6(2001:db8::1), IPv4(192.0.2.10)]
120-
* ... RFC6724 output order: [IPv6(...), IPv4(...)]
121-
* ... PREFER_IPV6 keeps IPv6 first
122-
* ... using Happy Eyeballs ... pref=PREFER_IPV6
123-
* ... scheduling connect to IPv6 in 0 ms, IPv4 in 50 ms
124-
* ... winner: connected to IPv6(...)
125-
* }</pre>
83+
* # Optional system properties (the runner forwards -D...):
84+
* # -Dhc.he.pref=INTERLEAVE|PREFER_IPV4|PREFER_IPV6|IPV4_ONLY|IPV6_ONLY (default: INTERLEAVE)
85+
* # -Dhc.he.delay.ms=250 (Happy Eyeballs attempt spacing; default 250)
86+
* # -Dhc.he.other.ms=50 (first other-family offset; default 50; clamped ≤ attempt delay)
87+
* # -Dhc.connect.ms=10000 (TCP connect timeout; default 10000)
12688
*
127-
* <p><u>C) {@code INTERLEAVE}</u> — alternate families while honoring RFC-sorted groups</p>
128-
* <pre>{@code
129-
* ... familyPreference=INTERLEAVE
130-
* ... RFC6724 output order: [IPv4(...), IPv6(...), IPv6(...), IPv4(...)]
131-
* ... INTERLEAVE starting family=IPv4 -> [IPv4(...), IPv6(...), IPv4(...), IPv6(...)]
132-
* ... final ordered list: [IPv4, IPv6, IPv4, IPv6]
133-
* ... HEv2 pref=INTERLEAVE; attempts interleaved with 50 ms "other family" offset
134-
* }</pre>
89+
* ./run-example.sh AsyncClientHappyEyeballs http://neverssl.com/ \
90+
* -Dhc.he.pref=INTERLEAVE -Dhc.he.delay.ms=250 -Dhc.he.other.ms=50 -Dhc.connect.ms=8000
91+
* </pre>
13592
*
136-
* <p><b>Legend (how to read the resolver lines)</b>:</p>
93+
* <h3>What to expect</h3>
13794
* <ul>
138-
* <li><code>delegate returned N addresses</code> — raw A/AAAA from the underlying resolver.</li>
139-
* <li><code>after family filter ...</code> — hard filter for {@code IPV4_ONLY}/{@code IPV6_ONLY}; no filter for {@code PREFER_*}/{@code INTERLEAVE}.</li>
140-
* <li><code>RFC6724 inferred source addresses</code> — UDP DatagramSocket.connect() (no packets) to discover the OS-selected source per destination.</li>
141-
* <li><code>candidate dst=... src=... dst[scope=..., prec=..., label=...]</code> — attributes used by RFC 6724 compare:
142-
* Rules applied here: 1 (avoid unusable), 2 (scope match), 5 (label match), 6 (precedence), 8 (smaller scope),
143-
* 9 (longest common prefix for v6). Equal → stable order.</li>
144-
* <li><code>output order</code> — RFC 6724 ordering prior to preference reshaping.</li>
145-
* <li><code>INTERLEAVE / PREFER_*</code> — final family reshaping before handing to the connection layer.</li>
146-
* <li>Happy-Eyeballs lines show pacing (<code>attemptDelay</code>) and first other-family offset
147-
* (<code>otherFamilyDelay</code>), plus the eventual <em>winner</em>.</li>
95+
* <li>For dual-stack hosts, the client schedules interleaved IPv6/IPv4 connects per the preference and delays.</li>
96+
* <li>On networks without working IPv6, the IPv6 attempt will likely fail quickly while IPv4 succeeds.</li>
97+
* <li>If you force {@code IPV6_ONLY} on a network without IPv6 routing, you’ll get
98+
* {@code java.net.SocketException: Network is unreachable} — that’s expected.</li>
14899
* </ul>
149100
*
150-
* <p><b>Implementation tip</b>:
151-
* for the clearest logs, keep the resolver’s bias and the client’s connection preference aligned
152-
* (e.g., construct {@code new Rfc6724AddressSelectingDnsResolver(SystemDefaultDnsResolver.INSTANCE, pref)}
153-
* and use the same {@code pref} in {@link org.apache.hc.client5.http.config.ConnectionConfig#setProtocolFamilyPreference}).</p>
154-
*
155-
* <p><b>Production note</b>: leave these categories at DEBUG and your root at INFO; enable on demand when diagnosing
156-
* dual-stack reachability (e.g., broken IPv6 will typically show an inferred source of {@code 0.0.0.0} for the v6 dst,
157-
* causing RFC 6724 Rule 1 to de-prioritize it).</p>
101+
* <h3>Tip</h3>
102+
* <p>For the clearest behavior, align the resolver bias and the connection preference:
103+
* construct the resolver with the same {@link ProtocolFamilyPreference} that you set in
104+
* {@link ConnectionConfig}.</p>
158105
*/
159-
160106
public final class AsyncClientHappyEyeballs {
161107

108+
private AsyncClientHappyEyeballs() {
109+
}
110+
162111
public static void main(final String[] args) throws Exception {
163-
// Wrap the system resolver with RFC 6724 destination selection
112+
// --- Read settings from system properties (with sensible defaults) ---
113+
final ProtocolFamilyPreference pref = parsePref(System.getProperty("hc.he.pref"), ProtocolFamilyPreference.INTERLEAVE);
114+
final long attemptDelayMs = parseLong(System.getProperty("hc.he.delay.ms"), 250L);
115+
final long otherFamilyDelayMs = Math.min(parseLong(System.getProperty("hc.he.other.ms"), 50L), attemptDelayMs);
116+
final long connectMs = parseLong(System.getProperty("hc.connect.ms"), 10000L); // 10s default
117+
118+
// --- Resolve targets from CLI args (or fall back to ipv6-test.com pair) ---
119+
final List<URI> targets = new ArrayList<URI>();
120+
if (args != null && args.length > 0) {
121+
for (int i = 0; i < args.length; i++) {
122+
final URI u = safeParse(args[i]);
123+
if (u != null) {
124+
targets.add(u);
125+
} else {
126+
System.out.println("Skipping invalid URI: " + args[i]);
127+
}
128+
}
129+
} else {
130+
try {
131+
targets.add(new URI("http://ipv6-test.com/"));
132+
targets.add(new URI("https://ipv6-test.com/"));
133+
} catch (final URISyntaxException ignore) {
134+
}
135+
}
136+
137+
// --- Print banner so the runner shows the configuration up front ---
138+
System.out.println("Happy Eyeballs: pref=" + pref
139+
+ ", attemptDelay=" + attemptDelayMs + "ms"
140+
+ ", otherFamilyDelay=" + otherFamilyDelayMs + "ms"
141+
+ ", connectTimeout=" + connectMs + "ms");
142+
143+
// --- DNS resolver with RFC 6724 selection (biased using the same pref for clarity) ---
164144
final Rfc6724AddressSelectingDnsResolver dnsResolver =
165-
new Rfc6724AddressSelectingDnsResolver(SystemDefaultDnsResolver.INSTANCE);
145+
new Rfc6724AddressSelectingDnsResolver(SystemDefaultDnsResolver.INSTANCE, pref);
166146

167-
// Enable Happy Eyeballs pacing & family policy via ConnectionConfig
147+
// --- Connection config enabling HEv2 pacing and family preference ---
168148
final ConnectionConfig connectionConfig = ConnectionConfig.custom()
169-
.setStaggeredConnectEnabled(true) // off by default
170-
.setHappyEyeballsAttemptDelay(TimeValue.ofMilliseconds(250)) // pacing between attempts
171-
.setHappyEyeballsOtherFamilyDelay(TimeValue.ofMilliseconds(50)) // delay before first other-family try
172-
.setProtocolFamilyPreference(ProtocolFamilyPreference.IPV6_ONLY) // or PREFER_IPV6 / IPV4_ONLY / etc.
149+
.setStaggeredConnectEnabled(true)
150+
.setHappyEyeballsAttemptDelay(TimeValue.ofMilliseconds(attemptDelayMs))
151+
.setHappyEyeballsOtherFamilyDelay(TimeValue.ofMilliseconds(otherFamilyDelayMs))
152+
.setProtocolFamilyPreference(pref).setConnectTimeout(Timeout.ofMilliseconds(connectMs))
153+
173154
.build();
174155

175-
// TLS strategy (the builder's build() may be deprecated in your snapshot; it's fine for examples)
156+
final RequestConfig requestConfig = RequestConfig.custom()
157+
.build();
158+
159+
// --- TLS strategy (uses system properties for trust/key stores, ALPN, etc.) ---
176160
final TlsStrategy tls = ClientTlsStrategyBuilder.create()
177-
.useSystemProperties().buildAsync();
161+
.useSystemProperties()
162+
.buildAsync();
178163

179-
// Connection manager wires in DNS + ConnectionConfig + TLS
164+
// --- Connection manager wires in DNS + ConnectionConfig + TLS ---
180165
final PoolingAsyncClientConnectionManager cm =
181166
PoolingAsyncClientConnectionManagerBuilder.create()
182167
.setDnsResolver(dnsResolver)
@@ -186,15 +171,24 @@ public static void main(final String[] args) throws Exception {
186171

187172
final CloseableHttpAsyncClient client = HttpAsyncClients.custom()
188173
.setConnectionManager(cm)
174+
.setDefaultRequestConfig(requestConfig)
189175
.build();
190176

191177
client.start();
192178

193-
final String[] schemes = {URIScheme.HTTP.id, URIScheme.HTTPS.id};
194-
for (final String scheme : schemes) {
179+
// --- Execute each target once ---
180+
for (int i = 0; i < targets.size(); i++) {
181+
final URI uri = targets.get(i);
182+
final HttpHost host = new HttpHost(
183+
uri.getScheme(),
184+
uri.getHost(),
185+
computePort(uri)
186+
);
187+
final String path = buildPathAndQuery(uri);
188+
195189
final SimpleHttpRequest request = SimpleRequestBuilder.get()
196-
.setHttpHost(new HttpHost(scheme, "ipv6-test.com"))
197-
.setPath("/")
190+
.setHttpHost(host)
191+
.setPath(path)
198192
.build();
199193

200194
System.out.println("Executing request " + request);
@@ -221,13 +215,95 @@ public void cancelled() {
221215

222216
try {
223217
future.get();
224-
} catch (java.util.concurrent.ExecutionException e) {
225-
System.out.println(request + " -> " + e.getCause());
218+
} catch (final java.util.concurrent.ExecutionException ex) {
219+
// Show the root cause without a giant stack trace in the example
220+
System.out.println(request + " -> " + ex.getCause());
226221
}
227222
}
228223

229224
System.out.println("Shutting down");
230225
client.close(CloseMode.GRACEFUL);
231226
cm.close(CloseMode.GRACEFUL);
232227
}
228+
229+
// ------------ helpers (Java 8 friendly) ------------
230+
231+
private static int computePort(final URI uri) {
232+
final int p = uri.getPort();
233+
if (p >= 0) {
234+
return p;
235+
}
236+
final String scheme = uri.getScheme();
237+
if ("http".equalsIgnoreCase(scheme)) {
238+
return 80;
239+
}
240+
if ("https".equalsIgnoreCase(scheme)) {
241+
return 443;
242+
}
243+
return -1;
244+
}
245+
246+
private static String buildPathAndQuery(final URI uri) {
247+
String path = uri.getRawPath();
248+
if (path == null || path.isEmpty()) {
249+
path = "/";
250+
}
251+
final String query = uri.getRawQuery();
252+
if (query != null && !query.isEmpty()) {
253+
return path + "?" + query;
254+
}
255+
return path;
256+
}
257+
258+
private static long parseLong(final String s, final long defVal) {
259+
if (s == null) {
260+
return defVal;
261+
}
262+
try {
263+
return Long.parseLong(s.trim());
264+
} catch (final NumberFormatException ignore) {
265+
return defVal;
266+
}
267+
}
268+
269+
private static ProtocolFamilyPreference parsePref(final String s, final ProtocolFamilyPreference defVal) {
270+
if (s == null) {
271+
return defVal;
272+
}
273+
final String u = s.trim().toUpperCase(java.util.Locale.ROOT);
274+
if ("IPV6_ONLY".equals(u)) {
275+
return ProtocolFamilyPreference.IPV6_ONLY;
276+
}
277+
if ("IPV4_ONLY".equals(u)) {
278+
return ProtocolFamilyPreference.IPV4_ONLY;
279+
}
280+
if ("PREFER_IPV6".equals(u)) {
281+
return ProtocolFamilyPreference.PREFER_IPV6;
282+
}
283+
if ("PREFER_IPV4".equals(u)) {
284+
return ProtocolFamilyPreference.PREFER_IPV4;
285+
}
286+
if ("INTERLEAVE".equals(u)) {
287+
return ProtocolFamilyPreference.INTERLEAVE;
288+
}
289+
return defVal;
290+
}
291+
292+
private static URI safeParse(final String s) {
293+
try {
294+
final URI u = new URI(s);
295+
final String scheme = u.getScheme();
296+
if (!URIScheme.HTTP.same(scheme) && !URIScheme.HTTPS.same(scheme)) {
297+
System.out.println("Unsupported scheme (only http/https): " + s);
298+
return null;
299+
}
300+
if (u.getHost() == null) {
301+
System.out.println("Missing host in URI: " + s);
302+
return null;
303+
}
304+
return u;
305+
} catch (final URISyntaxException ex) {
306+
return null;
307+
}
308+
}
233309
}

httpclient5/src/test/resources/log4j2.xml

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,15 @@
1515
See the License for the specific language governing permissions and
1616
limitations under the License.
1717
-->
18-
<Configuration status="WARN">
18+
<Configuration status="WARN" name="XMLConfigTest">
1919
<Appenders>
20-
<Console name="Console" target="SYSTEM_OUT">
21-
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss,SSS} %-5level [%t][%c] %msg%n"/>
20+
<Console name="STDOUT">
21+
<PatternLayout pattern="%d %-5level [%t][%logger]%notEmpty{[%markerSimpleName]} %msg%n%xThrowable" />
2222
</Console>
2323
</Appenders>
2424
<Loggers>
25-
<!-- Happy Eyeballs scheduler + decisions -->
26-
<Logger name="org.apache.hc.client5.http.impl.nio.MultihomeIOSessionRequester" level="debug" additivity="false">
27-
<AppenderRef ref="Console"/>
28-
</Logger>
29-
<!-- Optional: connection manager + operator -->
30-
<Logger name="org.apache.hc.client5.http.impl.nio" level="debug"/>
31-
<Logger name="org.apache.hc.client5.http.impl.async" level="debug"/>
32-
<Logger name="org.apache.hc.core5" level="debug"/>
33-
<Root level="info">
34-
<AppenderRef ref="Console"/>
25+
<Root level="FATAL">
26+
<AppenderRef ref="STDOUT" />
3527
</Root>
3628
</Loggers>
3729
</Configuration>

0 commit comments

Comments
 (0)