Repository navigation
feat(sandboxd): tenant managed volumes with hibernate and online growth - #369
Closed
minatoaquaMK2 wants to merge 75 commits into
Closed
minatoaquaMK2 wants to merge 75 commits into
minatoaquaMK2 wants to merge 75 commits into
Conversation
Keep tenant data disks across claim release and template replacement. Add node-local management, SDK methods, and durable writer ownership.
Preserve registry checks and native device identities across Linux and Darwin. Use the pinned CI toolchain to verify the full lint matrix.
VerifyMountedVolume compared the requested path with mountinfo literally, so a mount under a symlinked parent read as a bind and hibernate refused a valid claim. It now resolves the target like ObserveVolume and the live mount paths do, and its conflicts name the resolved guest path. The literal-root test now counts guest changes, not the read-only realpath lookup. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…olume run Managed expiry and release of a sleep-capable claim go through releaseManagedSleepLocked, which drops the claim without cancelling its live volume run. The run keeps issuing device calls against a removed sandbox, a catalog ref the run already dropped from the hold keeps its admission charge, and the claim's per-volume diagnostics outlive it. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…opped releaseManagedSleepLocked, used by managed expiry and by release of a sleep-capable claim, dropped the claim without cancelVolumeRun. The run kept calling the device, its diagnostics stayed in volumeErrors, and a catalog ref it had already dropped from the hold never left admission. Cancel it under m.mu like releaseResolved and charge its holds to the teardown. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…al write When a live catalog removal drops the ref from the managed hold in memory but the registry write fails, commitVolumeRun and the dirty catalog path in stageVolumeTarget return before releasing admission, and finishVolumeRun skips managed claims. The background retrier persists the drop, yet the catalog stays busy until restart. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
managedLiveRemove drops refs from the hold in memory before writing the registry, and the retrier persists that drop after a write failure. The callers returned on that error before releasing admission, and finishVolumeRun skips managed claims, so the catalog stayed busy until restart. Both callers now release whatever the hold no longer references, which is nothing when managedLiveRemove refused first. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
A live catalog change on a claim whose growth is unresolved returns ErrFailed, telling the caller its VMM is down and to release it. Release and Hibernate report the same state as 409 volume_grow_busy, and release is exactly what the growth fence refuses. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
beginVolumeRun folded an unresolved growth into the not-claimed hold check and returned ErrFailed, whose message tells the caller to release a VMM that is running and that the growth fence will not let go. It now returns volume_grow_busy like Release and Hibernate. checkVolumeRunLocked keeps ErrFailed: growth admission refuses a claim with an active run, so growth ownership mid-run only comes from a registry fault. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
A second claim of a growing managed volume gets 503 volume_storage_unavailable, an outage to retry, instead of the 409 volume_busy with holders it gets while the owner is running or asleep. lookupManaged rejects every non-ready state before admission can name the holder. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
lookupManaged rejected every non-ready state as a storage outage, so a claim of a volume its owner was growing got 503 with Retry-After. The volume is healthy and held, so answer 409 volume_busy with its holders, the same refusal admission gives while the owner runs or sleeps. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Create, delete, and grow answer an unmapped manager error with 503 volume_storage_unavailable and a "retry" message but no Retry-After, while managedStorageError with the same code sends one, so clients that key retries on the header treat the same outage two ways. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The create, delete, and grow fallbacks for an unmapped manager error wrote 503 volume_storage_unavailable with "retry" text but no Retry-After, unlike managedStorageError with the same code. One helper now writes that fallback with the header. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
A managed volume whose backing was replaced settles as recovery_required, which only an operator clears. PUT and claim still answer 503 volume_storage_unavailable with Retry-After, inviting a retry loop that cannot succeed. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
markRecovery and the recovery_required state answered PUT and claim with volume_storage_unavailable, so the server added Retry-After to a condition only an operator clears. They now return 503 volume_recovery_required, which joins the codes that omit Retry-After, and the API docs list it. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
GET reports a ready volume whose backing identity changed as recovery_required without persisting it, but a claim that arrives before PUT or DELETE marks the volume still gets volume_storage_unavailable with Retry-After for the same operator-only condition. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…claim lookupManaged answered a backing mismatch or missing backing with volume_storage_unavailable, while GET already reports the same observation as recovery_required. Claims now return volume_recovery_required for it too, without persisting the state, like GET. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Client.new and Template.new iterate volumes="work" into four one-letter volumes and send them, while set_volumes and patch_volumes already reject a bare string where a list is expected. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
_claim_body iterated volumes="work" into four one-letter volumes. It now raises TypeError before building the claim, matching how set_volumes and patch_volumes reject a bare string. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
govet's shadow check flags the PutTenant calls in the expiry and teardown tests added earlier on this branch. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ned backing A claim treats any ENOENT from verify as a lost backing, so a moved managed root or disks directory answers the permanent volume_recovery_required. A claim of a sleeping volume's lost backing does the same, though PUT keeps it transient because wake still owns the backing. Both should answer volume_storage_unavailable with a retry hint. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The e2e fake engine has no process identity, so a managed claim can never bind there. A PUT that answers 200 ready after the directory comes back shows the outage left neither a registry fault nor a recovery_required volume behind. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
verify returned the raw ENOENT from a moved managed root or disks directory, which every caller read as a lost backing file. It now returns errBackingMismatch only for a missing or mismatched backing file, so a directory outage stays volume_storage_unavailable and PUT no longer faults the registry over it. lookupManaged also released r.mu around verify and then answered recovery_required without rechecking. It now shares markRecovery's check: the record must be unchanged and not owned by a sleep or a fenced growth, or the answer stays transient. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
A guest mount conflict aborts hibernate preparation and resumes the guest. When that resume fails too, the caller only sees the conflict, which the server maps to 409 "close guest references and retry", while the sandbox is actually stuck aborting and needs sleep recovery. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
prepareManagedSleep joined the preparation error with the raw resume error. With a guest mount conflict first, the server ranks the conflict and answers "close guest references and retry", hiding that the guest never resumed. A failed resume now joins sleepRecoveryError(), as captureRunningManagedSleep already does, so the caller gets 409 managed_sleep_recovery_required. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…races The teardown tests asserted an empty admission entry, but releaseVolumes deletes an entry once its counts drop below one, so a double release looked identical to a correct one. A second read-only holder of the catalog now has to survive each race with readers:1, and a writer stays refused until that reader leaves. Test-only; these pass on the previous head. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The claim section lacked the growing volume's 409 volume_busy and the two managed 503 codes. Hibernate and wake lacked the guest mount 409, the sleep recovery 409, and the 503 below Cocoon v0.7.2, which carries no Retry-After. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
volumes={"name": "work"} iterates the mapping's keys and claims a
volume named "name". A bare mapping should raise TypeError like a bare
string, before any request.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
_claim_body rejected a bare string but iterated a bare mapping's keys,
so volumes={"name": "work"} claimed a volume named "name". It now
raises TypeError for either before sending the claim.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Contributor
Author
|
Pushed follow-ups from a second pass over the cases #339 covered. Each behavior fix is a failing test commit followed by its fix.
Three more questions, besides the one in the description:
|
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.
Why
Issue #300 asks for a tenant's data disk that outlives a claim, has one writer at a time, and survives hibernate. This PR replaces #368 with our current version. A tenant can create a disk on a node, attach it to a warm claim, keep it through release and restarts, hibernate and wake with it mounted, and grow it while the VM runs.
What changed
managed_volume_dirrecords each disk (owner, size, file identity) and which VM holds it (boot ID, PID, start time). A hold is freed only after the VM and its process are gone, so a crash or a slowvm rmcannot hand the disk to a second writer.PUT/GET/DELETE /v1/volumes/{name}andPOST /v1/volumes/{name}/grow, with Go and Python SDK methods. Hibernate of a claim with one mounted writable managed disk runs a guest hook to stop writers, unmounts, detaches and captures. Wake does the reverse.Scope
cocoon vm disk growandcocoon vm disk inspect --disk-id, which cocoon does not ship yet. On current cocoon, growth answers 503volume_grow_unsupported. We can split growth into its own PR until cocoon has them.Question for maintainers
After a restart, if one leftover VM (no claim, only catalog volumes attached) cannot be confirmed removed, startup marks managed storage uncertain. Every managed volume call then answers 503, and every claim that holds a volume is marked failed, until the next restart. Nothing retries that VM's removal, so its catalog volumes stay held even after it is gone.
We lean to B. Which do you prefer?
Tradeoffs
registry.json, besideclaims.json, so a hibernated claim keeps its writer lock and a crash mid-attach cannot free a disk early.claims.jsonkeeps its flat format. We can move holds intoclaims.jsonif you prefer that.Blast Radius
Nodes without
managed_volume_dirkeep their behavior, with two shared fixes. A guest mount conflict or a too-old cocoon on hibernate or wake now answers 409 or 503 instead of 500. The Python SDK now rejects a bare string or dict as a claim's volume list.Verification
go test -racefor sandboxd and e2e pass, except two tests that also fail on main when run as root. golangci-lint passes on linux and darwin, and ruff, pytest and mypy pass.