feat: upgrade all environments button#2941
Conversation
|
Container images for this PR have been built successfully!
Built from commit d6a6d37 |
Graphite Automations"Warn authors when publishing large PRs" took an action on this PR • (06/14/26)1 teammate was notified to this PR based on Kyle Mendell's automation. |
0a26ce2 to
ee0f34e
Compare
ee0f34e to
d6a6d37
Compare
| async function handleConfirm() { | ||
| phase = 'running'; | ||
| reconnecting = false; | ||
| try { | ||
| job = await systemUpgradeService.triggerUpdateAll(); | ||
| } catch { | ||
| toast.error(m.environments_update_all_trigger_failed()); | ||
| resetState(); | ||
| return; | ||
| } | ||
|
|
||
| if (job && (job.status === 'completed' || job.status === 'failed')) { | ||
| phase = 'finished'; | ||
| return; | ||
| } | ||
|
|
||
| pollActive = true; | ||
| schedulePoll(); |
There was a problem hiding this comment.
handleConfirm races with dismiss: polling starts after dialog is closed
triggerUpdateAll() is awaited without any guard for mid-flight dismissal. If the user presses Escape (or the Close button) while the initial POST is in flight, resetState() sets pollActive = false and phase = 'confirm'. When the await resumes, the code unconditionally sets pollActive = true and calls schedulePoll(), restarting background polling even though the dialog is visually closed. Since the component is never conditionally rendered, onDestroy won't fire until page navigation, so the polls continue indefinitely. Adding an early-exit check like if (phase !== 'running') return; immediately after the await (and after the catch block) closes the window.
Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/routes/(app)/environments/update-all-dialog.svelte
Line: 71-88
Comment:
**`handleConfirm` races with dismiss: polling starts after dialog is closed**
`triggerUpdateAll()` is awaited without any guard for mid-flight dismissal. If the user presses Escape (or the Close button) while the initial POST is in flight, `resetState()` sets `pollActive = false` and `phase = 'confirm'`. When the await resumes, the code unconditionally sets `pollActive = true` and calls `schedulePoll()`, restarting background polling even though the dialog is visually closed. Since the component is never conditionally rendered, `onDestroy` won't fire until page navigation, so the polls continue indefinitely. Adding an early-exit check like `if (phase !== 'running') return;` immediately after the `await` (and after the `catch` block) closes the window.
How can I resolve this? If you propose a fix, please make it concise.
Checklist
mainbranchWhat This PR Implements
Fixes: #2574
Changes Made
Testing Done
AI Tool Used (if applicable)
Additional Context
Disclaimer Greptiles Reviews use AI, make sure to check over its work.
To better help train Greptile on our codebase, if the comment is useful and valid Like the comment, if its not helpful or invalid Dislike
To have Greptile Re-Review the changes, mention
greptileai.Greptile Summary
This PR implements a "Update All" button on the Environments page that triggers a fleet-wide Arcane self-upgrade: the manager upgrades itself first (job persisted as
pending_restart), restarts, and then on the next boot resumes upgrading each remote agent sequentially via a newEnvironmentUpdateJobmodel and migration.StartUpdateAll/ResumeUpdateAllOnStartup/runAgentsPhaseInternalorchestration inSystemUpgradeService, backed by a newenvironment_update_jobsDB table and two API endpoints (POST /environments/{id}/system/upgrade/all,GET .../all/status). Concurrent-call guard viaupdatingAllatomic + DB-level active-job check.update-all-dialog.sveltewith phased UI (confirm → running → finished), live status polling with reconnect handling, and the deletedenvironment-upgrade-service.tsconsolidated intosystem-upgrade-service.ts.Confidence Score: 4/5
Safe to merge after fixing the dialog race condition; backend orchestration is solid.
The backend orchestration, persistence, and concurrency guards are well thought out. The single issue that needs attention is in the dialog: if the user dismisses the dialog (via Escape or the Close button) while the initial triggerUpdateAll() POST is still in flight, handleConfirm resumes after the await and unconditionally sets pollActive = true, restarting background polling against a closed dialog for the lifetime of the page.
frontend/src/routes/(app)/environments/update-all-dialog.svelte — handleConfirm needs a dismissal guard after the async triggerUpdateAll() call.
Comments Outside Diff (1)
frontend/src/routes/(app)/environments/update-all-dialog.svelte, line 1369-1374 (link)$stateupdated inside$effectreset()assigns tophase,job, andreconnecting, all of which are$state. Updating reactive state from inside an$effectis explicitly flagged in the project's Svelte guidelines because it can cause unnecessary re-renders or subtle cycles. The intent here — "reset state when the dialog transitions from closed to open" — is better modelled as a reactive guard inhandleConfirm/handleClose, or by factoring the effect so the side-effectingstopPolling()and the state mutations are kept separate.Rule Used: What: Avoid updating
$stateinside$effectblo... (source)Prompt To Fix With AI
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix All With AI
Reviews (3): Last reviewed commit: "feat: upgrade all environments button" | Re-trigger Greptile