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: 0 additions & 2 deletions packages/bun-usockets/src/context.c
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,6 @@ static void us_internal_init_listen_socket(struct us_listen_socket_t *ls,
s->flags.allow_half_open = (options & LIBUS_SOCKET_ALLOW_HALF_OPEN);
s->unclassified_send_failures = 0;
s->read_eof = 0;
s->fin_deferred = 0;
s->next = 0;
s->prev = 0;
s->connect_state = NULL;
Expand Down Expand Up @@ -545,7 +544,6 @@ static inline void us_internal_init_connect_socket(struct us_socket_t *s,
s->flags.last_write_failed = 0;
s->unclassified_send_failures = 0;
s->read_eof = 0;
s->fin_deferred = 0;
s->connect_state = NULL;
s->connect_next = NULL;
}
Expand Down
88 changes: 24 additions & 64 deletions packages/bun-usockets/src/eventing/libuv.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,6 @@

#ifdef LIBUS_USE_LIBUV

/* The shared dispatch follows socket adoption (a tunneled/upgraded socket
* moves; the old allocation stays readable with flags.adopted set and prev
* pointing at the live one) and skips closed sockets. The paused-probe below
* must honor the same contract - dereferencing the raw poll cast crashed the
* CONNECT-tunnel tests on the aarch64 agent. */
/* Windows does not reliably latch a received RST in SO_ERROR (POSIX does);
* the reset surfaces on the next I/O. A zero-byte send observes it without
* touching the stream: 0 on a healthy socket, SOCKET_ERROR with a fatal
Expand All @@ -39,19 +34,19 @@ int us_internal_libuv_peer_reset_probe(LIBUS_SOCKET_DESCRIPTOR fd) {
}
int err = WSAGetLastError();
/* WSAESHUTDOWN means our own shutdown(SD_SEND) ran; that is not a peer
* reset. The fin_deferred sweep probes sockets after local shutdown. */
* reset (us_socket_stalled_write_means_peer_gone can ask after one). */
return err != WSAEWOULDBLOCK && err != WSAESHUTDOWN;
}

/* The shared dispatch follows socket adoption (a tunneled/upgraded socket
* moves; the old allocation stays readable with flags.adopted set and prev
* pointing at the live one) and skips closed sockets. poll_cb's probes must
* honor the same contract - dereferencing the raw poll cast crashed the
* CONNECT-tunnel tests on the aarch64 agent. */
static struct us_socket_t *us_internal_poll_cb_adopted_socket(struct us_poll_t *wp) {
return us_internal_socket_follow_adopted((struct us_socket_t *)wp);
}

static int us_internal_poll_cb_socket_is_probeable(struct us_poll_t *wp) {
struct us_socket_t *s = us_internal_poll_cb_adopted_socket(wp);
return !s->flags.is_closed && s->flags.is_paused;
}

/* uv_poll_t->data always (except for most times after calling us_poll_stop)
* points to the us_poll_t */
static void poll_cb(uv_poll_t *p, int status, int events) {
Expand Down Expand Up @@ -87,61 +82,26 @@ static void poll_cb(uv_poll_t *p, int status, int events) {
* never cut at an EAGAIN. */
if (kind == POLL_TYPE_SOCKET_SHUT_DOWN) {
eof = 1;
events |= UV_READABLE;
} else if (kind == POLL_TYPE_SOCKET && us_internal_poll_cb_socket_is_probeable(wp)) {
/* A paused socket polls without READABLE, so the read loop cannot
* discover terminal states for it - and the pause contract forbids
* consuming deferred bytes. MSG_PEEK discriminates without consuming:
* an error is an abortive reset (our libuv patch reports AFD_POLL_ABORT
* as DISCONNECT so it reaches write-only polls at all) and must close
* now like epoll's unmaskable EPOLLERR; 0 is a graceful FIN with no
* data, deferred by the shared dispatch's existing paused-EOF contract
* until resume; pending data keeps the pause honored untouched. */
char probe;
ssize_t peeked = bsd_recv(us_poll_fd(wp), &probe, 1, MSG_PEEK);
if (peeked == 0) {
eof = 1;
events |= UV_READABLE;
} else if (peeked < 0 && !bsd_would_block()) {
error = 1;
events |= UV_READABLE;
} else if (peeked > 0) {
struct us_socket_t *sock = us_internal_poll_cb_adopted_socket(wp);
if (us_socket_get_error(sock) != 0 || us_internal_libuv_peer_reset_probe(us_poll_fd(wp))) {
/* Data is buffered ahead of whatever ended the connection. If the
* peer ABORTED, the kernel already discarded the stream's tail and
* a paused socket that never resumes would otherwise never learn -
* node's paused sockets error immediately on a reset, buffered
* data included. SO_ERROR separates that from a graceful FIN
* behind data, which stays deferred until resume. */
error = 1;
events |= UV_READABLE;
} else if (!sock->fin_deferred) {
/* Graceful FIN deferred behind data. This one-shot DISCONNECT
* report is now consumed, so a LATER reset (an error-path peer
* ends, flushes, then destroys - FIN, then RST) has no event left
* to ride. Mark the socket; the sweep timer escalates via
* SO_ERROR. */
sock->fin_deferred = 1;
sock->group->loop->data.fin_deferred_count++;
}
}
/* A paused socket keeps the hint only; the dispatcher leaves it for
* resume(), whose poll change re-arms DISCONNECT and lands here again. */
events |= us_poll_events(wp) & LIBUS_SOCKET_READABLE;
} else if (kind == POLL_TYPE_SOCKET &&
!(us_poll_events(wp) & LIBUS_SOCKET_READABLE)) {
/* A half-open data socket whose end was already delivered: the EOF path
* moved its poll to WRITABLE-only (loop.c), and us_poll_change re-adds
* UV_DISCONNECT unconditionally, so AFD keeps reporting the FIN's
* level-triggered DISCONNECT. Re-adding READABLE here made recv()
* rediscover the same EOF and busy-loop on_end; keeping DISCONNECT
* armed would complete instantly forever. But the peer's later RST
* must still close the socket (epoll parity: EPOLLERR is unmaskable),
* so ask the kernel which of the two this wakeup is: a dead peer
* surfaces via SO_ERROR or the zero-byte send probe and closes through
* the shared error path; a FIN re-report quiesces with only the
* ABORT-only subscription (UV_PRIORITIZED) kept armed so the RST still
* has an event to ride. Non-SOCKET kinds keep the unconditional
* READABLE below: SEMI_SOCKET checks error/eof (set from status) and
* listen polls READABLE only. */
/* A data socket that is not reading: paused, or half-open with its end
* already delivered (the EOF path moved its poll to WRITABLE-only, and
* us_poll_change re-adds UV_DISCONNECT unconditionally, so AFD keeps
* reporting the FIN's level-triggered DISCONNECT). Re-adding READABLE
* here would pull bytes a paused caller asked to defer, or rediscover
* the same EOF and busy-loop on_end; keeping DISCONNECT armed would
* complete instantly forever. A dead peer surfaces via SO_ERROR or the
* zero-byte send probe and goes through the shared error path (which
* reads off whatever is still queued and closes); a FIN, fresh on a
* paused socket or re-reported on a half-open one, quiesces with only
* the ABORT-only subscription (UV_PRIORITIZED) kept armed so a later
* RST still has an event to ride, and a paused socket meets the FIN
* again through recv() once resume() re-arms READABLE. Non-SOCKET kinds
* keep the unconditional READABLE below: SEMI_SOCKET checks error/eof
* (set from status) and listen polls READABLE only. */
struct us_socket_t *sock = us_internal_poll_cb_adopted_socket(wp);
/* A reported UV_PRIORITIZED is AFD's own ABORT signal and needs no
* probe; the probe covers a reset that arrives while PRIORITIZED was
Expand Down
6 changes: 1 addition & 5 deletions packages/bun-usockets/src/internal/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -326,11 +326,7 @@ struct us_socket_t {
* would-block/transient nor a known peer-gone error (see
* us_socket_write_check_error). Reset by any send that makes progress.
* Lives in the pad-to-pointer gap before `group`, so it costs nothing. */
/* 7 bits fit the 32-cap retry counter; the spare bit marks a paused
* socket whose peer FIN was deferred behind buffered data (libuv path -
* the sweep escalates via SO_ERROR when the peer later resets). */
unsigned char unclassified_send_failures : 7;
unsigned char fin_deferred : 1;
unsigned char unclassified_send_failures;

struct us_socket_group_t *group;
/* NULL for plain TCP. Direct BoringSSL `SSL*`; set by us_internal_ssl_attach
Expand Down
7 changes: 0 additions & 7 deletions packages/bun-usockets/src/internal/loop_data.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,6 @@ struct us_internal_loop_data_t {
long long sweep_next_tick_ns;
#endif
int sweep_timer_count;
#ifdef LIBUS_USE_LIBUV
/* Sockets whose peer FIN was deferred behind buffered data while paused
* (poll_cb's MSG_PEEK probe): the sweep escalates them via SO_ERROR when
* the peer later resets, since the one-shot DISCONNECT report was already
* consumed by the FIN. Zero cost while no socket is in that state. */
int fin_deferred_count;
#endif
struct us_internal_async *wakeup_async;
struct us_socket_group_t *head;
/* QUIC engines on this loop. us_quic_loop_process walks the list from
Expand Down
59 changes: 22 additions & 37 deletions packages/bun-usockets/src/loop.c
Original file line number Diff line number Diff line change
Expand Up @@ -389,30 +389,6 @@ void us_internal_free_closed_sockets(struct us_loop_t *loop) {
#ifdef LIBUS_USE_LIBUV
void sweep_timer_cb(struct us_internal_callback_t *cb) {
us_internal_timer_sweep(cb->loop);
/* Escalate paused sockets whose peer FIN was deferred behind buffered
* data and whose peer has since reset (poll_cb consumed the only
* DISCONNECT report on the FIN; AFD has no event left to deliver the
* abort to a read-less poll). Zero cost unless such sockets exist;
* closing unlinks the socket, so restart the walk after each close. */
while (cb->loop->data.fin_deferred_count > 0) {
struct us_socket_t *victim = 0;
for (struct us_socket_group_t *g = cb->loop->data.head; g && !victim; g = g->next) {
for (struct us_socket_t *s = g->head_sockets; s; s = s->next) {
if (s->fin_deferred && !s->flags.is_closed
&& (us_socket_get_error(s) != 0
|| us_internal_libuv_peer_reset_probe(us_poll_fd(&s->p)))) {
victim = s;
break;
}
}
}
if (!victim) {
break;
}
victim->fin_deferred = 0;
cb->loop->data.fin_deferred_count--;
us_internal_socket_close_raw(victim, LIBUS_SOCKET_CLOSE_CODE_CONNECTION_RESET, 0);
}
}
#endif

Expand Down Expand Up @@ -535,7 +511,6 @@ void us_internal_dispatch_ready_poll(struct us_poll_t *p, int error, int eof, in
s->flags.last_write_failed = 0;
s->unclassified_send_failures = 0;
s->read_eof = 0;
s->fin_deferred = 0;

/* We always use nodelay */
bsd_socket_nodelay(client_fd, 1);
Expand Down Expand Up @@ -613,7 +588,19 @@ void us_internal_dispatch_ready_poll(struct us_poll_t *p, int error, int eof, in
}
}

if (events & LIBUS_SOCKET_READABLE) {
/* An error event (EPOLLERR, EV_EOF with the socket error in fflags, an AFD
* abort) is the connection's death and this dispatch closes the socket with
* it below. The kernel keeps the receive queue on a reset, so the tail of the
* peer's stream may still be queued ahead of the error, and closing without
* reading would discard it (a streamed response cut short although every
* byte arrived, #39846). So the read loop runs for an error even when this
* event carried no READABLE bit or the socket is paused: a pause is flow
* control, and there is no later for a dead connection to flow into. recv()
* then returns the data and after it the error, which is what libuv reports
* to node as well. A socket parked in the low-priority queue is not linked
* where on_data expects it and takes the plain error close. */
const int drain_for_error = error && !s->read_eof && s->flags.low_prio_state != 1;
if ((events & LIBUS_SOCKET_READABLE) || drain_for_error) {
/* Contexts may prioritize down sockets that are currently readable, e.g. when SSL handshake has to be done.
* SSL handshakes are CPU intensive, so we limit the number of handshakes per loop iteration, and move the rest
* to the low-priority queue */
Expand All @@ -622,7 +609,7 @@ void us_internal_dispatch_ready_poll(struct us_poll_t *p, int error, int eof, in
* non-SSL arm dispatched a full vtable lookup just to read
* NULL — no Zig handler defines isLowPrio and every C++ vtable
* sets is_low_prio = nullptr — so it's been dropped. */
if (s->ssl && us_internal_ssl_is_low_prio(s)) {
if (!error && s->ssl && us_internal_ssl_is_low_prio(s)) {
if (flags->low_prio_state == 2) {
flags->low_prio_state = 0; /* Socket has been delayed and now it's time to process incoming data for one iteration */
} else if (loop->data.low_prio_budget > 0) {
Expand Down Expand Up @@ -756,7 +743,7 @@ void us_internal_dispatch_ready_poll(struct us_poll_t *p, int error, int eof, in
* buffer. This is what the comment above always described; it
* was keyed on the error flag, which kqueue does not set for
* a peer FIN. */
if (s && !us_socket_is_closed(s) && !s->flags.is_paused && (eof || error)) {
if (s && !us_socket_is_closed(s) && (error || (!s->flags.is_paused && eof))) {
continue;
}
/* Stop if on_data paused us (us_socket_pause from the data
Expand Down Expand Up @@ -788,7 +775,7 @@ void us_internal_dispatch_ready_poll(struct us_poll_t *p, int error, int eof, in
* a large response on Windows only). recv() returning
* 0 or WSAEWOULDBLOCK ends the loop, so this is
* bounded by the kernel receive buffer. */
if (s && !us_socket_is_closed(s) && !s->flags.is_paused && (eof || error)) {
if (s && !us_socket_is_closed(s) && (error || (!s->flags.is_paused && eof))) {
continue;
}
/* Windows AFD_POLL_ABORT is not level-triggered the way
Expand Down Expand Up @@ -870,14 +857,12 @@ void us_internal_dispatch_ready_poll(struct us_poll_t *p, int error, int eof, in
eof = 0;
}
if (eof && error && !read_fin) {
/* An error event whose read loop did not reach a FIN (the socket is
* paused, or on_data paused it mid-drain): the eof hint next to the
* error flag is the reset taking both directions down (EPOLLHUP beside
* EPOLLERR; EV_EOF with the error in fflags), not an end of stream, so
* it must not take the end path below. That path dispatched on_end for
* a reset, and a TLS socket's on_end closes with a clean code itself,
* so the error close never ran. A FIN this dispatch did read still
* delivers its end first; the error close follows either way. */
/* The eof hint next to an error flag is the reset taking both directions
* down (EPOLLHUP beside EPOLLERR; EV_EOF with the error in fflags), not an
* end of stream, so it must not take the end path below (a TLS socket's
* on_end closes with a clean code itself, and the error would be lost). A
* FIN this dispatch did read still delivers its end first; the error
* close follows either way. */
eof = 0;
}
if(eof && s) {
Expand Down
15 changes: 0 additions & 15 deletions packages/bun-usockets/src/socket.c
Original file line number Diff line number Diff line change
Expand Up @@ -264,12 +264,6 @@ void us_connecting_socket_close(struct us_connecting_socket_t *c) {
* handshake/secureConnection event. openssl.c re-enters here once that
* graceful path is done. */
struct us_socket_t *us_internal_socket_close_raw(struct us_socket_t *s, int code, void *reason) {
#ifdef LIBUS_USE_LIBUV
if (s->fin_deferred) {
s->fin_deferred = 0;
s->group->loop->data.fin_deferred_count--;
}
#endif
if (s->ssl && s->ssl_in_use) {
/* A JS callback running from inside SSL_do_handshake/SSL_read (ALPN, SNI,
* keylog, ...) destroyed this socket. Closing now frees the SSL and
Expand Down Expand Up @@ -465,7 +459,6 @@ struct us_socket_t *us_socket_from_fd(struct us_socket_group_t *group, unsigned
s->flags.last_write_failed = 0;
s->unclassified_send_failures = 0;
s->read_eof = 0;
s->fin_deferred = 0;
s->connect_state = NULL;

/* We always use nodelay */
Expand Down Expand Up @@ -859,14 +852,6 @@ void us_socket_pause(struct us_socket_t *s) {
}

void us_socket_resume(struct us_socket_t *s) {
#ifdef LIBUS_USE_LIBUV
/* Reads flow again: normal delivery discovers the deferred FIN (and any
* reset behind it), so the sweep no longer owns this socket. */
if (s->fin_deferred) {
s->fin_deferred = 0;
s->group->loop->data.fin_deferred_count--;
}
#endif
if (!s->flags.is_paused) return;
s->flags.is_paused = 0;
// closed cannot be resumed
Expand Down
Loading