fix(vmm): restore-path resilience — configurable timeout, per-VM partial-failure reporting, pre-restore orphan detection - #302
Conversation
Fix summaryFollow-up commit Correctness / dead code:
Misleading comments:
Lock scoping / contention:
Env configurability:
The prewarm (Phase 3) and memfd (Phase 1.5) paths retain their pre-existing early-bail behavior — they are host-resource failures rather than per-VM failures, and converting them is outside this PR's scope. |
WaylandYang
left a comment
There was a problem hiding this comment.
Thank you for improving error reporting and making the socket wait configurable; the structured per-child failures and lock scoping are useful changes. The implementation still does not provide the resilience claimed by this PR or fully close #301.
The socket waits are performed serially with a full timeout per child, so total delay can grow to n × timeout instead of a single batch deadline. On any spawn/socket/restore failure, all successfully restored siblings are still dropped, so the change reports partial failures but does not preserve partial success. Orphan detection is logging-only and neither cleans up nor gates a restore that is known likely to collide. Controller restart/reconciliation remains unchanged as well.
Please use a concurrent/shared-deadline wait, define and implement an explicit partial-success contract (or narrow the PR claim), and make orphan/restart handling actionable and race-safe before treating #301 as resolved. Add tests for multiple slow/mixed-success children and restart with a real orphaned Firecracker. Thanks for the contribution—the pieces here are useful, but the current behavior still matches the failure modes the PR says it removes.
e34bbe4 to
669eb6c
Compare
WaylandYang
left a comment
There was a problem hiding this comment.
Thank you for adding concurrent socket waits, structured partial success, and an orphan metric. These are useful improvements, but the current head still does not satisfy the PR's resilience claim or #301's acceptance criteria, and it conflicts with main with no CI.
Blocking issues:
- Orphan handling remains observation-only. Incrementing a metric does not reap or gate on an untracked Firecracker, so a restore known to be at risk still proceeds. Rebase with the startup recovery work where applicable, implement a safe cleanup/gating path for truly untracked runtime orphans, or narrow this PR and keep #301 open.
ForkResult.childrencompacts successful VMs while only failures retainchild_index; callers cannot reliably map each surviving VM back to its requested child/resource slot. Return an indexed success type (for example{ child_index, vm }) or an equivalent explicit mapping.- The controller still drops every successful sibling when any non-retryable partial failure occurs, contradicting the claim that one child no longer dooms the batch. Either expose/retain partial success through the controller API or explicitly define the controller as all-or-nothing and narrow the PR/issue-closing claim.
- The integration test uses
n = 2withper_child_netns = false, a shared-TAP/shared-IP topology that #281/#300 reject and that can fail independently with EBUSY. It also pre-creates a directory, which satisfies the currentPath::exists()socket wait and shifts the error to restore. Use a valid per-child-netns setup or an injected spawn/socket failure and assert the exact intended phase and original child mapping.
Please resolve the main conflicts, address these contract/test issues, keep #301 open until its restart/orphan criteria are actually complete, and add the README-required DCO sign-offs to every resulting commit. Thanks for the contribution; the partial-result machinery is promising, but its public semantics need to be unambiguous and tested in a valid topology.
55d3879 to
8e5aea8
Compare
…t topology, docs Address the four blockers from the latest review round on deeplethe#302: 1. ForkResult.children is now Vec<ForkChild> ({ child_index, vm }) so surviving VMs retain their 1-based within-batch index. Callers can map a surviving VM back to its resource slot (netns/cgroup at netns_offset + child_index). All callers updated. 2. The integration test now uses per_child_netns=true (valid multi-child topology) instead of n=2 + per_child_netns=false (invalid shared-TAP rejected by deeplethe#300). Failure injection changed from pre-creating a directory at child-2.sock to deliberately not provisioning child 2's netns — a clean Spawn-phase failure. wait_for_sock now bails when the socket path is a directory (not a socket). 3. Controller partial-success handler documented as intentionally all-or-nothing: a sandbox requires all N children, so partial success is NOT exposed through the API. The per-child failure reporting is used for diagnostics and retry classification only. Issue deeplethe#301 remains open for the broader partial-success-through-controller work. 4. Orphan detection documented as a point-in-time diagnostic aid, not a gate — issue deeplethe#301 stays open for lifecycle-level orphan reap/gate. Signed-off-by: jrimmer <jason@rimmer.net>
…t topology, docs Address the four blockers from the latest review round on deeplethe#302: 1. ForkResult.children is now Vec<ForkChild> ({ child_index, vm }) so surviving VMs retain their 1-based within-batch index. Callers can map a surviving VM back to its resource slot (netns/cgroup at netns_offset + child_index). All callers updated. 2. The integration test now uses per_child_netns=true (valid multi-child topology) instead of n=2 + per_child_netns=false (invalid shared-TAP rejected by deeplethe#300). Failure injection changed from pre-creating a directory at child-2.sock to deliberately not provisioning child 2's netns — a clean Spawn-phase failure. wait_for_sock now bails when the socket path is a directory (not a socket). 3. Controller partial-success handler documented as intentionally all-or-nothing: a sandbox requires all N children, so partial success is NOT exposed through the API. The per-child failure reporting is used for diagnostics and retry classification only. Issue deeplethe#301 remains open for the broader partial-success-through-controller work. 4. Orphan detection documented as a point-in-time diagnostic aid, not a gate — issue deeplethe#301 stays open for lifecycle-level orphan reap/gate. Signed-off-by: jrimmer <jason@rimmer.net>
8e5aea8 to
b41c67a
Compare
…t topology, docs Address the four blockers from the latest review round on deeplethe#302: 1. ForkResult.children is now Vec<ForkChild> ({ child_index, vm }) so surviving VMs retain their 1-based within-batch index. Callers can map a surviving VM back to its resource slot (netns/cgroup at netns_offset + child_index). All callers updated. 2. The integration test now uses per_child_netns=true (valid multi-child topology) instead of n=2 + per_child_netns=false (invalid shared-TAP rejected by deeplethe#300). Failure injection changed from pre-creating a directory at child-2.sock to deliberately not provisioning child 2's netns — a clean Spawn-phase failure. wait_for_sock now bails when the socket path is a directory (not a socket). 3. Controller partial-success handler documented as intentionally all-or-nothing: a sandbox requires all N children, so partial success is NOT exposed through the API. The per-child failure reporting is used for diagnostics and retry classification only. Issue deeplethe#301 remains open for the broader partial-success-through-controller work. 4. Orphan detection documented as a point-in-time diagnostic aid, not a gate — issue deeplethe#301 stays open for lifecycle-level orphan reap/gate. Signed-off-by: jrimmer <jason@rimmer.net>
b41c67a to
baf02d7
Compare
…t topology, docs Address the four blockers from the latest review round on deeplethe#302: 1. ForkResult.children is now Vec<ForkChild> ({ child_index, vm }) so surviving VMs retain their 1-based within-batch index. Callers can map a surviving VM back to its resource slot (netns/cgroup at netns_offset + child_index). All callers updated. 2. The integration test now uses per_child_netns=true (valid multi-child topology) instead of n=2 + per_child_netns=false (invalid shared-TAP rejected by deeplethe#300). Failure injection changed from pre-creating a directory at child-2.sock to deliberately not provisioning child 2's netns — a clean Spawn-phase failure. wait_for_sock now bails when the socket path is a directory (not a socket). 3. Controller partial-success handler documented as intentionally all-or-nothing: a sandbox requires all N children, so partial success is NOT exposed through the API. The per-child failure reporting is used for diagnostics and retry classification only. Issue deeplethe#301 remains open for the broader partial-success-through-controller work. 4. Orphan detection documented as a point-in-time diagnostic aid, not a gate — issue deeplethe#301 stays open for lifecycle-level orphan reap/gate. Signed-off-by: jrimmer <jason@rimmer.net>
baf02d7 to
f2751b9
Compare
WaylandYang
left a comment
There was a problem hiding this comment.
Re-reviewed the narrowed restore-path scope at the current head. Successful children retain their original child_index, the partial-failure integration setup now uses a valid per-child-netns topology, socket waits are concurrent, all callers handle ForkChild correctly, and the controller explicitly preserves all-or-nothing semantics while keeping issue #301 open. CI is green and I found no remaining blocker. Approved. Non-blocking follow-up: classify a partial batch as retryable only when all failures are busy, so a mixed permanent + busy result does not trigger avoidable whole-batch retries.
|
#281 has now merged after a fresh latest-main CI run. I rechecked this branch against current main and #302 now has real content conflicts in Please rebase onto current main and preserve both sides of the controller integration:
In particular, please do not resolve the tuple/AppState conflicts by dropping either the TAP ownership fields or the orphan metric. Rerun the full CI suite and retain DCO sign-offs on every contributor commit. I did not find a new blocker in the VMM portion; this is an integration rebase required by the merge order, and I will re-review promptly once the rebased head is green. |
|
Repository branch flow has moved to |
f2751b9 to
f4119ec
Compare
|
Thanks for the rebase guidance — #302 is rebased onto Both sides preserved #281's shared-TAP lifecycle (kept in
#302's restore-path resilience (kept):
Conflict resolution detail ( All commits DCO-signed. The branch is now on |
WaylandYang
left a comment
There was a problem hiding this comment.
Thanks for resolving the #281 rebase and preserving both sides. The current head is not reviewable for approval yet because the required rust job fails under -D warnings: last_err is declared at crates/forkd-controller/src/http.rs:1398 and assigned at line 1439, but never read. Please either remove the dead tracking or, preferably, include the final restore error in the exhausted-retry response/log so the variable serves its diagnostic purpose. Push the fix and rerun CI; I will re-review the updated head.
Squash of 10 commits on fix/restore-path-resilience for rebasing onto current dev (which now contains deeplethe#281's shared-TAP lifecycle). Original commits: - 858f63b fix(vmm): restore-path resilience for issue deeplethe#301 - ce3d800 fix(vmm): apply review fixes for deeplethe#301 - 0d73362 fix(vmm): make DEFAULT_SOCKET_WAIT_SECS pub for cross-crate use - e820848 fix(controller): use Vm::pid() accessor, not private field - 1ddf40c fix(controller): drop needless borrow in workspace orphan scan call - ca52820 fix(vmm): partial-success contract + concurrent socket wait for restore_many_with - 9e863de fix(controller): expose orphan-firecracker detection as a metric - 2aca840 test(vmm): integration test for partial-success restore - 9342e7a test(vmm): accept Restore or SocketWait phase in partial-success test - f2751b9 fix(vmm): review fixes for deeplethe#301 — indexed success, valid test topology, docs Signed-off-by: jrimmer <jason@rimmer.net>
f4119ec to
1010559
Compare
|
Rebased onto current What was addressed
Behavior preserved:
RebaseRebased onto
All commits DCO-signed. The rebased head is |
Signed-off-by: jrimmer <jason@rimmer.net>
Summary
Partially addresses #301. Three changes to stop a single child failure from dooming the whole restore batch, and to surface contention from orphaned firecracker processes before a restore is attempted.
Issue #301 remains open — the orphan-reap/gate and graceful-reconcile criteria are not yet satisfied. This PR narrows to the restore-path resilience subset (criteria #2 and #4) and documents the remaining gaps.
Problem
Intermittent failures trace back to the forkd controller's snapshot restore path:
wait_for_socktimeout was hardcoded at 10s — under load it's too tight.restore_many_withbailed on the first child failure, silently dropping already-spawned siblings.Changes
1. Configurable per-child socket wait timeout (
ForkOptsfield)ForkOpts::socket_wait_timeout_secs(default 10s, matching the historical budget) drives thewait_for_sockcalls inrestore_many_with.create_sandbox,spawn_one_for_workspace) default to 30s, configurable viaFORKD_SOCKET_WAIT_TIMEOUT_SECSenv var.wait_for_socknow bails when the socket path is a directory (not a Unix socket) — previouslyPath::exists()returned true for a directory, masking the failure.2. Per-VM partial-failure reporting (
RestoreError/RestoreFailure/ForkChild)RestorePhase(enum),RestoreFailure(struct),RestoreError(struct withfailures: Vec<RestoreFailure>),ForkChild(struct withchild_index+vm), implementingDisplay + std::error::Errorwhere applicable.ForkResult.childrenis nowVec<ForkChild>— each surviving child carries its 1-based within-batchchild_indexso callers can map a surviving VM back to its resource slot (netns/cgroup atnetns_offset + child_index). This preserves the child-to-VM mapping that a plainVec<Vm>would lose when failures are filtered out.restore_many_withnow collects per-child results in each phase instead of bailing on the first failure:RestoreErrorcarrying every failure is returned.RestoreErroris returned. Concurrent socket wait: one thread per child, all with the SAME timeout so the batch is bounded by one deadline, not n × timeout.RestoreErrornames every failed child (index, phase, error, pid when known) so callers can report which specific child failed rather than a single bail that loses batch context.3. Pre-restore orphan detection (
scan_firecracker_orphans)OrphanProcessstruct +scan_firecracker_orphans(known_pids)function scans/procfor running firecracker processes whose PIDs are NOT in the caller'slive_vmsset./proc/<pid>/comm(exact "firecracker" match),/proc/<pid>/cmdline,/proc/<pid>/stat(elapsed time).create_sandboxandspawn_one_for_workspacerestores via awarn_orphan_firecrackershelper that scopes thelive_vmslock to just PID collection, dropping it before the/procscan so the scan doesn't serialize concurrent sandbox operations.forkd_orphan_firecrackers_detected_totalmetric so operators can alert on the precursor.Controller-level all-or-nothing semantics
The controller's
create_sandboxhandler is intentionally all-or-nothing: a sandbox requires all N children, so whenrestore_many_withreturns partial success (some children restored, some failed), the controller drops the surviving children (kills their firecracker processes) and either retries the whole batch (if the failure is a transient "busy" condition) or returns a structured error. The per-child failure reporting fromrestore_many_withis used at the controller level for diagnostics and retry classification only — partial success is NOT exposed through the API.This is a deliberate design choice, not a limitation to be fixed later: a half-populated sandbox is worse than no sandbox (it would hold resources but be unable to serve requests). Issue #301 tracks the broader partial-success-through-controller discussion; this PR does not close it.
Known limitations (issue #301 remains open)
Orphan detection is diagnostic-only: the pre-restore scan logs and counts orphans but does not reap or gate on them. A lifecycle-level orphan reap/gate path (kill or block on untracked firecracker processes before restore) is tracked in Controller restore-path resilience: stop mass-pruning sandboxes on restart and restore failures #301.
Graceful reconcile is tracked separately: the controller restart mass-prune issue (Controller restore-path resilience: stop mass-pruning sandboxes on restart and restore failures #301 criterion KSM directed hints for fork families #5) is addressed by fix(controller): kill orphaned Firecracker processes on startup #299 (startup orphan kill) and Close PID-reuse race in orphan-Firecracker termination (pidfd + durable identity) #304 (pidfd durable identity), not this PR.
Controller restart does not reattach surviving VMs: Controller restore-path resilience: stop mass-pruning sandboxes on restart and restore failures #301 criterion KSM directed hints for fork families #5 (a controller restart should not mass-prune alive VMs) requires reconcile() changes that distinguish "VM died" from "controller crashed with surviving VMs". This is out of scope for this PR.
Tests
restore_error_display_lists_all_failures— Display lists every failure with phase, pid, and error.restore_error_single_failure_display— single Spawn-phase failure (no pid).restore_phase_eq_distinguishes_phases— phase enum equality.scan_firecracker_orphans_non_linux_returns_empty— stub contract on non-Linux.scan_firecracker_orphans_empty_known_set_non_linux— empty known set contract.orphan_process_struct_shape— OrphanProcess field stability.restore_many_partial_spawn_failure_keeps_siblings(ignored, requires Linux + KVM + root) — integration test: boots a parent VM, snapshots it, restores two children with per-child netns. Child 1's netns is pre-provisioned; child 2's is not, soip netns execfails at spawn. Asserts child 1 survives withchild_index=1, child 2 fails at Spawn phase.Out of scope
The concurrent-RW-mount-of-shared-rootfs issue (
restore_many_withsends the same vmstate to all children) needs an immutable baseline + per-VM writable layer and is tracked as a separate architectural initiative. The prewarm (Phase 3) and memfd (Phase 1.5) paths retain their pre-existing early-bail behavior — they are host-resource failures rather than per-VM failures, and converting them is outside this change's scope.