-
Notifications
You must be signed in to change notification settings - Fork 5k
usockets(kqueue): stop a non-reading socket's read knote from waking the loop on data #39949
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
47c7090
3e9ad46
6626954
a221d16
64e7356
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4259,6 +4259,98 @@ describe("allowHalfOpen socket whose peer resets behind pending writes", () => { | |
| }); | ||
| }); | ||
|
|
||
| // A paused socket must not wake the event loop for data it is not going to read. epoll and | ||
| // AFD do not report data without readable interest; kqueue keeps a read knote registered on a | ||
| // non-reading socket so the peer's FIN/RST still arrive, and that knote used to fire once per | ||
| // incoming segment. The paused socket lives in a child so nothing else turns its loop: it | ||
| // samples the usockets loop iteration counter, we send SEGMENTS one-byte writes from here, | ||
| // then it samples again. A loop that wakes per segment reports ~SEGMENTS iterations; one that | ||
| // does not reports the handful caused by our two control lines. | ||
| it("a paused socket does not wake the event loop for every segment its peer sends", async () => { | ||
| const SEGMENTS = 200; | ||
| await using child = spawn({ | ||
| cmd: [ | ||
| bunExe(), | ||
| "-e", | ||
| ` | ||
| const { getEventLoopStats } = require("bun:internal-for-testing"); | ||
|
Comment on lines
+4275
to
+4276
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Use a module-scope import in the child program.
As per coding guidelines, “Only use dynamic import or require when the test is specifically testing something related to dynamic import or require. Otherwise, always use module-scope import statements.” 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| let socket; | ||
| const server = Bun.listen({ | ||
| hostname: "127.0.0.1", | ||
| port: 0, | ||
| socket: { | ||
| open(s) { | ||
| socket = s; | ||
| s.pause(); | ||
| process.stdout.write("port " + server.port + "\\n"); | ||
| }, | ||
| data() { | ||
| process.stdout.write("data while paused\\n"); | ||
| }, | ||
| close() {}, | ||
| drain() {}, | ||
| }, | ||
| }); | ||
| process.stdout.write("port " + server.port + "\\n"); | ||
| let before; | ||
| for await (const line of console) { | ||
| if (line === "start") { | ||
| before = getEventLoopStats().iteration; | ||
| process.stdout.write("started\\n"); | ||
| } else if (line === "stop") { | ||
| process.stdout.write("iterations " + (getEventLoopStats().iteration - before) + "\\n"); | ||
| server.stop(true); | ||
| process.exit(0); | ||
| } | ||
| } | ||
| `, | ||
| ], | ||
| env: bunEnv, | ||
| stdin: "pipe", | ||
| stdout: "pipe", | ||
| stderr: "inherit", | ||
| }); | ||
| const reader = child.stdout.getReader(); | ||
| let buffered = ""; | ||
| async function line() { | ||
| while (!buffered.includes("\n")) { | ||
| const { value, done } = await reader.read(); | ||
| if (done) return buffered; | ||
| buffered += new TextDecoder().decode(value); | ||
| } | ||
| const i = buffered.indexOf("\n"); | ||
| const out = buffered.slice(0, i); | ||
| buffered = buffered.slice(i + 1); | ||
| return out; | ||
| } | ||
| const port = Number((await line()).split(" ")[1]); | ||
| const peer = await Bun.connect({ | ||
| hostname: "127.0.0.1", | ||
| port, | ||
| socket: { data() {}, open() {}, close() {}, drain() {} }, | ||
| }); | ||
| expect(await line()).toBe(`port ${port}`); // open() ran: the socket is paused | ||
| child.stdin.write("start\n"); | ||
| await child.stdin.flush(); | ||
| expect(await line()).toBe("started"); | ||
| for (let i = 0; i < SEGMENTS; i++) { | ||
| peer.write("x"); | ||
| peer.flush(); | ||
| // Separate segments need separate event-loop turns on our side; this paces the sender, | ||
| // it is not waiting for a condition in the child. | ||
| await new Promise<void>(resolve => setTimeout(resolve, 1)); | ||
|
Comment on lines
+4339
to
+4341
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Replace the fixed delay with an event-driven barrier. The 1 ms timeout does not prove that each write becomes a distinct peer-side segment or event-loop turn. Timer scheduling and TCP coalescing can make this test pass when the per-segment wakeup regression remains. Use an observable condition that establishes the required sender progress before the next write. As per coding guidelines, “Do not use 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| } | ||
| child.stdin.write("stop\n"); | ||
| await child.stdin.flush(); | ||
| const result = await line(); | ||
| peer.end(); | ||
|
Comment on lines
+4327
to
+4346
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Nit: Extended reasoning...What the issue isThe new test creates a client socket with Why the repo's rules flag thisREVIEW.md's Tests must be hermetic section is explicit:
Step-by-step trace
Actual impact (why this is a nit, not blocking)In practice the leak is short-lived: the child process is correctly guarded by FixOne-word change plus dropping the now-redundant manual close (per REVIEW.md's "no manual close alongside using peer = await Bun.connect({
hostname: "127.0.0.1",
port,
socket: { data() {}, open() {}, close() {}, drain() {} },
});
// ... (delete the `peer.end();` on line 4346)This matches the pattern already used throughout the rest of |
||
| expect(result).toMatch(/^iterations \d+$/); | ||
| // Two stdin lines and process bookkeeping account for a few turns; per-segment wakeups | ||
| // would put this near SEGMENTS. | ||
| expect(Number(result.split(" ")[1])).toBeLessThan(SEGMENTS / 4); | ||
| expect(await child.exited).toBe(0); | ||
| }); | ||
|
|
||
| // A paused socket polls for nothing, but a peer reset still reaches it (epoll reports EPOLLERR | ||
| // regardless of interest; kqueue keeps a read knote registered while reads are off, see | ||
| // epoll_kqueue.c). The reset is the end of the connection, so the pause no longer protects | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.