Skip to content

client: happy eyeballs: retire the primary socket when a racer wins - #3649

Closed
saghul wants to merge 1 commit into
warmcat:mainfrom
saghul:fix-he-racer-promote-orphan
Closed

client: happy eyeballs: retire the primary socket when a racer wins#3649
saghul wants to merge 1 commit into
warmcat:mainfrom
saghul:fix-he-racer-promote-orphan

Conversation

@saghul

@saghul saghul commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Found while implementing the new parallel-connect event lib ops (d31f2d830 "event-loop: HE races") in a custom event lib driving lws off a libuv loop.

When a racing (parallel) connect is seen to have completed, the LCCCR_CONNECTED path in lws_client_connect_3_connect() promotes it with promote_parallel_fd() alone:

			case LCCCR_CONNECTED:
				...
				lws_sul_cancel(&wsi->sul_happy_eyeballs);
				if (pidx != -1)
					promote_parallel_fd(wsi, pidx);

Nothing removes the primary socket from the fds table or closes it. promote_parallel_fd() then moves wsi->position_in_fds_table to the racer's slot, so the primary's slot is orphaned: it still maps the primary fd to this wsi, but any later __remove_wsi_socket_from_fds() on the wsi targets the racer's slot instead, so that entry can never be removed.

Observable effects:

  • pt->fds never drains, so the

    while (pt->fds_count) {
            struct lws *wsi = wsi_from_fd(context, pt->fds[0].fd);
            if (wsi) { lws_close_free_wsi(wsi, ...); ... }
    }

    loop in lws_context_destroy() spins forever at 100% CPU (the wsi it finds is closed over and over, and its position never matches slot 0). Caught with sample as an endless lws_context_destroy__lws_close_free_wsi cycle.

  • the primary socket is leaked.

The conn_good is_parallel path already does this correctly (remove from fds, then promote_parallel op or compatible_close, then promote_parallel_fd); this change makes the LCCCR_CONNECTED path do the same. __remove_wsi_socket_from_fds() already fixes up parallel_conns[].position_in_fds_table when it moves the end entry into the hole, so the remaining racers' slots stay correct.

How easily it is reached, once an event lib implements the parallel ops: on Windows the win32_sul_connect_async_check sul probes racer fds directly, so any racer that connects before a slow primary lands here; on POSIX it needs the companion fix that gives the racing fd its POLLOUT.

Tested on macOS with a libuv-based event lib implementing the parallel ops and the happy-eyeballs delay temporarily forced to 1us, so every multi-address connect races and racers routinely win: before, the process spins at exit after the fetch completes; with this plus the POLLOUT fix, four https fetches complete and the process exits promptly. Full test suite (329 tests) green, ASAN clean.

When a racing (parallel) connect is seen to have completed in the
LCCCR_CONNECTED path, promote_parallel_fd() moves wsi->desc and
wsi->position_in_fds_table to the racer, but nothing removes the primary
socket from the fds table or closes it.  Its slot keeps mapping the primary fd
to this wsi while the wsi's position_in_fds_table now points at the racer's
slot, so the slot can never be removed: __remove_wsi_socket_from_fds() on this
wsi will always target the racer's slot instead.

The observable effect is that pt->fds never drains, so the

	while (pt->fds_count) {
		struct lws *wsi = wsi_from_fd(context, pt->fds[0].fd);
		...
	}

loop in lws_context_destroy() spins forever at 100% CPU, and the primary
socket is leaked.

Retire the primary before promoting, the same way the conn_good is_parallel
path already does (remove from fds, then promote_parallel or close).  Reached
easily with an event lib that implements the parallel ops: on Windows the
win32 connect-check sul probes racer fds directly, and on POSIX once the
racing fd actually gets its POLLOUT.
@saghul

saghul commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Related happy-eyeballs / parallel-connect fixes from the same investigation, all independent of each other: #3648 (racing fd gets the POLLOUT), #3649 (primary retired when a racer wins), #3650 (fds-removal io stop routed through the parallel ops).

@sonarqubecloud

Copy link
Copy Markdown

@saghul saghul closed this Aug 1, 2026
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.

1 participant