Skip to content

fix(fastify): non-blocking listen() + main-thread pump#1066

Merged
proggeramlug merged 2 commits into
mainfrom
worktree-agent-a5d7a82f1ac8e5c67
May 19, 2026
Merged

fix(fastify): non-blocking listen() + main-thread pump#1066
proggeramlug merged 2 commits into
mainfrom
worktree-agent-a5d7a82f1ac8e5c67

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Summary

Closes the in-process fastify timeout from the compat sweep. /tmp/perry-compat-sweep/fastify/entry.ts was hanging at Server listening on http://0.0.0.0:18932 until gtimeout 30s killed it (rc=124). Now exits cleanly with ok=true.

Root cause

js_fastify_listen (in BOTH perry-stdlib's bundled adapter crates/perry-stdlib/src/fastify/server.rs AND perry-ext-fastify crates/perry-ext-fastify/src/server.rs) entered a blocking inner event_loop(...) that never returned, so:

await app.listen({ port: 18932 });   // never resumed
const r = await fetch("http://127.0.0.1:18932/ping");  // unreachable
const j = await r.json();             // unreachable
console.log("ok=" + j.ok);            // unreachable
await app.close();                    // unreachable

This is the same shape of bug that #604 fixed for node:http in perry-ext-http-server. Apply that pattern to fastify.

Fix

  • listen() returns immediately after spawning the accept loop.
  • Per-server mpsc receiver moved from the event_loop stack into FastifyServerHandle (Mutex<Option<...>>) so the main pump can find it.
  • New js_fastify_process_pending drains pending requests from every registered handle each tick. Wired into perry-stdlib's js_stdlib_process_pending.
  • New js_fastify_has_active keeps the runtime's main event loop alive while any server is in the "listening" state. Wired into perry-stdlib's js_stdlib_has_active_handles.
  • app.close() previously fell through to "unknown native method" and silently no-op'd. Added an entry to the codegen NATIVE_MODULE_TABLE routing to a new js_fastify_app_close that 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.
  • New external-fastify-pump feature, activated by the well-known flip when bundled-fastify is stripped (import 'fastify' → perry-ext-fastify). Mirrors external-{net,ws,http-server}-pump.

Files touched

  • crates/perry-stdlib/src/fastify/server.rs — non-blocking listen, new process_pending/has_active, app_close
  • crates/perry-ext-fastify/src/server.rs — same shape for the external crate
  • crates/perry-stdlib/Cargo.toml — new external-fastify-pump feature
  • crates/perry-stdlib/src/common/async_bridge.rs — wire the new pump and has-active gate
  • crates/perry-stdlib/src/common/dispatch.rs — route app.close() runtime dispatch
  • crates/perry-stdlib/src/common/handle.rs — new iter_handle_ids_of helper
  • crates/perry-codegen/src/lower_call.rs — new fastify close entry in NATIVE_MODULE_TABLE
  • crates/perry-codegen/src/runtime_decls.rs — declare js_fastify_app_close
  • crates/perry/src/commands/compile/optimized_libs.rs — activate external-fastify-pump on the flip
  • test-files/test_fastify_in_process.ts — new smoke test mirroring the compat-sweep fixture

Test plan

  • Compat-sweep fastify fixture now passes (rc=0, prints ok=true).
  • Output matches Node: ok=true.
  • scripts/run_fastify_tests.sh end-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 to ok=true rc=0.

No version bump or changelog change (maintainer folds those in at merge time).

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().
@proggeramlug proggeramlug force-pushed the worktree-agent-a5d7a82f1ac8e5c67 branch from 9f535f6 to 26783cd Compare May 19, 2026 06:30
@proggeramlug proggeramlug merged commit 634e1f5 into main May 19, 2026
9 checks passed
@proggeramlug proggeramlug deleted the worktree-agent-a5d7a82f1ac8e5c67 branch May 19, 2026 07:16
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant