Repository navigation
fix(fastify): bound request-pump re-entrancy to avoid MAX_TRY_DEPTH - #5698
proggeramlug merged 1 commit into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThis PR changes ChangesDeferred pending pump
Sequence Diagram(s)sequenceDiagram
participant js_fastify_process_pending as js_fastify_process_pending
participant try_recv_pending_request as try_recv_pending_request
participant drain_server_requests_into_deferred as drain_server_requests_into_deferred
participant DEFERRED as DEFERRED
js_fastify_process_pending->>try_recv_pending_request: receive the next pending request
js_fastify_process_pending->>js_fastify_process_pending: re-enter while a handler awaits
js_fastify_process_pending->>drain_server_requests_into_deferred: capture queued requests from each server
drain_server_requests_into_deferred->>DEFERRED: append captured requests
js_fastify_process_pending->>DEFERRED: tail-drain deferred requests
js_fastify_process_pending->>js_fastify_process_pending: dispatch each deferred request
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/perry-ext-fastify/src/server.rs`:
- Around line 557-571: The deferred-request tail loop in server.rs can keep
dispatching nested work without giving newly arrived upgrades a chance to run,
which weakens the intended upgrade-first behavior. Refactor the existing
upgrade-drain logic into a reusable helper from the main request-pump flow, and
invoke it again while draining DEFERRED in the same outer frame before
processing more deferred requests. Keep the fix centered around the
process_request path and the upgrade_rx drain so upgrade handling is re-checked
between deferred batches.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 30d527a7-f236-4e0b-bfee-d958cebd0fc8
📒 Files selected for processing (1)
crates/perry-ext-fastify/src/server.rs
`js_fastify_process_pending` dispatches each pending request by running its handler, and a handler that `await`s drives `wait_for_promise` → `js_run_stdlib_pump`, which re-enters this same pump. Every nested dispatch pushes another `call_closure2_catching` try-frame (setjmp), so under sustained awaited-handler load the recursion could blow `MAX_TRY_DEPTH` (128 — "Try block nesting too deep") and abort the process. The pump had no guard against this. Add a thread-local `IN_PROGRESS` re-entrancy guard plus a thread-local `DEFERRED` queue. On a nested entry the pump captures every server's pending requests into `DEFERRED` (without dispatching) and returns; the OUTER frame drains `DEFERRED` at the tail of its loop, so every dispatch happens at the outer (bounded) try-frame depth instead of recursively. A nested entry that occurs during the tail drain is itself deferred and picked up by the same loop, so depth stays bounded regardless of arrival timing. WebSocket upgrades keep priority throughout: the tail loop re-drains each server's upgrades before every deferred request (via the extracted `drain_server_upgrades` helper), so a long deferred drain can't starve a late upgrade (PerryTS#1113). `DEFERRED` is capped at 4096 entries: a pathological async storm that exceeds the cap drops the newest pending, whose `response_tx` Drop makes the awaiting hyper service-fn resolve `Err` → an error response. Memory is bounded (backpressure), not leaked. The deferred dispatch happens in the same outer pump tick, so an awaited request observed by a nested pump call is served without waiting for the next event-loop iteration. The drain/cap/backpressure logic is extracted into `try_recv_pending_request` and `drain_server_requests_into_deferred` so it is unit-testable without a runtime. Adds tests: - `deferred_drain_respects_cap_and_drains_channel_fully`: over-cap entries are dropped, kept entries stay in FIFO order, and the channel is fully drained (nothing stranded in the mpsc). - `deferred_drain_under_cap_moves_all_without_dropping`: under cap, all captured in arrival order with no response channel closed. - `deferred_drain_over_cap_signals_dropped_clients`: a dropped (over-cap) request closes its response channel, so the client gets an error response, never a hang. - `deferred_drain_no_loss_across_servers_and_repeated_passes`: across servers and repeated nested-entry drains, every request is captured exactly once in per-server FIFO order; a second pass captures nothing. The full guard → defer → outer-tail-dispatch path is exercised end to end by the async handlers in `scripts/run_fastify_tests.sh`.
542b150 to
e2fab89
Compare
Summary
js_fastify_process_pendingre-enters itself when a dispatched handlerawaits (process_request→wait_for_promise→js_run_stdlib_pump→ back into the pump), and each nested dispatch pushes acall_closure2_catchingtry-frame (setjmp). The pump had no re-entrancy guard, so under sustained awaited-handler load the recursion could blowMAX_TRY_DEPTH(128, "Try block nesting too deep") and abort the process. This adds anIN_PROGRESSguard + a thread-localDEFERREDqueue so nested entries capture pending requests instead of dispatching them, and the outer frame drains the queue at its tail — every dispatch stays at the outer (bounded) try-depth, with no added latency. Cap-bounded (4096) with drop-newest backpressure.Changes
crates/perry-ext-fastify/src/server.rsjs_fastify_process_pendinggains a thread-localIN_PROGRESSre-entrancy guard and a thread-localDEFERRED: VecDeque<(Handle, FastifyPendingRequest)>. Nested entry drains every server's pending intoDEFERRED(no dispatch) and returns; aResetReentryGuard(Drop) clearsIN_PROGRESSon every exit; a tail loop dispatchesDEFERREDin the outer frame.try_recv_pending_request(locksrequest_rxonly for thetry_recv, never across dispatch) anddrain_server_requests_into_deferred(cap-respecting drain; over-cap pending are dropped →response_txDrop → service-fnErr→ error response).drain_server_upgradesand re-run before every deferred-request dispatch in the tail loop, so a long (nested-await-replenished) deferred drain can't starve a late upgrade (FastifyInstance.server returns a number — blocks ws/http2 upgrade piggybacking #1113).try_recvwas replaced by the identical-semantics helper call.#[cfg(test)] mod testsinserver.rs(4 tests, below).Related issue
n/a — robustness fix for the request pump's re-entrancy.
Test plan
This touches the hot request-pump path, so it was verified comprehensively — three novel checks beyond the usual, plus end-to-end and an adversarial audit:
1. Real-channel drain (not a mirror). Builds a live
FastifyServerHandle+ mpsc channel and exercises the actualdrain_server_requests_into_deferred/try_recv_pending_requesthelpers — asserts the cap is enforced, FIFO order is preserved, and the channel is fully drained even past the cap (no request stranded).2. Backpressure contract. Proves that an over-cap (dropped) request's
response_rxresolvesClosed— i.e. the client is signalled with an error response, never a hang or a leak.3. No-loss / no-duplication property. Across multiple servers and repeated nested-entry drains, asserts every queued request lands in the deferred queue exactly once in per-server FIFO order, and a second pass captures nothing.
End-to-end:
run_fastify_tests.shexercisesasyncroute handlers and an async error handler, which drive the exact re-entry path the guard wraps — 12/12 with the guard in place (no regression in real serving).Adversarial soundness audit (independent multi-agent review of the change) confirmed, with line-level evidence: the guard prevents nested dispatch and resets on every exit (incl. unwind); the tail-drain runs at outer try-depth; no
request_rxlock is held across dispatch and noget_handleruns insideiter_handle_ids_of; theDEFERREDRefCellis never double-borrowed; no request is lost or double-dispatched; and the top-level path is behaviorally unchanged.cargo build --releaseclean — built-p perry; the full-workspace build needs the GTK/gdk-pixbuflibs forperry-ui-*, which CI provides.cargo test --workspace --exclude perry-ui-ios --exclude perry-ui-tvos --exclude perry-ui-watchos --exclude perry-ui-gtk4 --exclude perry-ui-android --exclude perry-ui-windowspasses — ran the affected crate (cargo test -p perry-ext-fastify, 26/26) plusscripts/run_fastify_tests.sh(12/12); the full workspace test is deferred to CI (the baseperry-uicrate needsgdk-pixbuf, unavailable locally).test-files/or a#[test]in the affected crate — 4 new#[test]s for the deferred-queue drain/cap/backpressure/no-loss invariants.docs/src/— n/a, internal pump change, no public API surface.-p perry-ui-<backend>locally on that platform — n/a.Screenshots / output
n/a — internal request-pump robustness change (no user-visible output difference; correctness/latency under sustained awaited-handler load).
Checklist
feat:/fix:/docs:/chore:prefix convention used in the log —fix(fastify): …Summary by CodeRabbit
Bug Fixes
Tests