Skip to content
Closed
2 changes: 2 additions & 0 deletions packages/bun-usockets/src/context.c
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ static void us_internal_init_listen_socket(struct us_listen_socket_t *ls,
s->flags.adopted = 0;
s->flags.allow_half_open = (options & LIBUS_SOCKET_ALLOW_HALF_OPEN);
s->unclassified_send_failures = 0;
s->fatal_send_errno = 0;
s->next = 0;
s->prev = 0;
s->connect_state = NULL;
Expand Down Expand Up @@ -491,6 +492,7 @@ static inline void us_internal_init_connect_socket(struct us_socket_t *s,
s->flags.adopted = 0;
s->flags.last_write_failed = 0;
s->unclassified_send_failures = 0;
s->fatal_send_errno = 0;
s->connect_state = NULL;
s->connect_next = NULL;
}
Expand Down
5 changes: 5 additions & 0 deletions packages/bun-usockets/src/internal/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,11 @@ struct us_socket_t {
* the sweep escalates via SO_ERROR when the peer later resets). */
unsigned char unclassified_send_failures : 7;
unsigned char fin_deferred : 1;
/* Nonzero when a send() from a us_socket_*write* helper failed with an
* errno that cannot recover (peer-gone, or the unclassified retry ceiling).
* loop.c closes the socket with this code on the next writable dispatch.
* POSIX errnos fit in a byte; lives in the pad-to-pointer gap. */
unsigned char fatal_send_errno;

struct us_socket_group_t *group;
/* NULL for plain TCP. Direct BoringSSL `SSL*`; set by us_internal_ssl_attach
Expand Down
13 changes: 13 additions & 0 deletions packages/bun-usockets/src/loop.c
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ void us_internal_dispatch_ready_poll(struct us_poll_t *p, int error, int eof, in
s->flags.adopted = 0;
s->flags.last_write_failed = 0;
s->unclassified_send_failures = 0;
s->fatal_send_errno = 0;

/* We always use nodelay */
bsd_socket_nodelay(client_fd, 1);
Expand Down Expand Up @@ -600,6 +601,18 @@ void us_internal_dispatch_ready_poll(struct us_poll_t *p, int error, int eof, in
return;
}

/* A send() from one of the us_socket_*write* helpers returned a
* fatal errno (peer-gone, or the unclassified retry ceiling hit).
* Without this, the TLS spill path re-armed the writable poll on
* every failure and spun at 100% for as long as the kernel kept
* the fd writable. Same close shape as the poll-error branch. */
if (UNLIKELY(s->fatal_send_errno)) {
int send_errno = s->fatal_send_errno;
s->fatal_send_errno = 0;
s = us_internal_socket_close_raw(s, send_errno > 2 ? send_errno : ECONNRESET, NULL);
return;
}

/* If we have no failed write or if we shut down, then stop polling for more writable */
if (!s->flags.last_write_failed || us_socket_is_shut_down(s)) {
us_poll_change(&s->p, loop, us_poll_events(&s->p) & LIBUS_SOCKET_READABLE);
Expand Down
169 changes: 103 additions & 66 deletions packages/bun-usockets/src/socket.c
Original file line number Diff line number Diff line change
Expand Up @@ -411,14 +411,92 @@ static void us_internal_rearm_writable(struct us_socket_t *s) {
LIBUS_SOCKET_WRITABLE | (s->flags.is_paused ? 0 : LIBUS_SOCKET_READABLE));
}

#ifndef _WIN32
/* send() errnos that mean the peer or the path to it is gone: no retry can
* ever succeed, so they are reported to the caller immediately (libuv fails
* the write request for these in uv__try_write, unix/stream.c; only the
* EAGAIN/ENOBUFS class waits for another writable event). Mirrored by the
* blanket `result < -1` fatal handling in h2_frame_parser's
* is_transport_fatal_write_result - classification lives here only. */
static int us_internal_send_errno_is_peer_gone(int e) {
switch (e) {
case EPIPE:
case ECONNRESET:
case ECONNABORTED:
case ENOTCONN:
case ETIMEDOUT:
case ENETDOWN:
case ENETUNREACH:
case EHOSTUNREACH:
return 1;
default:
return 0;
}
}

/* One retry runs per writable dispatch (one event-loop iteration), so 32
* consecutive failures is far beyond any observed transient race window
* (the macOS EPROTOTYPE race resolves within a dispatch or two) while still
* surfacing a genuinely wedged transport within microseconds. */
#define US_UNCLASSIFIED_SEND_RETRY_LIMIT 32

/* Classify a -1 from bsd_send/bsd_writev/bsd_write2/bsd_sendmsg. Returns 0
* when the caller should treat it like backpressure (re-arm the writable poll
* and wait), or the errno when the transport is dead and re-arming would spin.
* Applied by every us_socket_*write* helper so the TLS spill path (which
* routes through us_socket_raw_write from ssl_drain_spill) cannot busy-loop
* the writable dispatch on a socket whose send() keeps failing. */
static int us_internal_classify_send_error(struct us_socket_t *s) {
if (bsd_would_block() || bsd_send_is_transient_error()) {
return 0;
}
if (!us_internal_send_errno_is_peer_gone(errno) &&
s->unclassified_send_failures < US_UNCLASSIFIED_SEND_RETRY_LIMIT) {
s->unclassified_send_failures++;
return 0;
}
return errno ? errno : EPIPE;
}
Comment thread
robobun marked this conversation as resolved.
Resolved
#endif

/* Common tail for the us_socket_*write* helpers after a short write. On POSIX
* a -1 is routed through errno classification so a fatal errno latches
* fatal_send_errno (loop.c closes the socket after the writable dispatch)
* instead of re-arming for a retry that can never succeed. A partial write,
* would-block, transient exhaustion, or a still-bounded unclassified errno
* re-arms for a retry like before. The fatal path still re-arms once: the
* write may have been issued outside the writable dispatch, and loop.c's
* fatal_send_errno check runs there, so one more writable event is what
* delivers the close. It does not spin because the check precedes the retry. */
static void us_internal_on_short_write(struct us_socket_t *s, int written) {
#ifndef _WIN32
if (written < 0) {
int fatal = us_internal_classify_send_error(s);
if (fatal) {
s->fatal_send_errno = (unsigned char)fatal;
us_internal_rearm_writable(s);
return;
}
} else {
s->unclassified_send_failures = 0;
}
#else
(void)written;
#endif
s->flags.last_write_failed = 1;
us_internal_rearm_writable(s);
}
Comment thread
robobun marked this conversation as resolved.
Resolved

int us_socket_write2(struct us_socket_t *s, const char *header, int header_length, const char *payload, int payload_length) {
if (us_socket_is_closed(s) || us_socket_is_shut_down(s)) {
return 0;
}

int written = bsd_write2(us_poll_fd(&s->p), header, header_length, payload, payload_length);
if (written != header_length + payload_length) {
us_internal_rearm_writable(s);
us_internal_on_short_write(s, written);
} else {
s->unclassified_send_failures = 0;
}

return written < 0 ? 0 : written;
Expand Down Expand Up @@ -450,6 +528,7 @@ struct us_socket_t *us_socket_from_fd(struct us_socket_group_t *group, unsigned
s->flags.adopted = 0;
s->flags.last_write_failed = 0;
s->unclassified_send_failures = 0;
s->fatal_send_errno = 0;
s->connect_state = NULL;

/* We always use nodelay */
Expand Down Expand Up @@ -491,43 +570,14 @@ int us_socket_write(struct us_socket_t *s, const char *data, int length) {

int written = bsd_send(us_poll_fd(&s->p), data, length);
if (written != length) {
s->flags.last_write_failed = 1;
us_internal_rearm_writable(s);
us_internal_on_short_write(s, written);
} else {
s->unclassified_send_failures = 0;
}

return written < 0 ? 0 : written;
}

#ifndef _WIN32
/* send() errnos that mean the peer or the path to it is gone: no retry can
* ever succeed, so they are reported to the caller immediately (libuv fails
* the write request for these in uv__try_write, unix/stream.c; only the
* EAGAIN/ENOBUFS class waits for another writable event). Mirrored by the
* blanket `result < -1` fatal handling in h2_frame_parser's
* is_transport_fatal_write_result - classification lives here only. */
static int us_internal_send_errno_is_peer_gone(int e) {
switch (e) {
case EPIPE:
case ECONNRESET:
case ECONNABORTED:
case ENOTCONN:
case ETIMEDOUT:
case ENETDOWN:
case ENETUNREACH:
case EHOSTUNREACH:
return 1;
default:
return 0;
}
}

/* One retry runs per writable dispatch (one event-loop iteration), so 32
* consecutive failures is far beyond any observed transient race window
* (the macOS EPROTOTYPE race resolves within a dispatch or two) while still
* surfacing a genuinely wedged transport within microseconds. */
#define US_UNCLASSIFIED_SEND_RETRY_LIMIT 32
#endif

int us_socket_write_check_error(struct us_socket_t *s, const char *data, int length, int *fatal_write_error) {
if (fatal_write_error) *fatal_write_error = 0;
if (us_socket_is_closed(s) || us_socket_is_shut_down(s)) {
Expand All @@ -540,6 +590,17 @@ int us_socket_write_check_error(struct us_socket_t *s, const char *data, int len

int written = bsd_send(us_poll_fd(&s->p), data, length);
if (written < 0) {
#ifndef _WIN32
int fatal = us_internal_classify_send_error(s);
if (!fatal) {
s->flags.last_write_failed = 1;
us_internal_rearm_writable(s);
return 0;
}
/* Fatal send error: report the errno to callers that opt in and stop
* polling writable - retrying can never succeed. */
if (fatal_write_error) *fatal_write_error = fatal;
#else
/* bsd_send already retries EINTR; bsd_would_block() reads errno on
* POSIX and WSAGetLastError() on Windows. ENOBUFS/ENOMEM are
* transient kernel resource exhaustion on a healthy connection -
Expand All @@ -550,33 +611,6 @@ int us_socket_write_check_error(struct us_socket_t *s, const char *data, int len
us_internal_rearm_writable(s);
return 0;
}
#ifndef _WIN32
/* Anything else that is not a known peer-gone errno gets a BOUNDED
* retry through the same rearm/writable machinery as would-block.
* Kernels return racy, non-terminal errnos from send() on sockets
* that are still perfectly usable - macOS EPROTOTYPE while the
* connection is concurrently mutating is the canonical case, and
* libuv retries it (RETRY_ON_WRITE_ERROR,
* https://github.com/libuv/libuv/blob/v1.x/src/unix/stream.c) -
* so failing the write on first sight killed live connections.
* But the retry must not be unbounded: an errno that persists across
* many consecutive writable dispatches with no successful send in
* between means the transport is dead in a way the kernel will not
* name on the write side, and silently re-arming forever jams the
* connection (buffered bytes never drain and no error ever
* surfaces). After the limit, report the errno like the peer-gone
* class so the caller can fail the write. */
if (!us_internal_send_errno_is_peer_gone(errno) &&
s->unclassified_send_failures < US_UNCLASSIFIED_SEND_RETRY_LIMIT) {
s->unclassified_send_failures++;
s->flags.last_write_failed = 1;
us_internal_rearm_writable(s);
return 0;
}
/* Fatal send error: report the errno to callers that opt in and stop
* polling writable - retrying can never succeed. */
if (fatal_write_error) *fatal_write_error = errno;
#else
/* Windows: report the raw (positive) WSA code. The JS layer already
* traffics in raw negative WSA values on this platform (see
* SocketEmitEndNT / failWrite, which shape unnameable ones as
Expand Down Expand Up @@ -612,8 +646,9 @@ int us_socket_raw_writev(struct us_socket_t *s, const struct us_iovec_t *iov, in

ssize_t written = bsd_writev(us_poll_fd(&s->p), iov, count);
if (written != (ssize_t)total) {
s->flags.last_write_failed = 1;
us_internal_rearm_writable(s);
us_internal_on_short_write(s, written < 0 ? -1 : 0);
} else {
s->unclassified_send_failures = 0;
}

return written < 0 ? 0 : (int)written;
Expand All @@ -631,8 +666,9 @@ int us_socket_raw_write(struct us_socket_t *s, const char *data, int length) {

int written = bsd_send(us_poll_fd(&s->p), data, length);
if (written != length) {
s->flags.last_write_failed = 1;
us_internal_rearm_writable(s);
us_internal_on_short_write(s, written);
} else {
s->unclassified_send_failures = 0;
}

return written < 0 ? 0 : written;
Expand Down Expand Up @@ -668,8 +704,9 @@ int us_socket_ipc_write_fd(struct us_socket_t *s, const char *data, int length,
int sent = bsd_sendmsg(us_poll_fd(&s->p), &msg, 0);

if (sent != length) {
s->flags.last_write_failed = 1;
us_internal_rearm_writable(s);
us_internal_on_short_write(s, sent);
} else {
s->unclassified_send_failures = 0;
}

return sent < 0 ? 0 : sent;
Expand Down
Loading
Loading