fix(fastify): non-blocking listen() + main-thread pump#1066
Merged
Conversation
Closes the compat-sweep fastify hang: `js_fastify_listen` (in both perry-stdlib's bundled adapter AND perry-ext-fastify) entered a blocking inner event loop that never returned, so `await app.listen(...)` in user code never resumed — the in-process `fetch` against the same process and `app.close()` were both unreachable, and gtimeout killed the program at 30 s with rc=124. Root cause: pre-fix `event_loop(...)` ran for the process lifetime inside `listen()`, taking over the main TS thread. The same shape was fixed for `node:http` in #604 (perry-ext-http-server) — apply that pattern to fastify: * `listen()` returns immediately after spawning the accept loop. * Per-server mpsc receiver lives inside `FastifyServerHandle` (Mutex<Option<...>>) instead of on the `event_loop` stack. * New `js_fastify_process_pending` drains pending requests from every registered handle on every tick of perry-stdlib's main pump. * New `js_fastify_has_active` keeps the runtime alive while any server is in the "listening" state. * `app.close()` (previously fell through to "unknown method" and no-op'd) now routes through a new `js_fastify_app_close` entry in the codegen NATIVE_MODULE_TABLE; it walks the handle registry for matching servers, flips their listening flag, drops their rx, and fires shutdown — so the runtime exits naturally. Wiring: a new `external-fastify-pump` feature is activated by the well-known flip when `bundled-fastify` is stripped (`import 'fastify'` routed to perry-ext-fastify). Mirrors `external-{net,ws,http-server}-pump`. Test: `test-files/test_fastify_in_process.ts` mirrors the compat-sweep fixture (await listen + in-process fetch + close). 22 existing perry-stdlib fastify unit tests still pass; `scripts/run_fastify_tests.sh` end-to-end suite (10 routes) still passes.
The manifest_consistency test (#463) caught NATIVE_MODULE_TABLE / API_MANIFEST drift: the previous commit added fastify.close to NATIVE_MODULE_TABLE + runtime_decls + dispatch but forgot the matching manifest row, so the unimplemented-API gate would have errored on a real call to app.close().
9f535f6 to
26783cd
Compare
proggeramlug
added a commit
that referenced
this pull request
May 20, 2026
…erver + setInterval/async CPU-wedge regression (#1144) * fix(fastify,runtime): #1113 #1114 — bidirectional WS upgrade on app.server + setInterval/async CPU-wedge regression #1114: v0.5.1009 regressed when #1066 made app.listen() non-blocking, so shop-admin's post-listen setInterval+async-MySQL JobLoop now runs and exposes a latent two-part defect (only at ~68-file scale): (1) the fastify pump (now called every event-loop AND every await-poll iteration) allocated a Vec<Handle> per call -> GC madvise thrash; reuse a per-thread scratch buffer (bundled + ext-fastify, zero steady-state alloc). (2) js_wait_for_event's budget==0 path returned without sleeping, so a pinned-past deadline spun a core forever and starved the request pump (HTTP wedge); add an adaptive spin-throttle (caps a sustained budget-0 spin at ~1kHz, untouched fast path, PERRY_SPIN_THROTTLE=0 escape hatch). New event_pump unit test; all 4 serialized on shared global pump state. #1113: finish the v0.5.1011 boot-fix follow-up by porting perry-ext-http-server's #577 Phase-4 model into perry-ext-fastify — hyper .with_upgrades(), native tungstenite handshake, register_external_ws_stream, drain upgrades in the pump and fire FastifyApp::upgrade_handlers with (req, wsId, head). perry-ext-ws gains noServer + js_ws_handle_upgrade; codegen/manifest wired. Verified: issue's exact pattern boots clean, 101 + correct accept key, upgrade/handleUpgrade/connection all fire, /healthz unaffected. Version 0.5.1012; CHANGELOG + CLAUDE.md version bumped. * chore(docs): regen API manifest docs for ws.handleUpgrade (#1113) scripts/regen_api_docs.sh output for the new ws.handleUpgrade NATIVE_MODULE_TABLE/API_MANIFEST entry — keeps the api-docs-drift CI check green (entry count 936->937 + the handleUpgrade row). * style: cargo fmt
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
Closes the in-process fastify timeout from the compat sweep.
/tmp/perry-compat-sweep/fastify/entry.tswas hanging atServer listening on http://0.0.0.0:18932untilgtimeout 30skilled it (rc=124). Now exits cleanly withok=true.Root cause
js_fastify_listen(in BOTH perry-stdlib's bundled adaptercrates/perry-stdlib/src/fastify/server.rsAND perry-ext-fastifycrates/perry-ext-fastify/src/server.rs) entered a blocking innerevent_loop(...)that never returned, so:This is the same shape of bug that #604 fixed for
node:httpinperry-ext-http-server. Apply that pattern to fastify.Fix
listen()returns immediately after spawning the accept loop.event_loopstack intoFastifyServerHandle(Mutex<Option<...>>) so the main pump can find it.js_fastify_process_pendingdrains pending requests from every registered handle each tick. Wired into perry-stdlib'sjs_stdlib_process_pending.js_fastify_has_activekeeps the runtime's main event loop alive while any server is in the "listening" state. Wired into perry-stdlib'sjs_stdlib_has_active_handles.app.close()previously fell through to "unknown native method" and silently no-op'd. Added an entry to the codegenNATIVE_MODULE_TABLErouting to a newjs_fastify_app_closethat walks the handle registry for matching servers, flips their listening flag, drops their rx, and fires shutdown — so the runtime exits naturally after the user calls close.external-fastify-pumpfeature, activated by the well-known flip whenbundled-fastifyis stripped (import 'fastify'→ perry-ext-fastify). Mirrorsexternal-{net,ws,http-server}-pump.Files touched
crates/perry-stdlib/src/fastify/server.rs— non-blocking listen, new process_pending/has_active, app_closecrates/perry-ext-fastify/src/server.rs— same shape for the external cratecrates/perry-stdlib/Cargo.toml— newexternal-fastify-pumpfeaturecrates/perry-stdlib/src/common/async_bridge.rs— wire the new pump and has-active gatecrates/perry-stdlib/src/common/dispatch.rs— routeapp.close()runtime dispatchcrates/perry-stdlib/src/common/handle.rs— newiter_handle_ids_ofhelpercrates/perry-codegen/src/lower_call.rs— new fastifycloseentry in NATIVE_MODULE_TABLEcrates/perry-codegen/src/runtime_decls.rs— declarejs_fastify_app_closecrates/perry/src/commands/compile/optimized_libs.rs— activateexternal-fastify-pumpon the fliptest-files/test_fastify_in_process.ts— new smoke test mirroring the compat-sweep fixtureTest plan
ok=true).ok=true.scripts/run_fastify_tests.shend-to-end suite passes (10/10 routes).cargo test --release -p perry-stdlib --lib fastify— 22/22 fastify unit tests pass.test-files/test_fastify_in_process.ts(new) compiles and runs took=truerc=0.No version bump or changelog change (maintainer folds those in at merge time).