You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Perry's TypeScript→native compiler lowers user async () => ... arrows into a busy-wait loop rather than a real Future state machine. This makes any in-process await fetch(...) against a server running in the same process deadlock — the busy-wait holds the V8 thread, the server's accept future on deno's executor never settles its continuation, and the program times out (rc=124).
This blocks four compat sweep fixtures simultaneously, all with the same shape:
"Perry's TS→native compilation lowers user async () => ... arrows into a busy-wait loop (crates/perry-codegen/src/expr.rs:10687), so a native callback invoked via native_callback_trampoline blocks the V8 thread for the full callback duration. Inside the busy-wait, js_run_jsruntime_pump re-enters poll_event_loop — but op_perry_http_accept's rx.recv().await future on deno's executor never settles its V8 continuation while the trampoline frame holds an active scope. Fixing it cleanly needs real async lowering across the codegen, not a V8-side patch."
Workarounds already shipped (do NOT regress)
External clients work: Perry-compiled servers respond correctly to external curl/fetch. PR fix(jsruntime/http): V8 listen keepalive + express handler smoke #997 added JsRuntimeState::last_poll_was_pending to keep the codegen outer event loop alive while op_perry_http_listen's bind future is in flight, and made op_perry_fetch async via spawn_blocking ureq. Server lifecycle (listen → handle → close) is functional.
fastify via perry-stdlib works end-to-end because it doesn't route through the V8-fallback hyper bridge.
Lower async function and await to a real state machine (resume points, suspend frames) rather than busy-wait. The current busy-wait lives in crates/perry-codegen/src/expr.rs (~L10687 per #997 agent). The trampoline-frame issue is at the V8↔Perry boundary, specifically native_callback_trampoline holding a scope while pump re-entry tries to settle V8 promises.
A starting plan:
Identify every site that emits the busy-wait loop { pump(); if done break } pattern in codegen.
Replace with proper continuation-passing or state-machine lowering — store the async frame's locals + resume index, return control to the runtime, resume from the index when the awaited future settles.
Ensure native_callback_trampoline releases the V8 scope before pumping.
Verify each of the four blocked packages flips to PASS in /tmp/perry-compat-sweep/.
Expected: listening then body=pong. Actual: hangs at listening until rc=124.
Same shape works through an external curl while the server is up, confirming the deadlock is internal to the busy-wait+V8 scope interaction, not the server.
This single architectural change unblocks 4 of Perry's 16 sweep fixtures. Roughly: any npm package that imports node:http, runs a server, and exercises an in-process client against it. Without it, Perry can't run the canonical "self-test" idiom that 50%+ of compat smokes use.
Summary
Perry's TypeScript→native compiler lowers user
async () => ...arrows into a busy-wait loop rather than a real Future state machine. This makes any in-processawait fetch(...)against a server running in the same process deadlock — the busy-wait holds the V8 thread, the server's accept future on deno's executor never settles its continuation, and the program times out (rc=124).This blocks four compat sweep fixtures simultaneously, all with the same shape:
await fetch(self)hangsapp.fetch(req)chain doesn't pump cleanlyDiagnosis (from PR #997 agent)
Workarounds already shipped (do NOT regress)
curl/fetch. PR fix(jsruntime/http): V8 listen keepalive + express handler smoke #997 addedJsRuntimeState::last_poll_was_pendingto keep the codegen outer event loop alive whileop_perry_http_listen's bind future is in flight, and madeop_perry_fetchasync viaspawn_blockingureq. Server lifecycle (listen → handle → close) is functional.js_async_step_chain/js_async_step_doneto await V8 Promise handles instead of passing them through as primitives, which lifted jose and any package doingawait <V8-imported-function>(...).What's needed
Lower
async functionandawaitto a real state machine (resume points, suspend frames) rather than busy-wait. The current busy-wait lives incrates/perry-codegen/src/expr.rs(~L10687 per #997 agent). The trampoline-frame issue is at the V8↔Perry boundary, specificallynative_callback_trampolineholding a scope while pump re-entry tries to settle V8 promises.A starting plan:
loop { pump(); if done break }pattern in codegen.native_callback_trampolinereleases the V8 scope before pumping./tmp/perry-compat-sweep/.Reproducer
Expected:
listeningthenbody=pong. Actual: hangs atlisteninguntil rc=124.Same shape works through an external curl while the server is up, confirming the deadlock is internal to the busy-wait+V8 scope interaction, not the server.
Reference files
crates/perry-codegen/src/expr.rs~L10687 — busy-wait async loweringcrates/perry-jsruntime/src/ops.rs— hyper accept loop +op_perry_fetchcrates/perry-jsruntime/src/lib.rs—TOKIO_RUNTIME.enter()+with_runtimecrates/perry-jsruntime/src/interop.rs—native_callback_trampolinejs_async_step_chainawaits V8 Promise handles) — fixed a sibling marshalling bugImpact
This single architectural change unblocks 4 of Perry's 16 sweep fixtures. Roughly: any npm package that imports
node:http, runs a server, and exercises an in-process client against it. Without it, Perry can't run the canonical "self-test" idiom that 50%+ of compat smokes use.