fix: delete paused sandboxes transparently#940
Conversation
d914180 to
b022f47
Compare
PR #940 — Transparent deletion of paused sandboxesOverviewThis PR implements automatic resume-before-delete for paused sandboxes across three service layers (Cubelet → CubeMaster → CubeAPI), with appropriate error propagation, Key Strengths
Actionable Suggestions1. Extract shared response-reserve helper ( The 2. Add explanatory comment for error code conflation (
3. Move
Minor Observations
No Critical Issues Found
|
b022f47 to
485a736
Compare
|
Good work. I noticed the automatic resume timeout is 5 seconds. If we support cross-node pause resume in the future, the resume time might be longer (due to retrieving snapshots from remote shared storage). Could these timeouts be made more configurable? |
Thanks, that makes sense. The current 5-second resume timeout is actually part of the existing 30-second synchronous DELETE budget, which also has to cover the RPC call and cleanup. Exposing it as an independent setting could therefore create inconsistent deadlines or leave too little time for cleanup. Making this configuration effective would require revisiting the timeout budget across the full DELETE path. That would involve the API timeout, the CubeMaster-to-Cubelet RPC deadline, the Cubelet cleanup budget, and the corresponding configuration and tests. Since this PR is focused on fixing deletion of paused sandboxes within the existing synchronous DELETE behavior, I think adding an end-to-end configurable timeout model would broaden its scope considerably. I’d prefer to keep the current conservative budget here and track the timeout configuration work as a follow-up for cross-node pause/resume scenario. |
| deleteAutoResumeMaxDuration = 5 * time.Second | ||
| deleteDestroyReserve = 20 * time.Second | ||
| deleteResponseReserve = 5 * time.Second | ||
| deleteLifecycleLockMaxWait = 2 * time.Second |
There was a problem hiding this comment.
The 5/20/5-second split ( deleteAutoResumeMaxDuration, deleteDestroyReserve, deleteResponseReserve) in delete_resume.go is hardcoded in absolute seconds, but it implicitly assumes the caller's ctx deadline is ~30s (i.e. CubeMaster's common_timeout_insec, currently defaulting to 30s). These two configs live in different services and can drift independently — if common_timeout_insec is ever tuned down (e.g. to 10s) without updating these constants, latestResumeDeadline will always fail the !latestResumeDeadline.After(now) check, and every delete of a paused sandbox will short-circuit to the "not enough time" error path, not just occasionally but permanently.
Suggest deriving the resume budget
proportionally from the actual remaining time on ctx ( deadline, _ := ctx.Deadline(); remaining := time.Until(deadline)) instead of subtracting fixed absolute reserves. e.g. reserve a percentage of the remaining budget for destroy/response rather than a fixed number of seconds, so this stays correct regardless of what common_timeout_insec / destroy_dead_line are configured to. At minimum this coupling should be documented so operators don't hit a silent, hard-to-diagnose regression when tuning timeouts.
| sb, ctx, getErr = s.loadDestroySandboxRuntime(destroyCtx, req.SandboxID) | ||
| ctx = log.WithLogger(ctx, stepLog) | ||
|
|
||
| needsPausedPreflight := getErr == nil && constants.IsCubeRuntime(ctx) && |
There was a problem hiding this comment.
needsPausedPreflight is computed from a status read taken before acquiring sandboxLifecycleLocks. If the sandbox is observed as RUNNING at that point (so needsPausedPreflight == false), the request skips the lock
entirely and falls straight through to engine.Destroy(...) unprotected. If a concurrent Update(pause) wins sandboxLifecycleLocks and pauses the sandbox in the gap between that read and the actual destroy call, this Destroy request ends up doing a plain destroy against what is now a paused sandbox — the exact scenario this PR is meant to guard against — and it does so without holding the lock at all.
This isn't a regression introduced by this PR (the old code read status unlocked too), but since the PR's stated goal is "protect destroy from racing with pause/resume/commit/rollback via the lock," it's worth calling out that the guarantee is currently best-effort rather than fully closed: the decision to take the lock is itself made outside the lock. Would it be worth acquiring sandboxLifecycleLocks
unconditionally on entry (before reading status) and deciding the resume-vs-plain-destroy branch under the lock, rather than deciding whether to lock based on a stale read? If the current scope is intentionally narrower than that, a code comment noting the residual race would help future maintainers avoid assuming this is fully serialized.
|
Thanks for the PR! Please address the CI failures and the review comments above. Also, could you add more comprehensive e2e test cases (covering concurrency, PAUSING state, and resume timeouts)? |
485a736 to
2706a41
Compare
2706a41 to
c768df4
Compare
Allow DELETE to remove a stable paused sandbox without requiring the user to resume it first. Cubelet performs a bounded internal resume under the per-sandbox lifecycle lock. Before entering the existing destroy workflow, it persists a confirmed RUNNING state so durable metadata cannot remain paused after a successful runtime resume. CubeShim keeps its raw paused-delete guard unchanged. Preserve typed Cubelet failures through CubeMaster and map paused-delete capacity and retryable lifecycle failures in CubeAPI, including Retry-After responses. Add focused tests, structured deleteAutoResume logs, OpenAPI updates, and English/Chinese lifecycle documentation for the paused-delete contract. Signed-off-by: zhengyilei <zheng_yilei@qq.com>
c768df4 to
212fef6
Compare
@ls-ggg Thanks for the detailed review. Both inline issues have been addressed in the latest commit:
Additional focused coverage now includes task-loading failures, reconciliation failures, unexpected shim states, and the fallback for empty CubeMaster error messages. The duplicated resume-task test fake has also been consolidated. For validation, Cubelet was rebuilt and paused DELETE was verified against a real server. For concurrency validation, I ran five real-server trials with Pause and DELETE issued concurrently. In two trials, DELETE reached Cubelet while Pause still held the lifecycle lock and returned The real-server validation covers normal paused deletion and concurrent Pause/DELETE behavior. The Stable full-stack E2E coverage for those two fault paths ( The previous failed claude-response CI check was unrelated to this PR’s code or tests. It is the known workflow permission issue described in #844: normal review or comment activity can trigger the workflow, which then fails the actor permission check. All 10 PR checks were passing before this comment, as shown below.
|

Motivation
Deleting a paused sandbox currently fails because the runtime rejects the normal delete operation while the VM is paused. Users must manually resume the sandbox before deleting it.
This PR makes
DELETE /sandboxes/{sandboxID}work for a stable paused CubeBox. The sandbox is resumed as part of the delete operation and then removed through the existing destroy workflow. DELETE remains synchronous: success is returned only after cleanup completes.Design
CubeMaster remains the external DELETE entry point and continues to coordinate both synchronous and asynchronous destroy requests. The atomic resume-before-destroy transition is implemented in Cubelet's
Destroypath.Cubelet owns the authoritative runtime state, containerd task, and the per-sandbox lifecycle lock shared by pause, resume, rollback, and commit. Only paused or pausing Cube runtime deletes enter this bounded gate, so running deletes retain their existing direct path. Keeping the transition in Cubelet keeps state inspection, resume, reconciliation, and normal cleanup together.
This supersedes the API-only paused-delete workaround in #831. That workaround translated a CubeMaster internal error into
409 Conflictafter an extra sandbox-status lookup. This PR removes that fallback: DELETE now performs the bounded wake-up itself and returns typed capacity or retryable lifecycle errors only when the actual wake-up cannot proceed.Behavior
PAUSEDsandbox is resumed internally, then follows the normal destroy workflow.RUNNINGsandbox keeps the existing delete path and does not resume.RUNNING, DELETE continues. Otherwise the sandbox remains retryable.sandbox.resumedevent, reset idle timeout, or create a background retry.Error Semantics
DELETE for running sandbox and existing
404behavior are unchanged. For the paused-delete path:409 Conflict.PAUSINGor another lifecycle operation in progress returns503withRetry-After: 2.503withRetry-After: 5.Retry-After: 5follows CubeProxy's existing auto-resume failure backoff, whileRetry-After: 2follows its PAUSING gate. These headers tell the client when to retry; DELETE is not continued or retried in the background.Scope
This PR does not add a public DELETE parameter or protobuf field, bypass resume admission, change the existing CubeShim delete guard, alter ordinary running DELETE behavior, or add a new metric.
Validation
Focused Linux builder checks passed:
The full CubeAPI suite completed with
88 passed; 0 failed. The focused HTTP route test verifies CubeMaster130409-> HTTP409, alongside the503andRetry-Aftercases.Real-server validation built and atomically deployed the final Cubelet, CubeMaster, and CubeAPI binaries. All three services remained active and both health endpoints returned success.
Live E2E validation created a temporary sandbox, paused it, and deleted it directly without an explicit
resumeorconnectrequest:204, and GET reportedpaused;204 No Contentin629 ms;404;References
Fixes #219.
Replaces the API-only paused-delete workaround from #831.