Repository navigation
Finish the destroy work where a Cloud machine is first found gone - #15359
Conversation
|
Warning Review limit reachedNext included review available in 3 minutes. View limit detailsLimit details: You’ve used all 10 included reviews currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Repository: manaflow-ai/cmux/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (7)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
All contributors have signed the CLA ✍️ ✅ |
CI failure attributionCI passes on Written by |
|
Cross-model review (Codex gpt-5.6-sol)
|
A status read and an access preflight both move the row to `destroyed` themselves when the provider no longer has the machine. Once either does, `destroyVm` can never see the row again (its lookup skips destroyed rows) and the provider-status cron skips it too, so the model-plane revoke and the `vm.destroyed` usage event that the cron performs for the same transition have to happen at these write sites as well. Fails today: both retire the row and record nothing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Three places retire a Cloud machine's row after the provider says it no longer has the machine: the status read, an access operation's resume preflight, and the stats read. Each wrote `status = destroyed` and stopped there, while the reconcile cron doing the same transition also revoked the machine's model-plane tokens and recorded a `vm.destroyed` ledger event. That difference was permanent, not a race to lose. `destroyed` is terminal: `findUserVm` hides such a row from every destroy request and `reconciliationCandidates` drops it from the cron, so whichever of the three got there first left a machine that never appears as destroyed in the ledger and never has its route tokens marked revoked, with nothing able to finish the job afterwards. All three now go through one `applyObservedProviderStatus` that performs the write and, when the write lands on `destroyed`, the same revoke and ledger event as the cron. The status route hands `getVm` the model-plane revoker it already builds for delete. The new `provider_status_*` destroy reasons join the analytics allowlist so the event says which read noticed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review found that the previous commit took the row's new status from each caller. That let the access preflight and the stats read hardcode "destroyed" for a provider 404, including for a machine with a persistent home volume, which observedDbStatus maps to "paused" because the compute is gone and the machine is not. Those rows were terminalized and billed a vm.destroyed that never happened, and nothing revisits a terminal row to take either back. The status is now derived inside applyObservedProviderStatus, so every entrypoint agrees about what a 404 means. reopenBaseIfProviderDeleted is the one caller that must override it, and passes forceStatus with the reason: its row is a Base's active generation, and leaving it paused would hand the same dead provider id back on every later open. Also threads the model-plane revoker through getVmStats from its route, covers the stats entrypoint including the home-volume case, and fixes the two test-file regressions the previous commit shipped. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
758ef73 to
c7da06f
Compare
|
Review: a review subagent went over Fixed:
Left:
Root cause of the two regressions, stated plainly because it is the useful part: the previous revision's validation was one test file. This one ran the whole suite, twice, with a baseline to compare against. |
|
Second review round, against Correction to my previous comment on this PR. Its "Left" section said the access preflight threads no revoker because the credentials it would mark are "already inert ... which is exactly the state this write puts it in". That is false, and it is false precisely for the case this PR introduces. Review: blocking (1)
Fixed
Left, disclosed in the description rather than fixed
Verified at The four mechanical questions I asked the reviewer all checked out: no wrong row status at any of the five call sites of the shared helper, |
|
Subagent review at c7da06f (second round): request changes, one blocking item (a code comment justified omitting the revoker with a false claim). Addressed in 38ee5a0, with the paused token behavior disclosed in the description. First round (758ef73) blockers B1 to B3 and S1 to S3 were addressed in c7da06f. 38ee5a0 is the fix for that review (comment and type changes plus one test tweak), so no re-review. Landing: rewriting the author of 38ee5a0 to the linked identity so CLA Assistant can pass, merging main after #15414, then auto-merge. |
The access preflight carried a comment claiming a revoke was pointless there because the credentials were already inert. That is false for the case this branch introduces: authenticateRouteToken and authenticateVmAuthorization both accept `paused`, so route tokens on a volume-backed machine stay valid after this write, for the rest of their 30-day lifetime. Keep the behavior, which is what getVm and the reconcile cron already do for the same observation, and replace the comment with what is true. Also drop the stale "next fleet refresh drops it" line, correct "seven call sites" to eight, stop asserting a resurrection path that is not implemented, and type usageEventSource as VmDestroySource so an unknown source cannot silently degrade in PostHog. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
38ee5a0 to
d02fb73
Compare
|
recheck |
|
Merge receipt for |
762c3ed Recover a Cloud machine graph stuck on an equal-cursor conflict (manaflow-ai#15328) 524ebff ci: replay the fuzz regressions on sidebar, split and window changes (manaflow-ai#15412) 818d475 Let a user's Cloud open dial even right after a background link failure (manaflow-ai#15291) 97491a7 Let the Cloud toolbar name the machine-list failure it has (manaflow-ai#15236) 0abac32 PR media: adopt CI's build only, start when CI completes, run for every app PR (manaflow-ai#15418) 7f08715 ci(seed): keep the trusted seed on the Mac before the R2 upload (manaflow-ai#15411) 5663c13 Finish the destroy work where a Cloud machine is first found gone (manaflow-ai#15359) # Conflicts: # .github/workflows/ci.yml # .github/workflows/pr-media.yml # .github/workflows/seed-derived-data.yml # .github/workflows/test-e2e.yml
Three code paths retire a Cloud machine's row when the provider reports it no longer has the machine: the status read (
getVm, behindGET /api/vm/[id]), the resume preflight that every access operation runs (attach, exec, ssh, open-port, fork, resize), and the stats read. Each one wrote the row's new status and stopped there. The reconcile cron performs the same transition and also revokes the machine's model-plane route tokens and records avm.destroyedledger event.That gap could not be repaired later.
destroyedis terminal:findUserVmfilters destroyed rows out, so aDELETE /api/vm/[id]for that machine answers 404 without runningdestroyVm's revoke or its ledger write, andreconciliationCandidatesalso filters them out, so the cron never looks at the row again. Whichever of the three paths noticed the machine first left a machine that is absent from the destroy ledger for good, with its route-token rows still marked unrevoked.After this change all three go through one
applyObservedProviderStatus, which performs the status write and, when that write lands ondestroyed, does the same revoke and ledger event the cron does. The status and stats routes hand their workflows the model-plane revoker they already build forDELETE. Theprovider_status_read,provider_status_accessandprovider_status_statsreasons join the analytics allowlist, so the event says which read noticed instead of degrading tounknown.A provider 404 does not always mean destroyed
The helper derives the row's new status itself, from
observedDbStatus, rather than accepting one from the caller. That is the part worth reading closely, because getting it wrong is worse than the bug this PR set out to fix.A 404 from the provider on a machine with a persistent home volume means the compute is gone and the volume is not.
observedDbStatus, whichgetVmand the reconcile cron have always used, maps that case topausedrather than to a terminal status. The preflight and stats paths used to hardcodedestroyed, disagreeing with both of them, which terminalized such a row and would have billed avm.destroyedfor a machine the provider never destroyed. Nothing revisits a terminal row, so neither could be taken back. An earlier revision of this PR kept that hardcoding and added the ledger write on top of it, which would have made the damage permanent and billable instead of merely permanent; the review caught it.reopenBaseIfProviderDeletedis the one caller that overrides the mapping, through an explicitforceStatus, and the reason is in a comment at the call site: its row is a Base's active generation, andbeginBaseOpenonly allocates a replacement once this row has stopped being a machine the Base could open. Leaving itpausedwould hand the same dead provider id back on every later open, forever. The home volume is not lost by that:beginBaseOpenretains the old generation rather than deleting it, which is where a normal reset leaves it too.What this does and does not fix
The ledger consequence is the substantive one.
vm.destroyedis whatcloud_vm_destroyedproduct analytics is built from, including machine lifetime, so machines whose disappearance was noticed by a read rather than the cron were missing from it entirely.The credential side is narrower than it looks, and worth stating plainly rather than overselling: a route token for a row that reached
destroyedwas already unusable.authenticateVmAuthorizationjoins the token tocloud_vmsand requiresstatus in ('provisioning','running','paused'), and no write can move a row out ofdestroyed, so the missing revoke left an inaccuraterevoked_at IS NULLrow rather than a working credential. The revoke is now accurate, and it still matters for the interval before the row goes terminal. No billing or money path depends on the missing event; billing stop keys on the row's ownstatusanddestroyed_at, which these paths did already write.Two consequences of routing the preflight through the shared mapping are worth naming, because both are behavior changes and neither is an improvement on its own.
Route tokens now outlive the observation on a volume-backed machine.
authenticateRouteTokenandauthenticateVmAuthorizationacceptprovisioning,runningandpaused, so a row that lands onpausedkeeps usable route tokens for the rest of their 30-day lifetime. The old hardcodeddestroyedat this one call site killed them immediately. The preflight still threads no revoker, deliberately: revoking here would put one of its eight call sites at odds withgetVmand the cron, which leave tokens alive for exactly the same observation, and a sleeping machine is supposed to keep its tokens. A volume-backed row that must lose its tokens has to be destroyed, by the user or by account deletion, both of which revoke.The preflight was also the last automatic path that retired a volume-backed row whose sandbox the provider had removed. Such rows now stay
paused: the cron'sreconciliationCandidatessees the observed status already matches and reportsunchanged, and they keep counting against a Go plan's saved-machine allowance. A user can still delete them. I left this as is because the alternative is to keep one entrypoint disagreeing with the other two about what a 404 means, which is the bug at the top of this description; the durable fix is a recovery or retirement path for volume-backed rows whose compute is gone, which is larger than this PR.Validation
Run from
web/, against a real PostgreSQL 16 with the repo's migrations applied, so theCMUX_DB_TEST=1database cases execute rather than skip.Red, at the tests-only commit
ecaf0697f14,bun test tests/vm-workflows.test.ts:Red again for the home-volume defect the review found, with the tests of the final commit against the code of the one before it (
fa6b69c58cf),bun test tests/vm-stats-not-found.test.ts:Green at the head of this PR:
bun test tests/vm-workflows.test.tsis 148 pass, 0 fail across 148 tests, with the database cases executing;bun test tests/vm-stats-not-found.test.tsis 8 pass, 0 fail.Whole-suite, because the previous revision of this PR shipped two regressions in files I had not run.
bun testover all ofweb/at this branch is 3695 pass, 304 fail, 4000 tests across 370 files. At the merge base it is 3679 pass, 305 fail, 3985 tests across 369 files. Those ~300 failures are an artifact of running 370 files in parallel against one database, where thedbTestfiles truncate shared tables out from under each other; the number is what matters and the comparison is the point. Diffing the two runs by failing test name, no test fails on this branch that does not already fail at the merge base.Also from
web/:bun x tsc --noEmitclean;bun run lint:complexitypasses with the baseline untouched at 42 grandfathered findings and no new entry.No fleet dogfood. cmux#8029 disabled Vercel branch previews while keeping
maindeployments, so an unmerged change to this app has no preview URL an app build could talk to, and a fleet build would exercisemainrather than this branch. The database-backed runs above are the substitute.Changelog
Fixed: a Cloud machine the provider had already removed could be recorded as gone without its destroy being logged or its model-plane tokens marked revoked, and a sleeping machine with a saved home directory could be wrongly recorded as destroyed.
🤖 Generated with Claude Code