Skip to content

fix(resilience): release admission lease on client abort after SSE response (#14456) - #14457

Merged
diegosouzapw merged 2 commits into
diegosouzapw:release/v3.8.51from
xiaoyaner0201:fix/14456-admission-lease-abort-release
Sep 22, 2026
Merged

diegosouzapw merged 2 commits into
diegosouzapw:release/v3.8.51from
xiaoyaner0201:fix/14456-admission-lease-abort-release

Conversation

@xiaoyaner0201

@xiaoyaner0201 xiaoyaner0201 commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Related to #14456 — partial lifecycle fix; does not close the production incident.

Scope clarification

This PR releases an admission lease when the supplied inbound request signal aborts after the handler has produced an SSE Response. It does not currently enforce a combo deadline or clean up a handler promise that remains pending before returning a response.

The initial description overstated this as the root cause and complete fix for production saturation. The incident recurred in the unpatched deployment after increasing the heavy cap to 200; long-duration 504 log rows require separate investigation. They have not been causally matched to individual held leases. See the corrected observations and open questions in #14456.

Implementation

  • Add optional request-signal cleanup to releaseChatAdmissionWhenDone.
  • Pass the request signal through both release paths in /v1/responses, both in /v1/chat/completions, and the shared withChatAdmission middleware.
  • Detach the listener on release, avoid repeated lease release, and request cancellation of the upstream reader on abort. Requesting cancellation does not by itself prove the underlying transport/work has terminated.
  • Extract the release functions into src/shared/middleware/chatAdmissionRelease.ts and re-export them from chatBodyAdmission.ts. This keeps existing import paths while satisfying the frozen file-size gate without changing its baseline.

Uncovered phase: handler has not returned a Response

At head 7ed95a5952b98383e65b848ca82c4c25c8bc9c50:

return releaseChatAdmissionWhenDone(await responsePromise, lease, options);

releaseChatAdmissionAfterHandler awaits handler settlement before installing the new abort hook. If the client aborts while responsePromise is still pending, the lease remains held until settlement. If that promise never settles, this PR alone does not release the lease.

This is relevant to the reported timeout/lifetime discrepancy but is not proof of its full production cause. Returning capacity while the underlying operation is still running can itself defeat resource protection; a follow-up needs to verify actual cancellation/cleanup, not just decrement the counter.

Verification at the current head

Original regression suite and CI

The four added helper tests cover abort after wrapping, an already-aborted signal passed to the response wrapper, clean completion followed by late abort, and explicit reader cancellation. They passed locally, together with the related admission suites (39 tests reported in the original run).

The behavioral sabotage run failed the abort assertions (actual: 1, expected: 0) with the abort hook removed. This establishes helper-level fix sensitivity; it does not reproduce the deployed HTTP disconnect chain or demonstrate that all observed 499s leaked slots.

GitHub checks for 7ed95a5952b98383e65b848ca82c4c25c8bc9c50: 13 successful, 1 skipped (Build (advisory)), 2 neutral (Mergify). The earlier candidate-owned file-size failure was resolved by extraction. These are check results, not a claim of merge, deployment or incident resolution.

Follow-up characterization using the actual PR module

A bounded local Node v26.8.2 probe imported the exact exported chatAdmissionRelease.ts, with fake leases and handler promises but no copied production implementation:

Case Result
Fulfilled non-SSE JSON 504 Lease released
Rejected handler promise Lease released
Existing SSE response followed by request abort Lease released
Wrapped response reader.cancel, with no signal Lease released
Abort while handler remains pending, later resolves to 504 0 releases while pending, 1 after settlement
Already-aborted signal while handler remains pending, later resolves to SSE 0 releases while pending, 1 after settlement

Those last two cases characterize a gap, not passing acceptance criteria for a complete fix. The probe is not a live Next.js/Codex/upstream reproduction. The repository tests currently do not assert cancellation while the handler promise remains pending.

Production evidence limitations and corrections

  • A returned JSON 504 already takes the non-streaming release path; do not classify every 504 as a lease leak.
  • A 180000ms timeout message alongside a much larger call_logs.duration warrants investigation, but does not alone prove when the timeout fired, when cleanup ended, or which admission lease was held.
  • waiting=0, a count of long-duration historical rows equal to the cap, and a socket snapshot are not sufficient causal evidence.
  • Cancelling the wrapped body's reader can invoke that body's cancel hook. The earlier claim that keepalive reader cancellation necessarily bypasses it was incorrect.
  • docker restart resets process-local counters. Recreating the container is required for changed environment values, not to reset a module-level singleton.

Local lint baseline note

A plain ESLint invocation previously reported an unused callCloudWithMachineId import at line 3 of src/app/api/v1/chat/completions/route.ts, reproduced on the clean base 2b8f89a652b5b67ac9182d2cf0c858027327a781. The repository's base-relative No new ESLint warnings check passed. No baseline was widened or finding suppressed.

Related work

#13648 / #13676 cover queue-wait policy; #12135 covers SSE occupancy; #13621 investigates retained backing strings; #14430 adds an in-flight byte ledger. They do not establish this deployment's complete root cause. This narrow change can be folded into related lifecycle work if that is preferable.

Still required before claiming #14456 resolved: real request-path reproduction of the pending-handler/timeout behavior, verified cancellation/settlement, lease-baseline recovery across repeated requests, and deployment verification.

…ouzapw#14456)

releaseChatAdmissionWhenDone wired lease release into three consumer-driven
paths only: pull-to-done, pull-throws, and cancel. A client that disconnects
mid-stream may stop pulling and never cancel, so none of them run and the
heavyweight slot is charged for the lifetime of the process. Once activeHeavy
reaches OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT every heavy request is shed with
503 chat_admission_busy, with waiting=0 and no real load.

Observe the request signal as a fallback release path and route all five call
sites through it. Release is idempotent so a late abort after a clean finish
cannot double-decrement.
check:file-size freezes chatBodyAdmission.ts at 1206 lines and the fix pushed
it to 1242. Move the release binding into its own module and re-export it from
the original path, so every existing import site is unchanged. File is now
1159 lines; check:file-size passes.
@xiaoyaner0201 xiaoyaner0201 changed the title fix(resilience): release admission lease on client disconnect (#14456) fix(resilience): release admission lease on client abort after SSE response (#14456) Sep 22, 2026
@xiaoyaner0201

Copy link
Copy Markdown
Contributor Author

Scope correction (2026-09-22)

The current PR is a partial lifecycle fix, not a claim that all production 504 saturation is resolved.

Using the exact exported module at head 7ed95a5952b98383e65b848ca82c4c25c8bc9c50, the verified behavior is:

  • resolved non-SSE JSON 504 → lease released;
  • rejected handler promise → lease released;
  • already-created SSE response + request abort → lease released;
  • wrapper reader cancellation → lease released;
  • request abort while the handler promise is still pending → no release during the pending phase; release only after the handler settles.

The timeout/lifetime discrepancy observed in deployment therefore remains open. The next fix must reproduce the real route with timestamps for admission, combo deadline, request abort, handler settlement, upstream cancellation, and lease release. Decrementing a counter without terminating the underlying work would not be a complete fix.

@diegosouzapw
diegosouzapw merged commit 49de88f into diegosouzapw:release/v3.8.51 Sep 22, 2026
16 checks passed
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.

2 participants