cf-ip-happy: limit concurrent attempts

Introduce a limit on the concurrent connect attempts of 6:

- document this in CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS
- close the oldest attempt before opening a new one that would
  exceed the limit
- closing failed attempts early to avoid sockets use beyong their
  usefulness
- add tests for limits in unit2600

These changes are externally visible as file descriptors will be
reassigned where we previously kept the old one around and started a
new socket, allocating always a new descriptor.

Closes #21252
This commit is contained in:
Stefan Eissing 2026-04-07 11:57:37 +02:00 committed by Daniel Stenberg
parent 44c19a2cce
commit db9b6fa82e
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
3 changed files with 104 additions and 29 deletions

View file

@ -86,6 +86,7 @@ struct test_case {
timediff_t max_duration_ms;
CURLcode result_exp;
const char *pref_family;
uint32_t max_concurrent;
};
struct ai_family_stats {
@ -101,6 +102,8 @@ struct test_result {
struct curltime ended;
struct ai_family_stats cf4;
struct ai_family_stats cf6;
uint32_t max_concurrent;
uint32_t ongoing;
};
static const struct test_case *current_tc;
@ -120,8 +123,10 @@ struct cf_test_ctx {
static void cf_test_destroy(struct Curl_cfilter *cf, struct Curl_easy *data)
{
struct cf_test_ctx *ctx = cf->ctx;
infof(data, "%04dms: cf[%s] destroyed",
(int)curlx_timediff_ms(curlx_now(), current_tr->started), ctx->id);
current_tr->ongoing--;
infof(data, "%04dms: cf[%s] destroyed, now %u ongoing",
(int)curlx_timediff_ms(curlx_now(), current_tr->started),
ctx->id, current_tr->ongoing);
curlx_free(ctx);
cf->ctx = NULL;
}
@ -198,6 +203,9 @@ static CURLcode cf_test_create(struct Curl_cfilter **pcf,
ctx->ai_family = addr->family;
ctx->transport = transport;
ctx->started = curlx_now();
current_tr->ongoing++;
if(current_tr->ongoing > current_tr->max_concurrent)
current_tr->max_concurrent = current_tr->ongoing;
#ifdef USE_IPV6
if(ctx->ai_family == AF_INET6) {
ctx->stats = &current_tr->cf6;
@ -218,7 +226,8 @@ static CURLcode cf_test_create(struct Curl_cfilter **pcf,
if(ctx->stats->creations == 1)
ctx->stats->first_created = created_at;
ctx->stats->last_created = created_at;
infof(data, "%04dms: cf[%s] created", (int)created_at, ctx->id);
infof(data, "%04dms: cf[%s] created, now %u ongoing",
(int)created_at, ctx->id, current_tr->ongoing);
result = Curl_cf_create(&cf, &cft_test, ctx);
if(result)
@ -294,6 +303,11 @@ static void check_result(const struct test_case *tc, struct test_result *tr)
fail(msg);
}
}
if(tr->max_concurrent != tc->max_concurrent) {
curl_msprintf(msg, "%d: expected max %u ongoing, but reported %u",
tc->id, tc->max_concurrent, tr->max_concurrent);
fail(msg);
}
}
static void test_connect(CURL *easy, const struct test_case *tc)
@ -358,37 +372,51 @@ static CURLcode test_unit2600(const char *arg)
static const struct test_case TEST_CASES[] = {
/* TIMEOUT_MS, FAIL_MS CREATED DURATION Result, HE_PREF */
/* CNCT HE v4 v6 v4 v6 MIN MAX */
/* CNCT HE v4 v6 v4 v6 MIN MAX MAX_CONCURRENT */
{ 1, TURL, "test.com:123:192.0.2.1", CURL_IPRESOLVE_WHATEVER,
CNCT_TMOT, 150, 250, 250, 1, 0, 200, TC_TMOT, R_FAIL, NULL },
CNCT_TMOT, 150, 250, 250, 1, 0, 200, TC_TMOT, R_FAIL, NULL,
1 },
/* 1 ipv4, fails after ~200ms, reports COULDNT_CONNECT */
{ 2, TURL, "test.com:123:192.0.2.1,192.0.2.2", CURL_IPRESOLVE_WHATEVER,
CNCT_TMOT, 150, 250, 250, 2, 0, 400, TC_TMOT, R_FAIL, NULL },
CNCT_TMOT, 150, 250, 250, 2, 0, 400, TC_TMOT, R_FAIL, NULL,
2 },
/* 2 ipv4, fails after ~400ms, reports COULDNT_CONNECT */
#ifdef USE_IPV6
{ 3, TURL, "test.com:123:::1", CURL_IPRESOLVE_WHATEVER,
CNCT_TMOT, 150, 250, 250, 0, 1, 200, TC_TMOT, R_FAIL, NULL },
CNCT_TMOT, 150, 250, 250, 0, 1, 200, TC_TMOT, R_FAIL, NULL,
1 },
/* 1 ipv6, fails after ~200ms, reports COULDNT_CONNECT */
{ 4, TURL, "test.com:123:::1,::2", CURL_IPRESOLVE_WHATEVER,
CNCT_TMOT, 150, 250, 250, 0, 2, 400, TC_TMOT, R_FAIL, NULL },
CNCT_TMOT, 150, 250, 250, 0, 2, 400, TC_TMOT, R_FAIL, NULL,
2 },
/* 2 ipv6, fails after ~400ms, reports COULDNT_CONNECT */
{ 5, TURL, "test.com:123:192.0.2.1,::1", CURL_IPRESOLVE_WHATEVER,
CNCT_TMOT, 150, 250, 250, 1, 1, 350, TC_TMOT, R_FAIL, "v6" },
CNCT_TMOT, 150, 250, 250, 1, 1, 350, TC_TMOT, R_FAIL, "v6",
2 },
/* mixed ip4+6, v6 always first, v4 kicks in on HE, fails after ~350ms */
{ 6, TURL, "test.com:123:::1,192.0.2.1", CURL_IPRESOLVE_WHATEVER,
CNCT_TMOT, 150, 250, 250, 1, 1, 350, TC_TMOT, R_FAIL, "v6" },
CNCT_TMOT, 150, 250, 250, 1, 1, 350, TC_TMOT, R_FAIL, "v6",
2 },
/* mixed ip6+4, v6 starts, v4 never starts due to high HE, TIMEOUT */
{ 7, TURL, "test.com:123:192.0.2.1,::1", CURL_IPRESOLVE_V4,
CNCT_TMOT, 150, 500, 500, 1, 0, 400, TC_TMOT, R_FAIL, NULL },
CNCT_TMOT, 150, 500, 500, 1, 0, 400, TC_TMOT, R_FAIL, NULL,
1 },
/* mixed ip4+6, but only use v4, check it uses full connect timeout,
although another address of the 'wrong' family is available */
{ 8, TURL, "test.com:123:::1,192.0.2.1", CURL_IPRESOLVE_V6,
CNCT_TMOT, 150, 500, 500, 0, 1, 400, TC_TMOT, R_FAIL, NULL },
CNCT_TMOT, 150, 500, 500, 0, 1, 400, TC_TMOT, R_FAIL, NULL,
1 },
/* mixed ip4+6, but only use v6, check it uses full connect timeout,
although another address of the 'wrong' family is available */
{ 9, TURL, "test.com:123:::1,192.0.2.1,::2,::3", CURL_IPRESOLVE_WHATEVER,
CNCT_TMOT, 50, 400, 400, 1, 3, 550, TC_TMOT, R_FAIL, NULL },
CNCT_TMOT, 50, 400, 400, 1, 3, 550, TC_TMOT, R_FAIL, NULL,
4 },
/* 1 v4, 3 v6, fails after (3*HE)+400ms, ~550ms, COULDNT_CONNECT */
{ 10, TURL, "test.com:123:::1,192.0.2.1,::2,::3,::4,::5,::6,::7,::8",
CURL_IPRESOLVE_WHATEVER,
CNCT_TMOT, 20, 500, 500, 1, 8, 550, TC_TMOT, R_FAIL, NULL,
6 },
/* 1 v4, 8 v6, MUST meet limit of 6 concurrent attempts */
#endif
};