Skip to content

fix(clusters): Restart Cluster reports a failed cluster load instead of staying disabled - #1867

Merged
dawsontoth merged 3 commits into
claude/1339-replicated-set-configurationfrom
claude/1853-restart-cluster-load-failure
Oct 12, 2026
Merged

dawsontoth merged 3 commits into
claude/1339-replicated-set-configurationfrom
claude/1853-restart-cluster-load-failure

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

A mole reporting clearly that the lanterns could not be reached

Stacked on #1861 (claude/1339-replicated-set-configuration). Merge #1861 first, then retarget this PR to stage.

⊙ Problem

Restart Cluster fetched the cluster and built a client for each instance before entering any try. When that failed (a central manager 5xx or 403, a dropped connection, a malformed instance FQDN), the click handler rejected unhandled. The Restart button stayed disabled until the page remounted, the "Restarting" toast spun for its full 60s without an error, and the rejection reached the console as an unhandled one (#1853). #1861 fixed the same failure in the config editor's save.

❓ Your call: Is the stack worth it? The shared helper replaces client-building code that only exists on #1861's branch, so this PR has to wait for #1861. The alternative is to base it on stage, fix only the restart hook, and keep the duplicate copy. That is easy to undo: rebase onto stage and drop the useRollingConfigUpdate hunk.

💡 Solution

Also fixed (found by the pre-push review; both bugs are older than this branch):

  • Cancel during an instance's availability check. All three Cancel-toast loops checked the Cancel flag only at the top of each iteration. A Cancel clicked while the probe was in flight was ignored, and the restart, config write or license install still went out. Each loop now checks the flag again after the probe, before its first request that changes the instance: restart, config update, and apply licenses (useApplyLicensesClick.tsx).
  • Misleading "none were RUNNING" message. When every RUNNING instance failed to restart, the toast said "No instances were in a RUNNING state". The message is now keyed on there being no RUNNING target (restart, config update), so that case now reads "Only 0 RUNNING instances were restarted of N".

❓ Your call: A failed load no longer reaches RUM. Before this change it was reported as an unhandled rejection; now the toast handles it and nothing is logged, which is how #1861 treats the config save. To keep the signal, add a console.error in the catch: the RUM SDK reports those with error.source: "console". That is a one-line change either way.

⚖️ Alternatives

✅ Verification

  • Fails on base: run against the base hook, the load-failure tests in useRestartClusterClick.test.tsx fail with the reported bug: onRestartClick rejects with Error: 503 (the cluster fetch) or TypeError: Invalid URL (client construction). The four review-fix tests also fail before their fix: Cancel during the check for restart, config update (useRollingConfigUpdate.test.tsx) and licenses (useApplyLicensesClick.test.tsx, a new harness), plus the all-failed message for restart and config update. All of them pass with the fix.
  • Helper unit tests (getClusterRestartTargets.test.ts) pin the RUNNING filter, the reverse order, the Fabric Connect flag, a missing instance list, and that a failed fetch rejects before any client is built.
  • Mutation checks: reverting the zero-instance guard makes its test fail, and so does removing the finally that clears the pending state.
  • Full suite (pre-commit hook, Node 24.21.0): 400 files, 3,714 passed, 11 skipped. tsc -b, oxlint and dprint check all exited 0.
  • End-to-end route: not observable end-to-end without making central manager fail on demand. The tests drive the real hook through renderHook, with the real restart tracker, and assert that the cluster is never marked as restarting when the load fails. I did not verify in a browser.
  • Cross-model review: two rounds. Codex graded round 1 CHANGES and round 2 LGTM, with its major finding resolved. I rejected gemini's two round-2 minors on the code: every loop already skips cancelled iterations through its outer guard, and useRollingConfigUpdate already has the empty-cluster success guard from feat(config): fan cluster config saves out with replicated set_configuration on Harper 5.2+ #1861. The Harper adjudicator could not run because its Claude OAuth session had expired, and the Cursor leg could not run because the 1Password SSH agent refused to sign.
  • gemini-code-assist feedback: took all three suggestions in 1bbbc43. The cancel checks added after the probe now break instead of continue, because the flag never resets and later iterations only re-checked it. The finally that releases each instance's restart marker still runs, and all of the cancel-during-check tests above pass. The branch also merges feat(config): fan cluster config saves out with replicated set_configuration on Harper 5.2+ #1861's matching change to its own loop-top guard (3581be6). These are refactors with unchanged behavior, so no further review round ran.

Closes #1853

🤖 Generated with Claude Code

🤖 Generated by Claude Code (Anthropic Claude); posted via @dawsontoth.

Related PRs: #1861 overlaps (this PR's base; it reuses #1861's cluster-load catch and replaces its target planning with the shared helper), #1899 overlaps (reworks the same restart and config-update loops in useRestartClusterClick.tsx and useRollingConfigUpdate.tsx)
Complexity: medium

Review-Coverage: authored=claude; ran=codex,gemini; blocked=cursor-composer(no-receipt),domain(auth); declined=cursor-grok,cursor-kimi,cursor-muse; rounds=2; full=1 @ 1bbbc43

Review-Attention: read ~6m (raised: degraded review) @ 1bbbc43

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new helper function getClusterRestartTargets to centralize the logic of retrieving and preparing running instances for cluster restarts, which is then integrated into useRestartClusterClick and useRollingConfigUpdate to reduce duplication. It also adds cancellation checks after verifying instance responsiveness across several hooks. The review feedback suggests optimizing these cancellation checks by replacing continue with break inside the loops, as the canceled state is persistent and breaking early avoids unnecessary iterations.

Comment thread src/hooks/useRestartClusterClick.tsx
Comment thread src/hooks/useApplyLicensesClick.tsx
Comment thread src/hooks/useRollingConfigUpdate.tsx
@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 74.05% 12304 / 16614
🔵 Statements 74.12% 13100 / 17673
🔵 Functions 68.66% 3263 / 4752
🔵 Branches 69.55% 9660 / 13888
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/hooks/getClusterRestartTargets.ts 100% 100% 100% 100%
src/hooks/useApplyLicensesClick.tsx 81.08% 47.61% 50% 80.55% 87-94, 114, 125-140
src/hooks/useRestartClusterClick.tsx 90.19% 88.88% 42.85% 90% 32, 68, 146, 157, 179
src/hooks/useRollingConfigUpdate.tsx 93.65% 97.43% 60% 93.54% 103, 193, 204, 227
Generated in workflow #2286 for commit 3afc3a9 by the Vitest Coverage Report Action

dawsontoth and others added 3 commits October 12, 2026 17:18
…of staying disabled

useRestartClusterClick awaited getClusterInfo, and built the per-instance
clients, outside any try. When either failed, the click handler rejected
unhandled: the Restart button stayed disabled until remount, the
"Restarting" toast spun until its 60s duration ran out, and the rejection
reached the console as an unhandled one.

Mirror useRollingConfigUpdate's handling from #1861: load the restart
targets inside a try, end the restart toast with "Could not load the
cluster's instances, so nothing was restarted.", and clear the pending
state in a finally.

Both hooks built their targets the same way (fetch the cluster, check
Fabric Connect, keep RUNNING instances, build a client each, reverse), so
that now lives in one getClusterRestartTargets helper.

Also stop reporting an empty cluster as "Cluster fully restarted!", the
same zero-instance fix #1861 made to the config update.

Closes #1853

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The restart, config-update and apply-licenses loops only looked at their
Cancel flag at the top of each iteration. A Cancel clicked while an
instance's availability check was in flight was therefore ignored for that
instance: the restart, the config write, or the license install went out
anyway. Recheck the flag once the check returns, before the first request
that changes the instance.

Also stop telling the user no instance was RUNNING when every RUNNING
instance failed to restart. Both cluster hooks keyed that message on
nothing having restarted; key it on there being no RUNNING target, so a
cluster whose restarts all failed reads "Only 0 RUNNING instances were
restarted of N".

Found by the pre-push cross-model review of the previous commit; both
predate it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s during the check

Cancellation never unsets, so continuing only re-checked the flag on every remaining target.

Refs #1853

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dawsontoth
dawsontoth marked this pull request as ready for review October 12, 2026 21:19
@dawsontoth
dawsontoth force-pushed the claude/1853-restart-cluster-load-failure branch from 1bbbc43 to 3afc3a9 Compare October 12, 2026 21:19
@dawsontoth
dawsontoth requested a review from a team as a code owner October 12, 2026 21:19
@dawsontoth
dawsontoth merged commit 3afc3a9 into stage Oct 12, 2026
@dawsontoth
dawsontoth deleted the claude/1853-restart-cluster-load-failure branch October 12, 2026 21:19
dawsontoth added a commit that referenced this pull request Oct 13, 2026
…ay what Cancel left

Review round 1 after the port (codex major, kept by the Harper domain
adjudicator): the cluster loop waits on a member whose restart got no
verdict, but when that watch also ran out it discarded the rejection and
restarted the next member. On a two-member cluster, a 504 after the
restart ran plus a member that stays down left no member serving, which
is exactly what the stop rule exists to prevent. The loop now stops
there too, and the report says Studio couldn't confirm that member came
back either, instead of claiming it accepted the restart.

The adjudicator's nit: a Cancel after a no-verdict member now yields the
"Outcome unknown" report, which never said Cancel skipped the rest. Both
loops now name the members Cancel left alone in that report.

Also from the round: drop the test comments that only narrated their
setup, and trim restartInstance's JSDoc to its contract (DESIGN.md
carries the reasoning).

Not taken: Cancel during set_configuration still restarts that member;
that check is #1867's, unchanged here. The watch outliving the progress
toasts' durations is not a problem: sonner never starts an auto-close
timer for a loading toast (sonner 2.0.8, dist/index.mjs:644).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

Restart Cluster stays disabled and its toast spins when the cluster fetch fails

1 participant