fix(jsruntime/http): V8 listen keepalive + express handler smoke#997
Merged
Conversation
Post-#994 the V8-fallback `http.createServer().listen(port, cb)` smoke was hanging in three ways. Fixes the first three (bind keepalive, express handler polyfills, cb ordering / async fetch op); the self-fetch deadlock inside `async () => await fetch(...)` listen callbacks is documented as a known limitation gated on real native async lowering. - `last_poll_was_pending` on `JsRuntimeState` so `jsruntime_has_active_handles` keeps the outer event loop alive while deno_core has refed ops in flight, e.g. an in-progress `TcpListener::bind` from `op_perry_http_listen`. Pre-fix the outer loop exited before bind resolved and "listening" never printed. - `setImmediate` / `clearImmediate` polyfill and `Buffer.allocUnsafeSlow` added so express's router + safe-buffer can run. Without these every `(_req, res) => res.send(...)` handler threw and the http shim returned 500. - `op_perry_fetch` made async (wraps blocking ureq in `spawn_blocking`), and `Server.listen` now schedules the user-supplied listening callback via `Promise.resolve().then(cb)` so the JS accept loop registers its first `op_perry_http_accept(sid)` op before the trampoline-driven callback gets a chance to block the V8 thread. Validated: `test_issue_997_v8_listen_keepalive.ts` (new) + `test_http_createserver_v8.ts` (existing) both pass; an external `curl http://127.0.0.1:port/ping` against a Perry-compiled `app.get('/ping', ...)` express server returns `pong` with 200.
6691168 to
541638b
Compare
This was referenced May 18, 2026
proggeramlug
added a commit
that referenced
this pull request
May 18, 2026
…1009) PR #973 lowered bare built-in idents (`Promise`, `Array`, `Date`, ...) as `PropertyGet { GlobalGet(0), name }` so they route through the globalThis singleton closure path. Two codegen call sites that specialize `.then()` dispatch were still pattern-matching the legacy `Expr::GlobalGet(_)` shape only: - `type_analysis::is_promise_expr` for `Promise.resolve/reject/all/race/ allSettled/any(...)` and `Array.fromAsync(...)`. - `lower_call.rs`'s fused `Promise.resolve(x).then(cb)` fast path that routes to `js_promise_resolved_then`. When `is_promise_expr` returned false, `.then(cb)` fell through to the generic native method dispatch which doesn't enqueue the callback — microtask-02..07 and edge-promises went silent in compile-smoke's Native no-fallback gate, and on Linux V8 surfaced the same shape as `TypeError: then is not a function`. The Native gate had been red on every PR since #973 (admin-bypassed on #997 / #1000 / #1003 / #1004). Extract `type_analysis::is_global_builtin_named(expr, name)` that matches both shapes (legacy `GlobalGet(_)` and the post-#973 `PropertyGet { GlobalGet(0), name }`) and route both call sites through it. Validation: `scripts/run_native_no_fallback_tests.sh` — 35 passed, 0 failed (was 28/7 pre-fix).
This was referenced May 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three bug fixes that unblock the express end-to-end smoke against the V8-fallback
http.createServerbridge from #994:JsRuntimeState::last_poll_was_pendingis set bypoll_v8_event_loop_oncewhenever deno_core reports refed ops in flight, andjsruntime_has_active_handlesreturns 1 while it's set. Pre-fix,app.listen(port, cb)returned to the codegen-emitted outer event loop whileop_perry_http_listen'sTcpListener::bindfuture was still suspended on the multi-thread runtime, the outer loop saw no active source, and the program exited before bind ever resolved.setImmediate/clearImmediateglobals (express's router uses them to break middleware recursion) andBuffer.allocUnsafeSlow(sosafe-buffer, used byexpress/lib/response.js, doesn't fall through to afor...incopyProps that silently drops non-enumerable static methods likeBuffer.isBuffer). Without these, every(_req, res) => res.send('pong')threw and the http shim returned 500.op_perry_fetch:Server.listennow schedules the listening callback viaPromise.resolve().then(() => cb())so the JS accept loop registers its firstop_perry_http_accept(sid)op before the trampoline-driven callback gets a chance to block the V8 thread.op_perry_fetchis now an async op wrapping the blocking ureq call intokio::task::spawn_blocking, with the polyfill updated toawait core.ops.op_perry_fetch(...).External clients (
curl http://127.0.0.1:port/...) hit a Perry-compiled express server and get the route body. The self-fetch case (async () => { await fetch('http://127.0.0.1:port/...') }inside the listen-cb) still deadlocks because Perry's native async lowering busy-waits on the V8 thread inside trampolines, blocking the IIFE's accept loop continuation — documented in CHANGELOG as a deferred issue gated on real Future-style state-machine lowering.Verification
test-files/test_issue_997_v8_listen_keepalive.ts: synchronous listen-cb that closes the server immediately. Pre-fix hangs forever undertimeout; post-fix printslisteningand exits 0.test-files/test_http_createserver_v8.tsstill passes (200 hello+ exit 0).app.get('/ping', (_req, res) => res.send('pong'))) returnspongvia external curl. Confirmed locally.{"msg":"pong"}) still works through perry-stdlib's bundled fastify path.Test plan
target/release/perry test-files/test_issue_997_v8_listen_keepalive.ts -o /tmp/t && timeout 5 /tmp/tprintslisteningand exits 0target/release/perry test-files/test_http_createserver_v8.ts -o /tmp/t2 && timeout 10 /tmp/t2prints200 hello