Skip to content

Commit 52cfae3

Browse files
authored
fix(runtime): avoid idle waits for queued fork IPC (#392)
1 parent c8b4277 commit 52cfae3

6 files changed

Lines changed: 68 additions & 0 deletions

File tree

‎packages/runtime/src/scr_async.c‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
* their empty implementation makes the absence explicit at link time
5454
* without weakening async/timer support. */
5555
bool scr_children_pending(void) { return false; }
56+
bool scr_children_ready(void) { return false; }
5657
bool scr_children_reffed_pending(void) { return false; }
5758
bool scr_children_failed_pending(void) { return false; }
5859
void scr_children_poll(void) {}
@@ -2614,6 +2615,11 @@ bool scr_loop_run(ScrPromise *top_level) {
26142615
/* Pending immediates are always-ready work: no sleep — run due timers
26152616
* (Node's timers phase precedes check), then the check phase below. */
26162617
if (scr_pending_immediates > 0) due = now;
2618+
/* Child polling can drain a pipe into the IPC queue after its dispatch
2619+
* station has run. Completed sends and disconnects can also become ready
2620+
* during that turn. No fd will wake us for this userspace work: return to
2621+
* dispatch without sleeping, still allowing due timers and immediates. */
2622+
if (scr_children_ready()) due = now;
26172623
bool evw = scr_events_watching_fn != NULL && scr_events_watching_fn();
26182624
if (io) {
26192625
if (kids && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS;

‎packages/runtime/src/scr_child.c‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5660,6 +5660,20 @@ static bool scr_ipc_pending(void) {
56605660
return false;
56615661
}
56625662

5663+
bool scr_children_ready(void) {
5664+
for (ScrIpc *ipc = scr_ipcs; ipc != NULL; ipc = ipc->next) {
5665+
if (ipc->disconnect_pending || (ipc->n_pending > 0 && ipc->n_message > 0)) {
5666+
return true;
5667+
}
5668+
bool writer_pending = scr_child_writer_pending(ipc->writer);
5669+
if (ipc->n_send > 0 && (ipc->send_error != NULL || !writer_pending)) {
5670+
return true;
5671+
}
5672+
if (ipc->local_closing && !writer_pending) return true;
5673+
}
5674+
return false;
5675+
}
5676+
56635677
double scr_process_fork_target(double target_count) {
56645678
if (scr_process_fork_id != -2) return scr_process_fork_id;
56655679
double target = -1;

‎packages/runtime/src/scr_runtime.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2806,6 +2806,9 @@ const char *scr_signal_name(int sig);
28062806
* pending child: non-kqueue platforms, spawn failures awaiting their
28072807
* first-pass settle, or a child whose exit filter could not be armed. */
28082808
bool scr_children_pending(void);
2809+
/* Work already queued in userspace: dispatch can progress without another
2810+
* pipe/exit notification. Connected channels and blocked writes are not ready. */
2811+
bool scr_children_ready(void);
28092812
bool scr_children_failed_pending(void);
28102813
void scr_children_poll(void);
28112814
bool scr_children_wait(double max_wait_ms);

‎scripts/sandbox-test.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ const hostLaneContractPattern = [
137137
"udp-loopback-pair",
138138
"1564-fs-watch.ts",
139139
"1470-child-lifecycle.ts",
140+
"2963-child-fork-dispatch/main.ts",
140141
"read-all: chunked writes with delays, then EOF",
141142
].join("|");
142143
const hostInvariantContractFiles = [
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { fork } from "node:child_process";
2+
3+
const child = fork(new URL("./worker.ts", import.meta.url), [], {
4+
stdio: ["ignore", "ignore", "inherit", "ipc"],
5+
});
6+
7+
// Start the deadline after the child is ready so process startup is not part
8+
// of the bound. Queued IPC must not wait for the idle poll timeout (1 second
9+
// per handoff), or for this timer to wake the loop. No periodic wakeups.
10+
let replies = 0;
11+
let sent = 0;
12+
let disconnected = false;
13+
child.once("message", () => {
14+
const deadline = setTimeout(() => {
15+
console.log("IPC stalled");
16+
}, 500);
17+
child.on("message", (message: { value: number }) => {
18+
replies++;
19+
if (message.value < 3) {
20+
child.send({ value: message.value + 1 }, (error) => {
21+
if (error) throw error;
22+
sent++;
23+
});
24+
}
25+
});
26+
child.once("disconnect", () => {
27+
disconnected = true;
28+
clearTimeout(deadline);
29+
});
30+
child.send({ value: 1 }, (error) => {
31+
if (error) throw error;
32+
sent++;
33+
});
34+
});
35+
child.once("exit", (code) => {
36+
console.log("replies", replies, "sent", sent, "disconnected", disconnected, "exit", code);
37+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
process.on("message", (message: { value: number }) => {
2+
process.send?.({ value: message.value }, (error) => {
3+
if (error) throw error;
4+
if (message.value === 3) process.disconnect();
5+
});
6+
});
7+
process.send?.({ value: 0 });

0 commit comments

Comments
 (0)