mirror of
https://github.com/moparisthebest/curl
synced 2025-02-28 09:21:50 -05:00
resolve: apply Happy Eyeballs philosophy to parallel c-ares queries
Closes #3699
This commit is contained in:
parent
dbce1bd86f
commit
80208d68dc
@ -89,8 +89,20 @@ struct ResolverResults {
|
|||||||
int num_pending; /* number of ares_gethostbyname() requests */
|
int num_pending; /* number of ares_gethostbyname() requests */
|
||||||
Curl_addrinfo *temp_ai; /* intermediary result while fetching c-ares parts */
|
Curl_addrinfo *temp_ai; /* intermediary result while fetching c-ares parts */
|
||||||
int last_status;
|
int last_status;
|
||||||
|
struct curltime happy_eyeballs_dns_time; /* when this timer started, or 0 */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* How long we are willing to wait for additional parallel responses after
|
||||||
|
obtaining a "definitive" one.
|
||||||
|
|
||||||
|
This is intended to equal the c-ares default timeout. cURL always uses that
|
||||||
|
default value. Unfortunately, c-ares doesn't expose its default timeout in
|
||||||
|
its API, but it is officially documented as 5 seconds.
|
||||||
|
|
||||||
|
See query_completed_cb() for an explanation of how this is used.
|
||||||
|
*/
|
||||||
|
#define HAPPY_EYEBALLS_DNS_TIMEOUT 5000
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Curl_resolver_global_init() - the generic low-level asynchronous name
|
* Curl_resolver_global_init() - the generic low-level asynchronous name
|
||||||
* resolve API. Called from curl_global_init() to initialize global resolver
|
* resolve API. Called from curl_global_init() to initialize global resolver
|
||||||
@ -347,6 +359,29 @@ CURLcode Curl_resolver_is_resolved(struct connectdata *conn,
|
|||||||
|
|
||||||
waitperform(conn, 0);
|
waitperform(conn, 0);
|
||||||
|
|
||||||
|
/* Now that we've checked for any last minute results above, see if there are
|
||||||
|
any responses still pending when the EXPIRE_HAPPY_EYEBALLS_DNS timer
|
||||||
|
expires. */
|
||||||
|
if(res
|
||||||
|
&& res->num_pending
|
||||||
|
/* This is only set to non-zero if the timer was started. */
|
||||||
|
&& (res->happy_eyeballs_dns_time.tv_sec
|
||||||
|
|| res->happy_eyeballs_dns_time.tv_usec)
|
||||||
|
&& (Curl_timediff(Curl_now(), res->happy_eyeballs_dns_time)
|
||||||
|
>= HAPPY_EYEBALLS_DNS_TIMEOUT)) {
|
||||||
|
/* Remember that the EXPIRE_HAPPY_EYEBALLS_DNS timer is no longer
|
||||||
|
running. */
|
||||||
|
memset(
|
||||||
|
&res->happy_eyeballs_dns_time, 0, sizeof(res->happy_eyeballs_dns_time));
|
||||||
|
|
||||||
|
/* Cancel the raw c-ares request, which will fire query_completed_cb() with
|
||||||
|
ARES_ECANCELLED synchronously for all pending responses. This will
|
||||||
|
leave us with res->num_pending == 0, which is perfect for the next
|
||||||
|
block. */
|
||||||
|
ares_cancel((ares_channel)data->state.resolver);
|
||||||
|
DEBUGASSERT(res->num_pending == 0);
|
||||||
|
}
|
||||||
|
|
||||||
if(res && !res->num_pending) {
|
if(res && !res->num_pending) {
|
||||||
if(dns) {
|
if(dns) {
|
||||||
(void)Curl_addrinfo_callback(conn, res->last_status, res->temp_ai);
|
(void)Curl_addrinfo_callback(conn, res->last_status, res->temp_ai);
|
||||||
@ -517,6 +552,66 @@ static void query_completed_cb(void *arg, /* (struct connectdata *) */
|
|||||||
/* A successful result overwrites any previous error */
|
/* A successful result overwrites any previous error */
|
||||||
if(res->last_status != ARES_SUCCESS)
|
if(res->last_status != ARES_SUCCESS)
|
||||||
res->last_status = status;
|
res->last_status = status;
|
||||||
|
|
||||||
|
/* If there are responses still pending, we presume they must be the
|
||||||
|
complementary IPv4 or IPv6 lookups that we started in parallel in
|
||||||
|
Curl_resolver_getaddrinfo() (for Happy Eyeballs). If we've got a
|
||||||
|
"definitive" response from one of a set of parallel queries, we need to
|
||||||
|
think about how long we're willing to wait for more responses. */
|
||||||
|
if(res->num_pending
|
||||||
|
/* Only these c-ares status values count as "definitive" for these
|
||||||
|
purposes. For example, ARES_ENODATA is what we expect when there is
|
||||||
|
no IPv6 entry for a domain name, and that's not a reason to get more
|
||||||
|
aggressive in our timeouts for the other response. Other errors are
|
||||||
|
either a result of bad input (which should affect all parallel
|
||||||
|
requests), local or network conditions, non-definitive server
|
||||||
|
responses, or us cancelling the request. */
|
||||||
|
&& (status == ARES_SUCCESS || status == ARES_ENOTFOUND)) {
|
||||||
|
/* Right now, there can only be up to two parallel queries, so don't
|
||||||
|
bother handling any other cases. */
|
||||||
|
DEBUGASSERT(res->num_pending == 1);
|
||||||
|
|
||||||
|
/* It's possible that one of these parallel queries could succeed
|
||||||
|
quickly, but the other could always fail or timeout (when we're
|
||||||
|
talking to a pool of DNS servers that can only successfully resolve
|
||||||
|
IPv4 address, for example).
|
||||||
|
|
||||||
|
It's also possible that the other request could always just take
|
||||||
|
longer because it needs more time or only the second DNS server can
|
||||||
|
fulfill it successfully. But, to align with the philosophy of Happy
|
||||||
|
Eyeballs, we don't want to wait _too_ long or users will think
|
||||||
|
requests are slow when IPv6 lookups don't actually work (but IPv4 ones
|
||||||
|
do).
|
||||||
|
|
||||||
|
So, now that we have a usable answer (some IPv4 addresses, some IPv6
|
||||||
|
addresses, or "no such domain"), we start a timeout for the remaining
|
||||||
|
pending responses. Even though it is typical that this resolved
|
||||||
|
request came back quickly, that needn't be the case. It might be that
|
||||||
|
this completing request didn't get a result from the first DNS server
|
||||||
|
or even the first round of the whole DNS server pool. So it could
|
||||||
|
already be quite some time after we issued the DNS queries in the
|
||||||
|
first place. Without modifying c-ares, we can't know exactly where in
|
||||||
|
its retry cycle we are. We could guess based on how much time has
|
||||||
|
gone by, but it doesn't really matter. Happy Eyeballs tells us that,
|
||||||
|
given usable information in hand, we simply don't want to wait "too
|
||||||
|
much longer" after we get a result.
|
||||||
|
|
||||||
|
We simply wait an additional amount of time equal to the default
|
||||||
|
c-ares query timeout. That is enough time for a typical parallel
|
||||||
|
response to arrive without being "too long". Even on a network
|
||||||
|
where one of the two types of queries is failing or timing out
|
||||||
|
constantly, this will usually mean we wait a total of the default
|
||||||
|
c-ares timeout (5 seconds) plus the round trip time for the successful
|
||||||
|
request, which seems bearable. The downside is that c-ares might race
|
||||||
|
with us to issue one more retry just before we give up, but it seems
|
||||||
|
better to "waste" that request instead of trying to guess the perfect
|
||||||
|
timeout to prevent it. After all, we don't even know where in the
|
||||||
|
c-ares retry cycle each request is.
|
||||||
|
*/
|
||||||
|
res->happy_eyeballs_dns_time = Curl_now();
|
||||||
|
Curl_expire(
|
||||||
|
conn->data, HAPPY_EYEBALLS_DNS_TIMEOUT, EXPIRE_HAPPY_EYEBALLS_DNS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1209,6 +1209,7 @@ typedef enum {
|
|||||||
EXPIRE_ASYNC_NAME,
|
EXPIRE_ASYNC_NAME,
|
||||||
EXPIRE_CONNECTTIMEOUT,
|
EXPIRE_CONNECTTIMEOUT,
|
||||||
EXPIRE_DNS_PER_NAME,
|
EXPIRE_DNS_PER_NAME,
|
||||||
|
EXPIRE_HAPPY_EYEBALLS_DNS, /* See asyn-ares.c */
|
||||||
EXPIRE_HAPPY_EYEBALLS,
|
EXPIRE_HAPPY_EYEBALLS,
|
||||||
EXPIRE_MULTI_PENDING,
|
EXPIRE_MULTI_PENDING,
|
||||||
EXPIRE_RUN_NOW,
|
EXPIRE_RUN_NOW,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user