mirror of
https://github.com/curl/curl.git
synced 2026-07-29 00:03:12 +03:00
Adjust restarts of inconclusive attempts, to also follow happy eyeballs timeout.
Update documentation.
This commit is contained in:
parent
95e09ee69e
commit
a96714f54b
2 changed files with 155 additions and 107 deletions
|
|
@ -15,7 +15,7 @@ Added-in: 7.59.0
|
|||
|
||||
# NAME
|
||||
|
||||
CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS - head start for IPv6 for happy eyeballs
|
||||
CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS - timing of connect attempts
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
|
|
@ -28,17 +28,65 @@ CURLcode curl_easy_setopt(CURL *handle, CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS,
|
|||
|
||||
# DESCRIPTION
|
||||
|
||||
Happy eyeballs is an algorithm that attempts to connect to both IPv4 and IPv6
|
||||
addresses for dual-stack hosts, preferring IPv6 first for *timeout*
|
||||
milliseconds. If the IPv6 address cannot be connected to within that time then
|
||||
a connection attempt is made to the IPv4 address in parallel. The first
|
||||
connection to be established is the one that is used.
|
||||
Happy eyeballs is an algorithm that controls connecting to a host that
|
||||
resolves to more than one IP address. A common setup is to expose an
|
||||
IPv4 and IPv6 address (dual-stack). Other host offer a range of addresses
|
||||
for one or both stacks.
|
||||
|
||||
## IP Addresses
|
||||
|
||||
When curl is built with IPv6 support, it attempts to connect to IPv6
|
||||
first, when available. When that fails, another connect attempt for
|
||||
the first IPv4 address (again, if available) is started. Should that
|
||||
fail, the next IPv6 address is used, then the next IPv4, etc. If there
|
||||
are only addresses for one stack, those are tried one after the other.
|
||||
|
||||
When there is neither a positive nor negative response to an attempt,
|
||||
another attempt is started after *timeout* has passed. And then another,
|
||||
after *timeout* has passed again. As long as there are addresses available.
|
||||
|
||||
When all addresses have been tried and failed, the transfer fails.
|
||||
All attempts are aborted after CURLOPT_CONNECTTIMEOUT_MS(3) has
|
||||
passed, counted from the first attempt onward.
|
||||
|
||||
The range of suggested useful values for *timeout* is limited. Happy
|
||||
Eyeballs RFC 6555 says "It is RECOMMENDED that connection attempts be paced
|
||||
150-250 ms apart to balance human factors against network load." libcurl
|
||||
currently defaults to 200 ms. Firefox and Chrome currently default to 300 ms.
|
||||
|
||||
As an example, for a host that resolves to 'a1_v4, a2_v4, a3_v6, a4_v6'
|
||||
curl will open a socket to 'a3_v6' first. When that does not report back,
|
||||
it will open another socket to 'a1_v4' after 200ms. The first socket is
|
||||
left open and might still succeed. When 200ms have gone by again, a
|
||||
socket for 'a4_v6' is opened. 200ms later, 'a2_v4' is tried.
|
||||
|
||||
At this point, there are 4 sockets open (unless the network has reported
|
||||
anything back). That took 3 times the happy eyeballs timeout, so 600ms
|
||||
in the default setting. When any of those four report a success, that
|
||||
socket is used for the transfer and the other three are closed.
|
||||
|
||||
There are situations where connect attempts fail, but the failure is
|
||||
considered being inconclusive. The QUIC protocol may encounter this.
|
||||
When a QUIC server restarts, it may send replies indicating that it
|
||||
is not accepting new connections right now, but maybe later.
|
||||
|
||||
Such "inclusive" connect attempt failures will cause a restart of
|
||||
the attempt, with the same address on a new socket, closing the
|
||||
previous one. Repeatedly until CURLOPT_CONNECTTIMEOUT_MS(3) strikes.
|
||||
|
||||
## HTTPS
|
||||
|
||||
When connection with the HTTPS protocol to a host that may talk HTTP/3,
|
||||
HTTP/2 or HTTP/1.1, curl applies a similar happy eyeballs strategy when
|
||||
attempting these versions.
|
||||
|
||||
When HTTPS only involves a TCP connection, the versions are negotiated
|
||||
via ALPN, the TLS extension, in a single connect. Since HTTP/3 runs on
|
||||
QUIC (which runs on UDP), it requires a separate connect atempt.
|
||||
|
||||
The HTTP/3 attempt is started first and, after *timeout* expired, the
|
||||
HTTP/2 (or 1.1) attempt is started in parallel.
|
||||
|
||||
# DEFAULT
|
||||
|
||||
CURL_HET_DEFAULT (currently defined as 200L)
|
||||
|
|
|
|||
|
|
@ -271,15 +271,18 @@ static CURLcode cf_ip_attempt_restart(struct cf_ip_attempt *a,
|
|||
{
|
||||
struct Curl_cfilter *cf_prev = a->cf;
|
||||
struct Curl_cfilter *wcf;
|
||||
CURLcode result;
|
||||
|
||||
/* When restarting, we tear down and existing filter *after* we
|
||||
* started up the new one. This gives us a new socket number and
|
||||
* probably a new local port. Which may prevent confusion. */
|
||||
a->result = CURLE_OK;
|
||||
a->connected = FALSE;
|
||||
a->inconclusive = FALSE;
|
||||
a->cf = NULL;
|
||||
|
||||
a->result = a->cf_create(&a->cf, data, cf->conn, a->addr, a->transport);
|
||||
if(!a->result) {
|
||||
result = a->cf_create(&a->cf, data, cf->conn, a->addr, a->transport);
|
||||
if(!result) {
|
||||
bool dummy;
|
||||
/* the new filter might have sub-filters */
|
||||
for(wcf = a->cf; wcf; wcf = wcf->next) {
|
||||
|
|
@ -290,7 +293,7 @@ static CURLcode cf_ip_attempt_restart(struct cf_ip_attempt *a,
|
|||
}
|
||||
if(cf_prev)
|
||||
Curl_conn_cf_discard_chain(&cf_prev, data);
|
||||
return a->result;
|
||||
return result;
|
||||
}
|
||||
|
||||
static void cf_ip_ballers_clear(struct Curl_cfilter *cf,
|
||||
|
|
@ -348,24 +351,28 @@ static CURLcode cf_ip_ballers_init(struct cf_ip_ballers *bs, int ip_version,
|
|||
static CURLcode cf_ip_ballers_run(struct cf_ip_ballers *bs,
|
||||
struct Curl_cfilter *cf,
|
||||
struct Curl_easy *data,
|
||||
struct curltime now,
|
||||
bool *connected)
|
||||
{
|
||||
CURLcode result = CURLE_OK;
|
||||
struct cf_ip_attempt *a = NULL, **panchor;
|
||||
bool do_next = FALSE, out_of_addresses = FALSE;
|
||||
bool do_more, more_possible;
|
||||
struct curltime now;
|
||||
timediff_t next_expire_ms;
|
||||
int i, ongoing, inconclusive;
|
||||
int i, inconclusive, ongoing;
|
||||
|
||||
if(bs->winner)
|
||||
return CURLE_OK;
|
||||
|
||||
evaluate:
|
||||
inconclusive = ongoing = i = 0;
|
||||
now = curlx_now();
|
||||
ongoing = inconclusive = 0;
|
||||
do_more = FALSE;
|
||||
more_possible = TRUE;
|
||||
|
||||
/* check if a running baller connects now */
|
||||
if(bs->running)
|
||||
CURL_TRC_CF(data, cf, "checking ip connect attempts");
|
||||
i = -1;
|
||||
for(panchor = &bs->running; *panchor; panchor = &((*panchor)->next)) {
|
||||
++i;
|
||||
a = *panchor;
|
||||
a->result = cf_ip_attempt_connect(a, data, connected);
|
||||
if(!a->result) {
|
||||
|
|
@ -383,39 +390,31 @@ evaluate:
|
|||
}
|
||||
return CURLE_OK;
|
||||
}
|
||||
/* still waiting */
|
||||
/* still running */
|
||||
++ongoing;
|
||||
}
|
||||
else if(a->inconclusive)
|
||||
else if(a->inconclusive) /* failed, but inconclusive */
|
||||
++inconclusive;
|
||||
++i;
|
||||
}
|
||||
if(bs->running)
|
||||
CURL_TRC_CF(data, cf, "checked connect attempts: "
|
||||
"%d ongoing, %d inconclusive", ongoing, inconclusive);
|
||||
|
||||
/* no baller connected yet, start another one? */
|
||||
if(!bs->started.tv_sec && !bs->started.tv_usec) {
|
||||
bs->started = now;
|
||||
do_next = TRUE;
|
||||
/* no attempt connected yet, start another one? */
|
||||
if(!ongoing) {
|
||||
if(!bs->started.tv_sec && !bs->started.tv_usec)
|
||||
bs->started = now;
|
||||
do_more = TRUE;
|
||||
}
|
||||
else {
|
||||
do_next = (curlx_timediff(now, bs->last_attempt_started) >=
|
||||
do_more = (curlx_timediff(now, bs->last_attempt_started) >=
|
||||
bs->attempt_delay_ms);
|
||||
if(do_next) {
|
||||
if(do_more)
|
||||
CURL_TRC_CF(data, cf, "happy eyeballs timeout expired, "
|
||||
"start next attempt");
|
||||
}
|
||||
else { /* delay does not count when all running failed */
|
||||
for(a = bs->running; a; a = a->next) {
|
||||
if(!a->result) /* still running */
|
||||
break;
|
||||
}
|
||||
if(!a) { /* exhausted list, no more running */
|
||||
CURL_TRC_CF(data, cf, "all previous attempts failed");
|
||||
do_next = TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(do_next) {
|
||||
if(do_more) {
|
||||
/* start the next attempt if there is another ip address to try.
|
||||
* Alternate between address families when possible. */
|
||||
const struct Curl_addrinfo *addr = NULL;
|
||||
|
|
@ -431,82 +430,85 @@ evaluate:
|
|||
addr = cf_ai_iter_next(&bs->addr_iter);
|
||||
ai_family = bs->addr_iter.ai_family;
|
||||
}
|
||||
if(!addr) {
|
||||
CURL_TRC_CF(data, cf, "no more addresses to try");
|
||||
out_of_addresses = TRUE;
|
||||
|
||||
if(addr) { /* try another address */
|
||||
result = cf_ip_attempt_new(&a, cf, data, addr, ai_family,
|
||||
bs->transport, bs->cf_create);
|
||||
CURL_TRC_CF(data, cf, "starting %s attempt for ipv%s -> %d",
|
||||
bs->running ? "next" : "first",
|
||||
(ai_family == AF_INET) ? "4" : "6", result);
|
||||
if(result)
|
||||
goto out;
|
||||
DEBUGASSERT(a);
|
||||
|
||||
/* append to running list */
|
||||
panchor = &bs->running;
|
||||
while(*panchor)
|
||||
panchor = &((*panchor)->next);
|
||||
*panchor = a;
|
||||
bs->last_attempt_started = now;
|
||||
bs->last_attempt_ai_family = ai_family;
|
||||
/* and run everything again */
|
||||
goto evaluate;
|
||||
}
|
||||
else if(inconclusive) {
|
||||
/* tried all addresses, no success but some where inconclusive.
|
||||
* Let's restart the inconclusive ones. */
|
||||
if(curlx_timediff(now, bs->last_attempt_started) >=
|
||||
bs->attempt_delay_ms) {
|
||||
CURL_TRC_CF(data, cf, "tried all addresses with inconclusive results"
|
||||
", restarting one");
|
||||
i = -1;
|
||||
for(a = bs->running; a; a = a->next) {
|
||||
++i;
|
||||
if(!a->inconclusive)
|
||||
continue;
|
||||
result = cf_ip_attempt_restart(a, cf, data);
|
||||
CURL_TRC_CF(data, cf, "restarted baller %d -> %d", i, result);
|
||||
if(result) /* serious failure */
|
||||
goto out;
|
||||
bs->last_attempt_started = now;
|
||||
goto evaluate;
|
||||
}
|
||||
DEBUGASSERT(0); /* should not come here */
|
||||
}
|
||||
/* attempt timeout for restart has not expired yet */
|
||||
goto out;
|
||||
}
|
||||
|
||||
result = cf_ip_attempt_new(&a, cf, data, addr, ai_family,
|
||||
bs->transport, bs->cf_create);
|
||||
CURL_TRC_CF(data, cf, "starting %s attempt for ipv%s -> %d",
|
||||
bs->running ? "next" : "first",
|
||||
(ai_family == AF_INET) ? "4" : "6", result);
|
||||
if(result)
|
||||
goto out;
|
||||
DEBUGASSERT(a);
|
||||
|
||||
/* append to running list */
|
||||
panchor = &bs->running;
|
||||
while(*panchor)
|
||||
panchor = &((*panchor)->next);
|
||||
*panchor = a;
|
||||
bs->last_attempt_started = now;
|
||||
bs->last_attempt_ai_family = ai_family;
|
||||
/* and run everything again */
|
||||
goto evaluate;
|
||||
else if(ongoing) {
|
||||
/* no more addresses, no inconclusive attempts */
|
||||
more_possible = FALSE;
|
||||
}
|
||||
else {
|
||||
CURL_TRC_CF(data, cf, "no more attempts to try");
|
||||
result = CURLE_COULDNT_CONNECT;
|
||||
i = 0;
|
||||
for(a = bs->running; a; a = a->next) {
|
||||
CURL_TRC_CF(data, cf, "baller %d: result=%d", i, a->result);
|
||||
if(a->result)
|
||||
result = a->result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out:
|
||||
if(!ongoing && out_of_addresses && inconclusive) {
|
||||
/* tried all addresses, no success but some where inconclusive.
|
||||
* Let's restart the inconclusive ones. */
|
||||
int restarted = 0;
|
||||
CURL_TRC_CF(data, cf, "tried all addresses, some with "
|
||||
"inconclusive results, restarting those");
|
||||
i = -1;
|
||||
for(a = bs->running; a; a = a->next) {
|
||||
++i;
|
||||
if(!a->inconclusive)
|
||||
continue;
|
||||
result = cf_ip_attempt_restart(a, cf, data);
|
||||
CURL_TRC_CF(data, cf, "restarted baller %d -> %d", i, result);
|
||||
if(result)
|
||||
goto out;
|
||||
++restarted;
|
||||
if(!result) {
|
||||
/* when do we need to be called again? */
|
||||
next_expire_ms = Curl_timeleft(data, &now, TRUE);
|
||||
if(more_possible) {
|
||||
timediff_t expire_ms, elapsed_ms;
|
||||
elapsed_ms = curlx_timediff(now, bs->last_attempt_started);
|
||||
expire_ms = CURLMAX(bs->attempt_delay_ms - elapsed_ms, 0);
|
||||
next_expire_ms = CURLMIN(next_expire_ms, expire_ms);
|
||||
}
|
||||
if(restarted)
|
||||
goto evaluate;
|
||||
}
|
||||
|
||||
if(!ongoing && out_of_addresses) {
|
||||
/* all attempts failed and we have no more addresses */
|
||||
CURL_TRC_CF(data, cf, "all ballers failed");
|
||||
result = CURLE_COULDNT_CONNECT;
|
||||
i = 0;
|
||||
for(a = bs->running; a; a = a->next) {
|
||||
CURL_TRC_CF(data, cf, "baller %d: result=%d", i, a->result);
|
||||
if(a->result)
|
||||
result = a->result;
|
||||
if(next_expire_ms <= 0) {
|
||||
failf(data, "Connection timeout after %" FMT_OFF_T " ms",
|
||||
curlx_timediff(now, data->progress.t_startsingle));
|
||||
return CURLE_OPERATION_TIMEDOUT;
|
||||
}
|
||||
return result;
|
||||
Curl_expire(data, next_expire_ms, EXPIRE_HAPPY_EYEBALLS);
|
||||
}
|
||||
|
||||
/* when do we need to be called again? */
|
||||
next_expire_ms = Curl_timeleft(data, &now, TRUE);
|
||||
if(!out_of_addresses) {
|
||||
timediff_t expire_ms, elapsed_ms;
|
||||
elapsed_ms = curlx_timediff(now, bs->last_attempt_started);
|
||||
expire_ms = CURLMAX(bs->attempt_delay_ms - elapsed_ms, 0);
|
||||
next_expire_ms = CURLMIN(next_expire_ms, expire_ms);
|
||||
}
|
||||
|
||||
if(next_expire_ms <= 0) {
|
||||
failf(data, "Connection timeout after %" FMT_OFF_T " ms",
|
||||
curlx_timediff(now, data->progress.t_startsingle));
|
||||
return CURLE_OPERATION_TIMEDOUT;
|
||||
}
|
||||
Curl_expire(data, next_expire_ms, EXPIRE_HAPPY_EYEBALLS);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -615,10 +617,8 @@ static CURLcode is_connected(struct Curl_cfilter *cf,
|
|||
struct cf_ip_happy_ctx *ctx = cf->ctx;
|
||||
struct connectdata *conn = cf->conn;
|
||||
CURLcode result;
|
||||
struct curltime now;
|
||||
|
||||
now = curlx_now();
|
||||
result = cf_ip_ballers_run(&ctx->ballers, cf, data, now, connected);
|
||||
result = cf_ip_ballers_run(&ctx->ballers, cf, data, connected);
|
||||
|
||||
if(!result)
|
||||
return CURLE_OK;
|
||||
|
|
@ -648,7 +648,7 @@ static CURLcode is_connected(struct Curl_cfilter *cf,
|
|||
proxy_name ? "via " : "",
|
||||
proxy_name ? proxy_name : "",
|
||||
proxy_name ? " " : "",
|
||||
curlx_timediff(now, data->progress.t_startsingle),
|
||||
curlx_timediff(curlx_now(), data->progress.t_startsingle),
|
||||
curl_easy_strerror(result));
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue