Skip to content

Commit ca44f50

Browse files
authored
fix(http): wake runtime after direct serve dispatch (#34387)
Fixes #34369. ## Root cause `Deno.serve` request dispatch now calls the JS request handler directly from a standalone hyper task. That path performs a V8 microtask checkpoint after invoking the callback, but it does not otherwise guarantee that the full `JsRuntime` event loop is polled again. If the handler reaches an async boundary backed by `node:net` / uv-compatible I/O, the TCP handle can be marked ready inside the uv-compat layer, but the outer runtime task still needs to be woken so the uv I/O phase runs. Previously the mpsc/op-driven request path provided that scheduling edge implicitly. This change stores the runtime waker in `ServerCallback` and wakes it after direct JS dispatch, restoring one full event-loop pass after request-handler invocation. ## Changes - Thread the runtime `OpState` waker into `ServerCallback`. - Wake the runtime after the post-dispatch microtask checkpoint. - Add a regression test covering `Deno.serve` awaiting `node:net` I/O from inside the request handler. ## Validation - `cargo build --profile release-lite --bin deno` - `./target/release-lite/deno run --allow-net=127.0.0.1:12477,127.0.0.1:12478 tests/specs/serve/node_net_wake/main.ts` I also checked the regression against an unfixed release-lite binary: it timed out after 5s. The fixed binary returned `ok` immediately. ## Performance Hello-world `Deno.serve`, release-lite builds, `oha -z 15s -c 100`, alternating baseline/fixed samples: | Build | Avg req/s | Avg p50 | Avg p99 | | --- | ---: | ---: | ---: | | baseline | 135,073 | 0.707 ms | 0.897 ms | | fixed | 134,452 | 0.709 ms | 0.903 ms | Throughput delta: -0.46%, within local run noise.
1 parent 41d7773 commit ca44f50

5 files changed

Lines changed: 85 additions & 2 deletions

File tree

ext/http/http_next.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,7 +1279,12 @@ where
12791279

12801280
let lifetime = resource.lifetime();
12811281
let callback_global = v8::Global::new(scope, callback);
1282-
let callback = Rc::new(ServerCallback::new(scope, isolate, callback_global));
1282+
let callback = Rc::new(ServerCallback::new(
1283+
scope,
1284+
isolate,
1285+
callback_global,
1286+
state.borrow().waker.clone(),
1287+
));
12831288

12841289
let options = {
12851290
let state = state.borrow();
@@ -1336,7 +1341,12 @@ where
13361341

13371342
let resource: Rc<HttpJoinHandle> = Rc::new(HttpJoinHandle::new());
13381343
let callback_global = v8::Global::new(scope, callback);
1339-
let callback = Rc::new(ServerCallback::new(scope, isolate, callback_global));
1344+
let callback = Rc::new(ServerCallback::new(
1345+
scope,
1346+
isolate,
1347+
callback_global,
1348+
state.borrow().waker.clone(),
1349+
));
13401350

13411351
let options = {
13421352
let state = state.borrow();

ext/http/service.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use std::future::Future;
99
use std::mem::ManuallyDrop;
1010
use std::pin::Pin;
1111
use std::rc::Rc;
12+
use std::sync::Arc;
1213
use std::sync::OnceLock;
1314
use std::task::Context;
1415
use std::task::Poll;
@@ -18,6 +19,7 @@ use std::task::ready;
1819
use deno_core::BufView;
1920
use deno_core::OpState;
2021
use deno_core::ResourceId;
22+
use deno_core::futures::task::AtomicWaker;
2123
use deno_core::v8;
2224
use deno_error::JsErrorBox;
2325
use http::request::Parts;
@@ -167,13 +169,15 @@ pub struct ServerCallback {
167169
isolate_ptr: v8::UnsafeRawIsolatePtr,
168170
context: v8::Global<v8::Context>,
169171
callback: v8::Global<v8::Function>,
172+
runtime_waker: Arc<AtomicWaker>,
170173
}
171174

172175
impl ServerCallback {
173176
pub fn new(
174177
scope: &mut v8::PinScope<'_, '_>,
175178
isolate: &mut v8::Isolate,
176179
callback: v8::Global<v8::Function>,
180+
runtime_waker: Arc<AtomicWaker>,
177181
) -> Self {
178182
let ctx = scope.get_current_context();
179183
let context = v8::Global::new(scope, ctx);
@@ -183,6 +187,7 @@ impl ServerCallback {
183187
isolate_ptr,
184188
context,
185189
callback,
190+
runtime_waker,
186191
}
187192
}
188193

@@ -231,6 +236,12 @@ impl ServerCallback {
231236
// to the first real async-op boundary in user code -- matching
232237
// how Bun's onRequest invokes its handler.
233238
pin_scope.perform_microtask_checkpoint();
239+
240+
// Request arrival is handled by a standalone hyper task, not by
241+
// an op future owned by JsRuntime. If the handler reached an
242+
// async boundary above, the full event loop must run at least
243+
// once to drive timers, ops, and uv-compatible Node I/O.
244+
self.runtime_waker.wake();
234245
}
235246
}
236247
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"args": "run --allow-net=127.0.0.1:12477,127.0.0.1:12478 main.ts",
3+
"output": "main.out",
4+
"exitCode": 0,
5+
"tempDir": true
6+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ok
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import net from "node:net";
2+
3+
const httpPort = 12477;
4+
const tcpPort = 12478;
5+
6+
const listener = Deno.listen({ hostname: "127.0.0.1", port: tcpPort });
7+
8+
(async () => {
9+
try {
10+
for await (const conn of listener) {
11+
(async () => {
12+
const buf = new Uint8Array(16);
13+
await conn.read(buf);
14+
await conn.write(new TextEncoder().encode("ok"));
15+
conn.close();
16+
})();
17+
}
18+
} catch {
19+
// Listener shutdown.
20+
}
21+
})();
22+
23+
const server = Deno.serve({
24+
hostname: "127.0.0.1",
25+
port: httpPort,
26+
onListen() {},
27+
}, async () => {
28+
const body = await new Promise<string>((resolve, reject) => {
29+
const socket = net.connect(tcpPort, "127.0.0.1");
30+
socket.on("connect", () => socket.write("x"));
31+
socket.on("data", (chunk) => {
32+
resolve(String(chunk));
33+
socket.destroy();
34+
});
35+
socket.on("error", reject);
36+
});
37+
return new Response(body);
38+
});
39+
40+
(async () => {
41+
try {
42+
const response = await fetch(`http://127.0.0.1:${httpPort}/`, {
43+
signal: AbortSignal.timeout(5000),
44+
});
45+
console.log(await response.text());
46+
await server.shutdown();
47+
listener.close();
48+
Deno.exit(0);
49+
} catch (error) {
50+
console.error(error);
51+
await server.shutdown().catch(() => {});
52+
listener.close();
53+
Deno.exit(2);
54+
}
55+
})();

0 commit comments

Comments
 (0)