Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
31 changes: 19 additions & 12 deletions libuv_reactor.c
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Loading