-
Notifications
You must be signed in to change notification settings - Fork 972
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
Conversation
@arturobernalg Allow me a few days to review. |
@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) { |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
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; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
3b9d109
to
999429c
Compare
Hi @rschmitt Let me know if you have any feedback. Thanks! |
There was a problem hiding this 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); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
df3bd5d
to
833fc54
Compare
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. |
fe6b360
to
25e1168
Compare
8283e01
to
f0b1668
Compare
72a14d7
to
0415171
Compare
f011c40
to
8d8aad0
Compare
8d8aad0
to
8a60365
Compare
8a60365
to
2a75ae2
Compare
@rschmitt @rhernandez35 Folks,, is there any chance you could do another review sometime soon? @arturobernalg Please in the meantime change the target branch to |
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.
2a75ae2
to
89e3d85
Compare
@rschmitt @rhernandez35 Is this feature still relevant? What shall we do with this PR? @arturobernalg Could you please change the target branch back to |
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: