Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions src/runtime/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ pub struct NewServer<const SSL: bool, const DEBUG: bool> {
pub(crate) all_closed_promise: jsc::JSPromiseStrong,

pub poll_ref: KeepAlive,
/// Ref'd while `pending_requests > 0` so `server.unref()` doesn't drop in-flight requests.
in_flight_keep_alive: KeepAlive,

pub(crate) flags: ServerFlags,

Expand Down Expand Up @@ -486,6 +488,11 @@ impl<const SSL: bool, const DEBUG: bool> NewServer<SSL, DEBUG> {
}

pub(crate) fn on_pending_request(&mut self) {
if self.pending_requests == 0 {
// SAFETY: `self.vm` is the live per-thread VM singleton backref.
self.in_flight_keep_alive
.ref_(unsafe { jsc::VirtualMachine::event_loop_ctx(self.vm.as_ptr()) });
}
self.pending_requests += 1;
}

Expand Down Expand Up @@ -1486,6 +1493,11 @@ impl<const SSL: bool, const DEBUG: bool> NewServer<SSL, DEBUG> {
#[inline]
pub(crate) fn on_request_complete(&mut self) {
self.pending_requests -= 1;
if self.pending_requests == 0 {
// SAFETY: `self.vm` is the live per-thread VM singleton backref.
self.in_flight_keep_alive
.unref(unsafe { jsc::VirtualMachine::event_loop_ctx(self.vm.as_ptr()) });
}
self.deinit_if_we_can();
}

Expand Down Expand Up @@ -1596,6 +1608,7 @@ impl<const SSL: bool, const DEBUG: bool> NewServer<SSL, DEBUG> {
self.unref();
self.notify_inspector_server_stopped();
if abrupt {
self.in_flight_keep_alive.disable();
self.flags.insert(ServerFlags::TERMINATED);
}
}
Expand All @@ -1604,6 +1617,9 @@ impl<const SSL: bool, const DEBUG: bool> NewServer<SSL, DEBUG> {
if abrupt || (self.pending_requests == 0 && !self.has_active_web_sockets()) {
self.unref();
}
if abrupt {
self.in_flight_keep_alive.disable();
}
// A graceful stop with work in flight keeps the ref (deinit_if_we_can
// unrefs when the drain completes): on Windows uv_run skips I/O with
// zero ref'd handles, so unrefing here wedged server.close() teardown.
Expand Down Expand Up @@ -2031,6 +2047,7 @@ impl<const SSL: bool, const DEBUG: bool> NewServer<SSL, DEBUG> {
h3_request_pool: core::ptr::null_mut(),
all_closed_promise: jsc::JSPromiseStrong::default(),
poll_ref: KeepAlive::default(),
in_flight_keep_alive: KeepAlive::default(),
flags: ServerFlags::default(),
plugins: None,
user_routes: Vec::new(),
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/server/server_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3422,7 +3422,7 @@ where
resp.end_without_body(true);
return;
}
this.pending_requests += 1;
this.on_pending_request();
req.set_yield(false);
// SAFETY: `request_pool` is non-null while the server is alive; `claim()`
// reserves a fresh slot whose `Drop` releases it on panic before init.
Expand Down
30 changes: 30 additions & 0 deletions test/js/bun/http/bun-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,36 @@ test("unref keeps process alive for ongoing connections", async () => {
expect(await bunRun(path.join(import.meta.dir, "unref-fixture-2.ts"))).toSpawn();
});

test("unref: an in-flight request keeps the process alive on its own", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), path.join(import.meta.dir, "unref-fixture-3.ts")],
env: bunEnv,
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
});
const stderrPromise = proc.stderr.text();
const exitedPromise = proc.exited;

// Read the port line while the subprocess keeps running.
const reader = proc.stdout.getReader();
const decoder = new TextDecoder();
let buf = "";
while (!buf.includes("\n")) {
const { value, done } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
}
reader.releaseLock();
const port = Number(buf.trim());
expect(port).toBeGreaterThan(0);

const res = await fetch(`http://127.0.0.1:${port}/`);
const body = await res.text();
const [stderr, exitCode] = await Promise.all([stderrPromise, exitedPromise]);
expect({ body, stderr, exitCode }).toEqual({ body: "ok", stderr: "", exitCode: 0 });
});

test("Bun does not crash when given invalid config", async () => {
await using server1 = Bun.serve({
fetch(request, server) {
Expand Down
13 changes: 13 additions & 0 deletions test/js/bun/http/unref-fixture-3.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading