[rush-daemon] Arbitrate environment restarts and bound client restart retries - #6079
Merged
Sean Larkin (TheLarkInn) merged 5 commits intoSep 24, 2026
Merged
Conversation
… retries Fixes #6057 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sean Larkin (TheLarkInn)
requested a review
from Ian Clanton-Thuon (iclanton)
as a code owner
September 24, 2026 02:24
…scheduler error mapping Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Restart deadlines can be exceeded, graph-control restarts bypass arbitration, and public retry documentation remains inconsistent.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 2
Open (3)
What changed in this PR
Adds daemon restart arbitration and bounded client retries to prevent concurrent environment changes from causing restart loops.
Changes:
- Serializes daemon restart candidates after active compatible requests drain.
- Retries successor daemons with backoff, then falls back in-process.
- Adds unit and integration coverage for arbitration and retries.
| File | Description |
|---|---|
libraries/rush-daemon/src/WorkspaceRestartArbiter.ts |
Implements restart ticket arbitration. |
libraries/rush-daemon/src/WorkspaceRequestLifecycle.ts |
Integrates arbitration into request preparation. |
libraries/rush-daemon/src/WorkspaceRequestAdmission.ts |
Applies admission budgets to drain waits. |
libraries/rush-daemon/src/test/WorkspaceRestartArbitration.test.ts |
Tests end-to-end restart ordering. |
libraries/rush-daemon/src/test/WorkspaceRestartArbiter.test.ts |
Tests arbiter ordering and cancellation. |
libraries/rush-client-core/src/test/fixtures/daemon.ts |
Adds repeated-restart fixture modes. |
libraries/rush-client-core/src/test/connectOrStartDaemon.test.ts |
Tests bounded successor retries. |
libraries/rush-client-core/src/executeWithDaemonRestart.ts |
Implements retries, backoff, and fallback. |
libraries/rush-client-core/src/DaemonClient.ts |
Adds the restart-exhaustion fallback reason. |
common/reviews/api/rush-client-core.api.md |
Updates the public API report. |
common/changes/@rushstack/rush-daemon/fix-restart-arbitration_2026-09-24-01-45.json |
Records the daemon patch. |
common/changes/@rushstack/rush-client-core/fix-restart-arbitration_2026-09-24-01-45.json |
Records the client-core patch. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
… arbitrate graph-control restarts; document bounded retries Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…restart-arbitration # Conflicts: # libraries/rush-client-core/src/test/connectOrStartDaemon.test.ts
…bmitting to the last successor in the restart-always test Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Mo Jazayeri (mojaza)
approved these changes
Sep 24, 2026
Sean Larkin (TheLarkInn)
deleted the
thelarkinn-fix-rushd-restart-arbitration
branch
September 24, 2026 21:13
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.


Summary
Concurrent
rush-clientrequests whose environments differ (e.g.AGENT=1..4) each asked the daemon for a tier-2 process restart. The daemon granted every one, so environments ping-ponged across successors; each client's single restart retry sawretryAfterRestartagain and exited 1 (The successor requested another restart; the single safe retry was exhausted.), and the workspace was left with no daemon. This PR makes the daemon arbitrate restarts and makes the client retry a bounded number of times, then fall back in-process.Root cause
libraries/rush-client-core/src/executeWithDaemonRestart.tsallowed exactly one retry; any secondretryAfterRestartthrewstartupFailed(a hard exit 1, sincelaunchClient.tscalls it outside the connect fallback).WorkspaceRequestLifecycle#prepareAsync: a request that classified asRestartimmediately took the workspace transition and queued for the exclusive gate lease. TheRequestScheduleris FIFO, so every later request, including ones whose env matched the running process, queued behind it and was then rejected withRestartPendingBeforeExecution. That let one mismatched request preempt all other queued work, and every successor repeated the same thing for the next env.Fix
Daemon (
@rushstack/rush-daemon)WorkspaceRestartArbiter: each dispatched request (except cancellablegraph watchobservers) holds a ticket. A request that needs a restart releases its gate lease and waits until no other ticket is still being served, i.e. every in-flight or queued request that this process can serve has drained. Only then does it take the exclusive transition and restart. Restart candidates are admitted one at a time. The others wake, seerestartPending, and get the typed pre-executionretryAfterRestart, so started work is never replayed.RequestAdmissionController.waitForRestartDrainAsync): it honorsnoWait, the remainingwaitTimeoutMsand abort, and reports failures through the existingadmissionErrorCodepath. An abandoned waiter never triggers a restart.Client (
@rushstack/rush-client-core)executeWithDaemonRestartAsyncfollows up to 6 successors. The first retry is immediate and later ones use jittered exponential backoff (50 ms up to 1 s). Each successor's ownership is attested before the next hand-off, and all of this stays inside the admission deadline.fallbackoutcome (reason: 'restartRetriesExhausted') instead of throwing.launchClientalready handlesfallbackby printing the message and running in-process Rush, soapps/rush-cli-clientis unchanged. This keeps the PR clear of FIX-startup-wedge ([rush] rush-client-core: a failed or slow daemon startup leaves a durable .starting reservation that wedges the workspace (every command waits ~16 s, daemon start refuses); an invalid RUSH_* env value triggers it and hides the real error #6050).Tests
rush-client-coreconnectOrStartDaemon.test.ts: newrestart-twicefixture mode. A second restart request is retried and succeeds with a decreasing admission budget.restart-alwaysnow returns the boundedrestartRetriesExhaustedfallback where it used to throw.rush-daemonWorkspaceRestartArbitration.test.ts(real host, real native builds, real successor): while a matching build is held in flight, a mismatched-env build arrives, and then another matching build arrives. Both matching builds complete (exit 0, noretryAfterRestart). The mismatched request resolves last withretryAfterRestart, and the successor is running.rush-daemonWorkspaceRestartArbiter.test.ts: unit coverage for drain ordering, one-at-a-time admission, and no-wait/timeout/abort accounting.Linux validation (WSL Ubuntu-24.04, node 22.23.2)
Synthetic workspace:
mkws-synth <ws> --projects 12 --shape wide --sleep-ms 5000. Warm the daemon, dirty the sources, then runenv _=fixed AGENT=<n> rush-client build --only p0<n>for n=1..4 at the same time (script:/workspaces/rushd-lab/fixes/restart-arbitration-repro.sh).single safe retry was exhausted;daemon statusafterwards: could not connect (no daemon).daemon admission failed (wait-timeout)at 31.1 s. A daemon is left running.RUSH_DAEMON_QUEUE_TIMEOUT_SECONDS=120: 4/4 exit 0 (13.2 s, 24.1 s, 34.4 s, 44.3 s), one successor per env. A daemon is left running.Note: with the 30 s default, a request can hit the wait-timeout after N environments ahead of it have each built and restarted. This is the same class as #6051 / #6067 (a wait for progress charged against the default deadline). Once #6067 lands, the restart-drain wait can follow its
waitTimeoutIsDefaultrule. #6047 removes the common volatile-var trigger, so most envs will no longer need a restart at all.rush build --to @rushstack/rush-client-core --to @rushstack/rush-daemon --to @rushstack/rush-cli-client(including lint) passes;rush test --only @rushstack/rush-client-core --only @rushstack/rush-daemonpasses.Compatibility with #6067
The restart-drain wait reports its own admission errors and doesn't call
RequestAdmissionController's private error mapping. So this PR is independent of #6067's signature changes and composes with them. Verified on Linux:pull/6079/headmerged withpull/6067/headmerges cleanly,rush build --to @rushstack/rush-daemon --to @rushstack/rush-cli-client(including lint) passes, andrush test --only @rushstack/rush-daemon --only @rushstack/rush-client-corepasses.Follow-ups (optional)
_, SHLVL) is the most common trigger; see [rush] rushd: request environment fingerprint covers every env var, so per-shell variables (PWD, OLDPWD, SHLVL, _, TERM, WSL_INTEROP, ...) restart the daemon and discard the warm graph #6047.waitTimeoutIsDefaultrule to the restart-drain wait once it lands.single safe retry was exhaustedno longer exists. Those requests now retry up to 6 successors and then fall back in-process, and restarts are serialized. If that edit changes the daemon's own runtime (for example a self-hosted rushstack workspace whose build rewritesrush-daemon/rush-liboutput), each such build still costs one restart. Making the runtime fingerprint ignore outputs the build itself rewrites would be a separate fix.Fixes #6057
This came out of the automated rushd Linux performance/behavior analysis ("Rushd Hive"; board thread #74, confirmations #98 and #102).