Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
20 changes: 16 additions & 4 deletions packages/bun-usockets/src/loop.c
Original file line number Diff line number Diff line change
Expand Up @@ -354,9 +354,16 @@ void us_internal_handle_low_priority_sockets(struct us_loop_t *loop) {
}

us_internal_socket_group_link_socket(s->group, s);
us_poll_change(&s->p, s->group->loop, us_poll_events(&s->p) | LIBUS_SOCKET_READABLE);

s->flags.low_prio_state = 2;

if (us_poll_change(&s->p, s->group->loop, us_poll_events(&s->p) | LIBUS_SOCKET_READABLE) != 0) {
Comment thread
robobun marked this conversation as resolved.
Resolved
/* The dispatcher took this socket out of epoll when it hung up in the queue, and the
* kernel refused to take it back: nothing would report it again (see us_socket_resume). */
int err = LIBUS_ERR;
us_internal_socket_close_raw(s, err > 2 ? err : LIBUS_ECONNRESET, NULL);
/* The caller picked the timeout of this tick before the close handler ran. */
us_wakeup_loop(loop);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
}

Expand Down Expand Up @@ -875,10 +882,15 @@ void us_internal_dispatch_ready_poll(struct us_poll_t *p, int error, int eof, in
* read_eof is set there is nothing left to drain and deferring would only
* lose the close. Error-flagged events keep the error path. A hangup
* takes the write side down too, so a hangup_closes_unsent socket does
* not wait when the write this event retried failed again. */
* not wait when the write this event retried failed again.
* A socket that waits in the low-priority queue has its reads off too, and
* us_internal_handle_low_priority_sockets is its resume(). READABLE in
* `events` means that the read loop ran here: that eof is behind the data. */
const int eof_deferrable = eof && s && !error && !us_socket_is_closed(s) && !s->read_eof;
const int unsent_is_lost = hangup && s && s->hangup_closes_unsent && s->flags.last_write_failed;
if (eof_deferrable && s->flags.is_paused && !unsent_is_lost) {
const int reads_are_off = eof_deferrable &&
(s->flags.is_paused || (s->flags.low_prio_state == 1 && !(events & LIBUS_SOCKET_READABLE)));
if (reads_are_off && !unsent_is_lost) {
#ifdef LIBUS_USE_EPOLL
/* EPOLLHUP is unmaskable: leave epoll while paused so it cannot re-fire; the unread tail stays in
* the kernel until resume() re-adds the fd via us_poll_change (end() while paused keeps it parked). */
Expand Down
17 changes: 17 additions & 0 deletions test/js/bun/net/socket-syscall-fault.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,23 @@ test.concurrent.skipIf(!fault.available() || !isLinux)(
},
);

// A socket that waits in the low-priority handshake queue leaves epoll the
// same way when its peer hangs up, and the queue registers it again when it
// hands the socket back. The fixture makes the first registration fail: that
// socket closes with the error, and the other 15 read what their peer sent.
test.concurrent.skipIf(!fault.available() || !isLinux)(
"a socket that the TLS handshake queue cannot register again closes with the error",
async () => {
expect(await runFixture("tls-parked-hangup-fixture.ts")).toEqual({
summary: { outcomes: { "handshake, data last words, close": 15, "close ENOMEM": 1 } },
signalCode: null,
exitCode: 0,
stderrTail: "",
});
},
60_000,
);

// An injected send() errno that is neither would-block/transient
// (EAGAIN/ENOBUFS/ENOMEM) nor a known peer-gone error (EPIPE/ECONNRESET/...)
// exercises the bounded unclassified-errno retry in
Expand Down
Loading
Loading