Skip to content

usockets: keep READABLE off a half-open socket whose on_end already fired - #35939

Closed
robobun wants to merge 5 commits into
mainfrom
farm/e84ea709/usockets-halfopen-end-loop
Closed

usockets: keep READABLE off a half-open socket whose on_end already fired#35939
robobun wants to merge 5 commits into
mainfrom
farm/e84ea709/usockets-halfopen-end-loop

Conversation

@robobun

@robobun robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Problem

With Bun.listen({ allowHalfOpen: true }), if the server's end handler does a socket.write() that the kernel only partially accepts, on_end is re-dispatched on every loop iteration forever. drain fires at most once between re-entries, so the payload never finishes flushing.

using server = Bun.listen({
  hostname: "127.0.0.1", port: 0, allowHalfOpen: true,
  socket: {
    open(s) { s.data = { sent: 0 }; },
    data() {},
    end(s) {                       // fires hundreds of times
      s.data.sent = s.write(Buffer.alloc(4 * 1024 * 1024, 0x61));
      if (s.data.sent >= 4 * 1024 * 1024) s.shutdown();
    },
    drain(s) { /* flush the rest */ },
    close() {},
  },
});

Cause

The half-open eof branch at loop.c sets the poll to WRITABLE only, then dispatches on_end. A partial write inside that handler calls us_internal_rearm_writable, which re-adds READABLE. On the next tick epoll reports the fd readable again (the peer's FIN is still there), recv() returns 0, eof is re-derived, and the half-open branch re-runs: on_end is re-dispatched, and the handler's partial write re-adds READABLE again.

Fix

Add a readable_ended bit to us_socket_t (in the existing bitfield gap; struct size unchanged) and set it the first time the half-open eof branch dispatches on_end. Then:

  • loop.c eof branch: if readable_ended is already set, drop READABLE and skip the re-dispatch instead of re-firing on_end. A backpressured write's rearm_writable still re-adds READABLE (the Windows/TLS half-close drain in libuv.c's UV_DISCONNECT handling relies on it being registered to keep the writable dispatch flowing), so this guard is what absorbs the 0-byte read.
  • us_internal_socket_raw_shutdown: when the peer FIN was already delivered, the poll can sit at WRITABLE-only (or 0), so events & READABLE is 0 and on kqueue/libuv nothing would wake the SHUT_DOWN close path. Arm READABLE explicitly so the next tick reads 0 bytes and closes via the existing is_shut_down branch. epoll got this for free via unmaskable EPOLLHUP; this makes the other backends match.
  • us_socket_resume: skip READABLE in the still-writable arm when readable_ended (the loop.c guard would catch it anyway; this saves one 0-byte read). The is_shut_down arm keeps READABLE unconditionally, same as raw_shutdown.

The bit is zero-initialized at every socket construction site (us_create_poll uses us_malloc, not calloc).

#34487 introduces the same readable_ended bit for the Windows UV_DISCONNECT level-triggered trigger of the same re-dispatch. This PR's loop.c guard covers both triggers and adds a Linux-reproducible test; the field placement is identical so whichever lands first, the other rebases cleanly.

How did you verify your code works?

New test in test/js/bun/net/tcp-server.test.ts clamps SO_SNDBUF (POSIX) so the write from end() is a partial write on every kernel, and asserts endCount == 1 with the full payload delivered.

Linux gate proof:

# packages/ reverted to main
$ bun bd test test/js/bun/net/tcp-server.test.ts -t allowHalfOpen
  { endCount: 2, received: 80384 }      # second end() terminates the socket
(fail)

# with this PR
$ bun bd test test/js/bun/net/tcp-server.test.ts -t allowHalfOpen
(pass)

Verified on windows-x64 at 30274ea: node-http-backpressure.test.ts "client FIN right after" 8/8, serve.test.ts "client half-close after the request" 5/5, tcp-server.test.ts allowHalfOpen pass.

test/js/node/net/node-net-allowHalfOpen.test.js, the test-net-*half* / test-net-*end* node parallel tests, node-net.test.ts and socket.test.ts all have the same pass/fail as main.


no test proof · iteration 3 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/net/tcp-server.test.ts

With allowHalfOpen, the eof branch drops READABLE and dispatches on_end.
If that handler's socket.write() is only partially accepted, the
backpressure re-arm (us_internal_rearm_writable) put READABLE back, so
the next epoll tick re-derived recv()==0 -> eof and re-fired on_end,
forever. Track the "peer FIN delivered" state and have the re-arm /
resume / shutdown paths respect it.
@robobun

robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 2:04 PM PT - Jul 26th, 2026

@robobun, your commit 30274ea has 1 failures in Build #82594 (All Failures):

  • 📦 Binary size — 12 over 0.50 MB
  • targetthis build canary: main #79916
    sizeΔ
    bun-darwin-aarch6458.11 MB57.58 MB+548.8 KB
    bun-darwin-x6463.48 MB62.95 MB+544.5 KB
    bun-linux-aarch6470.98 MB70.42 MB+576.0 KB
    bun-linux-x6472.47 MB71.95 MB+528.0 KB
    bun-linux-aarch64-musl64.82 MB64.32 MB+512.0 KB
    bun-linux-x64-musl66.98 MB66.45 MB+544.0 KB
    bun-linux-aarch64-android78.47 MB77.97 MB+512.0 KB
    bun-linux-x64-android80.62 MB80.10 MB+529.2 KB
    bun-freebsd-x6483.07 MB82.56 MB+528.0 KB
    bun-freebsd-aarch6484.84 MB84.31 MB+544.0 KB
    bun-windows-x6480.26 MB79.70 MB+570.0 KB
    bun-windows-aarch6470.86 MB70.34 MB+533.0 KB

    Add [skip size check] to the commit message if this increase is intentional.


🧪   To try this PR locally:

bunx bun-pr 35939

That installs a local version of the PR into your bun-35939 executable, so you can run:

bun-35939 --bun

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The socket layer adds readable_ended tracking, initializes it across socket creation paths, guards EOF and shutdown rearming, and adds a regression test for duplicate end() handling during partial writes.

Half-open EOF handling

Layer / File(s) Summary
Readable EOF state and initialization
packages/bun-usockets/src/internal/internal.h, packages/bun-usockets/src/context.c, packages/bun-usockets/src/loop.c, packages/bun-usockets/src/socket.c
Adds and initializes the readable_ended socket-state bit across listener, connector, accepted, and file-descriptor socket setup.
EOF event and shutdown flow
packages/bun-usockets/src/loop.c, packages/bun-usockets/src/socket.c
Records delivered EOF, prevents duplicate end dispatch, and conditionally re-arms readable events during writable handling, raw shutdown, and resume.
Partial-write regression coverage
test/js/bun/net/tcp-server.test.ts
Adds an allowHalfOpen test that forces partial writes and verifies a single end() call, complete payload delivery, and drain activity where supported.

Suggested reviewers: cirospaciari, jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise, specific, and accurately summarizes the main half-open socket READABLE fix.
Description check ✅ Passed The description includes both required sections and provides detailed problem, fix, and verification notes.

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Node http.createServer can 100% CPU after day uptime in Bun 1.3.13 and 1.4.0 #32600 - 100% CPU spin in long-running http.createServer matches the infinite on_end re-dispatch on half-open sockets that this PR fixes

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #32600

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. usockets: re-arm readable in raw_shutdown after read_eof so the close is delivered on Windows #34487 - Both fix infinite on_end re-dispatch on half-open sockets by introducing the same readable_ended bitfield and guarding the same code paths (loop.c EOF branch, us_internal_rearm_writable, us_socket_resume, us_internal_socket_raw_shutdown); usockets: re-arm readable in raw_shutdown after read_eof so the close is delivered on Windows #34487 targets the Windows/libuv trigger while this PR extends the mechanism to Linux/epoll

🤖 Generated with Claude Code

Comment thread packages/bun-usockets/src/socket.c Outdated
… test

- us_socket_resume: the is_shut_down arm keeps READABLE regardless of
  readable_ended, mirroring raw_shutdown (loop.c checks is_shut_down
  before readable_ended, so re-deriving eof there is what closes us on
  kqueue).
- loop.c: reword the readable_ended guard comment to name the triggers
  that actually bypass the rearm_writable/resume guards.
- test: clamp SO_SNDBUF via setSocketOptions so the write from end() is
  a partial write on every kernel, and assert drainCount >= 1.
setSocketOptions is a POSIX-only no-op on Windows, and Windows loopback
auto-tuning accepts 256 KiB in one send(). The primary assertion
(endCount == 1 with the full payload delivered) already proves the fix
there.
Comment thread test/js/bun/net/tcp-server.test.ts

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both prior findings are addressed — the is_shut_down arm of us_socket_resume keeps READABLE unconditionally again, and the drainCount assertion is gated on !isWindows with the stale comment fixed. I didn't find anything further, but this touches the core socket poll-mask / eof dispatch path across all three backends (epoll/kqueue/libuv), so it's worth a human look.

What was reviewed:

  • readable_ended is explicitly zeroed at all five socket-init sites (listen, connect, start_connections, accept, from_fd); us_create_poll uses us_malloc not calloc, so this is load-bearing.
  • The loop.c is_shut_down check runs before the new readable_ended guard, so re-arming READABLE in raw_shutdown/resume's shut-down arm still reaches close_raw, not the re-dispatch guard.
  • The new bitfield sits in the existing pad-to-pointer gap alongside the ssl_* bits — struct size unchanged.
Extended reasoning...

Overview

Adds a readable_ended bit to us_socket_t that latches the first time the half-open eof branch dispatches on_end, then consults it at the three sites that would otherwise re-add LIBUS_SOCKET_READABLE to the poll mask (us_internal_rearm_writable, us_socket_resume, and the loop.c eof re-entry). us_internal_socket_raw_shutdown gains a compensating branch that explicitly arms READABLE when readable_ended is set, so kqueue/libuv still wake the SHUT_DOWN close path (epoll got this via unmaskable EPOLLHUP). A regression test in tcp-server.test.ts reproduces the infinite end() re-dispatch on Linux by clamping SO_SNDBUF and issuing a partial write from the end handler.

Security risks

None identified. This is event-loop poll-mask bookkeeping on already-authenticated local sockets; no untrusted input parsing, no auth/crypto surface.

Level of scrutiny

High. loop.c's eof handling and us_poll_change mask decisions run on every TCP connection in the runtime, and the semantics differ across epoll (level-triggered, unmaskable EPOLLHUP), kqueue (one-shot EVFILT_WRITE, per-filter EV_EOF), and libuv/AFD (UV_DISCONNECT). A mistake here manifests as a hang or busy-spin that only reproduces on one platform. That's beyond the "simple/mechanical" bar for auto-approval.

Other factors

  • Two earlier review rounds surfaced (1) an internal inconsistency where us_socket_resume's is_shut_down arm would set the poll to 0 on kqueue, and (2) a Windows test flake from asserting drainCount where setSocketOptions is a no-op. Both were fixed (f158cfe, 9a53f43, 7ce958e) and re-verified in this pass.
  • I confirmed us_create_poll uses us_malloc (not calloc), so the explicit readable_ended = 0 at each init site is required; all five creation paths are covered.
  • The PR overlaps with #34487 (Windows UV_DISCONNECT trigger of the same re-dispatch) — a maintainer should decide how the two land relative to each other.
  • The new test wires close on both sides to resolve the awaited promises and terminates on the second end() re-entry, so the pre-fix failure mode is a clean assertion rather than a hang.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both of my earlier concerns are addressed (f158cfe keeps READABLE in us_socket_resume's shut-down arm to mirror raw_shutdown; 9a53f43/7ce958e gate the drainCount assertion on !isWindows and fix the stale 4 MiB comment). No new issues found.

I'm not auto-approving because this is core usockets poll-rearm / eof-dispatch C code with backend-specific semantics (the raw_shutdown change specifically compensates for kqueue/libuv lacking epoll's unmaskable EPOLLHUP), and it overlaps mechanically with open #34487 — a maintainer should decide how the two land.

What was reviewed:

  • readable_ended initialized at every us_socket_t construction site (listen/connect/accept/from_fd/start_connections); us_socket_adopt carries it via us_poll_resize memcpy, which is correct.
  • loop.c ordering: is_shut_down check precedes readable_ended, so a shut-down socket still reaches close_raw when eof is re-derived — consistent with the raw_shutdown/resume READABLE-on-shut-down invariant.
  • Test wires close on both sides to resolve, terminates on the second end() so the buggy build fails on the assertion instead of hanging, and the SO_SNDBUF clamp forces a partial write on POSIX.
Extended reasoning...

Overview

Adds a readable_ended bit to us_socket_t (in the existing pad-to-pointer bitfield gap; struct size unchanged) that latches the first half-open on_end dispatch. Four call sites now consult it: us_internal_rearm_writable and the non-shut-down arm of us_socket_resume skip re-adding READABLE; the loop.c eof branch drops READABLE and skips re-dispatch if already set; us_internal_socket_raw_shutdown explicitly arms READABLE when readable_ended so kqueue/libuv still wake the SHUT_DOWN close path. The bit is zero-initialized at all five socket construction sites. A new tcp-server.test.ts case reproduces the infinite end() re-dispatch on POSIX by clamping SO_SNDBUF and writing 256 KiB from the end handler.

Security risks

None. This is event-loop poll-mask bookkeeping on an already-authenticated TCP fd; no untrusted-input parsing, auth, or crypto is touched. The failure mode being fixed is a CPU spin (see linked #32600), and the fix only narrows when READABLE is armed.

Level of scrutiny

High. us_internal_dispatch_ready_poll and the poll-rearm helpers are on the hot path of every TCP/TLS connection, and the correctness of the raw_shutdown change depends on backend-specific poll semantics (unmaskable EPOLLHUP vs. kqueue EV_DELETE vs. libuv's UV_DISCONNECT OR-in). The change is small and well-reasoned, but a mistake here hangs or spins sockets on one platform only. The test's platform-gated drainCount assertion and the PR footer's "deferring to CI" both underline that this needs cross-platform CI green before merge.

Other factors

  • I left two prior rounds of inline feedback; both were addressed exactly as suggested and the threads are resolved.
  • github-actions flagged this as mechanically overlapping with open #34487 (same readable_ended bit, same guarded call sites, Windows vs. Linux trigger). A human should decide whether to merge one, both, or fold them together.
  • CI build #82509 was still in progress at the time of the last robobun update; cross-platform pass (especially macOS/kqueue and Windows) is the remaining gate.

Dropping READABLE from rearm_writable when readable_ended truncated
Windows/TLS half-close drains (serve.test.ts and node-http-backpressure
https cases): libuv.c poll_cb's UV_DISCONNECT handling relies on
READABLE being registered to keep the writable dispatch flowing after
the eof branch drops it.

loop.c's readable_ended guard already absorbs the 0-byte read without
re-dispatching on_end, so rearm_writable can keep its original
behaviour. Verified on windows-x64: node-http-backpressure.test.ts 8/8,
serve.test.ts half-close 5/5, tcp-server.test.ts allowHalfOpen pass.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both earlier findings are addressed and I didn't spot new issues in 30274ea, but this is core usockets poll-mask handling across epoll/kqueue/libuv with an overlapping open PR (#34487) touching the same mechanism — worth a maintainer's eyes to reconcile the two and sanity-check the cross-backend behavior.

What was reviewed:

  • readable_ended init at all four socket-creation sites (accept, connect, from_fd, listen) — all covered.
  • us_socket_resume's shut-down arm keeps READABLE unconditionally (matches raw_shutdown; the earlier kqueue hole is closed).
  • 30274ea's revert of rearm_writable to keep READABLE, relying on loop.c's new readable_ended guard to absorb the 0-byte read — traced that the guard runs before the allow_half_open re-dispatch, and the is_shut_down check precedes it so the SHUT_DOWN close path is unaffected.
  • Test: drainCount gated on !isWindows, stale 4 MiB comment fixed, both close events wired to resolve.
Extended reasoning...

Overview

Adds a readable_ended : 1 bit to us_socket_t (in the existing pad-to-pointer bitfield gap) and uses it to prevent the half-open eof branch in loop.c from re-dispatching on_end on every tick when a partial write inside the end handler re-arms READABLE. Touches internal.h (struct field), context.c + loop.c + socket.c (init + guards at the eof branch, rearm_writable comment, resume, raw_shutdown), plus a new regression test in tcp-server.test.ts.

Security risks

None identified — this is event-loop poll-mask bookkeeping on already-open sockets; no untrusted input parsing, no auth/crypto surface.

Level of scrutiny

High. packages/bun-usockets/ is the lowest layer of Bun's networking stack and every change here fans out across three backends (epoll, kqueue, libuv/Windows) whose edge/level-trigger and HUP semantics differ. The PR itself went through a design iteration (30274ea reverted an earlier per-caller READABLE guard in rearm_writable in favor of a central absorb in loop.c), and there is an open sibling PR #34487 implementing the same readable_ended mechanism for the Windows UV_DISCONNECT trigger — a maintainer should decide how the two land relative to each other.

Other factors

  • Both of my earlier inline findings (the resume shut-down arm setting poll=0 on kqueue, and the Windows drainCount flake / stale comment) were addressed in f158cfe / 9a53f43 / 7ce958e and are resolved.
  • The new test wires error/close paths to promise resolvers, uses port: 0, clamps SO_SNDBUF via bun:internal-for-testing, and gates the platform-dependent drainCount assertion on !isWindows.
  • The raw_shutdown change (arming READABLE when readable_ended) is a behavioral change on kqueue/libuv for the "peer FIN then our shutdown()" sequence — the reasoning in the comment is sound, but this is exactly the kind of cross-backend subtlety a maintainer should confirm against the macOS/Windows CI lanes.

@robobun

robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

CI on 30274ea: 193/196 jobs passed. The only hard failure is binary-size, which compares against canary baseline #79916 (~2600 builds behind main) and reports ~+500 KB on every target; this PR adds one bitfield bit and a handful of branches, not 500 KB. All test failures are flaky retries in unrelated areas (webview-chrome, next-pages, install, shell-hang, fetch-leak). tcp-server.test.ts, serve.test.ts, and node-http-backpressure.test.ts are green on every lane including both Windows targets.

Ready for review.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

Superseded: the readable_ended guard landed on main as read_eof via #37077 and #37920 (loop.c half-open eof branch, us_internal_rearm_writable, us_socket_resume). This PR's test passes on current main without the patch.

Jarred-Sumner pushed a commit that referenced this pull request Aug 15, 2026
…ackpressure pauses from holding the loop (#33974)

### Problem

- A `net.Socket` that has already sent its FIN (`end()`) and then pauses
under backpressure loses the tail of the peer's stream. The
eof-while-paused deferral on main (#32488) exempts shut-down sockets, so
the `is_shut_down` arm in `us_internal_dispatch_ready_poll`
(`packages/bun-usockets/src/loop.c`) closes the socket while bytes are
still queued in the kernel: kqueue reports `EV_EOF` on the readable
event carrying the final data, epoll latches `EPOLLHUP` once both
directions are down. Deterministic on Linux with a 1 MiB reply (`end
586752` of 1048576 on main); about a third of runs on macOS.
- The exemption's original reason was uws HTTP, but every uws HTTP
socket is `allow_half_open` and pairs `shutdown()` with `close()`, so it
never reaches this arm; what the exemption (and the earlier
`allow_half_open` version of this PR) actually left truncating were
`Bun.connect`/`Bun.listen` without `allowHalfOpen` and
`us_socket_from_fd` sockets.
- Deferring the FIN exposes a loop-hold bug in `src/js/node/net.ts`: the
three `push()`-returned-false pause sites and the two onread-mode ones
(callback returned `false`) pause the native handle but, unlike
`Socket.prototype.pause`, keep the process alive. With a deferred FIN, a
program that never reads a reply or a request sits forever; node (whose
`readStop`'d handles are inactive) exits. The not-shut-down deferral
already on main has this hang today (`net.connect()` to a server that
writes and goes away, never read: hangs on main, exits in node).

### Fix

- `loop.c`: defer the eof hint whenever the socket is paused and
`read_eof` is not set (a delivered FIN means nothing is left to drain,
so the close stays prompt); shut-down sockets included. The existing
epoll `us_poll_stop` applies to every deferral, so an AF_UNIX peer close
(`EPOLLHUP` on a socket we did not shut down) cannot spin either. A
second arm covers the batch race on epoll: an entry carrying `EPOLLHUP`
collected while paused still dispatches after an earlier entry in the
same batch resumed the socket, with no `READABLE` bit; it is left for
the next poll, which re-reports it with `READABLE` and lets the read
loop drain to `recv()==0`.
- `us_poll_change` returns the result of re-registering a parked fd (it
goes through `us_poll_start_rc`, so `EPOLL_CTL_ADD` failure is handled
and fault-injectable); `us_socket_resume` closes the socket with that
errno instead of leaving it registered nowhere.
- `net.ts`: all five stop-reading sites drop the hold the way
`Socket.prototype.pause` does (`kPausedUnref`;
`read()`/`_read()`/`resume()` and the onread tail drain restore it),
except while a write is waiting for drain, which re-refs in `_write` and
is released by the drain handlers. `kPausedUnref` is cleared on every
resume path and on `ref()`, so a drain never gives up a hold the user
re-took.
- Why this is right: a paused socket not observing the peer's FIN until
it resumes, and not keeping the process alive while it is not reading,
are both node's behavior (readStop); every fixture below was checked
against node v26 and matches it.
- Windows is affected too: `libuv.c` maps AFD `DISCONNECT` on a
shut-down socket to the same eof hint, so it takes the new arm; the
truncation tests now run there (they pass on current main there only
because AFD's single forced `recv()` happens to cover the remainder at
these sizes, and they exercise the resume re-report path on the branch).

### Tests

`test/js/node/net/node-net.test.ts` (red on main, green with the change,
unless noted):
- delivers every buffered byte before `'end'` when the data handler
pauses (main: truncated)
- stays parked without spinning the loop, then delivers the tail on
resume (main: `close 0`; also bounds the CPU of the parked second)
- client that never reads the reply and whose peer goes away exits
(main: hangs)
- onread client whose callback returns `false`, with and without `end()`
(both hang on main here: the bare onread pause held the loop whether or
not the FIN made it through the buffers)
- client that `end()`s and never reads / server that `end()`s without
reading the request exit (pass on main because main closes them; hang
with the `loop.c` change alone, which is the regression the `net.ts`
part prevents)
- a write still waiting for drain keeps the process alive until it
completes; a drain does not give up a hold re-taken with `ref()` (both
fail with the naive versions of the `net.ts` change)
- its named-pipe leak check is now awaited with a warmed baseline: left
dangling, its assertion landed inside whichever test was running ~1s
later and counted that test's sockets (this is what an earlier CI run
hit once these tests shifted the timing); the pipe transport keeps
exactly one wrapper reachable regardless of connection count, on main as
well, which is what the warm-up absorbs.

`test/js/bun/net/socket-syscall-fault.test.ts`: resume of a parked
socket whose re-registration fails surfaces `read ENOMEM` + `close`,
instead of a deaf socket (epoll only; main closes the socket before the
resume, so it is red there too).

Verified: full `node-net.test.ts`, `socket.test.ts`,
`tcp-server.test.ts`, `socket-syscall-fault.test.ts`,
`fetch-backpressure.test.ts`, `node-http.test.ts`,
`node-tls-server.test.ts`, `child_process.test.ts` have failure sets
identical to main in this environment (the shared failures are
no-network / `localhost` dual-stack ones); the 327
`test-net-*`/`test-tls-*` node tests show the same two pre-existing
failures as main. On Windows x64 (debug build): `node-net.test.ts` 77
pass / 0 fail, `node-http.test.ts`, `fetch-backpressure.test.ts`,
`socket-syscall-fault.test.ts` clean, the pause-related
`test-net-*`/`test-http-pause*` node tests pass. (`net.Socket write >
should allow reconnecting after end()` is flaky on Windows debug builds
on main as well: 1/12 there vs 4/24 here, a pre-existing 3ms race in
that test, unrelated to this change.)

### Background

- eof hint: the `eof` argument to the dispatcher. Where it comes from
differs per backend (kqueue `EV_EOF`, epoll `EPOLLHUP`, AFD
`DISCONNECT`, or `recv()` returning 0 inside the read loop); only the
last one proves the buffer is empty.
- parked socket: on epoll `EPOLLHUP` is level-triggered and cannot be
masked, so a paused socket that hung up is removed from the epoll set
(`us_poll_stop`) and added back on resume; that re-add is a fresh
registration.
- `read_eof`: set once the peer's FIN has been delivered as `on_end` on
a half-open socket.
- onread mode: the `onread` connect option delivers reads into a
caller-supplied buffer through a callback instead of the stream's
`push()`; returning `false` from the callback is that mode's way of
stopping reads.
- `kPausedUnref` / `kUserUnrefed`: `net.ts` bookkeeping for the handle's
hold on the event loop. `kUserUnrefed` records an explicit
`socket.unref()`; `kPausedUnref` records that a pause dropped the hold
on the user's behalf and the next resume should restore it.

Related: #35939 fixes a different problem in the same block (`on_end`
re-dispatch after a partial write); the two are independent.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants