You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@hc.apache.org by "arturobernalg (via GitHub)" <gi...@apache.org> on 2023/03/24 21:57:38 UTC

[GitHub] [httpcomponents-client] arturobernalg opened a new pull request, #428: Implement HappyEyeballsV2AsyncClientConnectionOperator

arturobernalg opened a new pull request, #428:
URL: https://github.com/apache/httpcomponents-client/pull/428

   This pull request adds a new implementation of the Happy Eyeballs algorithm for asynchronous client connections in Apache's HttpClient component. The new implementation, HappyEyeballsV2AsyncClientConnectionOperator, builds on the previous version by improving support for IPv6 and providing more fine-grained control over connection timing.
   
   The changes include:
   - Addition of HappyEyeballsV2AsyncClientConnectionOperator class and related test classes
   - Modifications to existing code to use the new operator where appropriate
   - Documentation updates
   
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org
For additional commands, e-mail: dev-help@hc.apache.org


[GitHub] [httpcomponents-client] arturobernalg commented on pull request #428: Implement HappyEyeballsV2AsyncClientConnectionOperator

Posted by "arturobernalg (via GitHub)" <gi...@apache.org>.
arturobernalg commented on PR #428:
URL: https://github.com/apache/httpcomponents-client/pull/428#issuecomment-1506094203

   Hi @rschmitt I've made a commit addressing the feedback you provided on the pull request. I believe I've covered all the points. There isn't a specific JUnit test for the rules; there's only an example test class. Currently, I'm looking into how to approach the testing since the class is internal and not exposed at the moment. 
   Please feel free to provide any further comments or suggestions. 


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org
For additional commands, e-mail: dev-help@hc.apache.org


[GitHub] [httpcomponents-client] arturobernalg commented on a diff in pull request #428: Implement HappyEyeballsV2AsyncClientConnectionOperator

Posted by "arturobernalg (via GitHub)" <gi...@apache.org>.
arturobernalg commented on code in PR #428:
URL: https://github.com/apache/httpcomponents-client/pull/428#discussion_r1157542710


##########
httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperator.java:
##########
@@ -0,0 +1,680 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.impl.nio;
+
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.SchemePortResolver;
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.impl.ConnPoolSupport;
+import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionOperator;
+import org.apache.hc.client5.http.nio.ManagedAsyncClientConnection;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.config.Lookup;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.reactor.ConnectionInitiator;
+import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.ConnectException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+
+/**
+ * The {@link AsyncClientConnectionOperator} implementation that uses Happy Eyeballs V2 algorithm to connect
+ * to the target server. Happy Eyeballs V2 (HEV2) algorithm is used to connect to the target server by concurrently
+ * attempting to establish multiple connections to different IP addresses. The first connection to complete
+ * successfully is selected and the others are closed. If all connections fail, the last error is rethrown.
+ * The algorithm also applies a configurable delay before subsequent connection attempts. HEV2 was introduced
+ * as a means to mitigate the latency issues caused by IPv4 and IPv6 co-existence in the Internet. HEV2 is defined
+ * in RFC 8305.
+ *
+ * <p>
+ * This connection operator maintains a connection pool for each unique route (combination of target host and
+ * target port) and selects the next connection from the pool to establish a new connection or reuse an
+ * existing connection. The connection pool uses a First-In-First-Out (FIFO) queue and has a configurable limit
+ * on the maximum number of connections that can be kept alive in the pool. Once the maximum number of connections
+ * has been reached, the oldest connection in the pool is closed to make room for a new one.
+ * </p>
+ *
+ * <p>
+ * This class is thread-safe and can be used in a multi-threaded environment.
+ * </p>
+ *
+ * <p>
+ * The HEV2 algorithm is configurable through the following parameters:
+ * <ul>
+ *   <li>{@code dualStackEnabled}: Whether to enable dual-stack connectivity. When set to {@code true},
+ *   the operator attempts to connect to both IPv4 and IPv6 addresses concurrently. When set to {@code false},
+ *   only IPv4 or IPv6 addresses are attempted depending on the address type of the target server.</li>
+ *   <li>{@code maxAttempts}: The maximum number of connection attempts to be made before failing. If all
+ *   attempts fail, the last error is rethrown.</li>
+ *   <li>{@code delay}: The delay (in milliseconds) to apply before subsequent connection attempts.</li>
+ *   <li>{@code connectTimeout}: The connection timeout (in milliseconds) for each attempt.</li>
+ * </ul>
+ * </p>
+ *
+ *
+ * <p>
+ * This class can be used with any {@link org.apache.hc.core5.http.nio.AsyncClientEndpoint} implementation
+ * that supports HTTP/1.1 or HTTP/2 protocols.
+ * </p>
+ *
+ * @since 5.3
+ */
+public class HappyEyeballsV2AsyncClientConnectionOperator implements AsyncClientConnectionOperator {
+
+    private static final Logger LOG = LoggerFactory.getLogger(AsyncClientConnectionOperator.class);
+
+    /**
+     * The underlying {@link AsyncClientConnectionOperator} that is used to establish connections
+     * to the target server.
+     */
+    private final AsyncClientConnectionOperator connectionOperator;
+
+    /**
+     * The DNS resolver used to resolve hostnames to IP addresses.
+     */
+    private final DnsResolver dnsResolver;
+
+    /**
+     * A lookup table used to determine the {@link TlsStrategy} to use for a given connection route.
+     */
+    private final Lookup<TlsStrategy> tlsStrategyLookup;
+
+    /**
+     * The default timeout for connection establishment attempts. If a connection cannot be established
+     * within this timeout, the attempt is considered failed.
+     */
+    private final Timeout timeout;
+
+    /**
+     * The minimum delay between connection establishment attempts.
+     */
+    private final Timeout minimumConnectionAttemptDelay;
+
+    /**
+     * The maximum delay between connection establishment attempts.
+     */
+    private final Timeout maximumConnectionAttemptDelay;
+
+    /**
+     * The current delay between connection establishment attempts.
+     */
+    private final Timeout connectionAttemptDelay;
+
+    /**
+     * The delay before resolution is started.
+     */
+    private final Timeout resolution_delay;
+
+    /**
+     * The number of IP addresses of each address family to include in the initial list of
+     * IP addresses to attempt connections to. This value is set to 2 by default, but can be
+     * increased to more aggressively favor a particular address family (e.g. set to 4 for IPv6).
+     */
+    private final int firstAddressFamilyCount;
+
+    /**
+     * The address family to use for establishing connections. This can be set to either
+     * {@link AddressFamily#IPv4} or {@link AddressFamily#IPv6}.
+     */
+    private final AddressFamily addressFamily;
+
+
+    /**
+     * The AddressFamily enum represents the possible address families that can be used when attempting to establish
+     * <p>
+     * connections using the Happy Eyeballs V2 algorithm.
+     *
+     * <p>
+     * The Happy Eyeballs V2 algorithm allows for concurrent connection attempts to be made to different IP addresses,
+     * <p>
+     * so this enum specifies whether connections should be attempted using IPv4 or IPv6 addresses.
+     *
+     * </p>
+     */
+    public enum AddressFamily {
+        IPv4, IPv6
+    }
+
+    /**
+     * Constructs a new {@link HappyEyeballsV2AsyncClientConnectionOperator} with the specified parameters.
+     *
+     * @param tlsStrategyLookup             the lookup object used to retrieve a {@link TlsStrategy} for a given {@link Route}
+     * @param connectionOperator            the underlying {@link AsyncClientConnectionOperator} to use for establishing connections
+     * @param dnsResolver                   the {@link DnsResolver} to use for resolving target hostnames
+     * @param timeout                       the timeout duration for establishing a connection
+     * @param resolution_delay              the configurable delay before subsequent DNS resolution attempts
+     * @param minimumConnectionAttemptDelay the minimum configurable delay between connection attempts
+     * @param maximumConnectionAttemptDelay the maximum configurable delay between connection attempts
+     * @param connectionAttemptDelay        the configurable delay before attempting to establish a connection
+     * @param firstAddressFamilyCount       the number of initial address families to use for establishing a connection
+     * @param addressFamily                 the preferred address family to use for establishing a connection
+     * @throws IllegalArgumentException if {@code firstAddressFamilyCount} is not positive
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(final Lookup<TlsStrategy> tlsStrategyLookup,
+                                                        final AsyncClientConnectionOperator connectionOperator,
+                                                        final DnsResolver dnsResolver,
+                                                        final Timeout timeout,
+                                                        final Timeout resolution_delay,
+                                                        final Timeout minimumConnectionAttemptDelay,
+                                                        final Timeout maximumConnectionAttemptDelay,
+                                                        final Timeout connectionAttemptDelay,
+                                                        final int firstAddressFamilyCount,
+                                                        final AddressFamily addressFamily) {
+        this.tlsStrategyLookup = Args.notNull(tlsStrategyLookup, "TLS strategy lookup");
+        this.connectionOperator = Args.notNull(connectionOperator, "Connection operator");
+        this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE;
+        this.timeout = timeout != null ? timeout : Timeout.ofMilliseconds(250);
+        this.resolution_delay = resolution_delay != null ? resolution_delay : Timeout.ofMilliseconds(50);
+        this.minimumConnectionAttemptDelay = minimumConnectionAttemptDelay != null ? minimumConnectionAttemptDelay : Timeout.ofMilliseconds(100);
+        this.maximumConnectionAttemptDelay = maximumConnectionAttemptDelay != null ? maximumConnectionAttemptDelay : Timeout.ofSeconds(2);
+        this.connectionAttemptDelay = connectionAttemptDelay != null ? connectionAttemptDelay : Timeout.ofMilliseconds(250);
+        this.firstAddressFamilyCount = Args.positive(firstAddressFamilyCount, "firstAddressFamilyCount");
+        this.addressFamily = addressFamily;
+    }
+
+    /**
+     * Constructs a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the specified
+     * {@link Lookup} for {@link TlsStrategy} and {@link SchemePortResolver} and {@link DnsResolver}.
+     * <p>
+     * The constructor internally creates a new instance of {@link DefaultAsyncClientConnectionOperator} with the
+     * specified {@link Lookup} for {@link TlsStrategy}, {@link SchemePortResolver} and {@link DnsResolver}. The
+     * created {@link AsyncClientConnectionOperator} is then passed to the main constructor along with default values
+     * for other parameters.
+     * </p>
+     *
+     * @param tlsStrategyLookup  The {@link Lookup} for {@link TlsStrategy}.
+     * @param schemePortResolver The {@link SchemePortResolver} to use for resolving scheme ports.
+     * @param dnsResolver        The {@link DnsResolver} to use for resolving hostnames to IP addresses.
+     * @throws IllegalArgumentException if the {@code tlsStrategyLookup} or {@code schemePortResolver} or {@code dnsResolver} parameter is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver,
+            final DnsResolver dnsResolver) {
+        this(tlsStrategyLookup,
+                new DefaultAsyncClientConnectionOperator(tlsStrategyLookup, schemePortResolver, dnsResolver),
+                dnsResolver,
+                null,
+                null,
+                null,
+                null,
+                null,
+                1,
+                AddressFamily.IPv6);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup
+     * and scheme-port resolver. The DNS resolver will be set to the system default resolver.
+     *
+     * @param tlsStrategyLookup  The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @param schemePortResolver The resolver instance for mapping scheme names to default port numbers.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver) {
+        this(tlsStrategyLookup, schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup.
+     * The scheme-port resolver and DNS resolver will be set to their default instances.
+     *
+     * @param tlsStrategyLookup The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup) {
+        this(tlsStrategyLookup, DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+
+    /**
+     * Attempts to connect to the given host and returns a Future that will be completed when the connection is established
+     * or when an error occurs. This method may attempt to connect to multiple IP addresses associated with the host,
+     * depending on the address family and the number of connection attempts to execute. The address family and number of
+     * connection attempts can be configured by calling the corresponding setters on this class.
+     *
+     * @param connectionInitiator the connection initiator to use when creating the connection
+     * @param host                the host to connect to
+     * @param localAddress        the local address to bind to when connecting, or null to use any available local address
+     * @param connectTimeout      the timeout to use when connecting, or null to use the default timeout
+     * @param attachment          the attachment to associate with the connection, or null if no attachment is needed
+     * @param callback            the callback to invoke when the connection is established or an error occurs, or null if no callback is needed
+     * @return a Future that will be completed when the connection is established or when an error occurs
+     */
+    @Override
+    public Future<ManagedAsyncClientConnection> connect(
+            final ConnectionInitiator connectionInitiator,
+            final HttpHost host,
+            final SocketAddress localAddress,
+            final Timeout connectTimeout,
+            final Object attachment,
+            final FutureCallback<ManagedAsyncClientConnection> callback) {
+
+        final CompletableFuture<ManagedAsyncClientConnection> connectionFuture = new CompletableFuture<>();
+
+        final Timeout conTimeout = connectTimeout != null ? connectTimeout : timeout;
+
+        resolveDnsAsync(host.getHostName())
+                .thenCompose(inetAddresses -> {
+                    final List<InetAddress> ipv4Addresses = new ArrayList<>();
+                    final List<InetAddress> ipv6Addresses = new ArrayList<>();
+
+                    for (final InetAddress inetAddress : inetAddresses) {
+                        if (inetAddress instanceof Inet4Address) {
+                            ipv4Addresses.add(inetAddress);
+                        } else if (inetAddress instanceof Inet6Address) {
+                            ipv6Addresses.add(inetAddress);
+                        }
+                    }
+
+                    sortAndInterleave(inetAddresses);
+
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> connectionFutures = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> attempts = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    if (addressFamily == AddressFamily.IPv4 && !ipv4Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                        }
+                    } else if (addressFamily == AddressFamily.IPv6 && !ipv6Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                        }
+                    } else {
+                        if (!ipv4Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                            }
+                        }
+                        if (!ipv6Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                            }
+                        }
+                    }
+
+                    // Execute the connection attempts concurrently using CompletableFuture.anyOf
+                    return CompletableFuture.anyOf(attempts.toArray(new CompletableFuture[0]))
+                            .thenCompose(result -> {
+                                if (result instanceof ManagedAsyncClientConnection) {
+                                    // If there is a result, cancel all other attempts and complete the connectionFuture
+                                    connectionFutures.forEach(future -> future.cancel(true));
+                                    connectionFuture.complete((ManagedAsyncClientConnection) result);
+                                } else {
+                                    // If there is an exception, complete the connectionFuture exceptionally with the exception
+                                    connectionFuture.completeExceptionally(new ConnectException("Failed to connect to any address for " + host));
+                                }
+                                // Invoke the callback if provided
+                                if (callback != null) {
+                                    connectionFuture.whenComplete((conn, ex) -> {
+                                        if (ex != null) {
+                                            callback.failed(new Exception(ex));
+                                        } else {
+                                            callback.completed(conn);
+                                        }
+                                    });
+                                }
+                                return connectionFuture;
+                            });
+                })
+                .exceptionally(e -> {
+                    connectionFuture.completeExceptionally(e);
+                    if (callback != null) {
+                        callback.failed(new Exception(e));
+                    }
+                    return null;
+                });
+
+        return connectionFuture;
+    }
+
+    /**
+     * Asynchronously resolves the DNS for the given host name and returns a CompletableFuture that will be completed
+     * with an array of InetAddress objects representing the IP addresses of the host.
+     * The resolution of AAAA records is delayed by the configured resolution delay to allow for a chance for A records to be
+     * returned first.
+     *
+     * @param host the host name to resolve DNS for
+     * @return a CompletableFuture that will be completed with an array of InetAddress objects representing the IP addresses
+     */
+    private CompletableFuture<InetAddress[]> resolveDnsAsync(final String host) {
+        final CompletableFuture<InetAddress[]> dnsFuture = new CompletableFuture<>();
+        CompletableFuture.runAsync(() -> {
+            try {
+                final InetAddress[] inetAddresses = dnsResolver.resolve(host);
+                // Introduce a delay before resolving AAAA records after receiving A records
+                resolution_delay.sleep();
+                dnsFuture.complete(inetAddresses);
+            } catch (final UnknownHostException | InterruptedException e) {
+                if (LOG.isDebugEnabled()) {
+                    LOG.debug("Failed to resolve DNS for host '{}': {}", host, e.getMessage(), e);
+                }
+                dnsFuture.completeExceptionally(e);
+            }
+        });
+        return dnsFuture;
+    }
+
+
+    /**
+     * Initiates an asynchronous connection attempt to the given list of IP addresses for the specified {@link HttpHost}.
+     *
+     * @param connectionInitiator the {@link ConnectionInitiator} to use for establishing the connection
+     * @param host                the {@link HttpHost} to connect to
+     * @param connectTimeout      the timeout for the connection attempt
+     * @param attachment          the attachment object to pass to the connection operator
+     * @param addresses           the list of IP addresses to attempt to connect to
+     * @param localAddress        the local socket address to bind the connection to, or {@code null} if not binding
+     * @return a {@link CompletableFuture} that completes with a {@link ManagedAsyncClientConnection} if the connection attempt succeeds,
+     * or exceptionally with an exception if all attempts fail
+     */
+    private CompletableFuture<ManagedAsyncClientConnection> connectAttempt(
+            final ConnectionInitiator connectionInitiator,
+            final HttpHost host,
+            final Timeout connectTimeout,
+            final Object attachment,
+            final List<InetAddress> addresses,
+            final SocketAddress localAddress) {
+
+        final CompletableFuture<ManagedAsyncClientConnection> connectionFuture = new CompletableFuture<>();
+
+        // Create a list of connection attempts to execute
+        final List<CompletableFuture<Void>> attempts = new ArrayList<>();
+        for (int i = 0; i < addresses.size(); i++) {
+            final InetAddress address = addresses.get(i);
+
+            if (LOG.isDebugEnabled()) {
+                LOG.info("Attempting to connect to {}", address);
+            }
+
+            final CompletableFuture<Void> attempt = new CompletableFuture<>();
+            attempts.add(attempt);
+            final HttpHost currentHost = new HttpHost(host.getSchemeName(), address, host.getHostName(), host.getPort());
+
+            connectionOperator.connect(
+                    connectionInitiator,
+                    currentHost,
+                    localAddress,
+                    connectTimeout,
+                    attachment,
+                    new FutureCallback<ManagedAsyncClientConnection>() {
+                        @Override
+                        public void completed(final ManagedAsyncClientConnection connection) {
+                            if (LOG.isDebugEnabled()) {
+                                LOG.debug("Successfully connected {}", ConnPoolSupport.getId(connection));
+                            }
+                            connectionFuture.complete(connection);
+                        }
+
+                        @Override
+                        public void failed(final Exception ex) {
+                            if (LOG.isDebugEnabled()) {
+                                LOG.debug("Failed to connect  {}", ConnPoolSupport.getId(address), ex);
+                            }
+                            attempt.completeExceptionally(ex);
+                        }
+
+                        @Override
+                        public void cancelled() {
+                            if (LOG.isDebugEnabled()) {
+                                LOG.debug("Cancelled connect for {}", ConnPoolSupport.getId(address));
+                            }
+                            attempt.cancel(true);
+                        }
+                    });
+
+            // Introduce a delay before executing the next connection attempt
+            if (i < addresses.size() - 1) {
+                try {
+                    final Duration delay = calculateDelay(i);
+                    delay.wait();

Review Comment:
   The RFC recommends introducing a delay before starting the next connection attempt, but it doesn't mandate a specific implementation. The RFC suggests a default delay of 250 milliseconds, but also notes that a more nuanced implementation can use historical RTT data to influence the delay. Therefore, it is up to the implementation to decide whether to introduce a delay and how to calculate that delay.
   Maybe would be better to use a non-blocking delay mechanism, such as Thread.sleep() or a ScheduledExecutorService. wdyt?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org
For additional commands, e-mail: dev-help@hc.apache.org


[GitHub] [httpcomponents-client] ok2c commented on pull request #428: Implement HappyEyeballsV2AsyncClientConnectionOperator

Posted by "ok2c (via GitHub)" <gi...@apache.org>.
ok2c commented on PR #428:
URL: https://github.com/apache/httpcomponents-client/pull/428#issuecomment-1675961947

   @rschmitt @rhernandez35 Folks,, is there any chance you could do another review sometime soon? @arturobernalg Please in the meantime change the target branch to `5.4.x`. 


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org
For additional commands, e-mail: dev-help@hc.apache.org


[GitHub] [httpcomponents-client] arturobernalg commented on a diff in pull request #428: Implement HappyEyeballsV2AsyncClientConnectionOperator

Posted by "arturobernalg (via GitHub)" <gi...@apache.org>.
arturobernalg commented on code in PR #428:
URL: https://github.com/apache/httpcomponents-client/pull/428#discussion_r1157540582


##########
httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperator.java:
##########
@@ -0,0 +1,680 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.impl.nio;
+
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.SchemePortResolver;
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.impl.ConnPoolSupport;
+import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionOperator;
+import org.apache.hc.client5.http.nio.ManagedAsyncClientConnection;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.config.Lookup;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.reactor.ConnectionInitiator;
+import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.ConnectException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+
+/**
+ * The {@link AsyncClientConnectionOperator} implementation that uses Happy Eyeballs V2 algorithm to connect
+ * to the target server. Happy Eyeballs V2 (HEV2) algorithm is used to connect to the target server by concurrently
+ * attempting to establish multiple connections to different IP addresses. The first connection to complete
+ * successfully is selected and the others are closed. If all connections fail, the last error is rethrown.
+ * The algorithm also applies a configurable delay before subsequent connection attempts. HEV2 was introduced
+ * as a means to mitigate the latency issues caused by IPv4 and IPv6 co-existence in the Internet. HEV2 is defined
+ * in RFC 8305.
+ *
+ * <p>
+ * This connection operator maintains a connection pool for each unique route (combination of target host and
+ * target port) and selects the next connection from the pool to establish a new connection or reuse an
+ * existing connection. The connection pool uses a First-In-First-Out (FIFO) queue and has a configurable limit
+ * on the maximum number of connections that can be kept alive in the pool. Once the maximum number of connections
+ * has been reached, the oldest connection in the pool is closed to make room for a new one.
+ * </p>
+ *
+ * <p>
+ * This class is thread-safe and can be used in a multi-threaded environment.
+ * </p>
+ *
+ * <p>
+ * The HEV2 algorithm is configurable through the following parameters:
+ * <ul>
+ *   <li>{@code dualStackEnabled}: Whether to enable dual-stack connectivity. When set to {@code true},
+ *   the operator attempts to connect to both IPv4 and IPv6 addresses concurrently. When set to {@code false},
+ *   only IPv4 or IPv6 addresses are attempted depending on the address type of the target server.</li>
+ *   <li>{@code maxAttempts}: The maximum number of connection attempts to be made before failing. If all
+ *   attempts fail, the last error is rethrown.</li>
+ *   <li>{@code delay}: The delay (in milliseconds) to apply before subsequent connection attempts.</li>
+ *   <li>{@code connectTimeout}: The connection timeout (in milliseconds) for each attempt.</li>
+ * </ul>
+ * </p>
+ *
+ *
+ * <p>
+ * This class can be used with any {@link org.apache.hc.core5.http.nio.AsyncClientEndpoint} implementation
+ * that supports HTTP/1.1 or HTTP/2 protocols.
+ * </p>
+ *
+ * @since 5.3
+ */
+public class HappyEyeballsV2AsyncClientConnectionOperator implements AsyncClientConnectionOperator {
+
+    private static final Logger LOG = LoggerFactory.getLogger(AsyncClientConnectionOperator.class);
+
+    /**
+     * The underlying {@link AsyncClientConnectionOperator} that is used to establish connections
+     * to the target server.
+     */
+    private final AsyncClientConnectionOperator connectionOperator;
+
+    /**
+     * The DNS resolver used to resolve hostnames to IP addresses.
+     */
+    private final DnsResolver dnsResolver;
+
+    /**
+     * A lookup table used to determine the {@link TlsStrategy} to use for a given connection route.
+     */
+    private final Lookup<TlsStrategy> tlsStrategyLookup;
+
+    /**
+     * The default timeout for connection establishment attempts. If a connection cannot be established
+     * within this timeout, the attempt is considered failed.
+     */
+    private final Timeout timeout;
+
+    /**
+     * The minimum delay between connection establishment attempts.
+     */
+    private final Timeout minimumConnectionAttemptDelay;
+
+    /**
+     * The maximum delay between connection establishment attempts.
+     */
+    private final Timeout maximumConnectionAttemptDelay;
+
+    /**
+     * The current delay between connection establishment attempts.
+     */
+    private final Timeout connectionAttemptDelay;
+
+    /**
+     * The delay before resolution is started.
+     */
+    private final Timeout resolution_delay;
+
+    /**
+     * The number of IP addresses of each address family to include in the initial list of
+     * IP addresses to attempt connections to. This value is set to 2 by default, but can be
+     * increased to more aggressively favor a particular address family (e.g. set to 4 for IPv6).
+     */
+    private final int firstAddressFamilyCount;
+
+    /**
+     * The address family to use for establishing connections. This can be set to either
+     * {@link AddressFamily#IPv4} or {@link AddressFamily#IPv6}.
+     */
+    private final AddressFamily addressFamily;
+
+
+    /**
+     * The AddressFamily enum represents the possible address families that can be used when attempting to establish
+     * <p>
+     * connections using the Happy Eyeballs V2 algorithm.
+     *
+     * <p>
+     * The Happy Eyeballs V2 algorithm allows for concurrent connection attempts to be made to different IP addresses,
+     * <p>
+     * so this enum specifies whether connections should be attempted using IPv4 or IPv6 addresses.
+     *
+     * </p>
+     */
+    public enum AddressFamily {
+        IPv4, IPv6
+    }
+
+    /**
+     * Constructs a new {@link HappyEyeballsV2AsyncClientConnectionOperator} with the specified parameters.
+     *
+     * @param tlsStrategyLookup             the lookup object used to retrieve a {@link TlsStrategy} for a given {@link Route}
+     * @param connectionOperator            the underlying {@link AsyncClientConnectionOperator} to use for establishing connections
+     * @param dnsResolver                   the {@link DnsResolver} to use for resolving target hostnames
+     * @param timeout                       the timeout duration for establishing a connection
+     * @param resolution_delay              the configurable delay before subsequent DNS resolution attempts
+     * @param minimumConnectionAttemptDelay the minimum configurable delay between connection attempts
+     * @param maximumConnectionAttemptDelay the maximum configurable delay between connection attempts
+     * @param connectionAttemptDelay        the configurable delay before attempting to establish a connection
+     * @param firstAddressFamilyCount       the number of initial address families to use for establishing a connection
+     * @param addressFamily                 the preferred address family to use for establishing a connection
+     * @throws IllegalArgumentException if {@code firstAddressFamilyCount} is not positive
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(final Lookup<TlsStrategy> tlsStrategyLookup,
+                                                        final AsyncClientConnectionOperator connectionOperator,
+                                                        final DnsResolver dnsResolver,
+                                                        final Timeout timeout,
+                                                        final Timeout resolution_delay,
+                                                        final Timeout minimumConnectionAttemptDelay,
+                                                        final Timeout maximumConnectionAttemptDelay,
+                                                        final Timeout connectionAttemptDelay,
+                                                        final int firstAddressFamilyCount,
+                                                        final AddressFamily addressFamily) {
+        this.tlsStrategyLookup = Args.notNull(tlsStrategyLookup, "TLS strategy lookup");
+        this.connectionOperator = Args.notNull(connectionOperator, "Connection operator");
+        this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE;
+        this.timeout = timeout != null ? timeout : Timeout.ofMilliseconds(250);
+        this.resolution_delay = resolution_delay != null ? resolution_delay : Timeout.ofMilliseconds(50);
+        this.minimumConnectionAttemptDelay = minimumConnectionAttemptDelay != null ? minimumConnectionAttemptDelay : Timeout.ofMilliseconds(100);
+        this.maximumConnectionAttemptDelay = maximumConnectionAttemptDelay != null ? maximumConnectionAttemptDelay : Timeout.ofSeconds(2);
+        this.connectionAttemptDelay = connectionAttemptDelay != null ? connectionAttemptDelay : Timeout.ofMilliseconds(250);
+        this.firstAddressFamilyCount = Args.positive(firstAddressFamilyCount, "firstAddressFamilyCount");
+        this.addressFamily = addressFamily;
+    }
+
+    /**
+     * Constructs a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the specified
+     * {@link Lookup} for {@link TlsStrategy} and {@link SchemePortResolver} and {@link DnsResolver}.
+     * <p>
+     * The constructor internally creates a new instance of {@link DefaultAsyncClientConnectionOperator} with the
+     * specified {@link Lookup} for {@link TlsStrategy}, {@link SchemePortResolver} and {@link DnsResolver}. The
+     * created {@link AsyncClientConnectionOperator} is then passed to the main constructor along with default values
+     * for other parameters.
+     * </p>
+     *
+     * @param tlsStrategyLookup  The {@link Lookup} for {@link TlsStrategy}.
+     * @param schemePortResolver The {@link SchemePortResolver} to use for resolving scheme ports.
+     * @param dnsResolver        The {@link DnsResolver} to use for resolving hostnames to IP addresses.
+     * @throws IllegalArgumentException if the {@code tlsStrategyLookup} or {@code schemePortResolver} or {@code dnsResolver} parameter is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver,
+            final DnsResolver dnsResolver) {
+        this(tlsStrategyLookup,
+                new DefaultAsyncClientConnectionOperator(tlsStrategyLookup, schemePortResolver, dnsResolver),
+                dnsResolver,
+                null,
+                null,
+                null,
+                null,
+                null,
+                1,
+                AddressFamily.IPv6);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup
+     * and scheme-port resolver. The DNS resolver will be set to the system default resolver.
+     *
+     * @param tlsStrategyLookup  The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @param schemePortResolver The resolver instance for mapping scheme names to default port numbers.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver) {
+        this(tlsStrategyLookup, schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup.
+     * The scheme-port resolver and DNS resolver will be set to their default instances.
+     *
+     * @param tlsStrategyLookup The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup) {
+        this(tlsStrategyLookup, DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+
+    /**
+     * Attempts to connect to the given host and returns a Future that will be completed when the connection is established
+     * or when an error occurs. This method may attempt to connect to multiple IP addresses associated with the host,
+     * depending on the address family and the number of connection attempts to execute. The address family and number of
+     * connection attempts can be configured by calling the corresponding setters on this class.
+     *
+     * @param connectionInitiator the connection initiator to use when creating the connection
+     * @param host                the host to connect to
+     * @param localAddress        the local address to bind to when connecting, or null to use any available local address
+     * @param connectTimeout      the timeout to use when connecting, or null to use the default timeout
+     * @param attachment          the attachment to associate with the connection, or null if no attachment is needed
+     * @param callback            the callback to invoke when the connection is established or an error occurs, or null if no callback is needed
+     * @return a Future that will be completed when the connection is established or when an error occurs
+     */
+    @Override
+    public Future<ManagedAsyncClientConnection> connect(
+            final ConnectionInitiator connectionInitiator,
+            final HttpHost host,
+            final SocketAddress localAddress,
+            final Timeout connectTimeout,
+            final Object attachment,
+            final FutureCallback<ManagedAsyncClientConnection> callback) {
+
+        final CompletableFuture<ManagedAsyncClientConnection> connectionFuture = new CompletableFuture<>();
+
+        final Timeout conTimeout = connectTimeout != null ? connectTimeout : timeout;
+
+        resolveDnsAsync(host.getHostName())
+                .thenCompose(inetAddresses -> {
+                    final List<InetAddress> ipv4Addresses = new ArrayList<>();
+                    final List<InetAddress> ipv6Addresses = new ArrayList<>();
+
+                    for (final InetAddress inetAddress : inetAddresses) {
+                        if (inetAddress instanceof Inet4Address) {
+                            ipv4Addresses.add(inetAddress);
+                        } else if (inetAddress instanceof Inet6Address) {
+                            ipv6Addresses.add(inetAddress);
+                        }
+                    }
+
+                    sortAndInterleave(inetAddresses);
+
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> connectionFutures = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> attempts = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    if (addressFamily == AddressFamily.IPv4 && !ipv4Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                        }
+                    } else if (addressFamily == AddressFamily.IPv6 && !ipv6Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                        }
+                    } else {
+                        if (!ipv4Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                            }
+                        }
+                        if (!ipv6Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                            }
+                        }
+                    }
+
+                    // Execute the connection attempts concurrently using CompletableFuture.anyOf
+                    return CompletableFuture.anyOf(attempts.toArray(new CompletableFuture[0]))
+                            .thenCompose(result -> {
+                                if (result instanceof ManagedAsyncClientConnection) {
+                                    // If there is a result, cancel all other attempts and complete the connectionFuture
+                                    connectionFutures.forEach(future -> future.cancel(true));
+                                    connectionFuture.complete((ManagedAsyncClientConnection) result);
+                                } else {
+                                    // If there is an exception, complete the connectionFuture exceptionally with the exception
+                                    connectionFuture.completeExceptionally(new ConnectException("Failed to connect to any address for " + host));
+                                }
+                                // Invoke the callback if provided
+                                if (callback != null) {
+                                    connectionFuture.whenComplete((conn, ex) -> {
+                                        if (ex != null) {
+                                            callback.failed(new Exception(ex));
+                                        } else {
+                                            callback.completed(conn);
+                                        }
+                                    });
+                                }
+                                return connectionFuture;
+                            });
+                })
+                .exceptionally(e -> {
+                    connectionFuture.completeExceptionally(e);
+                    if (callback != null) {
+                        callback.failed(new Exception(e));
+                    }
+                    return null;
+                });
+
+        return connectionFuture;
+    }
+
+    /**
+     * Asynchronously resolves the DNS for the given host name and returns a CompletableFuture that will be completed
+     * with an array of InetAddress objects representing the IP addresses of the host.
+     * The resolution of AAAA records is delayed by the configured resolution delay to allow for a chance for A records to be
+     * returned first.
+     *
+     * @param host the host name to resolve DNS for
+     * @return a CompletableFuture that will be completed with an array of InetAddress objects representing the IP addresses
+     */
+    private CompletableFuture<InetAddress[]> resolveDnsAsync(final String host) {
+        final CompletableFuture<InetAddress[]> dnsFuture = new CompletableFuture<>();
+        CompletableFuture.runAsync(() -> {
+            try {
+                final InetAddress[] inetAddresses = dnsResolver.resolve(host);
+                // Introduce a delay before resolving AAAA records after receiving A records
+                resolution_delay.sleep();

Review Comment:
   Instead of using Thread.sleep(), i could use non-blocking methods(ScheduledExecutorService) to delay the execution of the next connection attempt.  wdyt?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org
For additional commands, e-mail: dev-help@hc.apache.org


Re: [PR] Implement HappyEyeballsV2AsyncClientConnectionOperator [httpcomponents-client]

Posted by "ok2c (via GitHub)" <gi...@apache.org>.
ok2c commented on PR #428:
URL: https://github.com/apache/httpcomponents-client/pull/428#issuecomment-1853734717

   @rschmitt @rhernandez35 Is this feature still relevant? What shall we do with this PR? @arturobernalg Could you please change the target branch back to `master` so I could delete obsolete `5.4.x`?


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org
For additional commands, e-mail: dev-help@hc.apache.org


[GitHub] [httpcomponents-client] rschmitt commented on a diff in pull request #428: Implement HappyEyeballsV2AsyncClientConnectionOperator

Posted by "rschmitt (via GitHub)" <gi...@apache.org>.
rschmitt commented on code in PR #428:
URL: https://github.com/apache/httpcomponents-client/pull/428#discussion_r1157609000


##########
httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperator.java:
##########
@@ -0,0 +1,680 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.impl.nio;
+
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.SchemePortResolver;
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.impl.ConnPoolSupport;
+import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionOperator;
+import org.apache.hc.client5.http.nio.ManagedAsyncClientConnection;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.config.Lookup;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.reactor.ConnectionInitiator;
+import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.ConnectException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+
+/**
+ * The {@link AsyncClientConnectionOperator} implementation that uses Happy Eyeballs V2 algorithm to connect
+ * to the target server. Happy Eyeballs V2 (HEV2) algorithm is used to connect to the target server by concurrently
+ * attempting to establish multiple connections to different IP addresses. The first connection to complete
+ * successfully is selected and the others are closed. If all connections fail, the last error is rethrown.
+ * The algorithm also applies a configurable delay before subsequent connection attempts. HEV2 was introduced
+ * as a means to mitigate the latency issues caused by IPv4 and IPv6 co-existence in the Internet. HEV2 is defined
+ * in RFC 8305.
+ *
+ * <p>
+ * This connection operator maintains a connection pool for each unique route (combination of target host and
+ * target port) and selects the next connection from the pool to establish a new connection or reuse an
+ * existing connection. The connection pool uses a First-In-First-Out (FIFO) queue and has a configurable limit
+ * on the maximum number of connections that can be kept alive in the pool. Once the maximum number of connections
+ * has been reached, the oldest connection in the pool is closed to make room for a new one.
+ * </p>
+ *
+ * <p>
+ * This class is thread-safe and can be used in a multi-threaded environment.
+ * </p>
+ *
+ * <p>
+ * The HEV2 algorithm is configurable through the following parameters:
+ * <ul>
+ *   <li>{@code dualStackEnabled}: Whether to enable dual-stack connectivity. When set to {@code true},
+ *   the operator attempts to connect to both IPv4 and IPv6 addresses concurrently. When set to {@code false},
+ *   only IPv4 or IPv6 addresses are attempted depending on the address type of the target server.</li>
+ *   <li>{@code maxAttempts}: The maximum number of connection attempts to be made before failing. If all
+ *   attempts fail, the last error is rethrown.</li>
+ *   <li>{@code delay}: The delay (in milliseconds) to apply before subsequent connection attempts.</li>
+ *   <li>{@code connectTimeout}: The connection timeout (in milliseconds) for each attempt.</li>
+ * </ul>
+ * </p>
+ *
+ *
+ * <p>
+ * This class can be used with any {@link org.apache.hc.core5.http.nio.AsyncClientEndpoint} implementation
+ * that supports HTTP/1.1 or HTTP/2 protocols.
+ * </p>
+ *
+ * @since 5.3
+ */
+public class HappyEyeballsV2AsyncClientConnectionOperator implements AsyncClientConnectionOperator {
+
+    private static final Logger LOG = LoggerFactory.getLogger(AsyncClientConnectionOperator.class);
+
+    /**
+     * The underlying {@link AsyncClientConnectionOperator} that is used to establish connections
+     * to the target server.
+     */
+    private final AsyncClientConnectionOperator connectionOperator;
+
+    /**
+     * The DNS resolver used to resolve hostnames to IP addresses.
+     */
+    private final DnsResolver dnsResolver;
+
+    /**
+     * A lookup table used to determine the {@link TlsStrategy} to use for a given connection route.
+     */
+    private final Lookup<TlsStrategy> tlsStrategyLookup;
+
+    /**
+     * The default timeout for connection establishment attempts. If a connection cannot be established
+     * within this timeout, the attempt is considered failed.
+     */
+    private final Timeout timeout;
+
+    /**
+     * The minimum delay between connection establishment attempts.
+     */
+    private final Timeout minimumConnectionAttemptDelay;
+
+    /**
+     * The maximum delay between connection establishment attempts.
+     */
+    private final Timeout maximumConnectionAttemptDelay;
+
+    /**
+     * The current delay between connection establishment attempts.
+     */
+    private final Timeout connectionAttemptDelay;
+
+    /**
+     * The delay before resolution is started.
+     */
+    private final Timeout resolution_delay;
+
+    /**
+     * The number of IP addresses of each address family to include in the initial list of
+     * IP addresses to attempt connections to. This value is set to 2 by default, but can be
+     * increased to more aggressively favor a particular address family (e.g. set to 4 for IPv6).
+     */
+    private final int firstAddressFamilyCount;
+
+    /**
+     * The address family to use for establishing connections. This can be set to either
+     * {@link AddressFamily#IPv4} or {@link AddressFamily#IPv6}.
+     */
+    private final AddressFamily addressFamily;
+
+
+    /**
+     * The AddressFamily enum represents the possible address families that can be used when attempting to establish
+     * <p>
+     * connections using the Happy Eyeballs V2 algorithm.
+     *
+     * <p>
+     * The Happy Eyeballs V2 algorithm allows for concurrent connection attempts to be made to different IP addresses,
+     * <p>
+     * so this enum specifies whether connections should be attempted using IPv4 or IPv6 addresses.
+     *
+     * </p>
+     */
+    public enum AddressFamily {
+        IPv4, IPv6
+    }
+
+    /**
+     * Constructs a new {@link HappyEyeballsV2AsyncClientConnectionOperator} with the specified parameters.
+     *
+     * @param tlsStrategyLookup             the lookup object used to retrieve a {@link TlsStrategy} for a given {@link Route}
+     * @param connectionOperator            the underlying {@link AsyncClientConnectionOperator} to use for establishing connections
+     * @param dnsResolver                   the {@link DnsResolver} to use for resolving target hostnames
+     * @param timeout                       the timeout duration for establishing a connection
+     * @param resolution_delay              the configurable delay before subsequent DNS resolution attempts
+     * @param minimumConnectionAttemptDelay the minimum configurable delay between connection attempts
+     * @param maximumConnectionAttemptDelay the maximum configurable delay between connection attempts
+     * @param connectionAttemptDelay        the configurable delay before attempting to establish a connection
+     * @param firstAddressFamilyCount       the number of initial address families to use for establishing a connection
+     * @param addressFamily                 the preferred address family to use for establishing a connection
+     * @throws IllegalArgumentException if {@code firstAddressFamilyCount} is not positive
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(final Lookup<TlsStrategy> tlsStrategyLookup,
+                                                        final AsyncClientConnectionOperator connectionOperator,
+                                                        final DnsResolver dnsResolver,
+                                                        final Timeout timeout,
+                                                        final Timeout resolution_delay,
+                                                        final Timeout minimumConnectionAttemptDelay,
+                                                        final Timeout maximumConnectionAttemptDelay,
+                                                        final Timeout connectionAttemptDelay,
+                                                        final int firstAddressFamilyCount,
+                                                        final AddressFamily addressFamily) {
+        this.tlsStrategyLookup = Args.notNull(tlsStrategyLookup, "TLS strategy lookup");
+        this.connectionOperator = Args.notNull(connectionOperator, "Connection operator");
+        this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE;
+        this.timeout = timeout != null ? timeout : Timeout.ofMilliseconds(250);
+        this.resolution_delay = resolution_delay != null ? resolution_delay : Timeout.ofMilliseconds(50);
+        this.minimumConnectionAttemptDelay = minimumConnectionAttemptDelay != null ? minimumConnectionAttemptDelay : Timeout.ofMilliseconds(100);
+        this.maximumConnectionAttemptDelay = maximumConnectionAttemptDelay != null ? maximumConnectionAttemptDelay : Timeout.ofSeconds(2);
+        this.connectionAttemptDelay = connectionAttemptDelay != null ? connectionAttemptDelay : Timeout.ofMilliseconds(250);
+        this.firstAddressFamilyCount = Args.positive(firstAddressFamilyCount, "firstAddressFamilyCount");
+        this.addressFamily = addressFamily;
+    }
+
+    /**
+     * Constructs a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the specified
+     * {@link Lookup} for {@link TlsStrategy} and {@link SchemePortResolver} and {@link DnsResolver}.
+     * <p>
+     * The constructor internally creates a new instance of {@link DefaultAsyncClientConnectionOperator} with the
+     * specified {@link Lookup} for {@link TlsStrategy}, {@link SchemePortResolver} and {@link DnsResolver}. The
+     * created {@link AsyncClientConnectionOperator} is then passed to the main constructor along with default values
+     * for other parameters.
+     * </p>
+     *
+     * @param tlsStrategyLookup  The {@link Lookup} for {@link TlsStrategy}.
+     * @param schemePortResolver The {@link SchemePortResolver} to use for resolving scheme ports.
+     * @param dnsResolver        The {@link DnsResolver} to use for resolving hostnames to IP addresses.
+     * @throws IllegalArgumentException if the {@code tlsStrategyLookup} or {@code schemePortResolver} or {@code dnsResolver} parameter is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver,
+            final DnsResolver dnsResolver) {
+        this(tlsStrategyLookup,
+                new DefaultAsyncClientConnectionOperator(tlsStrategyLookup, schemePortResolver, dnsResolver),
+                dnsResolver,
+                null,
+                null,
+                null,
+                null,
+                null,
+                1,
+                AddressFamily.IPv6);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup
+     * and scheme-port resolver. The DNS resolver will be set to the system default resolver.
+     *
+     * @param tlsStrategyLookup  The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @param schemePortResolver The resolver instance for mapping scheme names to default port numbers.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver) {
+        this(tlsStrategyLookup, schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup.
+     * The scheme-port resolver and DNS resolver will be set to their default instances.
+     *
+     * @param tlsStrategyLookup The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup) {
+        this(tlsStrategyLookup, DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+
+    /**
+     * Attempts to connect to the given host and returns a Future that will be completed when the connection is established
+     * or when an error occurs. This method may attempt to connect to multiple IP addresses associated with the host,
+     * depending on the address family and the number of connection attempts to execute. The address family and number of
+     * connection attempts can be configured by calling the corresponding setters on this class.
+     *
+     * @param connectionInitiator the connection initiator to use when creating the connection
+     * @param host                the host to connect to
+     * @param localAddress        the local address to bind to when connecting, or null to use any available local address
+     * @param connectTimeout      the timeout to use when connecting, or null to use the default timeout
+     * @param attachment          the attachment to associate with the connection, or null if no attachment is needed
+     * @param callback            the callback to invoke when the connection is established or an error occurs, or null if no callback is needed
+     * @return a Future that will be completed when the connection is established or when an error occurs
+     */
+    @Override
+    public Future<ManagedAsyncClientConnection> connect(
+            final ConnectionInitiator connectionInitiator,
+            final HttpHost host,
+            final SocketAddress localAddress,
+            final Timeout connectTimeout,
+            final Object attachment,
+            final FutureCallback<ManagedAsyncClientConnection> callback) {
+
+        final CompletableFuture<ManagedAsyncClientConnection> connectionFuture = new CompletableFuture<>();
+
+        final Timeout conTimeout = connectTimeout != null ? connectTimeout : timeout;
+
+        resolveDnsAsync(host.getHostName())
+                .thenCompose(inetAddresses -> {
+                    final List<InetAddress> ipv4Addresses = new ArrayList<>();
+                    final List<InetAddress> ipv6Addresses = new ArrayList<>();
+
+                    for (final InetAddress inetAddress : inetAddresses) {
+                        if (inetAddress instanceof Inet4Address) {
+                            ipv4Addresses.add(inetAddress);
+                        } else if (inetAddress instanceof Inet6Address) {
+                            ipv6Addresses.add(inetAddress);
+                        }
+                    }
+
+                    sortAndInterleave(inetAddresses);
+
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> connectionFutures = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> attempts = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    if (addressFamily == AddressFamily.IPv4 && !ipv4Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                        }
+                    } else if (addressFamily == AddressFamily.IPv6 && !ipv6Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                        }
+                    } else {
+                        if (!ipv4Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                            }
+                        }
+                        if (!ipv6Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                            }
+                        }
+                    }
+
+                    // Execute the connection attempts concurrently using CompletableFuture.anyOf
+                    return CompletableFuture.anyOf(attempts.toArray(new CompletableFuture[0]))
+                            .thenCompose(result -> {
+                                if (result instanceof ManagedAsyncClientConnection) {
+                                    // If there is a result, cancel all other attempts and complete the connectionFuture
+                                    connectionFutures.forEach(future -> future.cancel(true));
+                                    connectionFuture.complete((ManagedAsyncClientConnection) result);
+                                } else {
+                                    // If there is an exception, complete the connectionFuture exceptionally with the exception
+                                    connectionFuture.completeExceptionally(new ConnectException("Failed to connect to any address for " + host));
+                                }
+                                // Invoke the callback if provided
+                                if (callback != null) {
+                                    connectionFuture.whenComplete((conn, ex) -> {
+                                        if (ex != null) {
+                                            callback.failed(new Exception(ex));
+                                        } else {
+                                            callback.completed(conn);
+                                        }
+                                    });
+                                }
+                                return connectionFuture;
+                            });
+                })
+                .exceptionally(e -> {
+                    connectionFuture.completeExceptionally(e);
+                    if (callback != null) {
+                        callback.failed(new Exception(e));
+                    }
+                    return null;
+                });
+
+        return connectionFuture;
+    }
+
+    /**
+     * Asynchronously resolves the DNS for the given host name and returns a CompletableFuture that will be completed
+     * with an array of InetAddress objects representing the IP addresses of the host.
+     * The resolution of AAAA records is delayed by the configured resolution delay to allow for a chance for A records to be
+     * returned first.
+     *
+     * @param host the host name to resolve DNS for
+     * @return a CompletableFuture that will be completed with an array of InetAddress objects representing the IP addresses
+     */
+    private CompletableFuture<InetAddress[]> resolveDnsAsync(final String host) {
+        final CompletableFuture<InetAddress[]> dnsFuture = new CompletableFuture<>();
+        CompletableFuture.runAsync(() -> {
+            try {
+                final InetAddress[] inetAddresses = dnsResolver.resolve(host);
+                // Introduce a delay before resolving AAAA records after receiving A records
+                resolution_delay.sleep();
+                dnsFuture.complete(inetAddresses);
+            } catch (final UnknownHostException | InterruptedException e) {
+                if (LOG.isDebugEnabled()) {
+                    LOG.debug("Failed to resolve DNS for host '{}': {}", host, e.getMessage(), e);
+                }
+                dnsFuture.completeExceptionally(e);
+            }
+        });
+        return dnsFuture;
+    }
+
+
+    /**
+     * Initiates an asynchronous connection attempt to the given list of IP addresses for the specified {@link HttpHost}.
+     *
+     * @param connectionInitiator the {@link ConnectionInitiator} to use for establishing the connection
+     * @param host                the {@link HttpHost} to connect to
+     * @param connectTimeout      the timeout for the connection attempt
+     * @param attachment          the attachment object to pass to the connection operator
+     * @param addresses           the list of IP addresses to attempt to connect to
+     * @param localAddress        the local socket address to bind the connection to, or {@code null} if not binding
+     * @return a {@link CompletableFuture} that completes with a {@link ManagedAsyncClientConnection} if the connection attempt succeeds,
+     * or exceptionally with an exception if all attempts fail
+     */
+    private CompletableFuture<ManagedAsyncClientConnection> connectAttempt(
+            final ConnectionInitiator connectionInitiator,
+            final HttpHost host,
+            final Timeout connectTimeout,
+            final Object attachment,
+            final List<InetAddress> addresses,
+            final SocketAddress localAddress) {
+
+        final CompletableFuture<ManagedAsyncClientConnection> connectionFuture = new CompletableFuture<>();
+
+        // Create a list of connection attempts to execute
+        final List<CompletableFuture<Void>> attempts = new ArrayList<>();
+        for (int i = 0; i < addresses.size(); i++) {
+            final InetAddress address = addresses.get(i);
+
+            if (LOG.isDebugEnabled()) {
+                LOG.info("Attempting to connect to {}", address);
+            }
+
+            final CompletableFuture<Void> attempt = new CompletableFuture<>();
+            attempts.add(attempt);
+            final HttpHost currentHost = new HttpHost(host.getSchemeName(), address, host.getHostName(), host.getPort());
+
+            connectionOperator.connect(
+                    connectionInitiator,
+                    currentHost,
+                    localAddress,
+                    connectTimeout,
+                    attachment,
+                    new FutureCallback<ManagedAsyncClientConnection>() {
+                        @Override
+                        public void completed(final ManagedAsyncClientConnection connection) {
+                            if (LOG.isDebugEnabled()) {
+                                LOG.debug("Successfully connected {}", ConnPoolSupport.getId(connection));
+                            }
+                            connectionFuture.complete(connection);
+                        }
+
+                        @Override
+                        public void failed(final Exception ex) {
+                            if (LOG.isDebugEnabled()) {
+                                LOG.debug("Failed to connect  {}", ConnPoolSupport.getId(address), ex);
+                            }
+                            attempt.completeExceptionally(ex);
+                        }
+
+                        @Override
+                        public void cancelled() {
+                            if (LOG.isDebugEnabled()) {
+                                LOG.debug("Cancelled connect for {}", ConnPoolSupport.getId(address));
+                            }
+                            attempt.cancel(true);
+                        }
+                    });
+
+            // Introduce a delay before executing the next connection attempt
+            if (i < addresses.size() - 1) {
+                try {
+                    final Duration delay = calculateDelay(i);
+                    delay.wait();

Review Comment:
   Wait a minute, this isn't _even_ a sleep call, this is calling `Object::wait`!! This isn't correct at all and may put the thread to sleep for good if nothing calls `notifyAll` on the same `Duration` instance. Is this code even tested?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org
For additional commands, e-mail: dev-help@hc.apache.org


[GitHub] [httpcomponents-client] ok2c commented on pull request #428: Implement HappyEyeballsV2AsyncClientConnectionOperator

Posted by "ok2c (via GitHub)" <gi...@apache.org>.
ok2c commented on PR #428:
URL: https://github.com/apache/httpcomponents-client/pull/428#issuecomment-1484933339

   @arturobernalg Allow me a few days to review.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org
For additional commands, e-mail: dev-help@hc.apache.org


[GitHub] [httpcomponents-client] rschmitt commented on a diff in pull request #428: Implement HappyEyeballsV2AsyncClientConnectionOperator

Posted by "rschmitt (via GitHub)" <gi...@apache.org>.
rschmitt commented on code in PR #428:
URL: https://github.com/apache/httpcomponents-client/pull/428#discussion_r1156705619


##########
httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperatorBuilder.java:
##########
@@ -0,0 +1,272 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.nio;
+
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionOperator;
+import org.apache.hc.client5.http.ssl.ConscryptClientTlsStrategy;
+import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http.config.RegistryBuilder;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.ReflectionUtils;
+import org.apache.hc.core5.util.Timeout;
+
+/**
+ * A builder for creating instances of {@link HappyEyeballsV2AsyncClientConnectionOperator}.
+ *
+ * <p>This builder provides a fluent API for configuring various options of the
+ * {@link HappyEyeballsV2AsyncClientConnectionOperator}. Once all the desired options have been set,
+ * the {@link #build()} method can be called to create an instance of the connection operator.
+ *
+ * <p>The following options can be configured using this builder:
+ * <ul>
+ *     <li>The TLS strategy to be used for establishing TLS connections</li>
+ *     <li>The connection operator to be used for creating connections</li>
+ *     <li>The DNS resolver to be used for resolving hostnames</li>
+ *     <li>The timeout for establishing a connection</li>
+ *     <li>The delay between resolution of hostnames and connection establishment attempts</li>
+ *     <li>The minimum and maximum delays between connection establishment attempts</li>
+ *     <li>The delay between subsequent connection establishment attempts</li>
+ *     <li>The number of connections to be established with the first address family in the list</li>
+ *     <li>The preferred address family to be used for establishing connections</li>
+ * </ul>
+ *
+ * <p>If no options are explicitly set using this builder, default options will be used for each option.
+ *
+ * <p>This class is not thread-safe.
+ *
+ * @see HappyEyeballsV2AsyncClientConnectionOperator
+ * @since 5.3
+ */
+public class HappyEyeballsV2AsyncClientConnectionOperatorBuilder {
+
+    private AsyncClientConnectionOperator connectionOperator;
+    private DnsResolver dnsResolver;
+    private TlsStrategy tlsStrategy;
+    private Timeout timeout;
+    private Timeout minimumConnectionAttemptDelay;
+    private Timeout maximumConnectionAttemptDelay;
+    private Timeout connectionAttemptDelay;
+    private Timeout resolutionDelay;
+    private int firstAddressFamilyCount;
+    private HappyEyeballsV2AsyncClientConnectionOperator.AddressFamily addressFamily;
+
+
+    public static HappyEyeballsV2AsyncClientConnectionOperatorBuilder create() {
+        return new HappyEyeballsV2AsyncClientConnectionOperatorBuilder();
+    }
+
+
+    HappyEyeballsV2AsyncClientConnectionOperatorBuilder() {
+        super();
+    }
+
+
+    private boolean systemProperties;
+
+    /**
+     * Use system properties when creating and configuring default
+     * implementations.
+     */
+    public final HappyEyeballsV2AsyncClientConnectionOperatorBuilder useSystemProperties() {
+        this.systemProperties = true;
+        return this;
+    }
+
+    /**
+     * Sets the {@link AsyncClientConnectionOperator} to use for establishing connections.
+     *
+     * @param connectionOperator the {@link AsyncClientConnectionOperator} to use
+     * @return this {@link HappyEyeballsV2AsyncClientConnectionOperatorBuilder} instance
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withConnectionOperator(
+            final AsyncClientConnectionOperator connectionOperator) {
+        this.connectionOperator = connectionOperator;
+        return this;
+    }
+
+    /**
+     * Sets the {@link DnsResolver} to use for resolving host names to IP addresses.
+     *
+     * @param dnsResolver the {@link DnsResolver} to use
+     * @return this builder instance
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withDnsResolver(final DnsResolver dnsResolver) {
+        this.dnsResolver = dnsResolver;
+        return this;
+    }
+
+    /**
+     * Sets the {@link TlsStrategy} to use for creating TLS connections.
+     *
+     * @param tlsStrategy the {@link TlsStrategy} to use
+     * @return this {@link HappyEyeballsV2AsyncClientConnectionOperatorBuilder} instance
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withTlsStrategyLookup(final TlsStrategy tlsStrategy) {
+        this.tlsStrategy = tlsStrategy;
+        return this;
+    }
+
+    /**
+     * Set the timeout to use for connection attempts.
+     *
+     * @param timeout the timeout to use for connection attempts
+     * @return this builder
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withTimeout(final Timeout timeout) {
+        this.timeout = timeout;
+        return this;
+    }
+
+    /**
+     * Sets the minimum delay between connection attempts. The actual delay may be longer if a resolution delay has been
+     * specified, in which case the minimum connection attempt delay is added to the resolution delay.
+     *
+     * @param minimumConnectionAttemptDelay the minimum delay between connection attempts
+     * @return this builder instance
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withMinimumConnectionAttemptDelay(
+            final Timeout minimumConnectionAttemptDelay) {
+        this.minimumConnectionAttemptDelay = minimumConnectionAttemptDelay;
+        return this;
+    }
+
+    /**
+     * Sets the maximum delay between two connection attempts.
+     *
+     * @param maximumConnectionAttemptDelay the maximum delay between two connection attempts
+     * @return the builder instance
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withMaximumConnectionAttemptDelay(
+            final Timeout maximumConnectionAttemptDelay) {
+        this.maximumConnectionAttemptDelay = maximumConnectionAttemptDelay;
+        return this;
+    }
+
+    /**
+     * Sets the delay between two connection attempts.
+     *
+     * @param connectionAttemptDelay the delay between two connection attempts
+     * @return the builder instance
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withConnectionAttemptDelay(
+            final Timeout connectionAttemptDelay) {
+        this.connectionAttemptDelay = connectionAttemptDelay;
+        return this;
+    }
+
+    /**
+     * Sets the delay before attempting to resolve the next address in the list.
+     *
+     * @param resolutionDelay the delay before attempting to resolve the next address in the list
+     * @return the builder instance
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withResolutionDelay(final Timeout resolutionDelay) {
+        this.resolutionDelay = resolutionDelay;
+        return this;
+    }
+
+    /**
+     * Sets the number of first address families to try before falling back to the other address families.
+     *
+     * @param firstAddressFamilyCount the number of first address families to try
+     * @return this builder
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withFirstAddressFamilyCount(
+            final int firstAddressFamilyCount) {
+        this.firstAddressFamilyCount = firstAddressFamilyCount;
+        return this;
+    }
+
+    /**
+     * Sets the preferred address family to use for connections.
+     *
+     * @param addressFamily the preferred address family
+     * @return this builder
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withAddressFamily(final HappyEyeballsV2AsyncClientConnectionOperator.AddressFamily addressFamily) {
+        this.addressFamily = addressFamily;
+        return this;
+    }
+
+    /**
+     * Builds a {@link HappyEyeballsV2AsyncClientConnectionOperator} with the specified parameters.
+     *
+     * @return the {@link HappyEyeballsV2AsyncClientConnectionOperator} instance built with the specified parameters.
+     * @throws IllegalArgumentException if the connection operator is null.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator build() {
+        final TlsStrategy tlsStrategyCopy;
+        if (tlsStrategy != null) {
+            tlsStrategyCopy = tlsStrategy;
+        } else {
+            if (ReflectionUtils.determineJRELevel() <= 8 && ConscryptClientTlsStrategy.isSupported()) {
+                if (systemProperties) {
+                    tlsStrategyCopy = ConscryptClientTlsStrategy.getSystemDefault();
+                } else {
+                    tlsStrategyCopy = ConscryptClientTlsStrategy.getDefault();
+                }
+            } else {
+                if (systemProperties) {
+                    tlsStrategyCopy = DefaultClientTlsStrategy.getSystemDefault();
+                } else {
+                    tlsStrategyCopy = DefaultClientTlsStrategy.getDefault();
+                }
+            }
+        }
+
+
+        connectionOperator = Args.notNull(connectionOperator, "Connection operator");
+        final DnsResolver dnsResolverToUse = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE;
+        final Timeout timeoutToUse = timeout != null ? timeout : Timeout.ofMilliseconds(250);
+        final Timeout resolutionDelayToUse = resolutionDelay != null ? resolutionDelay : Timeout.ofMilliseconds(50);
+        final Timeout minimumConnectionAttemptDelayToUse = minimumConnectionAttemptDelay != null ? minimumConnectionAttemptDelay : Timeout.ofMilliseconds(100);
+        final Timeout maximumConnectionAttemptDelayToUse = maximumConnectionAttemptDelay != null ? maximumConnectionAttemptDelay : Timeout.ofSeconds(2);
+        final Timeout connectionAttemptDelayToUse = connectionAttemptDelay != null ? connectionAttemptDelay : Timeout.ofMilliseconds(250);
+        final int firstAddressFamilyCountToUse = Args.positive(firstAddressFamilyCount, "First Address Family");
+        final HappyEyeballsV2AsyncClientConnectionOperator.AddressFamily addressFamilyToUse = addressFamily;

Review Comment:
   These defaults are all duplicated in the `HappyEyeballsV2AsyncClientConnectionOperator` constructor.



##########
httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperator.java:
##########
@@ -0,0 +1,680 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.impl.nio;
+
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.SchemePortResolver;
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.impl.ConnPoolSupport;
+import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionOperator;
+import org.apache.hc.client5.http.nio.ManagedAsyncClientConnection;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.config.Lookup;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.reactor.ConnectionInitiator;
+import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.ConnectException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+
+/**
+ * The {@link AsyncClientConnectionOperator} implementation that uses Happy Eyeballs V2 algorithm to connect
+ * to the target server. Happy Eyeballs V2 (HEV2) algorithm is used to connect to the target server by concurrently
+ * attempting to establish multiple connections to different IP addresses. The first connection to complete
+ * successfully is selected and the others are closed. If all connections fail, the last error is rethrown.
+ * The algorithm also applies a configurable delay before subsequent connection attempts. HEV2 was introduced
+ * as a means to mitigate the latency issues caused by IPv4 and IPv6 co-existence in the Internet. HEV2 is defined
+ * in RFC 8305.
+ *
+ * <p>
+ * This connection operator maintains a connection pool for each unique route (combination of target host and
+ * target port) and selects the next connection from the pool to establish a new connection or reuse an
+ * existing connection. The connection pool uses a First-In-First-Out (FIFO) queue and has a configurable limit
+ * on the maximum number of connections that can be kept alive in the pool. Once the maximum number of connections
+ * has been reached, the oldest connection in the pool is closed to make room for a new one.
+ * </p>
+ *
+ * <p>
+ * This class is thread-safe and can be used in a multi-threaded environment.
+ * </p>
+ *
+ * <p>
+ * The HEV2 algorithm is configurable through the following parameters:
+ * <ul>
+ *   <li>{@code dualStackEnabled}: Whether to enable dual-stack connectivity. When set to {@code true},
+ *   the operator attempts to connect to both IPv4 and IPv6 addresses concurrently. When set to {@code false},
+ *   only IPv4 or IPv6 addresses are attempted depending on the address type of the target server.</li>
+ *   <li>{@code maxAttempts}: The maximum number of connection attempts to be made before failing. If all
+ *   attempts fail, the last error is rethrown.</li>
+ *   <li>{@code delay}: The delay (in milliseconds) to apply before subsequent connection attempts.</li>
+ *   <li>{@code connectTimeout}: The connection timeout (in milliseconds) for each attempt.</li>
+ * </ul>
+ * </p>
+ *
+ *
+ * <p>
+ * This class can be used with any {@link org.apache.hc.core5.http.nio.AsyncClientEndpoint} implementation
+ * that supports HTTP/1.1 or HTTP/2 protocols.
+ * </p>
+ *
+ * @since 5.3
+ */
+public class HappyEyeballsV2AsyncClientConnectionOperator implements AsyncClientConnectionOperator {
+
+    private static final Logger LOG = LoggerFactory.getLogger(AsyncClientConnectionOperator.class);
+
+    /**
+     * The underlying {@link AsyncClientConnectionOperator} that is used to establish connections
+     * to the target server.
+     */
+    private final AsyncClientConnectionOperator connectionOperator;
+
+    /**
+     * The DNS resolver used to resolve hostnames to IP addresses.
+     */
+    private final DnsResolver dnsResolver;
+
+    /**
+     * A lookup table used to determine the {@link TlsStrategy} to use for a given connection route.
+     */
+    private final Lookup<TlsStrategy> tlsStrategyLookup;
+
+    /**
+     * The default timeout for connection establishment attempts. If a connection cannot be established
+     * within this timeout, the attempt is considered failed.
+     */
+    private final Timeout timeout;
+
+    /**
+     * The minimum delay between connection establishment attempts.
+     */
+    private final Timeout minimumConnectionAttemptDelay;
+
+    /**
+     * The maximum delay between connection establishment attempts.
+     */
+    private final Timeout maximumConnectionAttemptDelay;
+
+    /**
+     * The current delay between connection establishment attempts.
+     */
+    private final Timeout connectionAttemptDelay;
+
+    /**
+     * The delay before resolution is started.
+     */
+    private final Timeout resolution_delay;
+
+    /**
+     * The number of IP addresses of each address family to include in the initial list of
+     * IP addresses to attempt connections to. This value is set to 2 by default, but can be
+     * increased to more aggressively favor a particular address family (e.g. set to 4 for IPv6).
+     */
+    private final int firstAddressFamilyCount;
+
+    /**
+     * The address family to use for establishing connections. This can be set to either
+     * {@link AddressFamily#IPv4} or {@link AddressFamily#IPv6}.
+     */
+    private final AddressFamily addressFamily;
+
+
+    /**
+     * The AddressFamily enum represents the possible address families that can be used when attempting to establish
+     * <p>
+     * connections using the Happy Eyeballs V2 algorithm.
+     *
+     * <p>
+     * The Happy Eyeballs V2 algorithm allows for concurrent connection attempts to be made to different IP addresses,
+     * <p>
+     * so this enum specifies whether connections should be attempted using IPv4 or IPv6 addresses.
+     *
+     * </p>
+     */
+    public enum AddressFamily {
+        IPv4, IPv6
+    }
+
+    /**
+     * Constructs a new {@link HappyEyeballsV2AsyncClientConnectionOperator} with the specified parameters.
+     *
+     * @param tlsStrategyLookup             the lookup object used to retrieve a {@link TlsStrategy} for a given {@link Route}
+     * @param connectionOperator            the underlying {@link AsyncClientConnectionOperator} to use for establishing connections
+     * @param dnsResolver                   the {@link DnsResolver} to use for resolving target hostnames
+     * @param timeout                       the timeout duration for establishing a connection
+     * @param resolution_delay              the configurable delay before subsequent DNS resolution attempts
+     * @param minimumConnectionAttemptDelay the minimum configurable delay between connection attempts
+     * @param maximumConnectionAttemptDelay the maximum configurable delay between connection attempts
+     * @param connectionAttemptDelay        the configurable delay before attempting to establish a connection
+     * @param firstAddressFamilyCount       the number of initial address families to use for establishing a connection
+     * @param addressFamily                 the preferred address family to use for establishing a connection
+     * @throws IllegalArgumentException if {@code firstAddressFamilyCount} is not positive
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(final Lookup<TlsStrategy> tlsStrategyLookup,
+                                                        final AsyncClientConnectionOperator connectionOperator,
+                                                        final DnsResolver dnsResolver,
+                                                        final Timeout timeout,
+                                                        final Timeout resolution_delay,
+                                                        final Timeout minimumConnectionAttemptDelay,
+                                                        final Timeout maximumConnectionAttemptDelay,
+                                                        final Timeout connectionAttemptDelay,
+                                                        final int firstAddressFamilyCount,
+                                                        final AddressFamily addressFamily) {
+        this.tlsStrategyLookup = Args.notNull(tlsStrategyLookup, "TLS strategy lookup");
+        this.connectionOperator = Args.notNull(connectionOperator, "Connection operator");
+        this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE;
+        this.timeout = timeout != null ? timeout : Timeout.ofMilliseconds(250);
+        this.resolution_delay = resolution_delay != null ? resolution_delay : Timeout.ofMilliseconds(50);
+        this.minimumConnectionAttemptDelay = minimumConnectionAttemptDelay != null ? minimumConnectionAttemptDelay : Timeout.ofMilliseconds(100);
+        this.maximumConnectionAttemptDelay = maximumConnectionAttemptDelay != null ? maximumConnectionAttemptDelay : Timeout.ofSeconds(2);
+        this.connectionAttemptDelay = connectionAttemptDelay != null ? connectionAttemptDelay : Timeout.ofMilliseconds(250);
+        this.firstAddressFamilyCount = Args.positive(firstAddressFamilyCount, "firstAddressFamilyCount");
+        this.addressFamily = addressFamily;
+    }
+
+    /**
+     * Constructs a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the specified
+     * {@link Lookup} for {@link TlsStrategy} and {@link SchemePortResolver} and {@link DnsResolver}.
+     * <p>
+     * The constructor internally creates a new instance of {@link DefaultAsyncClientConnectionOperator} with the
+     * specified {@link Lookup} for {@link TlsStrategy}, {@link SchemePortResolver} and {@link DnsResolver}. The
+     * created {@link AsyncClientConnectionOperator} is then passed to the main constructor along with default values
+     * for other parameters.
+     * </p>
+     *
+     * @param tlsStrategyLookup  The {@link Lookup} for {@link TlsStrategy}.
+     * @param schemePortResolver The {@link SchemePortResolver} to use for resolving scheme ports.
+     * @param dnsResolver        The {@link DnsResolver} to use for resolving hostnames to IP addresses.
+     * @throws IllegalArgumentException if the {@code tlsStrategyLookup} or {@code schemePortResolver} or {@code dnsResolver} parameter is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver,
+            final DnsResolver dnsResolver) {
+        this(tlsStrategyLookup,
+                new DefaultAsyncClientConnectionOperator(tlsStrategyLookup, schemePortResolver, dnsResolver),
+                dnsResolver,
+                null,
+                null,
+                null,
+                null,
+                null,
+                1,
+                AddressFamily.IPv6);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup
+     * and scheme-port resolver. The DNS resolver will be set to the system default resolver.
+     *
+     * @param tlsStrategyLookup  The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @param schemePortResolver The resolver instance for mapping scheme names to default port numbers.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver) {
+        this(tlsStrategyLookup, schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup.
+     * The scheme-port resolver and DNS resolver will be set to their default instances.
+     *
+     * @param tlsStrategyLookup The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup) {
+        this(tlsStrategyLookup, DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+
+    /**
+     * Attempts to connect to the given host and returns a Future that will be completed when the connection is established
+     * or when an error occurs. This method may attempt to connect to multiple IP addresses associated with the host,
+     * depending on the address family and the number of connection attempts to execute. The address family and number of
+     * connection attempts can be configured by calling the corresponding setters on this class.
+     *
+     * @param connectionInitiator the connection initiator to use when creating the connection
+     * @param host                the host to connect to
+     * @param localAddress        the local address to bind to when connecting, or null to use any available local address
+     * @param connectTimeout      the timeout to use when connecting, or null to use the default timeout
+     * @param attachment          the attachment to associate with the connection, or null if no attachment is needed
+     * @param callback            the callback to invoke when the connection is established or an error occurs, or null if no callback is needed
+     * @return a Future that will be completed when the connection is established or when an error occurs
+     */
+    @Override
+    public Future<ManagedAsyncClientConnection> connect(
+            final ConnectionInitiator connectionInitiator,
+            final HttpHost host,
+            final SocketAddress localAddress,
+            final Timeout connectTimeout,
+            final Object attachment,
+            final FutureCallback<ManagedAsyncClientConnection> callback) {
+
+        final CompletableFuture<ManagedAsyncClientConnection> connectionFuture = new CompletableFuture<>();
+
+        final Timeout conTimeout = connectTimeout != null ? connectTimeout : timeout;
+
+        resolveDnsAsync(host.getHostName())
+                .thenCompose(inetAddresses -> {
+                    final List<InetAddress> ipv4Addresses = new ArrayList<>();
+                    final List<InetAddress> ipv6Addresses = new ArrayList<>();
+
+                    for (final InetAddress inetAddress : inetAddresses) {
+                        if (inetAddress instanceof Inet4Address) {
+                            ipv4Addresses.add(inetAddress);
+                        } else if (inetAddress instanceof Inet6Address) {
+                            ipv6Addresses.add(inetAddress);
+                        }
+                    }
+
+                    sortAndInterleave(inetAddresses);
+
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> connectionFutures = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> attempts = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    if (addressFamily == AddressFamily.IPv4 && !ipv4Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                        }
+                    } else if (addressFamily == AddressFamily.IPv6 && !ipv6Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                        }
+                    } else {
+                        if (!ipv4Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                            }
+                        }
+                        if (!ipv6Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                            }
+                        }
+                    }
+
+                    // Execute the connection attempts concurrently using CompletableFuture.anyOf
+                    return CompletableFuture.anyOf(attempts.toArray(new CompletableFuture[0]))
+                            .thenCompose(result -> {
+                                if (result instanceof ManagedAsyncClientConnection) {
+                                    // If there is a result, cancel all other attempts and complete the connectionFuture
+                                    connectionFutures.forEach(future -> future.cancel(true));
+                                    connectionFuture.complete((ManagedAsyncClientConnection) result);
+                                } else {
+                                    // If there is an exception, complete the connectionFuture exceptionally with the exception
+                                    connectionFuture.completeExceptionally(new ConnectException("Failed to connect to any address for " + host));
+                                }
+                                // Invoke the callback if provided
+                                if (callback != null) {
+                                    connectionFuture.whenComplete((conn, ex) -> {
+                                        if (ex != null) {
+                                            callback.failed(new Exception(ex));
+                                        } else {
+                                            callback.completed(conn);
+                                        }
+                                    });
+                                }
+                                return connectionFuture;
+                            });
+                })
+                .exceptionally(e -> {
+                    connectionFuture.completeExceptionally(e);
+                    if (callback != null) {
+                        callback.failed(new Exception(e));
+                    }
+                    return null;
+                });
+
+        return connectionFuture;
+    }
+
+    /**
+     * Asynchronously resolves the DNS for the given host name and returns a CompletableFuture that will be completed
+     * with an array of InetAddress objects representing the IP addresses of the host.
+     * The resolution of AAAA records is delayed by the configured resolution delay to allow for a chance for A records to be
+     * returned first.
+     *
+     * @param host the host name to resolve DNS for
+     * @return a CompletableFuture that will be completed with an array of InetAddress objects representing the IP addresses
+     */
+    private CompletableFuture<InetAddress[]> resolveDnsAsync(final String host) {
+        final CompletableFuture<InetAddress[]> dnsFuture = new CompletableFuture<>();
+        CompletableFuture.runAsync(() -> {
+            try {
+                final InetAddress[] inetAddresses = dnsResolver.resolve(host);
+                // Introduce a delay before resolving AAAA records after receiving A records
+                resolution_delay.sleep();
+                dnsFuture.complete(inetAddresses);
+            } catch (final UnknownHostException | InterruptedException e) {

Review Comment:
   Shouldn't you catch `Exception` here, instead of just the checked exceptions?



##########
httpclient5/src/test/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperatorTest.java:
##########
@@ -0,0 +1,309 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.nio;
+
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.config.TlsConfig;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionOperator;
+import org.apache.hc.client5.http.nio.ManagedAsyncClientConnection;
+import org.apache.hc.client5.http.ssl.ConscryptClientTlsStrategy;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http.config.RegistryBuilder;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.http.protocol.BasicHttpContext;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.reactor.ConnectionInitiator;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.mockito.stubbing.Answer;
+
+import java.net.InetAddress;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+
+public class HappyEyeballsV2AsyncClientConnectionOperatorTest {
+
+    private AsyncClientConnectionOperator happyEyeballsV2AsyncClientConnectionOperator;
+
+    private ConnectionInitiator connectionInitiator;
+
+    private DnsResolver dnsResolver;
+
+    private SocketAddress socketAddress;
+
+    private AsyncClientConnectionOperator connectionOperator;
+
+
+    @BeforeEach
+    public void setup() {
+
+        dnsResolver = Mockito.mock(DnsResolver.class);
+        connectionOperator = Mockito.mock(AsyncClientConnectionOperator.class);
+
+        happyEyeballsV2AsyncClientConnectionOperator = new HappyEyeballsV2AsyncClientConnectionOperator
+                (RegistryBuilder.<TlsStrategy>create().register(URIScheme.HTTPS.getId(), ConscryptClientTlsStrategy.getDefault()).build(),
+                        connectionOperator,
+                        dnsResolver,
+                        Timeout.ofSeconds(1),
+                        Timeout.ofSeconds(1),
+                        Timeout.ofSeconds(1),
+                        Timeout.ofSeconds(1),
+                        Timeout.ofSeconds(1),
+                        1,
+                        HappyEyeballsV2AsyncClientConnectionOperator.AddressFamily.IPv4);
+
+        connectionInitiator = mock(ConnectionInitiator.class);
+        socketAddress = mock(SocketAddress.class);
+
+
+    }
+
+    @DisplayName("Test that application prioritizes IPv6 over IPv4 when both are available")

Review Comment:
   Again, the sorting rules are more complicated than this. For example, native IPv4 addresses are preferred to IPv6 addresses that use an encapsulation/tunneling transport like 6to4 or Teredo. IPv6 addresses will also be considered unusable, and therefore deprioritized, if no IPv6 _source_ address is available.



##########
httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperator.java:
##########
@@ -0,0 +1,680 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.impl.nio;
+
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.SchemePortResolver;
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.impl.ConnPoolSupport;
+import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionOperator;
+import org.apache.hc.client5.http.nio.ManagedAsyncClientConnection;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.config.Lookup;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.reactor.ConnectionInitiator;
+import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.ConnectException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+
+/**
+ * The {@link AsyncClientConnectionOperator} implementation that uses Happy Eyeballs V2 algorithm to connect
+ * to the target server. Happy Eyeballs V2 (HEV2) algorithm is used to connect to the target server by concurrently
+ * attempting to establish multiple connections to different IP addresses. The first connection to complete
+ * successfully is selected and the others are closed. If all connections fail, the last error is rethrown.
+ * The algorithm also applies a configurable delay before subsequent connection attempts. HEV2 was introduced
+ * as a means to mitigate the latency issues caused by IPv4 and IPv6 co-existence in the Internet. HEV2 is defined
+ * in RFC 8305.
+ *
+ * <p>
+ * This connection operator maintains a connection pool for each unique route (combination of target host and
+ * target port) and selects the next connection from the pool to establish a new connection or reuse an
+ * existing connection. The connection pool uses a First-In-First-Out (FIFO) queue and has a configurable limit
+ * on the maximum number of connections that can be kept alive in the pool. Once the maximum number of connections
+ * has been reached, the oldest connection in the pool is closed to make room for a new one.
+ * </p>
+ *
+ * <p>
+ * This class is thread-safe and can be used in a multi-threaded environment.
+ * </p>
+ *
+ * <p>
+ * The HEV2 algorithm is configurable through the following parameters:
+ * <ul>
+ *   <li>{@code dualStackEnabled}: Whether to enable dual-stack connectivity. When set to {@code true},
+ *   the operator attempts to connect to both IPv4 and IPv6 addresses concurrently. When set to {@code false},
+ *   only IPv4 or IPv6 addresses are attempted depending on the address type of the target server.</li>
+ *   <li>{@code maxAttempts}: The maximum number of connection attempts to be made before failing. If all
+ *   attempts fail, the last error is rethrown.</li>
+ *   <li>{@code delay}: The delay (in milliseconds) to apply before subsequent connection attempts.</li>
+ *   <li>{@code connectTimeout}: The connection timeout (in milliseconds) for each attempt.</li>
+ * </ul>
+ * </p>
+ *
+ *
+ * <p>
+ * This class can be used with any {@link org.apache.hc.core5.http.nio.AsyncClientEndpoint} implementation
+ * that supports HTTP/1.1 or HTTP/2 protocols.
+ * </p>
+ *
+ * @since 5.3
+ */
+public class HappyEyeballsV2AsyncClientConnectionOperator implements AsyncClientConnectionOperator {
+
+    private static final Logger LOG = LoggerFactory.getLogger(AsyncClientConnectionOperator.class);
+
+    /**
+     * The underlying {@link AsyncClientConnectionOperator} that is used to establish connections
+     * to the target server.
+     */
+    private final AsyncClientConnectionOperator connectionOperator;
+
+    /**
+     * The DNS resolver used to resolve hostnames to IP addresses.
+     */
+    private final DnsResolver dnsResolver;
+
+    /**
+     * A lookup table used to determine the {@link TlsStrategy} to use for a given connection route.
+     */
+    private final Lookup<TlsStrategy> tlsStrategyLookup;
+
+    /**
+     * The default timeout for connection establishment attempts. If a connection cannot be established
+     * within this timeout, the attempt is considered failed.
+     */
+    private final Timeout timeout;
+
+    /**
+     * The minimum delay between connection establishment attempts.
+     */
+    private final Timeout minimumConnectionAttemptDelay;
+
+    /**
+     * The maximum delay between connection establishment attempts.
+     */
+    private final Timeout maximumConnectionAttemptDelay;
+
+    /**
+     * The current delay between connection establishment attempts.
+     */
+    private final Timeout connectionAttemptDelay;
+
+    /**
+     * The delay before resolution is started.
+     */
+    private final Timeout resolution_delay;
+
+    /**
+     * The number of IP addresses of each address family to include in the initial list of
+     * IP addresses to attempt connections to. This value is set to 2 by default, but can be
+     * increased to more aggressively favor a particular address family (e.g. set to 4 for IPv6).
+     */
+    private final int firstAddressFamilyCount;
+
+    /**
+     * The address family to use for establishing connections. This can be set to either
+     * {@link AddressFamily#IPv4} or {@link AddressFamily#IPv6}.
+     */
+    private final AddressFamily addressFamily;
+
+
+    /**
+     * The AddressFamily enum represents the possible address families that can be used when attempting to establish
+     * <p>
+     * connections using the Happy Eyeballs V2 algorithm.
+     *
+     * <p>
+     * The Happy Eyeballs V2 algorithm allows for concurrent connection attempts to be made to different IP addresses,
+     * <p>
+     * so this enum specifies whether connections should be attempted using IPv4 or IPv6 addresses.
+     *
+     * </p>
+     */
+    public enum AddressFamily {
+        IPv4, IPv6
+    }
+
+    /**
+     * Constructs a new {@link HappyEyeballsV2AsyncClientConnectionOperator} with the specified parameters.
+     *
+     * @param tlsStrategyLookup             the lookup object used to retrieve a {@link TlsStrategy} for a given {@link Route}
+     * @param connectionOperator            the underlying {@link AsyncClientConnectionOperator} to use for establishing connections
+     * @param dnsResolver                   the {@link DnsResolver} to use for resolving target hostnames
+     * @param timeout                       the timeout duration for establishing a connection
+     * @param resolution_delay              the configurable delay before subsequent DNS resolution attempts
+     * @param minimumConnectionAttemptDelay the minimum configurable delay between connection attempts
+     * @param maximumConnectionAttemptDelay the maximum configurable delay between connection attempts
+     * @param connectionAttemptDelay        the configurable delay before attempting to establish a connection
+     * @param firstAddressFamilyCount       the number of initial address families to use for establishing a connection
+     * @param addressFamily                 the preferred address family to use for establishing a connection
+     * @throws IllegalArgumentException if {@code firstAddressFamilyCount} is not positive
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(final Lookup<TlsStrategy> tlsStrategyLookup,
+                                                        final AsyncClientConnectionOperator connectionOperator,
+                                                        final DnsResolver dnsResolver,
+                                                        final Timeout timeout,
+                                                        final Timeout resolution_delay,
+                                                        final Timeout minimumConnectionAttemptDelay,
+                                                        final Timeout maximumConnectionAttemptDelay,
+                                                        final Timeout connectionAttemptDelay,
+                                                        final int firstAddressFamilyCount,
+                                                        final AddressFamily addressFamily) {
+        this.tlsStrategyLookup = Args.notNull(tlsStrategyLookup, "TLS strategy lookup");
+        this.connectionOperator = Args.notNull(connectionOperator, "Connection operator");
+        this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE;
+        this.timeout = timeout != null ? timeout : Timeout.ofMilliseconds(250);
+        this.resolution_delay = resolution_delay != null ? resolution_delay : Timeout.ofMilliseconds(50);
+        this.minimumConnectionAttemptDelay = minimumConnectionAttemptDelay != null ? minimumConnectionAttemptDelay : Timeout.ofMilliseconds(100);
+        this.maximumConnectionAttemptDelay = maximumConnectionAttemptDelay != null ? maximumConnectionAttemptDelay : Timeout.ofSeconds(2);
+        this.connectionAttemptDelay = connectionAttemptDelay != null ? connectionAttemptDelay : Timeout.ofMilliseconds(250);
+        this.firstAddressFamilyCount = Args.positive(firstAddressFamilyCount, "firstAddressFamilyCount");
+        this.addressFamily = addressFamily;
+    }
+
+    /**
+     * Constructs a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the specified
+     * {@link Lookup} for {@link TlsStrategy} and {@link SchemePortResolver} and {@link DnsResolver}.
+     * <p>
+     * The constructor internally creates a new instance of {@link DefaultAsyncClientConnectionOperator} with the
+     * specified {@link Lookup} for {@link TlsStrategy}, {@link SchemePortResolver} and {@link DnsResolver}. The
+     * created {@link AsyncClientConnectionOperator} is then passed to the main constructor along with default values
+     * for other parameters.
+     * </p>
+     *
+     * @param tlsStrategyLookup  The {@link Lookup} for {@link TlsStrategy}.
+     * @param schemePortResolver The {@link SchemePortResolver} to use for resolving scheme ports.
+     * @param dnsResolver        The {@link DnsResolver} to use for resolving hostnames to IP addresses.
+     * @throws IllegalArgumentException if the {@code tlsStrategyLookup} or {@code schemePortResolver} or {@code dnsResolver} parameter is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver,
+            final DnsResolver dnsResolver) {
+        this(tlsStrategyLookup,
+                new DefaultAsyncClientConnectionOperator(tlsStrategyLookup, schemePortResolver, dnsResolver),
+                dnsResolver,
+                null,
+                null,
+                null,
+                null,
+                null,
+                1,
+                AddressFamily.IPv6);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup
+     * and scheme-port resolver. The DNS resolver will be set to the system default resolver.
+     *
+     * @param tlsStrategyLookup  The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @param schemePortResolver The resolver instance for mapping scheme names to default port numbers.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver) {
+        this(tlsStrategyLookup, schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup.
+     * The scheme-port resolver and DNS resolver will be set to their default instances.
+     *
+     * @param tlsStrategyLookup The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup) {
+        this(tlsStrategyLookup, DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+
+    /**
+     * Attempts to connect to the given host and returns a Future that will be completed when the connection is established
+     * or when an error occurs. This method may attempt to connect to multiple IP addresses associated with the host,
+     * depending on the address family and the number of connection attempts to execute. The address family and number of
+     * connection attempts can be configured by calling the corresponding setters on this class.
+     *
+     * @param connectionInitiator the connection initiator to use when creating the connection
+     * @param host                the host to connect to
+     * @param localAddress        the local address to bind to when connecting, or null to use any available local address
+     * @param connectTimeout      the timeout to use when connecting, or null to use the default timeout
+     * @param attachment          the attachment to associate with the connection, or null if no attachment is needed
+     * @param callback            the callback to invoke when the connection is established or an error occurs, or null if no callback is needed
+     * @return a Future that will be completed when the connection is established or when an error occurs
+     */
+    @Override
+    public Future<ManagedAsyncClientConnection> connect(
+            final ConnectionInitiator connectionInitiator,
+            final HttpHost host,
+            final SocketAddress localAddress,
+            final Timeout connectTimeout,
+            final Object attachment,
+            final FutureCallback<ManagedAsyncClientConnection> callback) {
+
+        final CompletableFuture<ManagedAsyncClientConnection> connectionFuture = new CompletableFuture<>();
+
+        final Timeout conTimeout = connectTimeout != null ? connectTimeout : timeout;
+
+        resolveDnsAsync(host.getHostName())
+                .thenCompose(inetAddresses -> {
+                    final List<InetAddress> ipv4Addresses = new ArrayList<>();
+                    final List<InetAddress> ipv6Addresses = new ArrayList<>();
+
+                    for (final InetAddress inetAddress : inetAddresses) {
+                        if (inetAddress instanceof Inet4Address) {
+                            ipv4Addresses.add(inetAddress);
+                        } else if (inetAddress instanceof Inet6Address) {
+                            ipv6Addresses.add(inetAddress);
+                        }
+                    }
+
+                    sortAndInterleave(inetAddresses);
+
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> connectionFutures = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> attempts = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    if (addressFamily == AddressFamily.IPv4 && !ipv4Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                        }
+                    } else if (addressFamily == AddressFamily.IPv6 && !ipv6Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                        }
+                    } else {
+                        if (!ipv4Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                            }
+                        }
+                        if (!ipv6Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                            }
+                        }
+                    }
+
+                    // Execute the connection attempts concurrently using CompletableFuture.anyOf
+                    return CompletableFuture.anyOf(attempts.toArray(new CompletableFuture[0]))
+                            .thenCompose(result -> {
+                                if (result instanceof ManagedAsyncClientConnection) {
+                                    // If there is a result, cancel all other attempts and complete the connectionFuture
+                                    connectionFutures.forEach(future -> future.cancel(true));
+                                    connectionFuture.complete((ManagedAsyncClientConnection) result);
+                                } else {
+                                    // If there is an exception, complete the connectionFuture exceptionally with the exception
+                                    connectionFuture.completeExceptionally(new ConnectException("Failed to connect to any address for " + host));
+                                }
+                                // Invoke the callback if provided
+                                if (callback != null) {
+                                    connectionFuture.whenComplete((conn, ex) -> {
+                                        if (ex != null) {
+                                            callback.failed(new Exception(ex));
+                                        } else {
+                                            callback.completed(conn);
+                                        }
+                                    });
+                                }
+                                return connectionFuture;
+                            });
+                })
+                .exceptionally(e -> {
+                    connectionFuture.completeExceptionally(e);
+                    if (callback != null) {
+                        callback.failed(new Exception(e));
+                    }
+                    return null;
+                });
+
+        return connectionFuture;
+    }
+
+    /**
+     * Asynchronously resolves the DNS for the given host name and returns a CompletableFuture that will be completed
+     * with an array of InetAddress objects representing the IP addresses of the host.
+     * The resolution of AAAA records is delayed by the configured resolution delay to allow for a chance for A records to be
+     * returned first.
+     *
+     * @param host the host name to resolve DNS for
+     * @return a CompletableFuture that will be completed with an array of InetAddress objects representing the IP addresses
+     */
+    private CompletableFuture<InetAddress[]> resolveDnsAsync(final String host) {
+        final CompletableFuture<InetAddress[]> dnsFuture = new CompletableFuture<>();
+        CompletableFuture.runAsync(() -> {
+            try {
+                final InetAddress[] inetAddresses = dnsResolver.resolve(host);
+                // Introduce a delay before resolving AAAA records after receiving A records
+                resolution_delay.sleep();
+                dnsFuture.complete(inetAddresses);
+            } catch (final UnknownHostException | InterruptedException e) {
+                if (LOG.isDebugEnabled()) {
+                    LOG.debug("Failed to resolve DNS for host '{}': {}", host, e.getMessage(), e);
+                }
+                dnsFuture.completeExceptionally(e);
+            }
+        });
+        return dnsFuture;
+    }
+
+
+    /**
+     * Initiates an asynchronous connection attempt to the given list of IP addresses for the specified {@link HttpHost}.
+     *
+     * @param connectionInitiator the {@link ConnectionInitiator} to use for establishing the connection
+     * @param host                the {@link HttpHost} to connect to
+     * @param connectTimeout      the timeout for the connection attempt
+     * @param attachment          the attachment object to pass to the connection operator
+     * @param addresses           the list of IP addresses to attempt to connect to
+     * @param localAddress        the local socket address to bind the connection to, or {@code null} if not binding
+     * @return a {@link CompletableFuture} that completes with a {@link ManagedAsyncClientConnection} if the connection attempt succeeds,
+     * or exceptionally with an exception if all attempts fail
+     */
+    private CompletableFuture<ManagedAsyncClientConnection> connectAttempt(
+            final ConnectionInitiator connectionInitiator,
+            final HttpHost host,
+            final Timeout connectTimeout,
+            final Object attachment,
+            final List<InetAddress> addresses,
+            final SocketAddress localAddress) {
+
+        final CompletableFuture<ManagedAsyncClientConnection> connectionFuture = new CompletableFuture<>();
+
+        // Create a list of connection attempts to execute
+        final List<CompletableFuture<Void>> attempts = new ArrayList<>();
+        for (int i = 0; i < addresses.size(); i++) {
+            final InetAddress address = addresses.get(i);
+
+            if (LOG.isDebugEnabled()) {
+                LOG.info("Attempting to connect to {}", address);
+            }
+
+            final CompletableFuture<Void> attempt = new CompletableFuture<>();
+            attempts.add(attempt);
+            final HttpHost currentHost = new HttpHost(host.getSchemeName(), address, host.getHostName(), host.getPort());
+
+            connectionOperator.connect(
+                    connectionInitiator,
+                    currentHost,
+                    localAddress,
+                    connectTimeout,
+                    attachment,
+                    new FutureCallback<ManagedAsyncClientConnection>() {
+                        @Override
+                        public void completed(final ManagedAsyncClientConnection connection) {
+                            if (LOG.isDebugEnabled()) {
+                                LOG.debug("Successfully connected {}", ConnPoolSupport.getId(connection));
+                            }
+                            connectionFuture.complete(connection);
+                        }
+
+                        @Override
+                        public void failed(final Exception ex) {
+                            if (LOG.isDebugEnabled()) {
+                                LOG.debug("Failed to connect  {}", ConnPoolSupport.getId(address), ex);
+                            }
+                            attempt.completeExceptionally(ex);
+                        }
+
+                        @Override
+                        public void cancelled() {
+                            if (LOG.isDebugEnabled()) {
+                                LOG.debug("Cancelled connect for {}", ConnPoolSupport.getId(address));
+                            }
+                            attempt.cancel(true);
+                        }
+                    });
+
+            // Introduce a delay before executing the next connection attempt
+            if (i < addresses.size() - 1) {
+                try {
+                    final Duration delay = calculateDelay(i);
+                    delay.wait();

Review Comment:
   What thread is this code running in? This will block for up to 2,000 milliseconds.



##########
httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/InetAddressComparator.java:
##########
@@ -0,0 +1,89 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.nio;
+
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.Internal;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.Comparator;
+/**
+ * Comparator that orders IPv6 addresses before IPv4 addresses, and then based on their byte values.
+ */
+@Contract(threading = ThreadingBehavior.STATELESS)
+@Internal
+class InetAddressComparator implements Comparator<InetAddress> {
+
+    /**
+     * Singleton instance of the comparator.
+     */
+    public static final InetAddressComparator INSTANCE = new InetAddressComparator();
+
+    /**
+     * Compares the specified IPv4 and IPv6 addresses.
+     *
+     * @param adr1 the first address to be compared
+     * @param adr2 the second address to be compared
+     * @return a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater
+     *         than the second
+     * @throws NullPointerException if either argument is null
+     */
+    @Override
+    public int compare(final InetAddress adr1, final InetAddress adr2) {
+        if (adr1 == null && adr2 == null) {
+            return 0;
+        }
+        if (adr1 == null) {
+            return -1;
+        }
+        if (adr2 == null) {
+            return 1;
+        }
+        // Compare IPv6 addresses first
+        if (adr1 instanceof Inet6Address && adr2 instanceof Inet4Address) {
+            return 1;
+        }
+        if (adr1 instanceof Inet4Address && adr2 instanceof Inet6Address) {
+            return -1;
+        }
+        // Compare based on address bytes

Review Comment:
   This appears to have nothing to do with the [address sorting algorithm](http://biplane.com.au/blog/?p=141) specified by [RFC 6724](https://www.rfc-editor.org/rfc/rfc6724#section-6), which is [mandated](https://www.rfc-editor.org/rfc/rfc8305#section-4) by RFC 8305. Furthermore, sorting addresses in this way is a bad idea, since it will defeat [round-robin DNS](https://en.wikipedia.org/wiki/Round-robin_DNS), an important load balancing technique.



##########
httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperator.java:
##########
@@ -0,0 +1,680 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.impl.nio;
+
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.SchemePortResolver;
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.impl.ConnPoolSupport;
+import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionOperator;
+import org.apache.hc.client5.http.nio.ManagedAsyncClientConnection;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.config.Lookup;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.reactor.ConnectionInitiator;
+import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.ConnectException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+
+/**
+ * The {@link AsyncClientConnectionOperator} implementation that uses Happy Eyeballs V2 algorithm to connect
+ * to the target server. Happy Eyeballs V2 (HEV2) algorithm is used to connect to the target server by concurrently
+ * attempting to establish multiple connections to different IP addresses. The first connection to complete
+ * successfully is selected and the others are closed. If all connections fail, the last error is rethrown.
+ * The algorithm also applies a configurable delay before subsequent connection attempts. HEV2 was introduced
+ * as a means to mitigate the latency issues caused by IPv4 and IPv6 co-existence in the Internet. HEV2 is defined
+ * in RFC 8305.
+ *
+ * <p>
+ * This connection operator maintains a connection pool for each unique route (combination of target host and
+ * target port) and selects the next connection from the pool to establish a new connection or reuse an
+ * existing connection. The connection pool uses a First-In-First-Out (FIFO) queue and has a configurable limit
+ * on the maximum number of connections that can be kept alive in the pool. Once the maximum number of connections
+ * has been reached, the oldest connection in the pool is closed to make room for a new one.
+ * </p>
+ *
+ * <p>
+ * This class is thread-safe and can be used in a multi-threaded environment.
+ * </p>
+ *
+ * <p>
+ * The HEV2 algorithm is configurable through the following parameters:
+ * <ul>
+ *   <li>{@code dualStackEnabled}: Whether to enable dual-stack connectivity. When set to {@code true},
+ *   the operator attempts to connect to both IPv4 and IPv6 addresses concurrently. When set to {@code false},
+ *   only IPv4 or IPv6 addresses are attempted depending on the address type of the target server.</li>
+ *   <li>{@code maxAttempts}: The maximum number of connection attempts to be made before failing. If all
+ *   attempts fail, the last error is rethrown.</li>
+ *   <li>{@code delay}: The delay (in milliseconds) to apply before subsequent connection attempts.</li>
+ *   <li>{@code connectTimeout}: The connection timeout (in milliseconds) for each attempt.</li>
+ * </ul>
+ * </p>
+ *
+ *
+ * <p>
+ * This class can be used with any {@link org.apache.hc.core5.http.nio.AsyncClientEndpoint} implementation
+ * that supports HTTP/1.1 or HTTP/2 protocols.
+ * </p>
+ *
+ * @since 5.3
+ */
+public class HappyEyeballsV2AsyncClientConnectionOperator implements AsyncClientConnectionOperator {
+
+    private static final Logger LOG = LoggerFactory.getLogger(AsyncClientConnectionOperator.class);
+
+    /**
+     * The underlying {@link AsyncClientConnectionOperator} that is used to establish connections
+     * to the target server.
+     */
+    private final AsyncClientConnectionOperator connectionOperator;
+
+    /**
+     * The DNS resolver used to resolve hostnames to IP addresses.
+     */
+    private final DnsResolver dnsResolver;
+
+    /**
+     * A lookup table used to determine the {@link TlsStrategy} to use for a given connection route.
+     */
+    private final Lookup<TlsStrategy> tlsStrategyLookup;
+
+    /**
+     * The default timeout for connection establishment attempts. If a connection cannot be established
+     * within this timeout, the attempt is considered failed.
+     */
+    private final Timeout timeout;
+
+    /**
+     * The minimum delay between connection establishment attempts.
+     */
+    private final Timeout minimumConnectionAttemptDelay;
+
+    /**
+     * The maximum delay between connection establishment attempts.
+     */
+    private final Timeout maximumConnectionAttemptDelay;
+
+    /**
+     * The current delay between connection establishment attempts.
+     */
+    private final Timeout connectionAttemptDelay;
+
+    /**
+     * The delay before resolution is started.
+     */
+    private final Timeout resolution_delay;
+
+    /**
+     * The number of IP addresses of each address family to include in the initial list of
+     * IP addresses to attempt connections to. This value is set to 2 by default, but can be
+     * increased to more aggressively favor a particular address family (e.g. set to 4 for IPv6).
+     */
+    private final int firstAddressFamilyCount;
+
+    /**
+     * The address family to use for establishing connections. This can be set to either
+     * {@link AddressFamily#IPv4} or {@link AddressFamily#IPv6}.
+     */
+    private final AddressFamily addressFamily;
+
+
+    /**
+     * The AddressFamily enum represents the possible address families that can be used when attempting to establish
+     * <p>
+     * connections using the Happy Eyeballs V2 algorithm.
+     *
+     * <p>
+     * The Happy Eyeballs V2 algorithm allows for concurrent connection attempts to be made to different IP addresses,
+     * <p>
+     * so this enum specifies whether connections should be attempted using IPv4 or IPv6 addresses.
+     *
+     * </p>
+     */
+    public enum AddressFamily {
+        IPv4, IPv6
+    }
+
+    /**
+     * Constructs a new {@link HappyEyeballsV2AsyncClientConnectionOperator} with the specified parameters.
+     *
+     * @param tlsStrategyLookup             the lookup object used to retrieve a {@link TlsStrategy} for a given {@link Route}
+     * @param connectionOperator            the underlying {@link AsyncClientConnectionOperator} to use for establishing connections
+     * @param dnsResolver                   the {@link DnsResolver} to use for resolving target hostnames
+     * @param timeout                       the timeout duration for establishing a connection
+     * @param resolution_delay              the configurable delay before subsequent DNS resolution attempts
+     * @param minimumConnectionAttemptDelay the minimum configurable delay between connection attempts
+     * @param maximumConnectionAttemptDelay the maximum configurable delay between connection attempts
+     * @param connectionAttemptDelay        the configurable delay before attempting to establish a connection
+     * @param firstAddressFamilyCount       the number of initial address families to use for establishing a connection
+     * @param addressFamily                 the preferred address family to use for establishing a connection
+     * @throws IllegalArgumentException if {@code firstAddressFamilyCount} is not positive
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(final Lookup<TlsStrategy> tlsStrategyLookup,
+                                                        final AsyncClientConnectionOperator connectionOperator,
+                                                        final DnsResolver dnsResolver,
+                                                        final Timeout timeout,
+                                                        final Timeout resolution_delay,
+                                                        final Timeout minimumConnectionAttemptDelay,
+                                                        final Timeout maximumConnectionAttemptDelay,
+                                                        final Timeout connectionAttemptDelay,
+                                                        final int firstAddressFamilyCount,
+                                                        final AddressFamily addressFamily) {
+        this.tlsStrategyLookup = Args.notNull(tlsStrategyLookup, "TLS strategy lookup");
+        this.connectionOperator = Args.notNull(connectionOperator, "Connection operator");
+        this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE;
+        this.timeout = timeout != null ? timeout : Timeout.ofMilliseconds(250);
+        this.resolution_delay = resolution_delay != null ? resolution_delay : Timeout.ofMilliseconds(50);
+        this.minimumConnectionAttemptDelay = minimumConnectionAttemptDelay != null ? minimumConnectionAttemptDelay : Timeout.ofMilliseconds(100);
+        this.maximumConnectionAttemptDelay = maximumConnectionAttemptDelay != null ? maximumConnectionAttemptDelay : Timeout.ofSeconds(2);
+        this.connectionAttemptDelay = connectionAttemptDelay != null ? connectionAttemptDelay : Timeout.ofMilliseconds(250);
+        this.firstAddressFamilyCount = Args.positive(firstAddressFamilyCount, "firstAddressFamilyCount");
+        this.addressFamily = addressFamily;
+    }
+
+    /**
+     * Constructs a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the specified
+     * {@link Lookup} for {@link TlsStrategy} and {@link SchemePortResolver} and {@link DnsResolver}.
+     * <p>
+     * The constructor internally creates a new instance of {@link DefaultAsyncClientConnectionOperator} with the
+     * specified {@link Lookup} for {@link TlsStrategy}, {@link SchemePortResolver} and {@link DnsResolver}. The
+     * created {@link AsyncClientConnectionOperator} is then passed to the main constructor along with default values
+     * for other parameters.
+     * </p>
+     *
+     * @param tlsStrategyLookup  The {@link Lookup} for {@link TlsStrategy}.
+     * @param schemePortResolver The {@link SchemePortResolver} to use for resolving scheme ports.
+     * @param dnsResolver        The {@link DnsResolver} to use for resolving hostnames to IP addresses.
+     * @throws IllegalArgumentException if the {@code tlsStrategyLookup} or {@code schemePortResolver} or {@code dnsResolver} parameter is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver,
+            final DnsResolver dnsResolver) {
+        this(tlsStrategyLookup,
+                new DefaultAsyncClientConnectionOperator(tlsStrategyLookup, schemePortResolver, dnsResolver),
+                dnsResolver,
+                null,
+                null,
+                null,
+                null,
+                null,
+                1,
+                AddressFamily.IPv6);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup
+     * and scheme-port resolver. The DNS resolver will be set to the system default resolver.
+     *
+     * @param tlsStrategyLookup  The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @param schemePortResolver The resolver instance for mapping scheme names to default port numbers.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver) {
+        this(tlsStrategyLookup, schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup.
+     * The scheme-port resolver and DNS resolver will be set to their default instances.
+     *
+     * @param tlsStrategyLookup The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup) {
+        this(tlsStrategyLookup, DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+
+    /**
+     * Attempts to connect to the given host and returns a Future that will be completed when the connection is established
+     * or when an error occurs. This method may attempt to connect to multiple IP addresses associated with the host,
+     * depending on the address family and the number of connection attempts to execute. The address family and number of
+     * connection attempts can be configured by calling the corresponding setters on this class.
+     *
+     * @param connectionInitiator the connection initiator to use when creating the connection
+     * @param host                the host to connect to
+     * @param localAddress        the local address to bind to when connecting, or null to use any available local address
+     * @param connectTimeout      the timeout to use when connecting, or null to use the default timeout
+     * @param attachment          the attachment to associate with the connection, or null if no attachment is needed
+     * @param callback            the callback to invoke when the connection is established or an error occurs, or null if no callback is needed
+     * @return a Future that will be completed when the connection is established or when an error occurs
+     */
+    @Override
+    public Future<ManagedAsyncClientConnection> connect(
+            final ConnectionInitiator connectionInitiator,
+            final HttpHost host,
+            final SocketAddress localAddress,
+            final Timeout connectTimeout,
+            final Object attachment,
+            final FutureCallback<ManagedAsyncClientConnection> callback) {
+
+        final CompletableFuture<ManagedAsyncClientConnection> connectionFuture = new CompletableFuture<>();
+
+        final Timeout conTimeout = connectTimeout != null ? connectTimeout : timeout;
+
+        resolveDnsAsync(host.getHostName())
+                .thenCompose(inetAddresses -> {
+                    final List<InetAddress> ipv4Addresses = new ArrayList<>();
+                    final List<InetAddress> ipv6Addresses = new ArrayList<>();
+
+                    for (final InetAddress inetAddress : inetAddresses) {
+                        if (inetAddress instanceof Inet4Address) {
+                            ipv4Addresses.add(inetAddress);
+                        } else if (inetAddress instanceof Inet6Address) {
+                            ipv6Addresses.add(inetAddress);
+                        }
+                    }
+
+                    sortAndInterleave(inetAddresses);
+
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> connectionFutures = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> attempts = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    if (addressFamily == AddressFamily.IPv4 && !ipv4Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                        }
+                    } else if (addressFamily == AddressFamily.IPv6 && !ipv6Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                        }
+                    } else {
+                        if (!ipv4Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                            }
+                        }
+                        if (!ipv6Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                            }
+                        }
+                    }
+
+                    // Execute the connection attempts concurrently using CompletableFuture.anyOf
+                    return CompletableFuture.anyOf(attempts.toArray(new CompletableFuture[0]))
+                            .thenCompose(result -> {
+                                if (result instanceof ManagedAsyncClientConnection) {
+                                    // If there is a result, cancel all other attempts and complete the connectionFuture
+                                    connectionFutures.forEach(future -> future.cancel(true));
+                                    connectionFuture.complete((ManagedAsyncClientConnection) result);
+                                } else {
+                                    // If there is an exception, complete the connectionFuture exceptionally with the exception
+                                    connectionFuture.completeExceptionally(new ConnectException("Failed to connect to any address for " + host));
+                                }
+                                // Invoke the callback if provided
+                                if (callback != null) {
+                                    connectionFuture.whenComplete((conn, ex) -> {
+                                        if (ex != null) {
+                                            callback.failed(new Exception(ex));
+                                        } else {
+                                            callback.completed(conn);
+                                        }
+                                    });
+                                }
+                                return connectionFuture;
+                            });
+                })
+                .exceptionally(e -> {
+                    connectionFuture.completeExceptionally(e);
+                    if (callback != null) {
+                        callback.failed(new Exception(e));
+                    }
+                    return null;
+                });
+
+        return connectionFuture;
+    }
+
+    /**
+     * Asynchronously resolves the DNS for the given host name and returns a CompletableFuture that will be completed
+     * with an array of InetAddress objects representing the IP addresses of the host.
+     * The resolution of AAAA records is delayed by the configured resolution delay to allow for a chance for A records to be
+     * returned first.
+     *
+     * @param host the host name to resolve DNS for
+     * @return a CompletableFuture that will be completed with an array of InetAddress objects representing the IP addresses
+     */
+    private CompletableFuture<InetAddress[]> resolveDnsAsync(final String host) {
+        final CompletableFuture<InetAddress[]> dnsFuture = new CompletableFuture<>();
+        CompletableFuture.runAsync(() -> {
+            try {
+                final InetAddress[] inetAddresses = dnsResolver.resolve(host);
+                // Introduce a delay before resolving AAAA records after receiving A records
+                resolution_delay.sleep();

Review Comment:
   I'm slightly concerned about async code, running in the shared `ForkJoinPool`, performing a blocking sleep of a configurable duration.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org
For additional commands, e-mail: dev-help@hc.apache.org


[GitHub] [httpcomponents-client] rhernandez35 commented on a diff in pull request #428: Implement HappyEyeballsV2AsyncClientConnectionOperator

Posted by "rhernandez35 (via GitHub)" <gi...@apache.org>.
rhernandez35 commented on code in PR #428:
URL: https://github.com/apache/httpcomponents-client/pull/428#discussion_r1157860807


##########
httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperator.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.impl.nio;
+
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.SchemePortResolver;
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.impl.ConnPoolSupport;
+import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionOperator;
+import org.apache.hc.client5.http.nio.ManagedAsyncClientConnection;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.config.Lookup;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.reactor.ConnectionInitiator;
+import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.ConnectException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The {@link AsyncClientConnectionOperator} implementation that uses Happy Eyeballs V2 algorithm to connect
+ * to the target server. Happy Eyeballs V2 (HEV2) algorithm is used to connect to the target server by concurrently
+ * attempting to establish multiple connections to different IP addresses. The first connection to complete
+ * successfully is selected and the others are closed. If all connections fail, the last error is rethrown.
+ * The algorithm also applies a configurable delay before subsequent connection attempts. HEV2 was introduced
+ * as a means to mitigate the latency issues caused by IPv4 and IPv6 co-existence in the Internet. HEV2 is defined
+ * in RFC 8305.
+ *
+ * <p>
+ * This connection operator maintains a connection pool for each unique route (combination of target host and
+ * target port) and selects the next connection from the pool to establish a new connection or reuse an
+ * existing connection. The connection pool uses a First-In-First-Out (FIFO) queue and has a configurable limit
+ * on the maximum number of connections that can be kept alive in the pool. Once the maximum number of connections
+ * has been reached, the oldest connection in the pool is closed to make room for a new one.
+ * </p>
+ *
+ * <p>
+ * This class is thread-safe and can be used in a multi-threaded environment.
+ * </p>
+ *
+ * <p>
+ * The HEV2 algorithm is configurable through the following parameters:
+ * <ul>
+ *   <li>{@code dualStackEnabled}: Whether to enable dual-stack connectivity. When set to {@code true},
+ *   the operator attempts to connect to both IPv4 and IPv6 addresses concurrently. When set to {@code false},
+ *   only IPv4 or IPv6 addresses are attempted depending on the address type of the target server.</li>
+ *   <li>{@code maxAttempts}: The maximum number of connection attempts to be made before failing. If all
+ *   attempts fail, the last error is rethrown.</li>
+ *   <li>{@code delay}: The delay (in milliseconds) to apply before subsequent connection attempts.</li>
+ *   <li>{@code connectTimeout}: The connection timeout (in milliseconds) for each attempt.</li>
+ * </ul>
+ * </p>
+ *
+ *
+ * <p>
+ * This class can be used with any {@link org.apache.hc.core5.http.nio.AsyncClientEndpoint} implementation
+ * that supports HTTP/1.1 or HTTP/2 protocols.
+ * </p>
+ *
+ * @since 5.3
+ */
+public class HappyEyeballsV2AsyncClientConnectionOperator implements AsyncClientConnectionOperator {
+
+    private static final Logger LOG = LoggerFactory.getLogger(AsyncClientConnectionOperator.class);
+
+    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
+
+    private final Duration resolutionDelay = Duration.ofMillis(500);
+
+
+
+    /**
+     * The underlying {@link AsyncClientConnectionOperator} that is used to establish connections
+     * to the target server.
+     */
+    private final AsyncClientConnectionOperator connectionOperator;
+
+    /**
+     * The DNS resolver used to resolve hostnames to IP addresses.
+     */
+    private final DnsResolver dnsResolver;
+
+    /**
+     * A lookup table used to determine the {@link TlsStrategy} to use for a given connection route.
+     */
+    private final Lookup<TlsStrategy> tlsStrategyLookup;
+
+    /**
+     * The default timeout for connection establishment attempts. If a connection cannot be established
+     * within this timeout, the attempt is considered failed.
+     */
+    private final Timeout timeout;
+
+    /**
+     * The minimum delay between connection establishment attempts.
+     */
+    private final Timeout minimumConnectionAttemptDelay;
+
+    /**
+     * The maximum delay between connection establishment attempts.
+     */
+    private final Timeout maximumConnectionAttemptDelay;
+
+    /**
+     * The current delay between connection establishment attempts.
+     */
+    private final Timeout connectionAttemptDelay;
+
+    /**
+     * The delay before resolution is started.
+     */
+    private final Timeout resolution_delay;
+
+    /**
+     * The number of IP addresses of each address family to include in the initial list of
+     * IP addresses to attempt connections to. This value is set to 2 by default, but can be
+     * increased to more aggressively favor a particular address family (e.g. set to 4 for IPv6).
+     */
+    private final int firstAddressFamilyCount;
+
+    /**
+     * The address family to use for establishing connections. This can be set to either
+     * {@link AddressFamily#IPv4} or {@link AddressFamily#IPv6}.
+     */
+    private final AddressFamily addressFamily;
+
+
+    /**
+     * The AddressFamily enum represents the possible address families that can be used when attempting to establish
+     * <p>
+     * connections using the Happy Eyeballs V2 algorithm.
+     *
+     * <p>
+     * The Happy Eyeballs V2 algorithm allows for concurrent connection attempts to be made to different IP addresses,
+     * <p>
+     * so this enum specifies whether connections should be attempted using IPv4 or IPv6 addresses.
+     *
+     * </p>
+     */
+    public enum AddressFamily {
+        IPv4, IPv6
+    }
+
+    /**
+     * Constructs a new {@link HappyEyeballsV2AsyncClientConnectionOperator} with the specified parameters.
+     *
+     * @param tlsStrategyLookup             the lookup object used to retrieve a {@link TlsStrategy} for a given {@link Route}
+     * @param connectionOperator            the underlying {@link AsyncClientConnectionOperator} to use for establishing connections
+     * @param dnsResolver                   the {@link DnsResolver} to use for resolving target hostnames
+     * @param timeout                       the timeout duration for establishing a connection
+     * @param resolution_delay              the configurable delay before subsequent DNS resolution attempts
+     * @param minimumConnectionAttemptDelay the minimum configurable delay between connection attempts
+     * @param maximumConnectionAttemptDelay the maximum configurable delay between connection attempts
+     * @param connectionAttemptDelay        the configurable delay before attempting to establish a connection
+     * @param firstAddressFamilyCount       the number of initial address families to use for establishing a connection
+     * @param addressFamily                 the preferred address family to use for establishing a connection
+     * @throws IllegalArgumentException if {@code firstAddressFamilyCount} is not positive
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(final Lookup<TlsStrategy> tlsStrategyLookup,
+                                                        final AsyncClientConnectionOperator connectionOperator,
+                                                        final DnsResolver dnsResolver,
+                                                        final Timeout timeout,
+                                                        final Timeout resolution_delay,
+                                                        final Timeout minimumConnectionAttemptDelay,
+                                                        final Timeout maximumConnectionAttemptDelay,
+                                                        final Timeout connectionAttemptDelay,
+                                                        final int firstAddressFamilyCount,
+                                                        final AddressFamily addressFamily) {
+        this.tlsStrategyLookup = Args.notNull(tlsStrategyLookup, "TLS strategy lookup");
+        this.connectionOperator = Args.notNull(connectionOperator, "Connection operator");
+        this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE;
+        this.timeout = timeout != null ? timeout : Timeout.ofMilliseconds(250);
+        this.resolution_delay = resolution_delay != null ? resolution_delay : Timeout.ofMilliseconds(50);
+        this.minimumConnectionAttemptDelay = minimumConnectionAttemptDelay != null ? minimumConnectionAttemptDelay : Timeout.ofMilliseconds(100);
+        this.maximumConnectionAttemptDelay = maximumConnectionAttemptDelay != null ? maximumConnectionAttemptDelay : Timeout.ofSeconds(2);
+        this.connectionAttemptDelay = connectionAttemptDelay != null ? connectionAttemptDelay : Timeout.ofMilliseconds(250);
+        this.firstAddressFamilyCount = Args.positive(firstAddressFamilyCount, "firstAddressFamilyCount");
+        this.addressFamily = addressFamily;
+    }
+
+    /**
+     * Constructs a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the specified
+     * {@link Lookup} for {@link TlsStrategy} and {@link SchemePortResolver} and {@link DnsResolver}.
+     * <p>
+     * The constructor internally creates a new instance of {@link DefaultAsyncClientConnectionOperator} with the
+     * specified {@link Lookup} for {@link TlsStrategy}, {@link SchemePortResolver} and {@link DnsResolver}. The
+     * created {@link AsyncClientConnectionOperator} is then passed to the main constructor along with default values
+     * for other parameters.
+     * </p>
+     *
+     * @param tlsStrategyLookup  The {@link Lookup} for {@link TlsStrategy}.
+     * @param schemePortResolver The {@link SchemePortResolver} to use for resolving scheme ports.
+     * @param dnsResolver        The {@link DnsResolver} to use for resolving hostnames to IP addresses.
+     * @throws IllegalArgumentException if the {@code tlsStrategyLookup} or {@code schemePortResolver} or {@code dnsResolver} parameter is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver,
+            final DnsResolver dnsResolver) {
+        this(tlsStrategyLookup,
+                new DefaultAsyncClientConnectionOperator(tlsStrategyLookup, schemePortResolver, dnsResolver),
+                dnsResolver,
+                null,
+                null,
+                null,
+                null,
+                null,
+                1,
+                AddressFamily.IPv6);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup
+     * and scheme-port resolver. The DNS resolver will be set to the system default resolver.
+     *
+     * @param tlsStrategyLookup  The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @param schemePortResolver The resolver instance for mapping scheme names to default port numbers.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver) {
+        this(tlsStrategyLookup, schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup.
+     * The scheme-port resolver and DNS resolver will be set to their default instances.
+     *
+     * @param tlsStrategyLookup The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup) {
+        this(tlsStrategyLookup, DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+
+    /**
+     * Attempts to connect to the given host and returns a Future that will be completed when the connection is established
+     * or when an error occurs. This method may attempt to connect to multiple IP addresses associated with the host,
+     * depending on the address family and the number of connection attempts to execute. The address family and number of
+     * connection attempts can be configured by calling the corresponding setters on this class.
+     *
+     * @param connectionInitiator the connection initiator to use when creating the connection
+     * @param host                the host to connect to
+     * @param localAddress        the local address to bind to when connecting, or null to use any available local address
+     * @param connectTimeout      the timeout to use when connecting, or null to use the default timeout
+     * @param attachment          the attachment to associate with the connection, or null if no attachment is needed
+     * @param callback            the callback to invoke when the connection is established or an error occurs, or null if no callback is needed
+     * @return a Future that will be completed when the connection is established or when an error occurs
+     */
+    @Override
+    public Future<ManagedAsyncClientConnection> connect(
+            final ConnectionInitiator connectionInitiator,
+            final HttpHost host,
+            final SocketAddress localAddress,
+            final Timeout connectTimeout,
+            final Object attachment,
+            final FutureCallback<ManagedAsyncClientConnection> callback) {
+
+        final CompletableFuture<ManagedAsyncClientConnection> connectionFuture = new CompletableFuture<>();
+
+        final Timeout conTimeout = connectTimeout != null ? connectTimeout : timeout;
+
+        resolveDnsAsync(host.getHostName())
+                .thenCompose(inetAddresses -> {
+                    final List<InetAddress> ipv4Addresses = new ArrayList<>();
+                    final List<InetAddress> ipv6Addresses = new ArrayList<>();
+
+                    for (final InetAddress inetAddress : inetAddresses) {
+                        if (inetAddress instanceof Inet4Address) {
+                            ipv4Addresses.add(inetAddress);
+                        } else if (inetAddress instanceof Inet6Address) {
+                            ipv6Addresses.add(inetAddress);
+                        }
+                    }
+
+                    sortAndInterleave(inetAddresses);
+
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> connectionFutures = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> attempts = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    if (addressFamily == AddressFamily.IPv4 && !ipv4Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                        }
+                    } else if (addressFamily == AddressFamily.IPv6 && !ipv6Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                        }
+                    } else {
+                        if (!ipv4Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                            }
+                        }
+                        if (!ipv6Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                            }
+                        }
+                    }
+
+                    // Execute the connection attempts concurrently using CompletableFuture.anyOf
+                    return CompletableFuture.anyOf(attempts.toArray(new CompletableFuture[0]))
+                            .thenCompose(result -> {
+                                if (result instanceof ManagedAsyncClientConnection) {
+                                    // If there is a result, cancel all other attempts and complete the connectionFuture
+                                    connectionFutures.forEach(future -> future.cancel(true));
+                                    connectionFuture.complete((ManagedAsyncClientConnection) result);
+                                } else {
+                                    // If there is an exception, complete the connectionFuture exceptionally with the exception
+                                    connectionFuture.completeExceptionally(new ConnectException("Failed to connect to any address for " + host));
+                                }
+                                // Invoke the callback if provided
+                                if (callback != null) {
+                                    connectionFuture.whenComplete((conn, ex) -> {
+                                        if (ex != null) {
+                                            callback.failed(new Exception(ex));
+                                        } else {
+                                            callback.completed(conn);
+                                        }
+                                    });
+                                }
+                                return connectionFuture;
+                            });
+                })
+                .exceptionally(e -> {
+                    connectionFuture.completeExceptionally(e);
+                    if (callback != null) {
+                        callback.failed(new Exception(e));
+                    }
+                    return null;
+                });
+
+        return connectionFuture;
+    }
+
+    /**
+     * Asynchronously resolves the DNS for the given host name and returns a CompletableFuture that will be completed
+     * with an array of InetAddress objects representing the IP addresses of the host.
+     * The resolution of AAAA records is delayed by the configured resolution delay to allow for a chance for A records to be
+     * returned first.
+     *
+     * @param host the host name to resolve DNS for
+     * @return a CompletableFuture that will be completed with an array of InetAddress objects representing the IP addresses
+     */
+    private CompletableFuture<InetAddress[]> resolveDnsAsync(final String host) {
+        final CompletableFuture<InetAddress[]> dnsFuture = new CompletableFuture<>();
+        CompletableFuture.runAsync(() -> {
+            try {
+                final InetAddress[] inetAddresses = dnsResolver.resolve(host);
+                dnsFuture.complete(inetAddresses);
+
+                // Introduce a delay before resolving AAAA records after receiving A records
+                if (inetAddresses.length > 0) {
+                    scheduler.schedule(() -> {
+                        try {
+                            final InetAddress[] inet6Addresses = dnsResolver.resolve(host);
+                            dnsFuture.complete(inet6Addresses);

Review Comment:
   You can't complete `dnsFuture` twice. The `complete` call on line 408 will always put `dnsFuture` into a finished state.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org
For additional commands, e-mail: dev-help@hc.apache.org


[GitHub] [httpcomponents-client] arturobernalg commented on pull request #428: Implement HappyEyeballsV2AsyncClientConnectionOperator

Posted by "arturobernalg (via GitHub)" <gi...@apache.org>.
arturobernalg commented on PR #428:
URL: https://github.com/apache/httpcomponents-client/pull/428#issuecomment-1500666698

   Hi @rschmitt 
   just wanted to let you know that I have implemented Destination Address Selection and improved the handling of async connections in HappyEyeballsV2AsyncClientConnectionOperator. 
   I have also added a new class to validate the HappyEyeballsV2 rules: HappyEyeballsV2RulesTest. Thought you might find it useful. 
   
   Let me know if you have any feedback. Thanks!


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org
For additional commands, e-mail: dev-help@hc.apache.org


[GitHub] [httpcomponents-client] arturobernalg commented on a diff in pull request #428: Implement HappyEyeballsV2AsyncClientConnectionOperator

Posted by "arturobernalg (via GitHub)" <gi...@apache.org>.
arturobernalg commented on code in PR #428:
URL: https://github.com/apache/httpcomponents-client/pull/428#discussion_r1157542710


##########
httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperator.java:
##########
@@ -0,0 +1,680 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.impl.nio;
+
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.SchemePortResolver;
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.impl.ConnPoolSupport;
+import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionOperator;
+import org.apache.hc.client5.http.nio.ManagedAsyncClientConnection;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.config.Lookup;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.reactor.ConnectionInitiator;
+import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.ConnectException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+
+/**
+ * The {@link AsyncClientConnectionOperator} implementation that uses Happy Eyeballs V2 algorithm to connect
+ * to the target server. Happy Eyeballs V2 (HEV2) algorithm is used to connect to the target server by concurrently
+ * attempting to establish multiple connections to different IP addresses. The first connection to complete
+ * successfully is selected and the others are closed. If all connections fail, the last error is rethrown.
+ * The algorithm also applies a configurable delay before subsequent connection attempts. HEV2 was introduced
+ * as a means to mitigate the latency issues caused by IPv4 and IPv6 co-existence in the Internet. HEV2 is defined
+ * in RFC 8305.
+ *
+ * <p>
+ * This connection operator maintains a connection pool for each unique route (combination of target host and
+ * target port) and selects the next connection from the pool to establish a new connection or reuse an
+ * existing connection. The connection pool uses a First-In-First-Out (FIFO) queue and has a configurable limit
+ * on the maximum number of connections that can be kept alive in the pool. Once the maximum number of connections
+ * has been reached, the oldest connection in the pool is closed to make room for a new one.
+ * </p>
+ *
+ * <p>
+ * This class is thread-safe and can be used in a multi-threaded environment.
+ * </p>
+ *
+ * <p>
+ * The HEV2 algorithm is configurable through the following parameters:
+ * <ul>
+ *   <li>{@code dualStackEnabled}: Whether to enable dual-stack connectivity. When set to {@code true},
+ *   the operator attempts to connect to both IPv4 and IPv6 addresses concurrently. When set to {@code false},
+ *   only IPv4 or IPv6 addresses are attempted depending on the address type of the target server.</li>
+ *   <li>{@code maxAttempts}: The maximum number of connection attempts to be made before failing. If all
+ *   attempts fail, the last error is rethrown.</li>
+ *   <li>{@code delay}: The delay (in milliseconds) to apply before subsequent connection attempts.</li>
+ *   <li>{@code connectTimeout}: The connection timeout (in milliseconds) for each attempt.</li>
+ * </ul>
+ * </p>
+ *
+ *
+ * <p>
+ * This class can be used with any {@link org.apache.hc.core5.http.nio.AsyncClientEndpoint} implementation
+ * that supports HTTP/1.1 or HTTP/2 protocols.
+ * </p>
+ *
+ * @since 5.3
+ */
+public class HappyEyeballsV2AsyncClientConnectionOperator implements AsyncClientConnectionOperator {
+
+    private static final Logger LOG = LoggerFactory.getLogger(AsyncClientConnectionOperator.class);
+
+    /**
+     * The underlying {@link AsyncClientConnectionOperator} that is used to establish connections
+     * to the target server.
+     */
+    private final AsyncClientConnectionOperator connectionOperator;
+
+    /**
+     * The DNS resolver used to resolve hostnames to IP addresses.
+     */
+    private final DnsResolver dnsResolver;
+
+    /**
+     * A lookup table used to determine the {@link TlsStrategy} to use for a given connection route.
+     */
+    private final Lookup<TlsStrategy> tlsStrategyLookup;
+
+    /**
+     * The default timeout for connection establishment attempts. If a connection cannot be established
+     * within this timeout, the attempt is considered failed.
+     */
+    private final Timeout timeout;
+
+    /**
+     * The minimum delay between connection establishment attempts.
+     */
+    private final Timeout minimumConnectionAttemptDelay;
+
+    /**
+     * The maximum delay between connection establishment attempts.
+     */
+    private final Timeout maximumConnectionAttemptDelay;
+
+    /**
+     * The current delay between connection establishment attempts.
+     */
+    private final Timeout connectionAttemptDelay;
+
+    /**
+     * The delay before resolution is started.
+     */
+    private final Timeout resolution_delay;
+
+    /**
+     * The number of IP addresses of each address family to include in the initial list of
+     * IP addresses to attempt connections to. This value is set to 2 by default, but can be
+     * increased to more aggressively favor a particular address family (e.g. set to 4 for IPv6).
+     */
+    private final int firstAddressFamilyCount;
+
+    /**
+     * The address family to use for establishing connections. This can be set to either
+     * {@link AddressFamily#IPv4} or {@link AddressFamily#IPv6}.
+     */
+    private final AddressFamily addressFamily;
+
+
+    /**
+     * The AddressFamily enum represents the possible address families that can be used when attempting to establish
+     * <p>
+     * connections using the Happy Eyeballs V2 algorithm.
+     *
+     * <p>
+     * The Happy Eyeballs V2 algorithm allows for concurrent connection attempts to be made to different IP addresses,
+     * <p>
+     * so this enum specifies whether connections should be attempted using IPv4 or IPv6 addresses.
+     *
+     * </p>
+     */
+    public enum AddressFamily {
+        IPv4, IPv6
+    }
+
+    /**
+     * Constructs a new {@link HappyEyeballsV2AsyncClientConnectionOperator} with the specified parameters.
+     *
+     * @param tlsStrategyLookup             the lookup object used to retrieve a {@link TlsStrategy} for a given {@link Route}
+     * @param connectionOperator            the underlying {@link AsyncClientConnectionOperator} to use for establishing connections
+     * @param dnsResolver                   the {@link DnsResolver} to use for resolving target hostnames
+     * @param timeout                       the timeout duration for establishing a connection
+     * @param resolution_delay              the configurable delay before subsequent DNS resolution attempts
+     * @param minimumConnectionAttemptDelay the minimum configurable delay between connection attempts
+     * @param maximumConnectionAttemptDelay the maximum configurable delay between connection attempts
+     * @param connectionAttemptDelay        the configurable delay before attempting to establish a connection
+     * @param firstAddressFamilyCount       the number of initial address families to use for establishing a connection
+     * @param addressFamily                 the preferred address family to use for establishing a connection
+     * @throws IllegalArgumentException if {@code firstAddressFamilyCount} is not positive
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(final Lookup<TlsStrategy> tlsStrategyLookup,
+                                                        final AsyncClientConnectionOperator connectionOperator,
+                                                        final DnsResolver dnsResolver,
+                                                        final Timeout timeout,
+                                                        final Timeout resolution_delay,
+                                                        final Timeout minimumConnectionAttemptDelay,
+                                                        final Timeout maximumConnectionAttemptDelay,
+                                                        final Timeout connectionAttemptDelay,
+                                                        final int firstAddressFamilyCount,
+                                                        final AddressFamily addressFamily) {
+        this.tlsStrategyLookup = Args.notNull(tlsStrategyLookup, "TLS strategy lookup");
+        this.connectionOperator = Args.notNull(connectionOperator, "Connection operator");
+        this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE;
+        this.timeout = timeout != null ? timeout : Timeout.ofMilliseconds(250);
+        this.resolution_delay = resolution_delay != null ? resolution_delay : Timeout.ofMilliseconds(50);
+        this.minimumConnectionAttemptDelay = minimumConnectionAttemptDelay != null ? minimumConnectionAttemptDelay : Timeout.ofMilliseconds(100);
+        this.maximumConnectionAttemptDelay = maximumConnectionAttemptDelay != null ? maximumConnectionAttemptDelay : Timeout.ofSeconds(2);
+        this.connectionAttemptDelay = connectionAttemptDelay != null ? connectionAttemptDelay : Timeout.ofMilliseconds(250);
+        this.firstAddressFamilyCount = Args.positive(firstAddressFamilyCount, "firstAddressFamilyCount");
+        this.addressFamily = addressFamily;
+    }
+
+    /**
+     * Constructs a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the specified
+     * {@link Lookup} for {@link TlsStrategy} and {@link SchemePortResolver} and {@link DnsResolver}.
+     * <p>
+     * The constructor internally creates a new instance of {@link DefaultAsyncClientConnectionOperator} with the
+     * specified {@link Lookup} for {@link TlsStrategy}, {@link SchemePortResolver} and {@link DnsResolver}. The
+     * created {@link AsyncClientConnectionOperator} is then passed to the main constructor along with default values
+     * for other parameters.
+     * </p>
+     *
+     * @param tlsStrategyLookup  The {@link Lookup} for {@link TlsStrategy}.
+     * @param schemePortResolver The {@link SchemePortResolver} to use for resolving scheme ports.
+     * @param dnsResolver        The {@link DnsResolver} to use for resolving hostnames to IP addresses.
+     * @throws IllegalArgumentException if the {@code tlsStrategyLookup} or {@code schemePortResolver} or {@code dnsResolver} parameter is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver,
+            final DnsResolver dnsResolver) {
+        this(tlsStrategyLookup,
+                new DefaultAsyncClientConnectionOperator(tlsStrategyLookup, schemePortResolver, dnsResolver),
+                dnsResolver,
+                null,
+                null,
+                null,
+                null,
+                null,
+                1,
+                AddressFamily.IPv6);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup
+     * and scheme-port resolver. The DNS resolver will be set to the system default resolver.
+     *
+     * @param tlsStrategyLookup  The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @param schemePortResolver The resolver instance for mapping scheme names to default port numbers.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver) {
+        this(tlsStrategyLookup, schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup.
+     * The scheme-port resolver and DNS resolver will be set to their default instances.
+     *
+     * @param tlsStrategyLookup The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup) {
+        this(tlsStrategyLookup, DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+
+    /**
+     * Attempts to connect to the given host and returns a Future that will be completed when the connection is established
+     * or when an error occurs. This method may attempt to connect to multiple IP addresses associated with the host,
+     * depending on the address family and the number of connection attempts to execute. The address family and number of
+     * connection attempts can be configured by calling the corresponding setters on this class.
+     *
+     * @param connectionInitiator the connection initiator to use when creating the connection
+     * @param host                the host to connect to
+     * @param localAddress        the local address to bind to when connecting, or null to use any available local address
+     * @param connectTimeout      the timeout to use when connecting, or null to use the default timeout
+     * @param attachment          the attachment to associate with the connection, or null if no attachment is needed
+     * @param callback            the callback to invoke when the connection is established or an error occurs, or null if no callback is needed
+     * @return a Future that will be completed when the connection is established or when an error occurs
+     */
+    @Override
+    public Future<ManagedAsyncClientConnection> connect(
+            final ConnectionInitiator connectionInitiator,
+            final HttpHost host,
+            final SocketAddress localAddress,
+            final Timeout connectTimeout,
+            final Object attachment,
+            final FutureCallback<ManagedAsyncClientConnection> callback) {
+
+        final CompletableFuture<ManagedAsyncClientConnection> connectionFuture = new CompletableFuture<>();
+
+        final Timeout conTimeout = connectTimeout != null ? connectTimeout : timeout;
+
+        resolveDnsAsync(host.getHostName())
+                .thenCompose(inetAddresses -> {
+                    final List<InetAddress> ipv4Addresses = new ArrayList<>();
+                    final List<InetAddress> ipv6Addresses = new ArrayList<>();
+
+                    for (final InetAddress inetAddress : inetAddresses) {
+                        if (inetAddress instanceof Inet4Address) {
+                            ipv4Addresses.add(inetAddress);
+                        } else if (inetAddress instanceof Inet6Address) {
+                            ipv6Addresses.add(inetAddress);
+                        }
+                    }
+
+                    sortAndInterleave(inetAddresses);
+
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> connectionFutures = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> attempts = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    if (addressFamily == AddressFamily.IPv4 && !ipv4Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                        }
+                    } else if (addressFamily == AddressFamily.IPv6 && !ipv6Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                        }
+                    } else {
+                        if (!ipv4Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                            }
+                        }
+                        if (!ipv6Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                            }
+                        }
+                    }
+
+                    // Execute the connection attempts concurrently using CompletableFuture.anyOf
+                    return CompletableFuture.anyOf(attempts.toArray(new CompletableFuture[0]))
+                            .thenCompose(result -> {
+                                if (result instanceof ManagedAsyncClientConnection) {
+                                    // If there is a result, cancel all other attempts and complete the connectionFuture
+                                    connectionFutures.forEach(future -> future.cancel(true));
+                                    connectionFuture.complete((ManagedAsyncClientConnection) result);
+                                } else {
+                                    // If there is an exception, complete the connectionFuture exceptionally with the exception
+                                    connectionFuture.completeExceptionally(new ConnectException("Failed to connect to any address for " + host));
+                                }
+                                // Invoke the callback if provided
+                                if (callback != null) {
+                                    connectionFuture.whenComplete((conn, ex) -> {
+                                        if (ex != null) {
+                                            callback.failed(new Exception(ex));
+                                        } else {
+                                            callback.completed(conn);
+                                        }
+                                    });
+                                }
+                                return connectionFuture;
+                            });
+                })
+                .exceptionally(e -> {
+                    connectionFuture.completeExceptionally(e);
+                    if (callback != null) {
+                        callback.failed(new Exception(e));
+                    }
+                    return null;
+                });
+
+        return connectionFuture;
+    }
+
+    /**
+     * Asynchronously resolves the DNS for the given host name and returns a CompletableFuture that will be completed
+     * with an array of InetAddress objects representing the IP addresses of the host.
+     * The resolution of AAAA records is delayed by the configured resolution delay to allow for a chance for A records to be
+     * returned first.
+     *
+     * @param host the host name to resolve DNS for
+     * @return a CompletableFuture that will be completed with an array of InetAddress objects representing the IP addresses
+     */
+    private CompletableFuture<InetAddress[]> resolveDnsAsync(final String host) {
+        final CompletableFuture<InetAddress[]> dnsFuture = new CompletableFuture<>();
+        CompletableFuture.runAsync(() -> {
+            try {
+                final InetAddress[] inetAddresses = dnsResolver.resolve(host);
+                // Introduce a delay before resolving AAAA records after receiving A records
+                resolution_delay.sleep();
+                dnsFuture.complete(inetAddresses);
+            } catch (final UnknownHostException | InterruptedException e) {
+                if (LOG.isDebugEnabled()) {
+                    LOG.debug("Failed to resolve DNS for host '{}': {}", host, e.getMessage(), e);
+                }
+                dnsFuture.completeExceptionally(e);
+            }
+        });
+        return dnsFuture;
+    }
+
+
+    /**
+     * Initiates an asynchronous connection attempt to the given list of IP addresses for the specified {@link HttpHost}.
+     *
+     * @param connectionInitiator the {@link ConnectionInitiator} to use for establishing the connection
+     * @param host                the {@link HttpHost} to connect to
+     * @param connectTimeout      the timeout for the connection attempt
+     * @param attachment          the attachment object to pass to the connection operator
+     * @param addresses           the list of IP addresses to attempt to connect to
+     * @param localAddress        the local socket address to bind the connection to, or {@code null} if not binding
+     * @return a {@link CompletableFuture} that completes with a {@link ManagedAsyncClientConnection} if the connection attempt succeeds,
+     * or exceptionally with an exception if all attempts fail
+     */
+    private CompletableFuture<ManagedAsyncClientConnection> connectAttempt(
+            final ConnectionInitiator connectionInitiator,
+            final HttpHost host,
+            final Timeout connectTimeout,
+            final Object attachment,
+            final List<InetAddress> addresses,
+            final SocketAddress localAddress) {
+
+        final CompletableFuture<ManagedAsyncClientConnection> connectionFuture = new CompletableFuture<>();
+
+        // Create a list of connection attempts to execute
+        final List<CompletableFuture<Void>> attempts = new ArrayList<>();
+        for (int i = 0; i < addresses.size(); i++) {
+            final InetAddress address = addresses.get(i);
+
+            if (LOG.isDebugEnabled()) {
+                LOG.info("Attempting to connect to {}", address);
+            }
+
+            final CompletableFuture<Void> attempt = new CompletableFuture<>();
+            attempts.add(attempt);
+            final HttpHost currentHost = new HttpHost(host.getSchemeName(), address, host.getHostName(), host.getPort());
+
+            connectionOperator.connect(
+                    connectionInitiator,
+                    currentHost,
+                    localAddress,
+                    connectTimeout,
+                    attachment,
+                    new FutureCallback<ManagedAsyncClientConnection>() {
+                        @Override
+                        public void completed(final ManagedAsyncClientConnection connection) {
+                            if (LOG.isDebugEnabled()) {
+                                LOG.debug("Successfully connected {}", ConnPoolSupport.getId(connection));
+                            }
+                            connectionFuture.complete(connection);
+                        }
+
+                        @Override
+                        public void failed(final Exception ex) {
+                            if (LOG.isDebugEnabled()) {
+                                LOG.debug("Failed to connect  {}", ConnPoolSupport.getId(address), ex);
+                            }
+                            attempt.completeExceptionally(ex);
+                        }
+
+                        @Override
+                        public void cancelled() {
+                            if (LOG.isDebugEnabled()) {
+                                LOG.debug("Cancelled connect for {}", ConnPoolSupport.getId(address));
+                            }
+                            attempt.cancel(true);
+                        }
+                    });
+
+            // Introduce a delay before executing the next connection attempt
+            if (i < addresses.size() - 1) {
+                try {
+                    final Duration delay = calculateDelay(i);
+                    delay.wait();

Review Comment:
   The RFC recommends introducing a delay before starting the next connection attempt, but it doesn't mandate a specific implementation. The RFC suggests a default delay of 250 milliseconds, but also notes that a more nuanced implementation can use historical RTT data to influence the delay. Therefore, it is up to the implementation to decide whether to introduce a delay and how to calculate that delay.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org
For additional commands, e-mail: dev-help@hc.apache.org


[GitHub] [httpcomponents-client] arturobernalg commented on a diff in pull request #428: Implement HappyEyeballsV2AsyncClientConnectionOperator

Posted by "arturobernalg (via GitHub)" <gi...@apache.org>.
arturobernalg commented on code in PR #428:
URL: https://github.com/apache/httpcomponents-client/pull/428#discussion_r1157540582


##########
httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperator.java:
##########
@@ -0,0 +1,680 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.impl.nio;
+
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.SchemePortResolver;
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.impl.ConnPoolSupport;
+import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionOperator;
+import org.apache.hc.client5.http.nio.ManagedAsyncClientConnection;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.config.Lookup;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.reactor.ConnectionInitiator;
+import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.ConnectException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+
+/**
+ * The {@link AsyncClientConnectionOperator} implementation that uses Happy Eyeballs V2 algorithm to connect
+ * to the target server. Happy Eyeballs V2 (HEV2) algorithm is used to connect to the target server by concurrently
+ * attempting to establish multiple connections to different IP addresses. The first connection to complete
+ * successfully is selected and the others are closed. If all connections fail, the last error is rethrown.
+ * The algorithm also applies a configurable delay before subsequent connection attempts. HEV2 was introduced
+ * as a means to mitigate the latency issues caused by IPv4 and IPv6 co-existence in the Internet. HEV2 is defined
+ * in RFC 8305.
+ *
+ * <p>
+ * This connection operator maintains a connection pool for each unique route (combination of target host and
+ * target port) and selects the next connection from the pool to establish a new connection or reuse an
+ * existing connection. The connection pool uses a First-In-First-Out (FIFO) queue and has a configurable limit
+ * on the maximum number of connections that can be kept alive in the pool. Once the maximum number of connections
+ * has been reached, the oldest connection in the pool is closed to make room for a new one.
+ * </p>
+ *
+ * <p>
+ * This class is thread-safe and can be used in a multi-threaded environment.
+ * </p>
+ *
+ * <p>
+ * The HEV2 algorithm is configurable through the following parameters:
+ * <ul>
+ *   <li>{@code dualStackEnabled}: Whether to enable dual-stack connectivity. When set to {@code true},
+ *   the operator attempts to connect to both IPv4 and IPv6 addresses concurrently. When set to {@code false},
+ *   only IPv4 or IPv6 addresses are attempted depending on the address type of the target server.</li>
+ *   <li>{@code maxAttempts}: The maximum number of connection attempts to be made before failing. If all
+ *   attempts fail, the last error is rethrown.</li>
+ *   <li>{@code delay}: The delay (in milliseconds) to apply before subsequent connection attempts.</li>
+ *   <li>{@code connectTimeout}: The connection timeout (in milliseconds) for each attempt.</li>
+ * </ul>
+ * </p>
+ *
+ *
+ * <p>
+ * This class can be used with any {@link org.apache.hc.core5.http.nio.AsyncClientEndpoint} implementation
+ * that supports HTTP/1.1 or HTTP/2 protocols.
+ * </p>
+ *
+ * @since 5.3
+ */
+public class HappyEyeballsV2AsyncClientConnectionOperator implements AsyncClientConnectionOperator {
+
+    private static final Logger LOG = LoggerFactory.getLogger(AsyncClientConnectionOperator.class);
+
+    /**
+     * The underlying {@link AsyncClientConnectionOperator} that is used to establish connections
+     * to the target server.
+     */
+    private final AsyncClientConnectionOperator connectionOperator;
+
+    /**
+     * The DNS resolver used to resolve hostnames to IP addresses.
+     */
+    private final DnsResolver dnsResolver;
+
+    /**
+     * A lookup table used to determine the {@link TlsStrategy} to use for a given connection route.
+     */
+    private final Lookup<TlsStrategy> tlsStrategyLookup;
+
+    /**
+     * The default timeout for connection establishment attempts. If a connection cannot be established
+     * within this timeout, the attempt is considered failed.
+     */
+    private final Timeout timeout;
+
+    /**
+     * The minimum delay between connection establishment attempts.
+     */
+    private final Timeout minimumConnectionAttemptDelay;
+
+    /**
+     * The maximum delay between connection establishment attempts.
+     */
+    private final Timeout maximumConnectionAttemptDelay;
+
+    /**
+     * The current delay between connection establishment attempts.
+     */
+    private final Timeout connectionAttemptDelay;
+
+    /**
+     * The delay before resolution is started.
+     */
+    private final Timeout resolution_delay;
+
+    /**
+     * The number of IP addresses of each address family to include in the initial list of
+     * IP addresses to attempt connections to. This value is set to 2 by default, but can be
+     * increased to more aggressively favor a particular address family (e.g. set to 4 for IPv6).
+     */
+    private final int firstAddressFamilyCount;
+
+    /**
+     * The address family to use for establishing connections. This can be set to either
+     * {@link AddressFamily#IPv4} or {@link AddressFamily#IPv6}.
+     */
+    private final AddressFamily addressFamily;
+
+
+    /**
+     * The AddressFamily enum represents the possible address families that can be used when attempting to establish
+     * <p>
+     * connections using the Happy Eyeballs V2 algorithm.
+     *
+     * <p>
+     * The Happy Eyeballs V2 algorithm allows for concurrent connection attempts to be made to different IP addresses,
+     * <p>
+     * so this enum specifies whether connections should be attempted using IPv4 or IPv6 addresses.
+     *
+     * </p>
+     */
+    public enum AddressFamily {
+        IPv4, IPv6
+    }
+
+    /**
+     * Constructs a new {@link HappyEyeballsV2AsyncClientConnectionOperator} with the specified parameters.
+     *
+     * @param tlsStrategyLookup             the lookup object used to retrieve a {@link TlsStrategy} for a given {@link Route}
+     * @param connectionOperator            the underlying {@link AsyncClientConnectionOperator} to use for establishing connections
+     * @param dnsResolver                   the {@link DnsResolver} to use for resolving target hostnames
+     * @param timeout                       the timeout duration for establishing a connection
+     * @param resolution_delay              the configurable delay before subsequent DNS resolution attempts
+     * @param minimumConnectionAttemptDelay the minimum configurable delay between connection attempts
+     * @param maximumConnectionAttemptDelay the maximum configurable delay between connection attempts
+     * @param connectionAttemptDelay        the configurable delay before attempting to establish a connection
+     * @param firstAddressFamilyCount       the number of initial address families to use for establishing a connection
+     * @param addressFamily                 the preferred address family to use for establishing a connection
+     * @throws IllegalArgumentException if {@code firstAddressFamilyCount} is not positive
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(final Lookup<TlsStrategy> tlsStrategyLookup,
+                                                        final AsyncClientConnectionOperator connectionOperator,
+                                                        final DnsResolver dnsResolver,
+                                                        final Timeout timeout,
+                                                        final Timeout resolution_delay,
+                                                        final Timeout minimumConnectionAttemptDelay,
+                                                        final Timeout maximumConnectionAttemptDelay,
+                                                        final Timeout connectionAttemptDelay,
+                                                        final int firstAddressFamilyCount,
+                                                        final AddressFamily addressFamily) {
+        this.tlsStrategyLookup = Args.notNull(tlsStrategyLookup, "TLS strategy lookup");
+        this.connectionOperator = Args.notNull(connectionOperator, "Connection operator");
+        this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE;
+        this.timeout = timeout != null ? timeout : Timeout.ofMilliseconds(250);
+        this.resolution_delay = resolution_delay != null ? resolution_delay : Timeout.ofMilliseconds(50);
+        this.minimumConnectionAttemptDelay = minimumConnectionAttemptDelay != null ? minimumConnectionAttemptDelay : Timeout.ofMilliseconds(100);
+        this.maximumConnectionAttemptDelay = maximumConnectionAttemptDelay != null ? maximumConnectionAttemptDelay : Timeout.ofSeconds(2);
+        this.connectionAttemptDelay = connectionAttemptDelay != null ? connectionAttemptDelay : Timeout.ofMilliseconds(250);
+        this.firstAddressFamilyCount = Args.positive(firstAddressFamilyCount, "firstAddressFamilyCount");
+        this.addressFamily = addressFamily;
+    }
+
+    /**
+     * Constructs a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the specified
+     * {@link Lookup} for {@link TlsStrategy} and {@link SchemePortResolver} and {@link DnsResolver}.
+     * <p>
+     * The constructor internally creates a new instance of {@link DefaultAsyncClientConnectionOperator} with the
+     * specified {@link Lookup} for {@link TlsStrategy}, {@link SchemePortResolver} and {@link DnsResolver}. The
+     * created {@link AsyncClientConnectionOperator} is then passed to the main constructor along with default values
+     * for other parameters.
+     * </p>
+     *
+     * @param tlsStrategyLookup  The {@link Lookup} for {@link TlsStrategy}.
+     * @param schemePortResolver The {@link SchemePortResolver} to use for resolving scheme ports.
+     * @param dnsResolver        The {@link DnsResolver} to use for resolving hostnames to IP addresses.
+     * @throws IllegalArgumentException if the {@code tlsStrategyLookup} or {@code schemePortResolver} or {@code dnsResolver} parameter is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver,
+            final DnsResolver dnsResolver) {
+        this(tlsStrategyLookup,
+                new DefaultAsyncClientConnectionOperator(tlsStrategyLookup, schemePortResolver, dnsResolver),
+                dnsResolver,
+                null,
+                null,
+                null,
+                null,
+                null,
+                1,
+                AddressFamily.IPv6);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup
+     * and scheme-port resolver. The DNS resolver will be set to the system default resolver.
+     *
+     * @param tlsStrategyLookup  The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @param schemePortResolver The resolver instance for mapping scheme names to default port numbers.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver) {
+        this(tlsStrategyLookup, schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup.
+     * The scheme-port resolver and DNS resolver will be set to their default instances.
+     *
+     * @param tlsStrategyLookup The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup) {
+        this(tlsStrategyLookup, DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+
+    /**
+     * Attempts to connect to the given host and returns a Future that will be completed when the connection is established
+     * or when an error occurs. This method may attempt to connect to multiple IP addresses associated with the host,
+     * depending on the address family and the number of connection attempts to execute. The address family and number of
+     * connection attempts can be configured by calling the corresponding setters on this class.
+     *
+     * @param connectionInitiator the connection initiator to use when creating the connection
+     * @param host                the host to connect to
+     * @param localAddress        the local address to bind to when connecting, or null to use any available local address
+     * @param connectTimeout      the timeout to use when connecting, or null to use the default timeout
+     * @param attachment          the attachment to associate with the connection, or null if no attachment is needed
+     * @param callback            the callback to invoke when the connection is established or an error occurs, or null if no callback is needed
+     * @return a Future that will be completed when the connection is established or when an error occurs
+     */
+    @Override
+    public Future<ManagedAsyncClientConnection> connect(
+            final ConnectionInitiator connectionInitiator,
+            final HttpHost host,
+            final SocketAddress localAddress,
+            final Timeout connectTimeout,
+            final Object attachment,
+            final FutureCallback<ManagedAsyncClientConnection> callback) {
+
+        final CompletableFuture<ManagedAsyncClientConnection> connectionFuture = new CompletableFuture<>();
+
+        final Timeout conTimeout = connectTimeout != null ? connectTimeout : timeout;
+
+        resolveDnsAsync(host.getHostName())
+                .thenCompose(inetAddresses -> {
+                    final List<InetAddress> ipv4Addresses = new ArrayList<>();
+                    final List<InetAddress> ipv6Addresses = new ArrayList<>();
+
+                    for (final InetAddress inetAddress : inetAddresses) {
+                        if (inetAddress instanceof Inet4Address) {
+                            ipv4Addresses.add(inetAddress);
+                        } else if (inetAddress instanceof Inet6Address) {
+                            ipv6Addresses.add(inetAddress);
+                        }
+                    }
+
+                    sortAndInterleave(inetAddresses);
+
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> connectionFutures = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> attempts = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    if (addressFamily == AddressFamily.IPv4 && !ipv4Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                        }
+                    } else if (addressFamily == AddressFamily.IPv6 && !ipv6Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                        }
+                    } else {
+                        if (!ipv4Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                            }
+                        }
+                        if (!ipv6Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                            }
+                        }
+                    }
+
+                    // Execute the connection attempts concurrently using CompletableFuture.anyOf
+                    return CompletableFuture.anyOf(attempts.toArray(new CompletableFuture[0]))
+                            .thenCompose(result -> {
+                                if (result instanceof ManagedAsyncClientConnection) {
+                                    // If there is a result, cancel all other attempts and complete the connectionFuture
+                                    connectionFutures.forEach(future -> future.cancel(true));
+                                    connectionFuture.complete((ManagedAsyncClientConnection) result);
+                                } else {
+                                    // If there is an exception, complete the connectionFuture exceptionally with the exception
+                                    connectionFuture.completeExceptionally(new ConnectException("Failed to connect to any address for " + host));
+                                }
+                                // Invoke the callback if provided
+                                if (callback != null) {
+                                    connectionFuture.whenComplete((conn, ex) -> {
+                                        if (ex != null) {
+                                            callback.failed(new Exception(ex));
+                                        } else {
+                                            callback.completed(conn);
+                                        }
+                                    });
+                                }
+                                return connectionFuture;
+                            });
+                })
+                .exceptionally(e -> {
+                    connectionFuture.completeExceptionally(e);
+                    if (callback != null) {
+                        callback.failed(new Exception(e));
+                    }
+                    return null;
+                });
+
+        return connectionFuture;
+    }
+
+    /**
+     * Asynchronously resolves the DNS for the given host name and returns a CompletableFuture that will be completed
+     * with an array of InetAddress objects representing the IP addresses of the host.
+     * The resolution of AAAA records is delayed by the configured resolution delay to allow for a chance for A records to be
+     * returned first.
+     *
+     * @param host the host name to resolve DNS for
+     * @return a CompletableFuture that will be completed with an array of InetAddress objects representing the IP addresses
+     */
+    private CompletableFuture<InetAddress[]> resolveDnsAsync(final String host) {
+        final CompletableFuture<InetAddress[]> dnsFuture = new CompletableFuture<>();
+        CompletableFuture.runAsync(() -> {
+            try {
+                final InetAddress[] inetAddresses = dnsResolver.resolve(host);
+                // Introduce a delay before resolving AAAA records after receiving A records
+                resolution_delay.sleep();

Review Comment:
   Instead of using Thread.sleep(), i could use non-blocking methods(ScheduledExecutorService) to delay the execution of the next connection attempt. 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org
For additional commands, e-mail: dev-help@hc.apache.org


[GitHub] [httpcomponents-client] rschmitt commented on a diff in pull request #428: Implement HappyEyeballsV2AsyncClientConnectionOperator

Posted by "rschmitt (via GitHub)" <gi...@apache.org>.
rschmitt commented on code in PR #428:
URL: https://github.com/apache/httpcomponents-client/pull/428#discussion_r1161038232


##########
httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/InetAddressComparator.java:
##########
@@ -0,0 +1,470 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.nio;
+
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.Internal;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+
+import java.io.IOException;
+import java.net.DatagramSocket;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Comparator;
+
+/**
+ * This class implements a comparator for {@link InetAddress} instances based on the Happy Eyeballs V2 algorithm.
+ * <p>
+ * The comparator is used to sort a list of IP addresses based on their reachability and preference.
+ *
+ * <p>
+ * The Happy Eyeballs algorithm is a mechanism for reducing connection latency when connecting to IPv6-capable
+ * <p>
+ * servers over networks where both IPv6 and IPv4 are available. The algorithm attempts to establish connections
+ * <p>
+ * using IPv6 and IPv4 in parallel, and selects the first connection to complete successfully.
+ *
+ * <p>
+ * This comparator implements the Happy Eyeballs V2 rules defined in RFC 8305. The following rules are used for
+ * <p>
+ * comparing two IP addresses:
+ *
+ * <ul>
+ * <li>Rule 1: Avoid unusable destinations.</li>
+ * <li>Rule 2: Prefer matching scope.</li>
+ * <li>Rule 3: Avoid deprecated addresses.</li>
+ * <li>Rule 4: Prefer higher precedence.</li>
+ * <li>Rule 5: Prefer matching label.</li>
+ * <li>Rule 6: Prefer smaller address.</li>
+ * <li>Rule 7: Prefer home network.</li>
+ * <li>Rule 8: Prefer public network.</li>
+ * <li>Rule 9: Prefer stable privacy addresses.</li>
+ * <li>Rule 10: Prefer temporary addresses.</li>
+ * </ul>
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc8305">RFC 8305 - Happy Eyeballs Version 2: Better Connectivity Using
+ * <p>
+ * bash
+ * Copy code
+ * Concurrency</a>
+ * @since 5.3
+ */
+@Contract(threading = ThreadingBehavior.STATELESS)
+@Internal
+class InetAddressComparator implements Comparator<InetAddress> {
+
+    /**
+     * Singleton instance of the comparator.
+     */
+    public static final InetAddressComparator INSTANCE = new InetAddressComparator();
+
+    /**
+     * Compares two IP addresses based on the Happy Eyeballs algorithm rules.
+     * <p>
+     * The method first orders the addresses based on their precedence, and then compares them based on other rules,
+     * <p>
+     * including avoiding unusable destinations, preferring matching scope, preferring global scope, preferring
+     * <p>
+     * IPv6 addresses, and preferring smaller address prefixes.
+     *
+     * @param addr1 the first address to be compared
+     * @param addr2 the second address to be compared
+     * @return a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater
+     * <p>
+     * than the second
+     */
+    @Override
+    public int compare(final InetAddress addr1, final InetAddress addr2) {
+        if (addr1 == null && addr2 == null) {
+            return 0;
+        }
+        if (addr1 == null) {
+            return -1;
+        }
+        if (addr2 == null) {
+            return 1;
+        }
+
+        // Rule 1: Avoid unusable destinations.
+        final boolean add1IsReachable;
+        final boolean add2IsReachable;
+        try {
+            add1IsReachable = addr1.isReachable(500);

Review Comment:
   You can't call this method, especially not from a comparator. The Javadoc states:
   
   ```
        * Test whether that address is reachable. Best effort is made by the
        * implementation to try to reach the host, but firewalls and server
        * configuration may block requests resulting in an unreachable status
        * while some specific ports may be accessible.
        * A typical implementation will use ICMP ECHO REQUESTs if the
        * privilege can be obtained, otherwise it will try to establish
        * a TCP connection on port 7 (Echo) of the destination host.
   ```
   
   I believe that this is supposed to work by looking at which source addresses are available. For example, if there's no IPv6 source address (for the client's end of the TCP connection), then IPv6 destination addresses are _ipso facto_ unusable.



##########
httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/InetAddressComparator.java:
##########
@@ -0,0 +1,470 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.nio;
+
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.Internal;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+
+import java.io.IOException;
+import java.net.DatagramSocket;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Comparator;
+
+/**
+ * This class implements a comparator for {@link InetAddress} instances based on the Happy Eyeballs V2 algorithm.
+ * <p>
+ * The comparator is used to sort a list of IP addresses based on their reachability and preference.
+ *
+ * <p>
+ * The Happy Eyeballs algorithm is a mechanism for reducing connection latency when connecting to IPv6-capable
+ * <p>
+ * servers over networks where both IPv6 and IPv4 are available. The algorithm attempts to establish connections
+ * <p>
+ * using IPv6 and IPv4 in parallel, and selects the first connection to complete successfully.
+ *
+ * <p>
+ * This comparator implements the Happy Eyeballs V2 rules defined in RFC 8305. The following rules are used for
+ * <p>
+ * comparing two IP addresses:
+ *
+ * <ul>
+ * <li>Rule 1: Avoid unusable destinations.</li>
+ * <li>Rule 2: Prefer matching scope.</li>
+ * <li>Rule 3: Avoid deprecated addresses.</li>
+ * <li>Rule 4: Prefer higher precedence.</li>
+ * <li>Rule 5: Prefer matching label.</li>
+ * <li>Rule 6: Prefer smaller address.</li>
+ * <li>Rule 7: Prefer home network.</li>
+ * <li>Rule 8: Prefer public network.</li>
+ * <li>Rule 9: Prefer stable privacy addresses.</li>
+ * <li>Rule 10: Prefer temporary addresses.</li>
+ * </ul>
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc8305">RFC 8305 - Happy Eyeballs Version 2: Better Connectivity Using
+ * <p>
+ * bash
+ * Copy code
+ * Concurrency</a>
+ * @since 5.3
+ */
+@Contract(threading = ThreadingBehavior.STATELESS)
+@Internal
+class InetAddressComparator implements Comparator<InetAddress> {
+
+    /**
+     * Singleton instance of the comparator.
+     */
+    public static final InetAddressComparator INSTANCE = new InetAddressComparator();
+
+    /**
+     * Compares two IP addresses based on the Happy Eyeballs algorithm rules.
+     * <p>
+     * The method first orders the addresses based on their precedence, and then compares them based on other rules,
+     * <p>
+     * including avoiding unusable destinations, preferring matching scope, preferring global scope, preferring
+     * <p>
+     * IPv6 addresses, and preferring smaller address prefixes.
+     *
+     * @param addr1 the first address to be compared
+     * @param addr2 the second address to be compared
+     * @return a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater
+     * <p>
+     * than the second
+     */
+    @Override
+    public int compare(final InetAddress addr1, final InetAddress addr2) {
+        if (addr1 == null && addr2 == null) {
+            return 0;
+        }
+        if (addr1 == null) {
+            return -1;
+        }
+        if (addr2 == null) {
+            return 1;
+        }
+
+        // Rule 1: Avoid unusable destinations.
+        final boolean add1IsReachable;
+        final boolean add2IsReachable;
+        try {
+            add1IsReachable = addr1.isReachable(500);
+        } catch (final IOException e) {
+            return -1;
+        }
+        try {
+
+            add2IsReachable = addr2.isReachable(500);
+        } catch (final IOException e) {
+            return 1;
+        }
+
+        if (add1IsReachable && !add2IsReachable) {
+            return -1;
+        } else if (!add1IsReachable && add2IsReachable) {
+            return 1;
+        }
+
+
+        // Rule 2: Prefer matching scope.
+        final int addr1Scope = getScope(addr1);
+        final int addr2Scope = getScope(addr2);
+        final int srcScope;
+        try {
+            srcScope = getScope(getLocalAddress());
+        } catch (final IOException e) {
+            return 0;
+        }
+
+        if (addr1Scope == srcScope && addr2Scope != srcScope) {
+            return -1;
+        } else if (addr1Scope != srcScope && addr2Scope == srcScope) {
+            return 1;
+        }
+
+        //Rule 3: Avoid deprecated addresses.
+        final boolean add1IsDeprecated = isDeprecated(addr1);
+        final boolean add2IsDeprecated = isDeprecated(addr2);
+
+        if (add1IsDeprecated && !add2IsDeprecated) {
+            return 1;
+        } else if (!add1IsDeprecated && add2IsDeprecated) {
+            return -1;
+        }
+
+
+        // Rule 4: Prefer home addresses.
+        final boolean add1IsLocal = addr1.isLinkLocalAddress() || addr1.isSiteLocalAddress();
+        final boolean add2IsLocal = addr1.isLinkLocalAddress() || addr1.isSiteLocalAddress();
+
+        if (add1IsLocal && !add2IsLocal) {
+            return -1;
+        } else if (!add1IsLocal && add2IsLocal) {
+            return 1;
+        }
+
+        // Rule 5: Avoid deprecated addresses.
+        final String label1;
+        try {
+            label1 = getLabel(addr1);
+        } catch (final SocketException e) {
+            return -1;
+        }
+        final String label2;
+        try {
+            label2 = getLabel(addr2);
+        } catch (final SocketException e) {
+            return 1;
+        }
+
+        if (label1.equals(label2)) {
+            return 0;
+        } else if (label1.isEmpty()) {
+            return 1;
+        } else if (label2.isEmpty()) {
+            return -1;
+        }
+
+        // Rule 6 rule: Prefer the smaller address.
+        final int add1Precedence = getPrecedence(addr1);
+        final int add2Precedence = getPrecedence(addr2);
+
+        if (add1Precedence > add2Precedence) {
+            return -1;
+        } else if (add1Precedence < add2Precedence) {
+            return 1;
+        }
+
+        // Rule 7: Prefer native transport.
+        final boolean add1IsIPv4 = addr1 instanceof Inet4Address;
+        final boolean add2IsIPv4 = addr2 instanceof Inet4Address;
+
+        if (add1IsIPv4 && !add2IsIPv4) {
+            return -1;
+        } else if (!add1IsIPv4 && add2IsIPv4) {
+            return 1;
+        } else if (addr1 instanceof Inet6Address && addr2 instanceof Inet6Address) {
+            final Inet6Address ipv6Addr1 = (Inet6Address) addr1;
+            final Inet6Address ipv6Addr2 = (Inet6Address) addr2;
+
+            if (ipv6Addr1.isIPv4CompatibleAddress() && !ipv6Addr2.isIPv4CompatibleAddress()) {
+                return -1;
+            } else if (!ipv6Addr1.isIPv4CompatibleAddress() && ipv6Addr2.isIPv4CompatibleAddress()) {
+                return 1;
+            }
+        }
+
+
+        // Rule 8: Prefer smaller scope.
+        final int add1Scope = addr1 instanceof Inet6Address ? ((Inet6Address) addr1).getScopeId() : -1;
+        final int add2Scope = addr2 instanceof Inet6Address ? ((Inet6Address) addr2).getScopeId() : -1;
+
+        if (add1Scope < add2Scope) {
+            return -1;
+        } else if (add1Scope > add2Scope) {
+            return 1;
+        }
+
+        // Rule 9: Use longest matching prefix.
+        final int prefixLen1 = getMatchingPrefixLength(addr1, addr1);
+        final int prefixLen2 = getMatchingPrefixLength(addr2, addr1);
+
+        if (prefixLen1 > prefixLen2) {
+            return -1;
+        } else if (prefixLen1 < prefixLen2) {
+            return 1;
+        }
+
+
+        // Rule 9: Use longest matching prefix.
+        final byte[] ba1 = addr1.getAddress();
+        final byte[] ba2 = addr2.getAddress();
+        int prefixLen = 0;
+        for (int i = 0; i < ba1.length; i++) {
+            if (ba1[i] == ba2[i]) {
+                prefixLen += 8;
+            } else {
+                final int xor = ba1[i] ^ ba2[i];
+                // Count the number of leading zeroes in the XOR result
+                final int zeroes = Integer.numberOfLeadingZeros(xor) - 24;
+                prefixLen += zeroes;
+                break;
+            }
+        }
+        if (prefixLen == 128) {
+            return 0;
+        } else if ((ba1.length == 4 && prefixLen >= 24) || (ba1.length == 16 && prefixLen >= 64)) {
+            return 1;
+        } else if ((ba2.length == 4 && prefixLen >= 24) || (ba2.length == 16 && prefixLen >= 64)) {
+            return -1;
+        }
+
+
+        // Rule 10: Otherwise, leave the order unchanged.
+        return 0;
+    }
+
+
+    /**
+     * Returns the scope of the given address. For IPv6 addresses, this is the identifier of the
+     * scope the address is associated with. For IPv4 addresses, this always returns -1.
+     *
+     * @param address the address to get the scope for.
+     * @return the scope of the given address.
+     */
+    private int getScope(final InetAddress address) {
+        if (address instanceof Inet6Address) {
+            final Inet6Address ipv6Addr = (Inet6Address) address;
+            final int scope = ipv6Addr.getScopeId();
+            if (scope > 0) {
+                return scope;
+            }
+        }
+        return -1;

Review Comment:
   Isn't `127.0.0.1/8` supposed to have special treatment?



##########
httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperator.java:
##########
@@ -0,0 +1,698 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.impl.nio;
+
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.SchemePortResolver;
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.impl.ConnPoolSupport;
+import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionOperator;
+import org.apache.hc.client5.http.nio.ManagedAsyncClientConnection;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.config.Lookup;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.reactor.ConnectionInitiator;
+import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.ConnectException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The {@link AsyncClientConnectionOperator} implementation that uses Happy Eyeballs V2 algorithm to connect
+ * to the target server. Happy Eyeballs V2 (HEV2) algorithm is used to connect to the target server by concurrently
+ * attempting to establish multiple connections to different IP addresses. The first connection to complete
+ * successfully is selected and the others are closed. If all connections fail, the last error is rethrown.
+ * The algorithm also applies a configurable delay before subsequent connection attempts. HEV2 was introduced
+ * as a means to mitigate the latency issues caused by IPv4 and IPv6 co-existence in the Internet. HEV2 is defined
+ * in RFC 8305.
+ *
+ * <p>
+ * This connection operator maintains a connection pool for each unique route (combination of target host and
+ * target port) and selects the next connection from the pool to establish a new connection or reuse an
+ * existing connection. The connection pool uses a First-In-First-Out (FIFO) queue and has a configurable limit
+ * on the maximum number of connections that can be kept alive in the pool. Once the maximum number of connections
+ * has been reached, the oldest connection in the pool is closed to make room for a new one.
+ * </p>
+ *
+ * <p>
+ * This class is thread-safe and can be used in a multi-threaded environment.
+ * </p>
+ *
+ * <p>
+ * The HEV2 algorithm is configurable through the following parameters:
+ * <ul>
+ *   <li>{@code dualStackEnabled}: Whether to enable dual-stack connectivity. When set to {@code true},
+ *   the operator attempts to connect to both IPv4 and IPv6 addresses concurrently. When set to {@code false},
+ *   only IPv4 or IPv6 addresses are attempted depending on the address type of the target server.</li>
+ *   <li>{@code maxAttempts}: The maximum number of connection attempts to be made before failing. If all
+ *   attempts fail, the last error is rethrown.</li>
+ *   <li>{@code delay}: The delay (in milliseconds) to apply before subsequent connection attempts.</li>
+ *   <li>{@code connectTimeout}: The connection timeout (in milliseconds) for each attempt.</li>
+ * </ul>
+ * </p>
+ *
+ *
+ * <p>
+ * This class can be used with any {@link org.apache.hc.core5.http.nio.AsyncClientEndpoint} implementation
+ * that supports HTTP/1.1 or HTTP/2 protocols.
+ * </p>
+ *
+ * @since 5.3
+ */
+public class HappyEyeballsV2AsyncClientConnectionOperator implements AsyncClientConnectionOperator {
+
+    private static final Logger LOG = LoggerFactory.getLogger(AsyncClientConnectionOperator.class);
+
+
+    /**
+     * The default delay used between subsequent DNS resolution attempts, in milliseconds.
+     */
+    private final Timeout DEFAULT_RESOLUTION_DELAY = Timeout.ofMilliseconds(50);
+    /**
+     * The default timeout duration for establishing a connection, in milliseconds.
+     */
+    private final Timeout DEFAULT_TIMEOUT = Timeout.ofMilliseconds(250);
+
+    /**
+     * The default minimum delay between connection attempts.
+     * This delay is used to prevent the connection operator from spamming connection attempts and to provide a reasonable
+     * delay between attempts for the user.
+     */
+    private final Timeout DEFAULT_MINIMUM_CONNECTION_ATTEMPT_DELAY = Timeout.ofMilliseconds(100);
+
+    /**
+     * The default maximum delay between connection attempts.
+     * This delay is used to prevent the connection operator from spamming connection attempts and to provide a reasonable
+     * delay between attempts for the user. This value is used to cap the delay between attempts to prevent the delay from becoming
+     * too long and causing unnecessary delays in the application's processing.
+     */
+    private final Timeout DEFAULT_MAXIMUM_CONNECTION_ATTEMPT_DELAY = Timeout.ofMilliseconds(2000);
+
+    /**
+     * The default delay before attempting to establish a connection.
+     * This delay is used to provide a reasonable amount of time for the underlying transport to be ready before attempting
+     * to establish a connection. This can help to improve the likelihood of successful connection attempts and reduce
+     * unnecessary delays in the application's processing.
+     */
+    private final Timeout DEFAULT_CONNECTION_ATTEMPT_DELAY = Timeout.ofMilliseconds(250);
+
+
+    /**
+     * The {@link ScheduledExecutorService} used by this connection operator to execute delayed tasks, such as DNS resolution and connection attempts.
+     * This executor is used to control the timing of tasks in order to optimize the performance of connection attempts. By default, a single thread is used
+     * to execute tasks sequentially, but this can be adjusted depending on the application's workload and number of instances of the connection operator.
+     * If multiple instances of the connection operator are being used in the same application, it may be more efficient to use a {@link java.util.concurrent.ThreadPoolExecutor}
+     * with a fixed number of threads instead of a single thread executor. This will allow tasks to be executed in parallel, which can improve the overall
+     * performance of the application.
+     * If the scheduler provided to the constructor is null, a new instance of {@link Executors#newSingleThreadScheduledExecutor()} will be used as the default.
+     */
+    private final ScheduledExecutorService scheduler;
+
+    /**
+     * The underlying {@link AsyncClientConnectionOperator} that is used to establish connections
+     * to the target server.
+     */
+    private final AsyncClientConnectionOperator connectionOperator;
+
+    /**
+     * The DNS resolver used to resolve hostnames to IP addresses.
+     */
+    private final DnsResolver dnsResolver;
+
+    /**
+     * A lookup table used to determine the {@link TlsStrategy} to use for a given connection route.
+     */
+    private final Lookup<TlsStrategy> tlsStrategyLookup;
+
+    /**
+     * The default timeout for connection establishment attempts. If a connection cannot be established
+     * within this timeout, the attempt is considered failed.
+     */
+    private final Timeout timeout;
+
+    /**
+     * The minimum delay between connection establishment attempts.
+     */
+    private final Timeout minimumConnectionAttemptDelay;
+
+    /**
+     * The maximum delay between connection establishment attempts.
+     */
+    private final Timeout maximumConnectionAttemptDelay;
+
+    /**
+     * The current delay between connection establishment attempts.
+     */
+    private final Timeout connectionAttemptDelay;
+
+    /**
+     * The delay before resolution is started.
+     */
+    private final Timeout resolution_delay;
+
+    /**
+     * The number of IP addresses of each address family to include in the initial list of
+     * IP addresses to attempt connections to. This value is set to 2 by default, but can be
+     * increased to more aggressively favor a particular address family (e.g. set to 4 for IPv6).
+     */
+    private final int firstAddressFamilyCount;
+
+    /**
+     * The address family to use for establishing connections. This can be set to either
+     * {@link AddressFamily#IPv4} or {@link AddressFamily#IPv6}.
+     */
+    private final AddressFamily addressFamily;
+
+    /**
+     * The AddressFamily enum represents the possible address families that can be used when attempting to establish
+     * <p>
+     * connections using the Happy Eyeballs V2 algorithm.
+     *
+     * <p>
+     * The Happy Eyeballs V2 algorithm allows for concurrent connection attempts to be made to different IP addresses,
+     * <p>
+     * so this enum specifies whether connections should be attempted using IPv4 or IPv6 addresses.
+     *
+     * </p>
+     */
+    public enum AddressFamily {
+        IPv4, IPv6
+    }
+
+    /**
+     * Constructs a new {@link HappyEyeballsV2AsyncClientConnectionOperator} with the specified parameters.
+     *
+     * @param tlsStrategyLookup             the lookup object used to retrieve a {@link TlsStrategy} for a given {@link Route}
+     * @param connectionOperator            the underlying {@link AsyncClientConnectionOperator} to use for establishing connections
+     * @param dnsResolver                   the {@link DnsResolver} to use for resolving target hostnames
+     * @param timeout                       the timeout duration for establishing a connection
+     * @param resolution_delay              the configurable delay before subsequent DNS resolution attempts
+     * @param minimumConnectionAttemptDelay the minimum configurable delay between connection attempts
+     * @param maximumConnectionAttemptDelay the maximum configurable delay between connection attempts
+     * @param connectionAttemptDelay        the configurable delay before attempting to establish a connection
+     * @param firstAddressFamilyCount       the number of initial address families to use for establishing a connection
+     * @param addressFamily                 the preferred address family to use for establishing a connection
+     * @param scheduler                     the {@link ScheduledExecutorService} to use for scheduling tasks
+     * @throws IllegalArgumentException if {@code firstAddressFamilyCount} is not positive
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(final Lookup<TlsStrategy> tlsStrategyLookup,
+                                                        final AsyncClientConnectionOperator connectionOperator,
+                                                        final DnsResolver dnsResolver,
+                                                        final Timeout timeout,
+                                                        final Timeout resolution_delay,
+                                                        final Timeout minimumConnectionAttemptDelay,
+                                                        final Timeout maximumConnectionAttemptDelay,
+                                                        final Timeout connectionAttemptDelay,
+                                                        final int firstAddressFamilyCount,
+                                                        final AddressFamily addressFamily,
+                                                        final ScheduledExecutorService scheduler) {
+        this.tlsStrategyLookup = Args.notNull(tlsStrategyLookup, "TLS strategy lookup");
+        this.connectionOperator = Args.notNull(connectionOperator, "Connection operator");
+        this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE;
+        this.timeout = timeout != null ? timeout : DEFAULT_TIMEOUT;
+        this.resolution_delay = resolution_delay != null ? resolution_delay : DEFAULT_RESOLUTION_DELAY;
+        this.minimumConnectionAttemptDelay = minimumConnectionAttemptDelay != null ? minimumConnectionAttemptDelay : DEFAULT_MINIMUM_CONNECTION_ATTEMPT_DELAY;
+        this.maximumConnectionAttemptDelay = maximumConnectionAttemptDelay != null ? maximumConnectionAttemptDelay : DEFAULT_MAXIMUM_CONNECTION_ATTEMPT_DELAY;
+        this.connectionAttemptDelay = connectionAttemptDelay != null ? connectionAttemptDelay : DEFAULT_CONNECTION_ATTEMPT_DELAY;
+        this.firstAddressFamilyCount = Args.positive(firstAddressFamilyCount, "firstAddressFamilyCount");
+        this.addressFamily = addressFamily != null ? addressFamily : AddressFamily.IPv6;
+        this.scheduler = scheduler != null ? scheduler : Executors.newSingleThreadScheduledExecutor();
+    }
+
+    /**
+     * Constructs a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the specified
+     * {@link Lookup} for {@link TlsStrategy} and {@link SchemePortResolver} and {@link DnsResolver}.
+     * <p>
+     * The constructor internally creates a new instance of {@link DefaultAsyncClientConnectionOperator} with the
+     * specified {@link Lookup} for {@link TlsStrategy}, {@link SchemePortResolver} and {@link DnsResolver}. The
+     * created {@link AsyncClientConnectionOperator} is then passed to the main constructor along with default values
+     * for other parameters.
+     * </p>
+     *
+     * @param tlsStrategyLookup  The {@link Lookup} for {@link TlsStrategy}.
+     * @param schemePortResolver The {@link SchemePortResolver} to use for resolving scheme ports.
+     * @param dnsResolver        The {@link DnsResolver} to use for resolving hostnames to IP addresses.
+     * @throws IllegalArgumentException if the {@code tlsStrategyLookup} or {@code schemePortResolver} or {@code dnsResolver} parameter is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver,
+            final DnsResolver dnsResolver) {
+        this(tlsStrategyLookup,
+                new DefaultAsyncClientConnectionOperator(tlsStrategyLookup, schemePortResolver, dnsResolver),
+                dnsResolver,
+                null,
+                null,
+                null,
+                null,
+                null,
+                1,
+                null,
+                null);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup
+     * and scheme-port resolver. The DNS resolver will be set to the system default resolver.
+     *
+     * @param tlsStrategyLookup  The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @param schemePortResolver The resolver instance for mapping scheme names to default port numbers.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver) {
+        this(tlsStrategyLookup, schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup.
+     * The scheme-port resolver and DNS resolver will be set to their default instances.
+     *
+     * @param tlsStrategyLookup The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup) {
+        this(tlsStrategyLookup, DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+
+    /**
+     * Attempts to connect to the given host and returns a Future that will be completed when the connection is established
+     * or when an error occurs. This method may attempt to connect to multiple IP addresses associated with the host,
+     * depending on the address family and the number of connection attempts to execute. The address family and number of
+     * connection attempts can be configured by calling the corresponding setters on this class.
+     *
+     * @param connectionInitiator the connection initiator to use when creating the connection
+     * @param host                the host to connect to
+     * @param localAddress        the local address to bind to when connecting, or null to use any available local address
+     * @param connectTimeout      the timeout to use when connecting, or null to use the default timeout
+     * @param attachment          the attachment to associate with the connection, or null if no attachment is needed
+     * @param callback            the callback to invoke when the connection is established or an error occurs, or null if no callback is needed
+     * @return a Future that will be completed when the connection is established or when an error occurs
+     */
+    @Override
+    public Future<ManagedAsyncClientConnection> connect(
+            final ConnectionInitiator connectionInitiator,
+            final HttpHost host,
+            final SocketAddress localAddress,
+            final Timeout connectTimeout,
+            final Object attachment,
+            final FutureCallback<ManagedAsyncClientConnection> callback) {
+
+        final CompletableFuture<ManagedAsyncClientConnection> connectionFuture = new CompletableFuture<>();
+
+        final Timeout conTimeout = connectTimeout != null ? connectTimeout : timeout;
+
+        resolveDnsAsync(host.getHostName())
+                .thenCompose(inetAddresses -> {
+                    final List<InetAddress> ipv4Addresses = new ArrayList<>();
+                    final List<InetAddress> ipv6Addresses = new ArrayList<>();
+
+                    for (final InetAddress inetAddress : inetAddresses) {
+                        if (inetAddress instanceof Inet4Address) {
+                            ipv4Addresses.add(inetAddress);
+                        } else if (inetAddress instanceof Inet6Address) {
+                            ipv6Addresses.add(inetAddress);
+                        }
+                    }
+
+                    // Sort the array of addresses using the custom Comparator
+                    Arrays.sort(inetAddresses, InetAddressComparator.INSTANCE);

Review Comment:
   Is this a [stable sort](https://www.geeksforgeeks.org/stable-and-unstable-sorting-algorithms/)? It's important to not change the order of IP addresses that are equal under the comparison rules.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org
For additional commands, e-mail: dev-help@hc.apache.org


[GitHub] [httpcomponents-client] ok2c commented on pull request #428: Implement HappyEyeballsV2AsyncClientConnectionOperator

Posted by "ok2c (via GitHub)" <gi...@apache.org>.
ok2c commented on PR #428:
URL: https://github.com/apache/httpcomponents-client/pull/428#issuecomment-1493756081

   @arturobernalg Looks good to me. @rschmitt Would you also like to review?


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org
For additional commands, e-mail: dev-help@hc.apache.org


[GitHub] [httpcomponents-client] rschmitt commented on a diff in pull request #428: Implement HappyEyeballsV2AsyncClientConnectionOperator

Posted by "rschmitt (via GitHub)" <gi...@apache.org>.
rschmitt commented on code in PR #428:
URL: https://github.com/apache/httpcomponents-client/pull/428#discussion_r1157606106


##########
httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperator.java:
##########
@@ -0,0 +1,680 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.impl.nio;
+
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.SchemePortResolver;
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.impl.ConnPoolSupport;
+import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionOperator;
+import org.apache.hc.client5.http.nio.ManagedAsyncClientConnection;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.config.Lookup;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.reactor.ConnectionInitiator;
+import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.ConnectException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+
+/**
+ * The {@link AsyncClientConnectionOperator} implementation that uses Happy Eyeballs V2 algorithm to connect
+ * to the target server. Happy Eyeballs V2 (HEV2) algorithm is used to connect to the target server by concurrently
+ * attempting to establish multiple connections to different IP addresses. The first connection to complete
+ * successfully is selected and the others are closed. If all connections fail, the last error is rethrown.
+ * The algorithm also applies a configurable delay before subsequent connection attempts. HEV2 was introduced
+ * as a means to mitigate the latency issues caused by IPv4 and IPv6 co-existence in the Internet. HEV2 is defined
+ * in RFC 8305.
+ *
+ * <p>
+ * This connection operator maintains a connection pool for each unique route (combination of target host and
+ * target port) and selects the next connection from the pool to establish a new connection or reuse an
+ * existing connection. The connection pool uses a First-In-First-Out (FIFO) queue and has a configurable limit
+ * on the maximum number of connections that can be kept alive in the pool. Once the maximum number of connections
+ * has been reached, the oldest connection in the pool is closed to make room for a new one.
+ * </p>
+ *
+ * <p>
+ * This class is thread-safe and can be used in a multi-threaded environment.
+ * </p>
+ *
+ * <p>
+ * The HEV2 algorithm is configurable through the following parameters:
+ * <ul>
+ *   <li>{@code dualStackEnabled}: Whether to enable dual-stack connectivity. When set to {@code true},
+ *   the operator attempts to connect to both IPv4 and IPv6 addresses concurrently. When set to {@code false},
+ *   only IPv4 or IPv6 addresses are attempted depending on the address type of the target server.</li>
+ *   <li>{@code maxAttempts}: The maximum number of connection attempts to be made before failing. If all
+ *   attempts fail, the last error is rethrown.</li>
+ *   <li>{@code delay}: The delay (in milliseconds) to apply before subsequent connection attempts.</li>
+ *   <li>{@code connectTimeout}: The connection timeout (in milliseconds) for each attempt.</li>
+ * </ul>
+ * </p>
+ *
+ *
+ * <p>
+ * This class can be used with any {@link org.apache.hc.core5.http.nio.AsyncClientEndpoint} implementation
+ * that supports HTTP/1.1 or HTTP/2 protocols.
+ * </p>
+ *
+ * @since 5.3
+ */
+public class HappyEyeballsV2AsyncClientConnectionOperator implements AsyncClientConnectionOperator {
+
+    private static final Logger LOG = LoggerFactory.getLogger(AsyncClientConnectionOperator.class);
+
+    /**
+     * The underlying {@link AsyncClientConnectionOperator} that is used to establish connections
+     * to the target server.
+     */
+    private final AsyncClientConnectionOperator connectionOperator;
+
+    /**
+     * The DNS resolver used to resolve hostnames to IP addresses.
+     */
+    private final DnsResolver dnsResolver;
+
+    /**
+     * A lookup table used to determine the {@link TlsStrategy} to use for a given connection route.
+     */
+    private final Lookup<TlsStrategy> tlsStrategyLookup;
+
+    /**
+     * The default timeout for connection establishment attempts. If a connection cannot be established
+     * within this timeout, the attempt is considered failed.
+     */
+    private final Timeout timeout;
+
+    /**
+     * The minimum delay between connection establishment attempts.
+     */
+    private final Timeout minimumConnectionAttemptDelay;
+
+    /**
+     * The maximum delay between connection establishment attempts.
+     */
+    private final Timeout maximumConnectionAttemptDelay;
+
+    /**
+     * The current delay between connection establishment attempts.
+     */
+    private final Timeout connectionAttemptDelay;
+
+    /**
+     * The delay before resolution is started.
+     */
+    private final Timeout resolution_delay;
+
+    /**
+     * The number of IP addresses of each address family to include in the initial list of
+     * IP addresses to attempt connections to. This value is set to 2 by default, but can be
+     * increased to more aggressively favor a particular address family (e.g. set to 4 for IPv6).
+     */
+    private final int firstAddressFamilyCount;
+
+    /**
+     * The address family to use for establishing connections. This can be set to either
+     * {@link AddressFamily#IPv4} or {@link AddressFamily#IPv6}.
+     */
+    private final AddressFamily addressFamily;
+
+
+    /**
+     * The AddressFamily enum represents the possible address families that can be used when attempting to establish
+     * <p>
+     * connections using the Happy Eyeballs V2 algorithm.
+     *
+     * <p>
+     * The Happy Eyeballs V2 algorithm allows for concurrent connection attempts to be made to different IP addresses,
+     * <p>
+     * so this enum specifies whether connections should be attempted using IPv4 or IPv6 addresses.
+     *
+     * </p>
+     */
+    public enum AddressFamily {
+        IPv4, IPv6
+    }
+
+    /**
+     * Constructs a new {@link HappyEyeballsV2AsyncClientConnectionOperator} with the specified parameters.
+     *
+     * @param tlsStrategyLookup             the lookup object used to retrieve a {@link TlsStrategy} for a given {@link Route}
+     * @param connectionOperator            the underlying {@link AsyncClientConnectionOperator} to use for establishing connections
+     * @param dnsResolver                   the {@link DnsResolver} to use for resolving target hostnames
+     * @param timeout                       the timeout duration for establishing a connection
+     * @param resolution_delay              the configurable delay before subsequent DNS resolution attempts
+     * @param minimumConnectionAttemptDelay the minimum configurable delay between connection attempts
+     * @param maximumConnectionAttemptDelay the maximum configurable delay between connection attempts
+     * @param connectionAttemptDelay        the configurable delay before attempting to establish a connection
+     * @param firstAddressFamilyCount       the number of initial address families to use for establishing a connection
+     * @param addressFamily                 the preferred address family to use for establishing a connection
+     * @throws IllegalArgumentException if {@code firstAddressFamilyCount} is not positive
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(final Lookup<TlsStrategy> tlsStrategyLookup,
+                                                        final AsyncClientConnectionOperator connectionOperator,
+                                                        final DnsResolver dnsResolver,
+                                                        final Timeout timeout,
+                                                        final Timeout resolution_delay,
+                                                        final Timeout minimumConnectionAttemptDelay,
+                                                        final Timeout maximumConnectionAttemptDelay,
+                                                        final Timeout connectionAttemptDelay,
+                                                        final int firstAddressFamilyCount,
+                                                        final AddressFamily addressFamily) {
+        this.tlsStrategyLookup = Args.notNull(tlsStrategyLookup, "TLS strategy lookup");
+        this.connectionOperator = Args.notNull(connectionOperator, "Connection operator");
+        this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE;
+        this.timeout = timeout != null ? timeout : Timeout.ofMilliseconds(250);
+        this.resolution_delay = resolution_delay != null ? resolution_delay : Timeout.ofMilliseconds(50);
+        this.minimumConnectionAttemptDelay = minimumConnectionAttemptDelay != null ? minimumConnectionAttemptDelay : Timeout.ofMilliseconds(100);
+        this.maximumConnectionAttemptDelay = maximumConnectionAttemptDelay != null ? maximumConnectionAttemptDelay : Timeout.ofSeconds(2);
+        this.connectionAttemptDelay = connectionAttemptDelay != null ? connectionAttemptDelay : Timeout.ofMilliseconds(250);
+        this.firstAddressFamilyCount = Args.positive(firstAddressFamilyCount, "firstAddressFamilyCount");
+        this.addressFamily = addressFamily;
+    }
+
+    /**
+     * Constructs a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the specified
+     * {@link Lookup} for {@link TlsStrategy} and {@link SchemePortResolver} and {@link DnsResolver}.
+     * <p>
+     * The constructor internally creates a new instance of {@link DefaultAsyncClientConnectionOperator} with the
+     * specified {@link Lookup} for {@link TlsStrategy}, {@link SchemePortResolver} and {@link DnsResolver}. The
+     * created {@link AsyncClientConnectionOperator} is then passed to the main constructor along with default values
+     * for other parameters.
+     * </p>
+     *
+     * @param tlsStrategyLookup  The {@link Lookup} for {@link TlsStrategy}.
+     * @param schemePortResolver The {@link SchemePortResolver} to use for resolving scheme ports.
+     * @param dnsResolver        The {@link DnsResolver} to use for resolving hostnames to IP addresses.
+     * @throws IllegalArgumentException if the {@code tlsStrategyLookup} or {@code schemePortResolver} or {@code dnsResolver} parameter is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver,
+            final DnsResolver dnsResolver) {
+        this(tlsStrategyLookup,
+                new DefaultAsyncClientConnectionOperator(tlsStrategyLookup, schemePortResolver, dnsResolver),
+                dnsResolver,
+                null,
+                null,
+                null,
+                null,
+                null,
+                1,
+                AddressFamily.IPv6);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup
+     * and scheme-port resolver. The DNS resolver will be set to the system default resolver.
+     *
+     * @param tlsStrategyLookup  The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @param schemePortResolver The resolver instance for mapping scheme names to default port numbers.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver) {
+        this(tlsStrategyLookup, schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup.
+     * The scheme-port resolver and DNS resolver will be set to their default instances.
+     *
+     * @param tlsStrategyLookup The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup) {
+        this(tlsStrategyLookup, DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+
+    /**
+     * Attempts to connect to the given host and returns a Future that will be completed when the connection is established
+     * or when an error occurs. This method may attempt to connect to multiple IP addresses associated with the host,
+     * depending on the address family and the number of connection attempts to execute. The address family and number of
+     * connection attempts can be configured by calling the corresponding setters on this class.
+     *
+     * @param connectionInitiator the connection initiator to use when creating the connection
+     * @param host                the host to connect to
+     * @param localAddress        the local address to bind to when connecting, or null to use any available local address
+     * @param connectTimeout      the timeout to use when connecting, or null to use the default timeout
+     * @param attachment          the attachment to associate with the connection, or null if no attachment is needed
+     * @param callback            the callback to invoke when the connection is established or an error occurs, or null if no callback is needed
+     * @return a Future that will be completed when the connection is established or when an error occurs
+     */
+    @Override
+    public Future<ManagedAsyncClientConnection> connect(
+            final ConnectionInitiator connectionInitiator,
+            final HttpHost host,
+            final SocketAddress localAddress,
+            final Timeout connectTimeout,
+            final Object attachment,
+            final FutureCallback<ManagedAsyncClientConnection> callback) {
+
+        final CompletableFuture<ManagedAsyncClientConnection> connectionFuture = new CompletableFuture<>();
+
+        final Timeout conTimeout = connectTimeout != null ? connectTimeout : timeout;
+
+        resolveDnsAsync(host.getHostName())
+                .thenCompose(inetAddresses -> {
+                    final List<InetAddress> ipv4Addresses = new ArrayList<>();
+                    final List<InetAddress> ipv6Addresses = new ArrayList<>();
+
+                    for (final InetAddress inetAddress : inetAddresses) {
+                        if (inetAddress instanceof Inet4Address) {
+                            ipv4Addresses.add(inetAddress);
+                        } else if (inetAddress instanceof Inet6Address) {
+                            ipv6Addresses.add(inetAddress);
+                        }
+                    }
+
+                    sortAndInterleave(inetAddresses);
+
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> connectionFutures = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> attempts = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    if (addressFamily == AddressFamily.IPv4 && !ipv4Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                        }
+                    } else if (addressFamily == AddressFamily.IPv6 && !ipv6Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                        }
+                    } else {
+                        if (!ipv4Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                            }
+                        }
+                        if (!ipv6Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                            }
+                        }
+                    }
+
+                    // Execute the connection attempts concurrently using CompletableFuture.anyOf
+                    return CompletableFuture.anyOf(attempts.toArray(new CompletableFuture[0]))
+                            .thenCompose(result -> {
+                                if (result instanceof ManagedAsyncClientConnection) {
+                                    // If there is a result, cancel all other attempts and complete the connectionFuture
+                                    connectionFutures.forEach(future -> future.cancel(true));
+                                    connectionFuture.complete((ManagedAsyncClientConnection) result);
+                                } else {
+                                    // If there is an exception, complete the connectionFuture exceptionally with the exception
+                                    connectionFuture.completeExceptionally(new ConnectException("Failed to connect to any address for " + host));
+                                }
+                                // Invoke the callback if provided
+                                if (callback != null) {
+                                    connectionFuture.whenComplete((conn, ex) -> {
+                                        if (ex != null) {
+                                            callback.failed(new Exception(ex));
+                                        } else {
+                                            callback.completed(conn);
+                                        }
+                                    });
+                                }
+                                return connectionFuture;
+                            });
+                })
+                .exceptionally(e -> {
+                    connectionFuture.completeExceptionally(e);
+                    if (callback != null) {
+                        callback.failed(new Exception(e));
+                    }
+                    return null;
+                });
+
+        return connectionFuture;
+    }
+
+    /**
+     * Asynchronously resolves the DNS for the given host name and returns a CompletableFuture that will be completed
+     * with an array of InetAddress objects representing the IP addresses of the host.
+     * The resolution of AAAA records is delayed by the configured resolution delay to allow for a chance for A records to be
+     * returned first.
+     *
+     * @param host the host name to resolve DNS for
+     * @return a CompletableFuture that will be completed with an array of InetAddress objects representing the IP addresses
+     */
+    private CompletableFuture<InetAddress[]> resolveDnsAsync(final String host) {
+        final CompletableFuture<InetAddress[]> dnsFuture = new CompletableFuture<>();
+        CompletableFuture.runAsync(() -> {
+            try {
+                final InetAddress[] inetAddresses = dnsResolver.resolve(host);
+                // Introduce a delay before resolving AAAA records after receiving A records
+                resolution_delay.sleep();

Review Comment:
   Yes, in async code delays should always be implemented by yielding the current thread and scheduling a continuation, rather than calling `Thread.sleep`. Ideally, the DNS lookup would also be made as an async call, but to my knowledge there's really nothing we can do about that, and in practical terms DNS is very fast. I'm not even sure Netty has an async implementation of DNS lookups.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org
For additional commands, e-mail: dev-help@hc.apache.org


[GitHub] [httpcomponents-client] rhernandez35 commented on a diff in pull request #428: Implement HappyEyeballsV2AsyncClientConnectionOperator

Posted by "rhernandez35 (via GitHub)" <gi...@apache.org>.
rhernandez35 commented on code in PR #428:
URL: https://github.com/apache/httpcomponents-client/pull/428#discussion_r1157861324


##########
httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperator.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.impl.nio;
+
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.SchemePortResolver;
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.impl.ConnPoolSupport;
+import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionOperator;
+import org.apache.hc.client5.http.nio.ManagedAsyncClientConnection;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.config.Lookup;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.reactor.ConnectionInitiator;
+import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.ConnectException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The {@link AsyncClientConnectionOperator} implementation that uses Happy Eyeballs V2 algorithm to connect
+ * to the target server. Happy Eyeballs V2 (HEV2) algorithm is used to connect to the target server by concurrently
+ * attempting to establish multiple connections to different IP addresses. The first connection to complete
+ * successfully is selected and the others are closed. If all connections fail, the last error is rethrown.
+ * The algorithm also applies a configurable delay before subsequent connection attempts. HEV2 was introduced
+ * as a means to mitigate the latency issues caused by IPv4 and IPv6 co-existence in the Internet. HEV2 is defined
+ * in RFC 8305.
+ *
+ * <p>
+ * This connection operator maintains a connection pool for each unique route (combination of target host and
+ * target port) and selects the next connection from the pool to establish a new connection or reuse an
+ * existing connection. The connection pool uses a First-In-First-Out (FIFO) queue and has a configurable limit
+ * on the maximum number of connections that can be kept alive in the pool. Once the maximum number of connections
+ * has been reached, the oldest connection in the pool is closed to make room for a new one.
+ * </p>
+ *
+ * <p>
+ * This class is thread-safe and can be used in a multi-threaded environment.
+ * </p>
+ *
+ * <p>
+ * The HEV2 algorithm is configurable through the following parameters:
+ * <ul>
+ *   <li>{@code dualStackEnabled}: Whether to enable dual-stack connectivity. When set to {@code true},
+ *   the operator attempts to connect to both IPv4 and IPv6 addresses concurrently. When set to {@code false},
+ *   only IPv4 or IPv6 addresses are attempted depending on the address type of the target server.</li>
+ *   <li>{@code maxAttempts}: The maximum number of connection attempts to be made before failing. If all
+ *   attempts fail, the last error is rethrown.</li>
+ *   <li>{@code delay}: The delay (in milliseconds) to apply before subsequent connection attempts.</li>
+ *   <li>{@code connectTimeout}: The connection timeout (in milliseconds) for each attempt.</li>
+ * </ul>
+ * </p>
+ *
+ *
+ * <p>
+ * This class can be used with any {@link org.apache.hc.core5.http.nio.AsyncClientEndpoint} implementation
+ * that supports HTTP/1.1 or HTTP/2 protocols.
+ * </p>
+ *
+ * @since 5.3
+ */
+public class HappyEyeballsV2AsyncClientConnectionOperator implements AsyncClientConnectionOperator {
+
+    private static final Logger LOG = LoggerFactory.getLogger(AsyncClientConnectionOperator.class);
+
+    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
+
+    private final Duration resolutionDelay = Duration.ofMillis(500);
+
+
+
+    /**
+     * The underlying {@link AsyncClientConnectionOperator} that is used to establish connections
+     * to the target server.
+     */
+    private final AsyncClientConnectionOperator connectionOperator;
+
+    /**
+     * The DNS resolver used to resolve hostnames to IP addresses.
+     */
+    private final DnsResolver dnsResolver;
+
+    /**
+     * A lookup table used to determine the {@link TlsStrategy} to use for a given connection route.
+     */
+    private final Lookup<TlsStrategy> tlsStrategyLookup;
+
+    /**
+     * The default timeout for connection establishment attempts. If a connection cannot be established
+     * within this timeout, the attempt is considered failed.
+     */
+    private final Timeout timeout;
+
+    /**
+     * The minimum delay between connection establishment attempts.
+     */
+    private final Timeout minimumConnectionAttemptDelay;
+
+    /**
+     * The maximum delay between connection establishment attempts.
+     */
+    private final Timeout maximumConnectionAttemptDelay;
+
+    /**
+     * The current delay between connection establishment attempts.
+     */
+    private final Timeout connectionAttemptDelay;
+
+    /**
+     * The delay before resolution is started.
+     */
+    private final Timeout resolution_delay;
+
+    /**
+     * The number of IP addresses of each address family to include in the initial list of
+     * IP addresses to attempt connections to. This value is set to 2 by default, but can be
+     * increased to more aggressively favor a particular address family (e.g. set to 4 for IPv6).
+     */
+    private final int firstAddressFamilyCount;
+
+    /**
+     * The address family to use for establishing connections. This can be set to either
+     * {@link AddressFamily#IPv4} or {@link AddressFamily#IPv6}.
+     */
+    private final AddressFamily addressFamily;
+
+
+    /**
+     * The AddressFamily enum represents the possible address families that can be used when attempting to establish
+     * <p>
+     * connections using the Happy Eyeballs V2 algorithm.
+     *
+     * <p>
+     * The Happy Eyeballs V2 algorithm allows for concurrent connection attempts to be made to different IP addresses,
+     * <p>
+     * so this enum specifies whether connections should be attempted using IPv4 or IPv6 addresses.
+     *
+     * </p>
+     */
+    public enum AddressFamily {
+        IPv4, IPv6
+    }
+
+    /**
+     * Constructs a new {@link HappyEyeballsV2AsyncClientConnectionOperator} with the specified parameters.
+     *
+     * @param tlsStrategyLookup             the lookup object used to retrieve a {@link TlsStrategy} for a given {@link Route}
+     * @param connectionOperator            the underlying {@link AsyncClientConnectionOperator} to use for establishing connections
+     * @param dnsResolver                   the {@link DnsResolver} to use for resolving target hostnames
+     * @param timeout                       the timeout duration for establishing a connection
+     * @param resolution_delay              the configurable delay before subsequent DNS resolution attempts
+     * @param minimumConnectionAttemptDelay the minimum configurable delay between connection attempts
+     * @param maximumConnectionAttemptDelay the maximum configurable delay between connection attempts
+     * @param connectionAttemptDelay        the configurable delay before attempting to establish a connection
+     * @param firstAddressFamilyCount       the number of initial address families to use for establishing a connection
+     * @param addressFamily                 the preferred address family to use for establishing a connection
+     * @throws IllegalArgumentException if {@code firstAddressFamilyCount} is not positive
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(final Lookup<TlsStrategy> tlsStrategyLookup,
+                                                        final AsyncClientConnectionOperator connectionOperator,
+                                                        final DnsResolver dnsResolver,
+                                                        final Timeout timeout,
+                                                        final Timeout resolution_delay,
+                                                        final Timeout minimumConnectionAttemptDelay,
+                                                        final Timeout maximumConnectionAttemptDelay,
+                                                        final Timeout connectionAttemptDelay,
+                                                        final int firstAddressFamilyCount,
+                                                        final AddressFamily addressFamily) {
+        this.tlsStrategyLookup = Args.notNull(tlsStrategyLookup, "TLS strategy lookup");
+        this.connectionOperator = Args.notNull(connectionOperator, "Connection operator");
+        this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE;
+        this.timeout = timeout != null ? timeout : Timeout.ofMilliseconds(250);
+        this.resolution_delay = resolution_delay != null ? resolution_delay : Timeout.ofMilliseconds(50);
+        this.minimumConnectionAttemptDelay = minimumConnectionAttemptDelay != null ? minimumConnectionAttemptDelay : Timeout.ofMilliseconds(100);
+        this.maximumConnectionAttemptDelay = maximumConnectionAttemptDelay != null ? maximumConnectionAttemptDelay : Timeout.ofSeconds(2);
+        this.connectionAttemptDelay = connectionAttemptDelay != null ? connectionAttemptDelay : Timeout.ofMilliseconds(250);
+        this.firstAddressFamilyCount = Args.positive(firstAddressFamilyCount, "firstAddressFamilyCount");
+        this.addressFamily = addressFamily;
+    }
+
+    /**
+     * Constructs a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the specified
+     * {@link Lookup} for {@link TlsStrategy} and {@link SchemePortResolver} and {@link DnsResolver}.
+     * <p>
+     * The constructor internally creates a new instance of {@link DefaultAsyncClientConnectionOperator} with the
+     * specified {@link Lookup} for {@link TlsStrategy}, {@link SchemePortResolver} and {@link DnsResolver}. The
+     * created {@link AsyncClientConnectionOperator} is then passed to the main constructor along with default values
+     * for other parameters.
+     * </p>
+     *
+     * @param tlsStrategyLookup  The {@link Lookup} for {@link TlsStrategy}.
+     * @param schemePortResolver The {@link SchemePortResolver} to use for resolving scheme ports.
+     * @param dnsResolver        The {@link DnsResolver} to use for resolving hostnames to IP addresses.
+     * @throws IllegalArgumentException if the {@code tlsStrategyLookup} or {@code schemePortResolver} or {@code dnsResolver} parameter is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver,
+            final DnsResolver dnsResolver) {
+        this(tlsStrategyLookup,
+                new DefaultAsyncClientConnectionOperator(tlsStrategyLookup, schemePortResolver, dnsResolver),
+                dnsResolver,
+                null,
+                null,
+                null,
+                null,
+                null,
+                1,
+                AddressFamily.IPv6);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup
+     * and scheme-port resolver. The DNS resolver will be set to the system default resolver.
+     *
+     * @param tlsStrategyLookup  The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @param schemePortResolver The resolver instance for mapping scheme names to default port numbers.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver) {
+        this(tlsStrategyLookup, schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup.
+     * The scheme-port resolver and DNS resolver will be set to their default instances.
+     *
+     * @param tlsStrategyLookup The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup) {
+        this(tlsStrategyLookup, DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+
+    /**
+     * Attempts to connect to the given host and returns a Future that will be completed when the connection is established
+     * or when an error occurs. This method may attempt to connect to multiple IP addresses associated with the host,
+     * depending on the address family and the number of connection attempts to execute. The address family and number of
+     * connection attempts can be configured by calling the corresponding setters on this class.
+     *
+     * @param connectionInitiator the connection initiator to use when creating the connection
+     * @param host                the host to connect to
+     * @param localAddress        the local address to bind to when connecting, or null to use any available local address
+     * @param connectTimeout      the timeout to use when connecting, or null to use the default timeout
+     * @param attachment          the attachment to associate with the connection, or null if no attachment is needed
+     * @param callback            the callback to invoke when the connection is established or an error occurs, or null if no callback is needed
+     * @return a Future that will be completed when the connection is established or when an error occurs
+     */
+    @Override
+    public Future<ManagedAsyncClientConnection> connect(
+            final ConnectionInitiator connectionInitiator,
+            final HttpHost host,
+            final SocketAddress localAddress,
+            final Timeout connectTimeout,
+            final Object attachment,
+            final FutureCallback<ManagedAsyncClientConnection> callback) {
+
+        final CompletableFuture<ManagedAsyncClientConnection> connectionFuture = new CompletableFuture<>();
+
+        final Timeout conTimeout = connectTimeout != null ? connectTimeout : timeout;
+
+        resolveDnsAsync(host.getHostName())
+                .thenCompose(inetAddresses -> {
+                    final List<InetAddress> ipv4Addresses = new ArrayList<>();
+                    final List<InetAddress> ipv6Addresses = new ArrayList<>();
+
+                    for (final InetAddress inetAddress : inetAddresses) {
+                        if (inetAddress instanceof Inet4Address) {
+                            ipv4Addresses.add(inetAddress);
+                        } else if (inetAddress instanceof Inet6Address) {
+                            ipv6Addresses.add(inetAddress);
+                        }
+                    }
+
+                    sortAndInterleave(inetAddresses);
+
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> connectionFutures = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> attempts = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    if (addressFamily == AddressFamily.IPv4 && !ipv4Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                        }
+                    } else if (addressFamily == AddressFamily.IPv6 && !ipv6Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                        }
+                    } else {
+                        if (!ipv4Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                            }
+                        }
+                        if (!ipv6Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                            }
+                        }
+                    }
+
+                    // Execute the connection attempts concurrently using CompletableFuture.anyOf
+                    return CompletableFuture.anyOf(attempts.toArray(new CompletableFuture[0]))
+                            .thenCompose(result -> {
+                                if (result instanceof ManagedAsyncClientConnection) {
+                                    // If there is a result, cancel all other attempts and complete the connectionFuture
+                                    connectionFutures.forEach(future -> future.cancel(true));
+                                    connectionFuture.complete((ManagedAsyncClientConnection) result);
+                                } else {
+                                    // If there is an exception, complete the connectionFuture exceptionally with the exception
+                                    connectionFuture.completeExceptionally(new ConnectException("Failed to connect to any address for " + host));
+                                }
+                                // Invoke the callback if provided
+                                if (callback != null) {
+                                    connectionFuture.whenComplete((conn, ex) -> {
+                                        if (ex != null) {
+                                            callback.failed(new Exception(ex));
+                                        } else {
+                                            callback.completed(conn);
+                                        }
+                                    });
+                                }
+                                return connectionFuture;
+                            });
+                })
+                .exceptionally(e -> {
+                    connectionFuture.completeExceptionally(e);
+                    if (callback != null) {
+                        callback.failed(new Exception(e));
+                    }
+                    return null;
+                });
+
+        return connectionFuture;
+    }
+
+    /**
+     * Asynchronously resolves the DNS for the given host name and returns a CompletableFuture that will be completed
+     * with an array of InetAddress objects representing the IP addresses of the host.
+     * The resolution of AAAA records is delayed by the configured resolution delay to allow for a chance for A records to be
+     * returned first.
+     *
+     * @param host the host name to resolve DNS for
+     * @return a CompletableFuture that will be completed with an array of InetAddress objects representing the IP addresses
+     */
+    private CompletableFuture<InetAddress[]> resolveDnsAsync(final String host) {
+        final CompletableFuture<InetAddress[]> dnsFuture = new CompletableFuture<>();
+        CompletableFuture.runAsync(() -> {
+            try {
+                final InetAddress[] inetAddresses = dnsResolver.resolve(host);
+                dnsFuture.complete(inetAddresses);
+
+                // Introduce a delay before resolving AAAA records after receiving A records
+                if (inetAddresses.length > 0) {
+                    scheduler.schedule(() -> {
+                        try {
+                            final InetAddress[] inet6Addresses = dnsResolver.resolve(host);
+                            dnsFuture.complete(inet6Addresses);

Review Comment:
   And please don't use `obtrudeValue` to work around `complete` being one-and-done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org
For additional commands, e-mail: dev-help@hc.apache.org


[GitHub] [httpcomponents-client] rhernandez35 commented on a diff in pull request #428: Implement HappyEyeballsV2AsyncClientConnectionOperator

Posted by "rhernandez35 (via GitHub)" <gi...@apache.org>.
rhernandez35 commented on code in PR #428:
URL: https://github.com/apache/httpcomponents-client/pull/428#discussion_r1157856752


##########
httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperator.java:
##########
@@ -0,0 +1,680 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.impl.nio;
+
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.SchemePortResolver;
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.impl.ConnPoolSupport;
+import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionOperator;
+import org.apache.hc.client5.http.nio.ManagedAsyncClientConnection;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.config.Lookup;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.reactor.ConnectionInitiator;
+import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.ConnectException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+
+/**
+ * The {@link AsyncClientConnectionOperator} implementation that uses Happy Eyeballs V2 algorithm to connect
+ * to the target server. Happy Eyeballs V2 (HEV2) algorithm is used to connect to the target server by concurrently
+ * attempting to establish multiple connections to different IP addresses. The first connection to complete
+ * successfully is selected and the others are closed. If all connections fail, the last error is rethrown.
+ * The algorithm also applies a configurable delay before subsequent connection attempts. HEV2 was introduced
+ * as a means to mitigate the latency issues caused by IPv4 and IPv6 co-existence in the Internet. HEV2 is defined
+ * in RFC 8305.
+ *
+ * <p>
+ * This connection operator maintains a connection pool for each unique route (combination of target host and
+ * target port) and selects the next connection from the pool to establish a new connection or reuse an
+ * existing connection. The connection pool uses a First-In-First-Out (FIFO) queue and has a configurable limit
+ * on the maximum number of connections that can be kept alive in the pool. Once the maximum number of connections
+ * has been reached, the oldest connection in the pool is closed to make room for a new one.
+ * </p>
+ *
+ * <p>
+ * This class is thread-safe and can be used in a multi-threaded environment.
+ * </p>
+ *
+ * <p>
+ * The HEV2 algorithm is configurable through the following parameters:
+ * <ul>
+ *   <li>{@code dualStackEnabled}: Whether to enable dual-stack connectivity. When set to {@code true},
+ *   the operator attempts to connect to both IPv4 and IPv6 addresses concurrently. When set to {@code false},
+ *   only IPv4 or IPv6 addresses are attempted depending on the address type of the target server.</li>
+ *   <li>{@code maxAttempts}: The maximum number of connection attempts to be made before failing. If all
+ *   attempts fail, the last error is rethrown.</li>
+ *   <li>{@code delay}: The delay (in milliseconds) to apply before subsequent connection attempts.</li>
+ *   <li>{@code connectTimeout}: The connection timeout (in milliseconds) for each attempt.</li>
+ * </ul>
+ * </p>
+ *
+ *
+ * <p>
+ * This class can be used with any {@link org.apache.hc.core5.http.nio.AsyncClientEndpoint} implementation
+ * that supports HTTP/1.1 or HTTP/2 protocols.
+ * </p>
+ *
+ * @since 5.3
+ */
+public class HappyEyeballsV2AsyncClientConnectionOperator implements AsyncClientConnectionOperator {
+
+    private static final Logger LOG = LoggerFactory.getLogger(AsyncClientConnectionOperator.class);
+
+    /**
+     * The underlying {@link AsyncClientConnectionOperator} that is used to establish connections
+     * to the target server.
+     */
+    private final AsyncClientConnectionOperator connectionOperator;
+
+    /**
+     * The DNS resolver used to resolve hostnames to IP addresses.
+     */
+    private final DnsResolver dnsResolver;
+
+    /**
+     * A lookup table used to determine the {@link TlsStrategy} to use for a given connection route.
+     */
+    private final Lookup<TlsStrategy> tlsStrategyLookup;
+
+    /**
+     * The default timeout for connection establishment attempts. If a connection cannot be established
+     * within this timeout, the attempt is considered failed.
+     */
+    private final Timeout timeout;
+
+    /**
+     * The minimum delay between connection establishment attempts.
+     */
+    private final Timeout minimumConnectionAttemptDelay;
+
+    /**
+     * The maximum delay between connection establishment attempts.
+     */
+    private final Timeout maximumConnectionAttemptDelay;
+
+    /**
+     * The current delay between connection establishment attempts.
+     */
+    private final Timeout connectionAttemptDelay;
+
+    /**
+     * The delay before resolution is started.
+     */
+    private final Timeout resolution_delay;
+
+    /**
+     * The number of IP addresses of each address family to include in the initial list of
+     * IP addresses to attempt connections to. This value is set to 2 by default, but can be
+     * increased to more aggressively favor a particular address family (e.g. set to 4 for IPv6).
+     */
+    private final int firstAddressFamilyCount;
+
+    /**
+     * The address family to use for establishing connections. This can be set to either
+     * {@link AddressFamily#IPv4} or {@link AddressFamily#IPv6}.
+     */
+    private final AddressFamily addressFamily;
+
+
+    /**
+     * The AddressFamily enum represents the possible address families that can be used when attempting to establish
+     * <p>
+     * connections using the Happy Eyeballs V2 algorithm.
+     *
+     * <p>
+     * The Happy Eyeballs V2 algorithm allows for concurrent connection attempts to be made to different IP addresses,
+     * <p>
+     * so this enum specifies whether connections should be attempted using IPv4 or IPv6 addresses.
+     *
+     * </p>
+     */
+    public enum AddressFamily {
+        IPv4, IPv6
+    }
+
+    /**
+     * Constructs a new {@link HappyEyeballsV2AsyncClientConnectionOperator} with the specified parameters.
+     *
+     * @param tlsStrategyLookup             the lookup object used to retrieve a {@link TlsStrategy} for a given {@link Route}
+     * @param connectionOperator            the underlying {@link AsyncClientConnectionOperator} to use for establishing connections
+     * @param dnsResolver                   the {@link DnsResolver} to use for resolving target hostnames
+     * @param timeout                       the timeout duration for establishing a connection
+     * @param resolution_delay              the configurable delay before subsequent DNS resolution attempts
+     * @param minimumConnectionAttemptDelay the minimum configurable delay between connection attempts
+     * @param maximumConnectionAttemptDelay the maximum configurable delay between connection attempts
+     * @param connectionAttemptDelay        the configurable delay before attempting to establish a connection
+     * @param firstAddressFamilyCount       the number of initial address families to use for establishing a connection
+     * @param addressFamily                 the preferred address family to use for establishing a connection
+     * @throws IllegalArgumentException if {@code firstAddressFamilyCount} is not positive
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(final Lookup<TlsStrategy> tlsStrategyLookup,
+                                                        final AsyncClientConnectionOperator connectionOperator,
+                                                        final DnsResolver dnsResolver,
+                                                        final Timeout timeout,
+                                                        final Timeout resolution_delay,
+                                                        final Timeout minimumConnectionAttemptDelay,
+                                                        final Timeout maximumConnectionAttemptDelay,
+                                                        final Timeout connectionAttemptDelay,
+                                                        final int firstAddressFamilyCount,
+                                                        final AddressFamily addressFamily) {
+        this.tlsStrategyLookup = Args.notNull(tlsStrategyLookup, "TLS strategy lookup");
+        this.connectionOperator = Args.notNull(connectionOperator, "Connection operator");
+        this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE;
+        this.timeout = timeout != null ? timeout : Timeout.ofMilliseconds(250);
+        this.resolution_delay = resolution_delay != null ? resolution_delay : Timeout.ofMilliseconds(50);
+        this.minimumConnectionAttemptDelay = minimumConnectionAttemptDelay != null ? minimumConnectionAttemptDelay : Timeout.ofMilliseconds(100);
+        this.maximumConnectionAttemptDelay = maximumConnectionAttemptDelay != null ? maximumConnectionAttemptDelay : Timeout.ofSeconds(2);
+        this.connectionAttemptDelay = connectionAttemptDelay != null ? connectionAttemptDelay : Timeout.ofMilliseconds(250);
+        this.firstAddressFamilyCount = Args.positive(firstAddressFamilyCount, "firstAddressFamilyCount");
+        this.addressFamily = addressFamily;
+    }
+
+    /**
+     * Constructs a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the specified
+     * {@link Lookup} for {@link TlsStrategy} and {@link SchemePortResolver} and {@link DnsResolver}.
+     * <p>
+     * The constructor internally creates a new instance of {@link DefaultAsyncClientConnectionOperator} with the
+     * specified {@link Lookup} for {@link TlsStrategy}, {@link SchemePortResolver} and {@link DnsResolver}. The
+     * created {@link AsyncClientConnectionOperator} is then passed to the main constructor along with default values
+     * for other parameters.
+     * </p>
+     *
+     * @param tlsStrategyLookup  The {@link Lookup} for {@link TlsStrategy}.
+     * @param schemePortResolver The {@link SchemePortResolver} to use for resolving scheme ports.
+     * @param dnsResolver        The {@link DnsResolver} to use for resolving hostnames to IP addresses.
+     * @throws IllegalArgumentException if the {@code tlsStrategyLookup} or {@code schemePortResolver} or {@code dnsResolver} parameter is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver,
+            final DnsResolver dnsResolver) {
+        this(tlsStrategyLookup,
+                new DefaultAsyncClientConnectionOperator(tlsStrategyLookup, schemePortResolver, dnsResolver),
+                dnsResolver,
+                null,
+                null,
+                null,
+                null,
+                null,
+                1,
+                AddressFamily.IPv6);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup
+     * and scheme-port resolver. The DNS resolver will be set to the system default resolver.
+     *
+     * @param tlsStrategyLookup  The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @param schemePortResolver The resolver instance for mapping scheme names to default port numbers.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup,
+            final SchemePortResolver schemePortResolver) {
+        this(tlsStrategyLookup, schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+    /**
+     * Creates a new instance of {@link HappyEyeballsV2AsyncClientConnectionOperator} using the provided TLS strategy lookup.
+     * The scheme-port resolver and DNS resolver will be set to their default instances.
+     *
+     * @param tlsStrategyLookup The lookup instance for {@link TlsStrategy} to be used for establishing connections.
+     * @throws IllegalArgumentException if {@code tlsStrategyLookup} is {@code null}.
+     */
+    public HappyEyeballsV2AsyncClientConnectionOperator(
+            final Lookup<TlsStrategy> tlsStrategyLookup) {
+        this(tlsStrategyLookup, DefaultSchemePortResolver.INSTANCE, null);
+    }
+
+
+    /**
+     * Attempts to connect to the given host and returns a Future that will be completed when the connection is established
+     * or when an error occurs. This method may attempt to connect to multiple IP addresses associated with the host,
+     * depending on the address family and the number of connection attempts to execute. The address family and number of
+     * connection attempts can be configured by calling the corresponding setters on this class.
+     *
+     * @param connectionInitiator the connection initiator to use when creating the connection
+     * @param host                the host to connect to
+     * @param localAddress        the local address to bind to when connecting, or null to use any available local address
+     * @param connectTimeout      the timeout to use when connecting, or null to use the default timeout
+     * @param attachment          the attachment to associate with the connection, or null if no attachment is needed
+     * @param callback            the callback to invoke when the connection is established or an error occurs, or null if no callback is needed
+     * @return a Future that will be completed when the connection is established or when an error occurs
+     */
+    @Override
+    public Future<ManagedAsyncClientConnection> connect(
+            final ConnectionInitiator connectionInitiator,
+            final HttpHost host,
+            final SocketAddress localAddress,
+            final Timeout connectTimeout,
+            final Object attachment,
+            final FutureCallback<ManagedAsyncClientConnection> callback) {
+
+        final CompletableFuture<ManagedAsyncClientConnection> connectionFuture = new CompletableFuture<>();
+
+        final Timeout conTimeout = connectTimeout != null ? connectTimeout : timeout;
+
+        resolveDnsAsync(host.getHostName())
+                .thenCompose(inetAddresses -> {
+                    final List<InetAddress> ipv4Addresses = new ArrayList<>();
+                    final List<InetAddress> ipv6Addresses = new ArrayList<>();
+
+                    for (final InetAddress inetAddress : inetAddresses) {
+                        if (inetAddress instanceof Inet4Address) {
+                            ipv4Addresses.add(inetAddress);
+                        } else if (inetAddress instanceof Inet6Address) {
+                            ipv6Addresses.add(inetAddress);
+                        }
+                    }
+
+                    sortAndInterleave(inetAddresses);
+
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> connectionFutures = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    final List<CompletableFuture<ManagedAsyncClientConnection>> attempts = new ArrayList<>();
+
+                    // Create a list of connection attempts to execute
+                    if (addressFamily == AddressFamily.IPv4 && !ipv4Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                        }
+                    } else if (addressFamily == AddressFamily.IPv6 && !ipv6Addresses.isEmpty()) {
+                        for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                            attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                    Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                        }
+                    } else {
+                        if (!ipv4Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv4Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv4Addresses.get(i)), localAddress));
+                            }
+                        }
+                        if (!ipv6Addresses.isEmpty()) {
+                            for (int i = 0; i < firstAddressFamilyCount && i < ipv6Addresses.size(); i++) {
+                                attempts.add(connectAttempt(connectionInitiator, host, conTimeout, attachment,
+                                        Collections.singletonList(ipv6Addresses.get(i)), localAddress));
+                            }
+                        }
+                    }
+
+                    // Execute the connection attempts concurrently using CompletableFuture.anyOf
+                    return CompletableFuture.anyOf(attempts.toArray(new CompletableFuture[0]))
+                            .thenCompose(result -> {
+                                if (result instanceof ManagedAsyncClientConnection) {
+                                    // If there is a result, cancel all other attempts and complete the connectionFuture
+                                    connectionFutures.forEach(future -> future.cancel(true));
+                                    connectionFuture.complete((ManagedAsyncClientConnection) result);
+                                } else {
+                                    // If there is an exception, complete the connectionFuture exceptionally with the exception
+                                    connectionFuture.completeExceptionally(new ConnectException("Failed to connect to any address for " + host));
+                                }
+                                // Invoke the callback if provided
+                                if (callback != null) {
+                                    connectionFuture.whenComplete((conn, ex) -> {
+                                        if (ex != null) {
+                                            callback.failed(new Exception(ex));
+                                        } else {
+                                            callback.completed(conn);
+                                        }
+                                    });
+                                }
+                                return connectionFuture;
+                            });
+                })
+                .exceptionally(e -> {
+                    connectionFuture.completeExceptionally(e);
+                    if (callback != null) {
+                        callback.failed(new Exception(e));
+                    }
+                    return null;
+                });
+
+        return connectionFuture;
+    }
+
+    /**
+     * Asynchronously resolves the DNS for the given host name and returns a CompletableFuture that will be completed
+     * with an array of InetAddress objects representing the IP addresses of the host.
+     * The resolution of AAAA records is delayed by the configured resolution delay to allow for a chance for A records to be
+     * returned first.
+     *
+     * @param host the host name to resolve DNS for
+     * @return a CompletableFuture that will be completed with an array of InetAddress objects representing the IP addresses
+     */
+    private CompletableFuture<InetAddress[]> resolveDnsAsync(final String host) {
+        final CompletableFuture<InetAddress[]> dnsFuture = new CompletableFuture<>();
+        CompletableFuture.runAsync(() -> {
+            try {
+                final InetAddress[] inetAddresses = dnsResolver.resolve(host);
+                // Introduce a delay before resolving AAAA records after receiving A records
+                resolution_delay.sleep();

Review Comment:
   Netty does, but it's an entirely bespoke implementation that doesn't touch the JDK's built-int resolver. The blocking here will hopefully become less of a problem for this client once Loom is available.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org
For additional commands, e-mail: dev-help@hc.apache.org


Re: [PR] Implement HappyEyeballsV2AsyncClientConnectionOperator [httpcomponents-client]

Posted by "ok2c (via GitHub)" <gi...@apache.org>.
ok2c closed pull request #428: Implement HappyEyeballsV2AsyncClientConnectionOperator
URL: https://github.com/apache/httpcomponents-client/pull/428


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@hc.apache.org
For additional commands, e-mail: dev-help@hc.apache.org