Skip to content

fix(resilience): admission lease cleanup is incomplete while the handler is still pending (#14456) - #14566

Merged
diegosouzapw merged 1 commit into
release/v3.8.51from
fix/14456-admission-lease-cleanup
Sep 24, 2026
Merged

diegosouzapw merged 1 commit into
release/v3.8.51from
fix/14456-admission-lease-cleanup

Conversation

@diegosouzapw

Copy link
Copy Markdown
Owner

Closes #14456

Root cause

releaseChatAdmissionAfterHandler (src/shared/middleware/chatAdmissionRelease.ts) only
installed its abort-aware release wrapper after await responsePromise resolved. If the
client aborted while the handler promise (e.g. an in-flight/stuck upstream LLM call) was still
pending, nothing observed the abort during that window, so the heavyweight admission slot stayed
held until the handler eventually settled — which can be very late or effectively never under a
stuck upstream. PR #14457 fixed the case where an SSE Response object already exists and the
client then disconnects; it did not cover this pending-handler phase.

Fix

releaseChatAdmissionAfterHandler now attaches the options.signal abort listener before
awaiting responsePromise, releasing the lease immediately (idempotently, via the existing
lease.released guard) if the client aborts during the pending phase. The listener is detached
once the promise settles either way, and if the abort-triggered release already fired during the
pending phase, the redundant stream-wrapping work in releaseChatAdmissionWhenDone is skipped.

This fix is scoped to the admission slot only — it does not cancel the underlying upstream
fetch/handler work still in flight; confirming that upstream work actually terminates remains a
separate, larger change per the issue's own "work remaining" list. Not closing that broader
follow-up scope; this PR only closes the specific admission-lease-cleanup defect.

Regression test

tests/unit/chat-admission-pending-handler-abort-14456.test.ts (new file, promoted from the
plan-file's TDD probe):

  • RED (unfixed code): an aborted client must not hold a heavyweight slot while the handler is still pending — AssertionError: 1 !== 0 (activeHeavy stayed at 1 after abort).
  • GREEN (fixed code): all 3 cases pass — abort-while-pending releases promptly, normal completion
    releases exactly once with no listener leak, and an already-aborted signal releases immediately.

Existing tests

Ran the full touched-area suite (58 tests, 0 failures): tests/unit/chat-body-admission.test.ts,
tests/unit/chat-admission-abandoned-stream-lease.test.ts (the #14457 coverage, for contrast),
tests/unit/chat-admission-wrapper.test.ts, tests/unit/responses-route-early-keepalive-wiring.test.ts.

Gates run

  • npm run typecheck:core → exit 0
  • npx eslint --suppressions-location config/quality/eslint-suppressions.json <changed files> → exit 0
  • node scripts/check/check-file-size.mjs → OK
  • node scripts/check/check-complexity-ratchets.mjs --base-ref origin/release/v3.8.51 → OK, 0 violations in the 1 changed file
  • node scripts/check/check-changelog-integrity.mjs → OK
  • node scripts/check/check-mutation-test-coverage.mjs --strict → pre-existing drift, unrelated to this PR (see below)

⚠️ check-mutation-test-coverage.mjs --strict reports 8 covering test(s) missing from
stryker.conf.json across open-sse/services/accountFallback.ts, src/sse/services/auth.ts,
open-sse/services/combo/comboStructure.ts and open-sse/services/combo/quotaScoring.ts — none
of which this PR touches (this PR's only source file is
src/shared/middleware/chatAdmissionRelease.ts, which is not in the 31-module mutation scope at
all). Inherited base drift, not introduced by this change.

Plan-file: _tasks/pipeline/bugs/2-implementing/14456-fix-resilience-admission-lease-cleanup-is-incomplete-while-the.plan.md

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.

fix(resilience): admission lease cleanup is incomplete while the handler is still pending

1 participant