From a97cd13a8e04406a3c190e35c17a336bb7ed5877 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+edmonddantes@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:02:50 +0300 Subject: [PATCH] Accept one connection at a time on a Windows listener, so a paused one delivers in arrival order (#305) --- CHANGELOG.md | 2 +- libuv_reactor.c | 31 +++++++++++++++++++------------ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce010ac..00f2e79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- **`stop()` on a listen event did not stop accepting** (#305). libuv has no call that undoes `uv_listen`, and `libuv_listen_stop` only dropped the event's reference count, so a stopped listener went on accepting and every connection reached its callbacks: true-async-server, which pauses its listeners at its connection cap, answered each connection that arrived during a pause with 503 instead of leaving it in the kernel backlog, 5 of 5 runs. A stopped listener now leaves the next connection unaccepted; libuv then stops watching the socket on Unix and posts no new AcceptEx on Windows, so the rest of the queue waits in the backlog. A later `start()` delivers the held connection on the next loop tick, never from inside `start()`, and a callback that pauses again during that delivery stops it. The first `stop()` switches a Windows TCP listener to one AcceptEx at a time, so only that first pause can hold the requests posted in advance; a listener that is never stopped keeps libuv's default. A failed `uv_accept` on Unix no longer leaves the socket unwatched: libuv restarts the watcher only after a successful one, and the listener now restarts it itself. Evidence: `sapi/asynctest/tests/test_listen_pause.c` in php-src, 5 of 5 cases red before and green after (3 of 3 runs); true-async-server's `h1/062`, `h1/063` and `h1/064` answer 503 before and pass 10 of 10 after. The accept rate of a Windows listener after its first pause is not measured. +- **`stop()` on a listen event did not stop accepting** (#305). libuv has no call that undoes `uv_listen`, and `libuv_listen_stop` only dropped the event's reference count, so a stopped listener went on accepting and every connection reached its callbacks: true-async-server, which pauses its listeners at its connection cap, answered each connection that arrived during a pause with 503 instead of leaving it in the kernel backlog, 5 of 5 runs. A stopped listener now leaves the next connection unaccepted; libuv then stops watching the socket on Unix and posts no new AcceptEx on Windows, so the rest of the queue waits in the backlog. A later `start()` delivers the held connection on the next loop tick, never from inside `start()`, and a callback that pauses again during that delivery stops it. A Windows TCP listener posts one AcceptEx at a time instead of libuv's 32: libuv keeps completed accepts in a LIFO stack, so a pause that held several handed the newest over first, and on the server's Windows CI a keep-alive connection that overtook an older one kept it waiting past its 5 s timeout (`h1/064`). With one, the listener holds at most one connection and the rest wait in the kernel backlog in arrival order, and worker threads that share one socket get an even share; the cost is one accept per loop iteration, not measured. A failed `uv_accept` on Unix no longer leaves the socket unwatched: libuv restarts the watcher only after a successful one, and the listener now restarts it itself. Evidence: `sapi/asynctest/tests/test_listen_pause.c` in php-src, 5 of 5 cases red before and green after (3 of 3 runs); true-async-server's `h1/062`, `h1/063` and `h1/064` answer 503 before and pass 10 of 10 after. - **`graceful_shutdown()` ignored its `$cancellationError` argument** (#302). The argument was parsed and dropped: `cancel_queued_coroutines()` built its own `AsyncCancellation("Graceful shutdown")` and every coroutine received that one, so a caller passing `new ServerShutdown('...')` could not tell its own shutdown from the default. The PHP function now hands the object to the scheduler, which cancels the coroutines with it and falls back to the default only when none was given; a shutdown already in progress keeps its own cancellation. Evidence: `tests/edge_cases/015-graceful_shutdown_custom_cancellation.phpt`, red before and green after. diff --git a/libuv_reactor.c b/libuv_reactor.c index 74cf50f..ea8544c 100644 --- a/libuv_reactor.c +++ b/libuv_reactor.c @@ -6916,12 +6916,10 @@ typedef struct /* stop() released the last reference. The connection callback then leaves * each connection to libuv, which on Unix stops watching the socket and on * Windows posts no new AcceptEx: the rest of the queue stays in the kernel - * backlog until start(). The first stop() switches a Windows TCP listener - * to one AcceptEx at a time, so only the first pause can hold the - * requests posted in advance. */ + * backlog until start(), in arrival order. */ bool paused; /* libuv reported a connection that is not accepted yet: the one in - * accepted_fd on Unix, completed AcceptEx requests on Windows. */ + * accepted_fd on Unix, the one completed AcceptEx on Windows. */ bool held; bool deliver_ready; /* Handles still open at dispose; the event is freed when this reaches 0. */ @@ -7132,6 +7130,22 @@ static bool libuv_listen_start(zend_async_event_t *event) async_listen_event_t *listen_event = (async_listen_event_t *) (event); if (!listen_event->listening) { + if (!listen_event->is_unix) { + /* One AcceptEx at a time on Windows (a no-op elsewhere), set before + * uv_listen. libuv keeps completed accepts in a LIFO stack, so a + * paused listener holding several would hand the newest over first; + * with one it holds at most one, and the rest wait in the kernel + * backlog in arrival order. Worker threads sharing one socket also + * get an even share this way. The cost is one accept per loop + * iteration. */ + const int error = uv_tcp_simultaneous_accepts(&listen_event->uv_handle.tcp, 0); + + if (error < 0) { + async_throw_error("Failed to limit pending accepts: %s", uv_strerror(error)); + return false; + } + } + const int error = uv_listen( (uv_stream_t *) &listen_event->uv_handle, listen_event->event.backlog, on_connection_event); @@ -7164,13 +7178,7 @@ static bool libuv_listen_stop(zend_async_event_t *event) EVENT_STOP_PROLOGUE(event); /* libuv cannot stop a uv_listen; the listener pauses instead, see `paused`. */ - async_listen_event_t *listen_event = (async_listen_event_t *) event; - listen_event->paused = true; - - if (!listen_event->is_unix) { - uv_tcp_simultaneous_accepts(&listen_event->uv_handle.tcp, 0); - } - + ((async_listen_event_t *) event)->paused = true; event->loop_ref_count = 0; ZEND_ASYNC_DECREASE_EVENT_COUNT(event); return true; @@ -7319,7 +7327,6 @@ zend_async_listen_event_t *libuv_socket_listen(const char *host, int port, int b // Set socket options uv_tcp_nodelay(&listen_event->uv_handle.tcp, 1); - uv_tcp_simultaneous_accepts(&listen_event->uv_handle.tcp, 1); // Bind to address struct sockaddr_storage addr;