Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement HappyEyeballsV2AsyncClientConnectionOperator #428

Closed

Conversation

arturobernalg
Copy link
Member

@arturobernalg arturobernalg commented Mar 24, 2023

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
  • Allow activate HappyEyeballsV2Async via Fast Fallback var in PoolingAsyncClientConnectionManagerBuilder.java

@ok2c
Copy link
Member

ok2c commented Mar 27, 2023

@arturobernalg Allow me a few days to review.

@ok2c
Copy link
Member

ok2c commented Apr 3, 2023

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

// Introduce a delay before resolving AAAA records after receiving A records
resolution_delay.sleep();
dnsFuture.complete(inetAddresses);
} catch (final UnknownHostException | InterruptedException e) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't you catch Exception here, instead of just the checked exceptions?

try {
final InetAddress[] inetAddresses = dnsResolver.resolve(host);
// Introduce a delay before resolving AAAA records after receiving A records
resolution_delay.sleep();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member Author

@arturobernalg arturobernalg Apr 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

if (i < addresses.size() - 1) {
try {
final Duration delay = calculateDelay(i);
delay.wait();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member Author

@arturobernalg arturobernalg Apr 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment on lines 247 to 254
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These defaults are all duplicated in the HappyEyeballsV2AsyncClientConnectionOperator constructor.

if (adr1 instanceof Inet4Address && adr2 instanceof Inet6Address) {
return -1;
}
// Compare based on address bytes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to have nothing to do with the address sorting algorithm specified by RFC 6724, which is mandated by RFC 8305. Furthermore, sorting addresses in this way is a bad idea, since it will defeat round-robin DNS, an important load balancing technique.


}

@DisplayName("Test that application prioritizes IPv6 over IPv4 when both are available")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

scheduler.schedule(() -> {
try {
final InetAddress[] inet6Addresses = dnsResolver.resolve(host);
dnsFuture.complete(inet6Addresses);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@arturobernalg
Copy link
Member Author

arturobernalg commented Apr 7, 2023

Hi @rschmitt
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!

Copy link
Contributor

@rschmitt rschmitt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a lot of the destination address sorting rules need to be reimplemented in terms of local address availability. It's also important to make sure we are using a stable sort. Finally, the compareTo method can't make any network calls.

These rules are complex enough that they really need dedicated unit testing. Generative testing may be a good idea, particularly to ensure that the sort is stable.

final boolean add1IsReachable;
final boolean add2IsReachable;
try {
add1IsReachable = addr1.isReachable(500);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

return scope;
}
}
return -1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't 127.0.0.1/8 supposed to have special treatment?

}

// Sort the array of addresses using the custom Comparator
Arrays.sort(inetAddresses, InetAddressComparator.INSTANCE);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a stable sort? It's important to not change the order of IP addresses that are equal under the comparison rules.

@arturobernalg arturobernalg force-pushed the feature/happyEyeballs branch 6 times, most recently from df3bd5d to 833fc54 Compare April 12, 2023 23:28
@arturobernalg
Copy link
Member Author

arturobernalg commented Apr 12, 2023

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 Destination Address Selection (rfc6724) 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.

@ok2c
Copy link
Member

ok2c commented Aug 12, 2023

@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.

@arturobernalg arturobernalg changed the base branch from 5.3.x to 5.4.x August 12, 2023 17:00
This commit adds the implementation of HappyEyeballsV2AsyncClientConnectionOperator, a new class that supports asynchronous connection establishment over both IPv4 and IPv6. The operator follows the Happy Eyeballs v2 algorithm to attempt to connect first over IPv6 and then fall back to IPv4 if needed.
@ok2c
Copy link
Member

ok2c commented Dec 13, 2023

@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?

@ok2c ok2c deleted the branch apache:5.4.x December 19, 2023 09:38
@ok2c ok2c closed this Dec 19, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants