Repository navigation
Stop a failed-closed isolated release from using up a threads.maxIsolated slot - #3607
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
Code Review
This pull request refactors and improves the admission logic for isolated applications under the threads.maxIsolated budget, ensuring that only applications with a dedicated worker are counted. It introduces helper functions like admittedIsolatedApplications, isolatedApplicationAdmissionRefusal, and failedClosedApplications to centralize this logic, and updates deployment and reconciliation processes. Feedback on the changes identifies an issue where in-memory refusals are instantiated as plain Error objects instead of RejectedReleaseError, which prevents them from being recognized as having their slots withdrawn and incorrectly charges them against the budget.
…led-closed release holds no place in threads.maxIsolated Deploy admission (`assertIsolationAdmission`) counted every present isolated application against `threads.maxIsolated`, including a first release whose canary rejected it and which stays on disk failed closed. The reconcile skips such a release, so with `maxIsolated: 1` a failed first deploy of one application made every other isolated deploy return 409. The selector boot and the reconcile use (`admittedIsolatedApplications`) and the failed-closed reader move to `server/threads/isolatedApplications.ts`; socketRouter keeps its reporting. Deploy admission asks the same selector about the candidate, placed at its own root-config entry and with its own refused release ignored, and also counts the workers a scoped reconcile leaves running. An already-live activation that re-arms a refused release is admitted first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-isolation-admission-shared-set
Admission left every failed-closed application out of the occupancy count, but only a canary's decided rejection withdraws its worker. A running application whose certification record becomes unreadable is refused and keeps running, so it still counts. `RejectedReleaseError` now says which refusals are decided. The new 503 branch throws `ServerError`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-isolation-admission-shared-set
A rejected release with a predecessor is restored, and its worker keeps running through the restore, so it still holds its place. Only a rejection with no earlier release stays live failed closed and has its dedicated worker withdrawn; `RejectedReleaseError.slotWithdrawn` now says exactly that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-isolation-admission-shared-set
…e rebase server/DESIGN.md reached its line budget on main, so the note on the shared selector and the occupancy count moves into the two components/DESIGN.md sentences that already describe the reconcile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-isolation-admission-shared-set
…e is certified again Admitting the already-live re-arm path through admitIsolation without a descriptor now follows the branch for an activation that publishes no routing: the grant is checked against the current root-config entry, as it is for a payload artifact that set none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-isolation-admission-shared-set
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-isolation-admission-shared-set
Main keeps a refusal in memory when it cannot write the rejection record, and reads it only while that release is still live, failed closed; deciding it already queued the withdrawal of the application's dedicated worker. It now reads as a `RejectedReleaseError` with `slotWithdrawn`, so admission does not count that worker, and its message matches a recorded refusal's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-isolation-admission-shared-set
…cation is isolated Main withdraws a refused release's dedicated worker only when the release is isolated; an application turned shared keeps its old worker until the pool restarts. Admission now exempts a refused running worker only if its application is still isolated in config. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-isolation-admission-shared-set
kriszyp
force-pushed
the
fix/isolated-admission-shared-set
branch
from
October 27, 2026 01:35
364821e to
ff2525b
Compare
kriszyp
marked this pull request as ready for review
October 27, 2026 02:16
Ethan-Arrowood
pushed a commit
to HarperFast/documentation
that referenced
this pull request
Nov 3, 2026
…ed-closed releases (#759) * Document that isolated admission follows startup order and skips failed-closed releases Docs for HarperFast/harper#3607: threads.maxIsolated counts only isolated applications that get a dedicated worker (running first, then root-config order); a failed-closed live release holds no place. deploy_component answers 409 when the application would get no worker in that order, including a refused release activated again by deployment_id, and 503 when refusals cannot be read. Dispatch-Task: docs-harper-3607 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Badge the v5.4.0 maxIsolated change, count retained workers, note the 503 on reactivation Dispatch-Task: docs-harper-3607 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Group the running-worker sentences and split the maxIsolated refusal sentence Dispatch-Task: docs-harper-3607 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
kriszyp
added a commit
that referenced
this pull request
Nov 5, 2026
…ed-closed releases (HarperFast/documentation#759) * Document that isolated admission follows startup order and skips failed-closed releases Docs for #3607: threads.maxIsolated counts only isolated applications that get a dedicated worker (running first, then root-config order); a failed-closed live release holds no place. deploy_component answers 409 when the application would get no worker in that order, including a refused release activated again by deployment_id, and 503 when refusals cannot be read. Dispatch-Task: docs-harper-3607 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Badge the v5.4.0 maxIsolated change, count retained workers, note the 503 on reactivation Dispatch-Task: docs-harper-3607 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Group the running-worker sentences and split the maxIsolated refusal sentence Dispatch-Task: docs-harper-3607 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.
⊙ Problem
With
threads.maxIsolated: 1, an isolated application whose first release fails its canary blocks every other isolated deploy with a 409 until it is redeployed or dropped. Deploy admission counted every isolated application on disk; the reconcile that starts workers skips a failed-closed release. They also disagreed when boot order would not admit an application on disk: it deployed, then nothing started it.🤔 Is the requirement right?
Yes. Admission now follows boot order, so some deploys that used to go live with no worker get a 409. Which application a scoped reconcile should prefer stays open.
💡 Solution
admittedIsolatedApplications. A failed-closed release holds no place.slotWithdrawn); look hardest here.⚖️ Alternatives
🤔 A deploy on a worker thread reads refusals from disk only
The operations API runs on main, which sees every refusal. A worker's
server.operationdeploy misses refusals main holds only in memory. This can only refuse more, never admit what the reconcile refuses; closing this gap needs a second field on the topology reply.🤔 An isolated deploy reads refusals twice
Kept: the pre-check refuses before staging; the locked check is authoritative. Cost: one directory read per deploy.
🔧 Changes
server/threads/isolatedApplications.ts— the selector and failed-closed reader, moved fromsocketRouter.ts, plusisolatedApplicationAdmissionRefusalfor deploys.server/threads/socketRouter.ts— boot and reconcile call the shared selector and keep their refusal reporting.components/operations.ts—assertIsolationAdmissionuses the shared admission and answers 503 when refusals cannot be read;admitIsolationwithout a descriptor admits the live entry and checks a component grant against it.components/Application.ts—recertifyLiveReleaseadmits before it arms a refused release;admitIsolation's descriptor is optional.components/releaseCertification.ts—RejectedReleaseError.slotWithdrawn, set only for a rejection with nothing to restore, and for a refusal main holds in memory.components/DESIGN.md— the shared selector and the occupancy count.✅ Verification
maxIsolated: 1integration suite: after A's first release fails closed, B deploys and serves through its mirror. A's fix gets 409 while B holds the place, and is certified once B is dropped. On base, B's deploy gets the 409. Without the new admission call, the same-ID activation answers 200uncertified.test:unit:main8323 passing;test:unit:resources4898 passing;test:integration:all2776 passing (fourollama-backendfailures: no local models).reference/components/applications.md.Unit test cases
unitTests/server/threads/isolatedApplications.test.js: running ones keep their place; config order up to the budget; unreadable refusals admit nothing new; a failed first release frees its place; a candidate's own refused release is ignored but a full budget still refuses it; two present with a budget of one; placement of a new integer-like name, a new name and a flip to isolated; occupancy counts every running worker unless its refusal withdrew the slot, and counts an application turned shared; a budget of zero.unitTests/components/releaseCertification.test.js: only a rejected release with nothing to restore reportsslotWithdrawn.unitTests/components/deployCertification.test.js: re-arming a rejected live release callsadmitIsolationwith no descriptor, arms nothing when refused, and leaves the record rejected.integrationTests/components/isolated-application-lifecycle.test.ts: the existing suite now shares its helpers with the new one.🤖 Generated by Claude Code (Claude Opus 5.5); posted via @kriszyp.
Release target: main — release certification, which leaves a failed first release in place, exists only on main
Related PRs: #3505 overlaps (this branch is rebased on it; both edit the admitIsolation callback), 1 other independent
Complexity: complicated
Deferred findings
3af96fffdf79ff4dminor concurrency —components/operations.ts— Isolated admission is per-component locked, so concurrent deploys of different apps can both pass — NOT FILEDfb2f1c1838d28ea0minor correctness —components/operations.ts— Worker-originated admission omits main's memory-only release refusals — NOT FILED7a35bd5966585ca9nit correctness —server/threads/isolatedApplications.ts— Disk refusal with slotWithdrawn false masks main's withdrawn in-memory entry — NOT FILED5ace95afe0f4a66dnit reliability —server/threads/isolatedApplications.ts— Running refused or no-longer-isolated workers hold no selector place; budget checked in two parts — NOT FILEDd1b1df8a04d8df60nit performance —components/operations.ts— Isolated deploy admission awaits a staging-root refusal scan, more than once per deploy — NOT FILED297fa6f6c1579f90nit performance —server/threads/isolatedApplications.ts— admitted.includes in loop is quadratic — NOT FILED1168f9f920830637nit maintainability —components/operations.ts— Isolation admission errors use banned handleHDBError instead of ClientError/ServerError — NOT FILEDLate Grok findings
4 surviving (highest major) — Grok reviewed 5651bea
server/threads/isolatedApplications.ts:136— capacity counting drops every running worker whose release is failed-closed, including one that is failed-closed only because its certification record became unreadable (components/releaseCertification.ts:225-231). Nothing queues that worker's withdrawal, so it keeps serving while B is admitted uncomponents/operations.ts:588— the newfailedClosedApplications()failure branch uses legacyhandleHDBError. AGENTS.md forbids that helper, andHdbErrorexposes the raw filesystem message instead of the deployment-context text; should benew ServerError(msg, 503)components/operations.ts:571— an isolated deploy scans.deploy-stagingfor refusals twice: once in the pre-check at:607, and again whenadmitIsolationre-runs admission under the preparation lock (components/Application.ts:5287). The cost lands on the deploy path, not a request pathintegrationTests/components/isolated-application-lifecycle.test.ts:85— added comments that narrate adjacent code, here and atcomponents/Application.ts:5286andunitTests/server/threads/isolatedApplications.test.js:125-128. Also, the comment atunitTests/components/deployCertification.test.js:307says "join", but the code it describes is the optional-join fallbacNot standing: 3 separate-issue. Grok ran 24m (24m from launch to report); the fold adjudication (
cursor-grok@5651beaf1202-6452690e, claude) took 1m.Origin — the dispatch brief this PR was written from
Isolated-application admission is computed in two places that disagree: assertIsolationAdmission (components/operations.ts) counts a failed-closed first release (directory and config entry left on disk) against maxIsolated, while admittedIsolatedApplications in server/threads/socketRouter.ts excludes it. With maxIsolated: 1, a failed first deploy of app A makes a deploy of app B return 409 until A is redeployed or dropped. Export one admitted-set function (socketRouter.ts or isolatedApplications.ts), failed-closed excluded, and have assertIsolationAdmission use it. Do not change scoped reconcileIsolatedWorkers ordering semantics (boot order vs requested app) in this PR; that is an open design question.
Acceptance
Test: maxIsolated: 1, deploy app A with a failing first release, then deploy app B -> B succeeds; existing isolation tests green. Show each new test failing on origin/main sources (forced rebuild) before the fix.
Dispatch: task
harper-isolation-admission-shared-set· queued by finding-triage/wave-40 · ran by claude/opus/xhigh · worker kzyp-xps-1Review-Coverage: authored=claude; ran=cursor-composer,gemini,codex,cursor-grok; late=cursor-grok:adjudicated; adjudicated=domain; declined=muse; rounds=8; full=2 @ ff2525b
Review-Attention: study ~15m (hot: isolatedApplications.ts, socketRouter.ts; decisions: slot-withdrawal-source, admission-two-checks, recertify-admits, do-less-alternative) @ ff2525b