Repository navigation
feat(minvmd,minimald): quiesce data volume on stop and gate READY on mount - #705
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (12)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughChangesState volume lifecycle
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant minvmd
participant rpc_client
participant minimald
participant state_volume
minvmd->>rpc_client: request guest shutdown
rpc_client->>minimald: send Shutdown RPC
minimald->>state_volume: release tracker and quiesce mount
minvmd->>minvmd: terminate VMM
Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Comment |
…mount Unit 2 of the per-VM writable ext4 volume spec (R2.1-R2.5): a clean `minvmd stop` now leaves the volume's ext4 journal closed, and a volume that cannot be mounted fails the boot loudly instead of ghost-READY. Guest (minimald): - quiesce_state_volume: syncfs + plain umount, falling back to remount-ro (clears INCOMPAT_RECOVER despite open fds) + MNT_DETACH; called from the Shutdown RPC handler after session drain, before the ack, with a 10s ceiling (R2.1, R2.2). - mount_state_volume fails closed: device absence is an error (minvmd attaches vdb on every boot), and a formatted volume that fails to mount gets one e2fsck -p repair + retry, never a reformat (R2.4). - MOUNT_FAILED beacon: a mount failure emits MOUNT_FAILED\n<reason>\n instead of READY and parks; no tmpfs fallback path remains (R2.4, R2.5). - The sessions manager releases the lcache read tracker on shutdown: its locked alog fd on the volume pinned the mount busy (EBUSY) and left the journal dirty on every stop. lcache/mctx grow a release_read_tracker() for this; the EBUSY warn now lists fd holders. Host (minvmd): - `stop` invokes the guest Shutdown RPC (force) over the bridge UDS before SIGTERM, via a new blocking rpc_client; failures warn and proceed with the unchanged SIGTERM->SIGKILL escalation (R2.3). The 15s client timeout contains the guest's 10s quiesce budget. - read_ready_beacon returns a typed BootBeacon; on MountFailed, boot/ run fail with a pre-existence-aware message: a pre-existing image may hold session data and is kept, a freshly provisioned blank is removed so the next boot is not misclassified (R2.5). e2e (gated MINVMD_E2E=1, verified on macOS/HVF): a clean stop leaves EXT4_VALID_FS set and INCOMPAT_RECOVER clear on the image; an unformattable fresh volume and an unmountable pre-existing volume both fail boot loudly with the right image disposition. boot_e2e (and the new tests) now also isolate HOME: dirs::state_dir() ignores XDG_STATE_HOME on macOS, so the tests otherwise clobber the real state dir. Refs: #583 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8f969a6 to
bd23a5e
Compare
|
Rebased onto latest main (over #690's socket/path rework). Two things beyond mechanical conflict resolution:
All proofs re-verified on macOS/HVF after the rebase: clean stop → 🤖 Generated with Claude Code |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/minvmd/src/cmd/mod.rs`:
- Around line 289-311: Track provisioning ownership from the volume
creation/provisioning function through the mount-failure path instead of relying
on the pre-spawn volume_preexisted check. Update the provisioning result and
callers to carry an atomic created_by_this_boot flag, then have
mount_failed_error remove the image only when that flag is true; preserve the
existing warning and pre-existing-image behavior otherwise.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d56ead12-e9ac-41be-a112-ad8d48472d0c
📒 Files selected for processing (15)
crates/lcache/src/lib.rscrates/mctx/src/lib.rscrates/minimald/src/guest.rscrates/minimald/src/main.rscrates/minimald/src/rpc.rscrates/minimald/src/server.rscrates/minimald/src/sessions.rscrates/minvmd/src/cmd/boot.rscrates/minvmd/src/cmd/mod.rscrates/minvmd/src/cmd/run.rscrates/minvmd/src/cmd/stop.rscrates/minvmd/src/lib.rscrates/minvmd/src/rpc_client.rscrates/minvmd/tests/boot_e2e.rscrates/minvmd/tests/volume_quiesce_e2e.rs
Review findings on the quiesce/loud-mount-failure work: - stop: split the Shutdown RPC deadline into a 5 s connect/handshake budget (a wedged guest must not stall the recovery command — libkrun accepts the bridge connect regardless of guest health, so only a completed handshake proves a live daemon) and a 120 s RPC budget (the ack lands only after the unbounded session drain plus the guest's 10 s quiesce; giving up mid-drain would SIGTERM with a dirty journal). - boot/run: the image pre-existence probe treats a stat error as "pre-existing" so a transient failure can never route a data-bearing image to the delete branch; the fresh blank image is discarded on every failed-boot arm (beacon error, READY timeout) — not just MOUNT_FAILED — so a stranded blank can't masquerade as data-at-risk on the next boot; mount_failed_error is now message-only. - the shutdown quiesce gates on Config.state_volume_mounted (set only when the guest actually mounted the volume) instead of the vsock transport, so a native --vsock daemon or a volume-less microVM never has its state dir synced-and-unmounted out from under it. - the guest instance-lock relocation to /run keys on being the VM init (is_minimal_microvm) rather than --vsock, keeping non-root native --vsock daemons working; a non-vsock daemon whose state-volume mount fails exits with an error instead of parking behind a beacon nobody reads; a failed MOUNT_FAILED emit is now logged rather than silently degrading to a READY timeout. - the data volume mounts via a strict (EBUSY-propagating) mount — the EBUSY-tolerant raw_mount is for idempotent pseudo-fs mounts only — and every mount_state_volume failure names its operation, so the MOUNT_FAILED reason can't misdirect (a pre-R1.7 rootfs EROFS no longer reads as a corrupt image needing fsck). - run --detach: the supervisor's stderr lands in <state>/run.log and both failure messages point at it, instead of /dev/null swallowing the mount-failure diagnosis. - volume_quiesce_e2e stops the VM on drop, so a mid-test panic no longer leaks a detached VM whose state dir the TempDir then unlinks. Refs: #583 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
VM e2e harnesses are now selected by binary-name convention instead of a hardcoded per-test list, so adding a test never requires a CI edit: - Harnesses in crates/minvmd/tests/ end in `_e2e` (non-root) or `_root_e2e` (root, run under sudo for CAP_NET_ADMIN). - The KVM lane's three enumerated steps collapse to two convention filtersets: `binary(/_e2e$/) and not binary(/_root_e2e$/)` and `binary(/_root_e2e$/)`. A new `*_e2e.rs` is picked up with no YAML. - Rename krun_smoke -> krun_smoke_e2e and vsock_relay_e2e -> vsock_relay_root_e2e; delete the retired bridge_e2e stub. - A unit test in minvmd (runs in core-tests, no libkrun) guards the suffix so a typo fails CI instead of silently dropping a harness. This newly runs volume_quiesce_e2e (added in #705 but never wired into a lane) in the KVM lane. The macOS lane tracks the rename only; its filterset conversion is coupled to the nextest-archive-on-mac work. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HbXL6jxHTY2YuLPaMxc1yU
…s every lane (#732) * test(minvmd,ci): auto-discover VM e2e harnesses by name suffix VM e2e harnesses are now selected by binary-name convention instead of a hardcoded per-test list, so adding a test never requires a CI edit: - Harnesses in crates/minvmd/tests/ end in `_e2e` (non-root) or `_root_e2e` (root, run under sudo for CAP_NET_ADMIN). - The KVM lane's three enumerated steps collapse to two convention filtersets: `binary(/_e2e$/) and not binary(/_root_e2e$/)` and `binary(/_root_e2e$/)`. A new `*_e2e.rs` is picked up with no YAML. - Rename krun_smoke -> krun_smoke_e2e and vsock_relay_e2e -> vsock_relay_root_e2e; delete the retired bridge_e2e stub. - A unit test in minvmd (runs in core-tests, no libkrun) guards the suffix so a typo fails CI instead of silently dropping a harness. This newly runs volume_quiesce_e2e (added in #705 but never wired into a lane) in the KVM lane. The macOS lane tracks the rename only; its filterset conversion is coupled to the nextest-archive-on-mac work. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HbXL6jxHTY2YuLPaMxc1yU * test(minimald,linux-native): fold the netns track into root e2e auto-discovery The netns-integration job hardcoded `cargo test -p minimald --test netns`. Rename netns.rs -> netns_root_e2e.rs and select it by the same convention the KVM lane uses: the renamed `minimald-root-e2e` job runs `cargo nextest run -p minimald -E 'binary(/_root_e2e$/)'`, so a new crates/minimald/tests/*_root_e2e.rs runs with no workflow edit. A minimald unit test guards the suffix; mesh_uc7 is allowlisted because its networking-wg feature is off by default and no lane runs it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HbXL6jxHTY2YuLPaMxc1yU * ci(macos): auto-discover VM e2e harnesses from a nextest archive The mac e2e job hardcoded two test names and ran them as direct binaries. Build a nextest archive before the codesign (`nextest run --archive-file` is offline, so the signature survives) and select harnesses by the `_e2e` convention, so a new crates/minvmd/tests/*_e2e.rs runs on mac with no workflow edit. This newly runs boot_e2e and volume_quiesce_e2e on mac. The full-boot harnesses spawn the codesigned minvmd as a subprocess (MINVMD_BIN). krun_smoke_e2e stays a separate kernel-less FFI-smoke step, excluded from the archive run: it boots in-process through krun_smoke_child, which cannot take the hypervisor entitlement from within the archive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HbXL6jxHTY2YuLPaMxc1yU * docs: update specs for the e2e test renames and bridge_e2e removal Reflect the auto-discovery migration in the frozen unit specs: krun_smoke -> krun_smoke_e2e in R1.5, and note that bridge_e2e was removed (superseded by minimald_session_e2e). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HbXL6jxHTY2YuLPaMxc1yU * refactor(minvmd,ci): drop the dead host-side tap relay + its KVM wiring minvmd's net::relay (open_tap/attach_to_switch/SwitchRelay) has had no production caller since #581 moved the per-PTask tap+relay into the guest minimald over the vsock shuttle; it was a frozen byte-for-byte fork of minimald's net::switch that never received the ingress gate the live copy grew. Its only caller was vsock_relay_root_e2e, which booted no VM and re-proved the relay against a hand-made netns — coverage already provided, with real PTask-to-PTask traffic, by minimald's netns_root_e2e UC6. Delete net/relay.rs, its module wiring, and the test; correct the net.rs module doc (the tap + relay run in the guest, not here). The KVM lane's root step existed only for that test, so it is removed along with the gvproxy materialize step and the vendor/gvproxy pin filter it needed; the lane keeps a single auto-discovered harness step (a future minvmd *_root_e2e would need its own sudo-wrapped step). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HbXL6jxHTY2YuLPaMxc1yU * refactor(minimald,ci): name and describe the root e2e proofs functionally Drop the internal use-case numbers (UC1/UC4/UC6/UC7) from the netns test function names, its module doc, the native-lane comments, and the minimald naming guard. Describe each proof by what it does — a no-network task cannot reach the internet, a static ingress mapping exposes then removes a task's port, two own-IP tasks reach each other over the gvproxy switch — and state why they need root: they create tap devices and configure network namespaces, which requires CAP_NET_ADMIN, so the unprivileged CI runner grants it via sudo. No behavior change; the harness is selected by binary name, not function name, so the renames are self-contained. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HbXL6jxHTY2YuLPaMxc1yU * refactor(minvmd,minimald,ci): rename harness e2e tests to _integration The harness-driven nextest proofs are component/subsystem INTEGRATION tests, not full-system e2e: each stands up one subsystem against real resources (boot a microVM, round-trip the vsock bridge, drive the libkrun FFI, check a real ext4 volume, exercise the tap/netns stack) rather than driving the `minimal` CLI through the whole system. The only true end-to-end proof is the CLI script (scripts/session-e2e.sh). Rename the auto-discovery suffix _e2e / _root_e2e -> _integration / _root_integration across the five harnesses, the KVM/native/mac lane filtersets, and both naming guards; the guard docs now state when to reach for an integration harness vs. a full-system e2e script. Rename the minvmd-scoped lifecycle script (lifecycle-e2e.sh -> minvmd-lifecycle.sh) and the native root job (minimald-root-e2e -> minimald-root-integration). Also scrub the workflow YAMLs of internal plan jargon (UC/N/R references) in favour of functional descriptions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HbXL6jxHTY2YuLPaMxc1yU --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A service manager stops a foreground 'minvmd run' with SIGTERM or SIGINT. The supervisor's signal watcher cancelled pending asks and then re-raised the signal, so the guest never received the Shutdown RPC and the VMM child kept running: the data volume was left with a dirty ext4 journal, and the recorded VMM pid outlived its supervisor. The watcher now runs the same stop 'minvmd stop' runs: quiesce the guest over the bridge UDS (informed by #705), SIGTERM the VMM child and wait, then wait for the supervisor's own teardown to record 'Stopped'. A run that is not Running-with-a-pid is refused, and any failure falls back to dying by the signal as before, so the process always terminates. 'die_by_signal' remains the backstop, not the path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…e supervisor (gominimal#2155) * fix(minvmd): stop the VM gracefully on SIGTERM/SIGINT A service manager stops a foreground 'minvmd run' with SIGTERM or SIGINT. The supervisor's signal watcher cancelled pending asks and then re-raised the signal, so the guest never received the Shutdown RPC and the VMM child kept running: the data volume was left with a dirty ext4 journal, and the recorded VMM pid outlived its supervisor. The watcher now runs the same stop 'minvmd stop' runs: quiesce the guest over the bridge UDS (informed by gominimal#705), SIGTERM the VMM child and wait, then wait for the supervisor's own teardown to record 'Stopped'. A run that is not Running-with-a-pid is refused, and any failure falls back to dying by the signal as before, so the process always terminates. 'die_by_signal' remains the backstop, not the path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(minvmd): bound the signal stop and end the supervisor by its signal Count the whole SIGTERM/SIGINT stop against a 15 s deadline from the signal's arrival, under launchd's 20 s default ExitTimeOut: the guest Shutdown ask gets only what the deadline leaves after the SIGTERM grace, and the Stopped wait ends at the deadline. Restore the default dispositions as soon as the first signal arrives, so a second SIGTERM or SIGINT during the graceful stop ends the process at once. Record the signal so the supervisor ends by it after its teardown, keeping the exit status, with the watcher as a backstop at the deadline. * fix(minvmd): give the signal stop's shutdown rpc the whole quiesce budget The guest Shutdown RPC on the signal path now runs until the quiesce deadline instead of the half of the budget left after the connect cap, so a guest still draining sessions is not cut off mid-drain. The stop and the signal stop share one quiesce-then-signal sequence; the signal stop takes the lifecycle write lock, and both derive the bridge socket from the state dir they act on. main ends a signal-stopped supervisor by its signal after flushing the log. The docs now say the graceful stop covers a signal sent to the supervisor alone (a terminal Ctrl-C also reaches the VMM), and name min stop --force as the recovery for a VMM orphaned by a SIGKILL of the supervisor. * fix(minvmd): abort the boot on a stop signal instead of orphaning the VMM A SIGTERM or SIGINT that reached the supervisor while the VM was still Starting was refused by the signal stop, so the watcher died by the signal at once and left the VMM child running with the inherited alive lock. The signal stop now signals the recorded VMM child without a guest ask, writes Stopped, and the watcher ends the process by the signal. * fix(minvmd): cut the signal stop's sigterm grace to its deadline Each round of signalling in a signal stop waited the whole 5 s SIGTERM grace, whatever was left of the stop's 15 s budget. A boot that respawned its VMM late in that budget could so run a round to about 20 s, the service manager's own stop timeout. The grace is now the smaller of 5 s and the time the deadline leaves, on the boot-abort rounds and on the running path alike. `minvmd stop` and `stop_at` keep the full grace. Also cover SIGINT sent to the supervisor alone (`kill -INT`) with the same end-to-end assertions as the SIGTERM case: the supervisor ends by the signal inside the stop bound, the record reads Stopped, and the ext4 journal is clean. A terminal Ctrl-C, which reaches the VMM child through the shared process group, stays outside what the graceful stop covers. * fix(minvmd): keep a boot aborted by a stop signal from forking another vmm A stop signal during the boot wrote Stopped and ended the process at once, while the boot forked its VMM child first and took the lifecycle lock to record the pid only afterwards. A child forked in that window, on a redraw or on the first boot before its pid was recorded, was never signalled: it was left orphaned, holding the inherited alive lock and the bridge socket. The boot now forks and records the pid under one hold of the lifecycle write lock, after checking the lifecycle is still Starting. The boot abort keeps that lock from its Stopped write until the process ends, so it either reads the child's pid and signals it, or the boot never forks. A Starting state with no pid recorded is aborted the same way instead of being refused. The watcher's own death paths also lost a detached supervisor's buffered log lines, because only main dropped the log guard. The guard now lives where dying by the signal can flush it from either thread. * docs(minvmd): rewrap the stop-signal watcher doc comment * fix(minvmd): exit when the stop signal does not end the supervisor die_by_signal re-raises the stop signal with its default action. Where that does not end the process (pid 1 in a container ignores a default-action signal it sends itself), the process lived on. A boot abort keeps the lifecycle lock until the process ends, so every later minvmd stop and min stop --force would block on it for good. Exit with 128 + the signal number after the raise, and make the function diverge. The signal stop bound's doc now says the log flush comes after it. * fix(minvmd): end the supervisor with _exit when its stop signal is ignored The fallback after the re-raised stop signal can run on the watcher thread while the main thread is parked on the lifecycle lock. process::exit runs exit handlers and static destructors from that thread, and one that waits on a parked thread would hang the path that must end the process. Use _exit; the log is already flushed. The doc now says a supervisor sees exit code 128 + the signal number in that case, not a death by signal. * chore(worker-iterate): address CodeRabbit findings on PR gominimal#2155 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: gominimal-aw-bot[bot] <281738952+gominimal-aw-bot[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Since #672, session worktrees and the package cache persist on the per-VM
/dev/vdbvolume — user data with no host copy. This PR fixes the failure modes that made that persistence unsafe: stopping a VM corrupted-by-default, and a broken volume failed silently.Failure modes fixed
1. Every
minvmd stopwas an unclean unmount — dirty journal, possible data loss.stopsent SIGTERM straight to the VMM with nothing syncing or unmounting the volume. Every boot after a stop replayed the ext4 journal ("recovering journal" on every startup), and withsync_mode=relaxedany writes from the final seconds before the kill — agit commitin a session, a build artifact — could vanish entirely.Now:
stopasks the in-VM minimald to shut down over the bridge UDS before signalling: sessions drain, the volume getssyncfs+ unmount (falling back to remount-ro, which closes the journal even with open fds), and only then does SIGTERM fire. A clean stop leaves a cleanly-unmounted filesystem; SIGTERM/SIGKILL escalation is unchanged when the guest doesn't answer.2. A VM whose volume failed to mount reported READY anyway — a ghost VM that eats data.
A mount failure was only logged; the guest silently kept its state on the boot tmpfs and signalled READY. Sessions appeared to work, wrote everything to RAM, and lost it all at the next stop — with zero indication anything was wrong. An undersized or corrupt volume image produced a "healthy" VM indistinguishable from a real one.
Now: the guest never substitutes tmpfs. On any mount failure it emits a distinct
MOUNT_FAILED\n<reason>\nbeacon instead of READY, andminvmd boot/runfail with a non-zero exit and the guest's actual failure reason.3. A failing volume offered no repair path and no safe disposition.
There was no distinction between "this image holds your sessions and won't mount" and "this blank image was just provisioned wrong" — and no repair attempt at all.
Now: a formatted volume that fails to mount gets one
e2fsck -prepair + mount retry before failing closed (never a reformat once a superblock exists). On failure, the host keys off whether the image pre-existed the boot: a pre-existing image may hold session data, so it is left untouched and the error points at repair orMINVMD_DATA_VOLUME_PATH; a blank image freshl 8000 y created this boot is removed so the next boot isn't misdiagnosed as data-at-risk.Found along the way
Two lifetime-held write fds on the volume made every unmount/remount fail
EBUSY, keeping the journal dirty even after a quiesce — the shutdown sequence only became verifiable once both were gone:alog/<n>.v1open for the daemon's lifetime; the sessions manager now releases it during shutdown (Cache::release_read_tracker).minimald.locklives in the provider dir, which in-guest sits on the volume; the vsock-mode instance lock now lives on the/runtmpfs (host-side placement unchanged — nothing outside the boot reads the guest's lock).The quiesce EBUSY warning now lists the offending fd holders via a
/procscan, so the next accidental pin is diagnosable from the console log.Verification
New gated e2e suite (
MINVMD_E2E=1, verified on macOS/HVF), asserting against the raw image bytes — no host ext4 tooling needed:EXT4_VALID_FSset,INCOMPAT_RECOVERclear);vm-up, non-zero exit, blank image removed;vm-up, non-zero exit, image preserved.VM-booting tests (including the pre-existing
boot_e2e) now also isolateHOME, sodirs-based path fallbacks can never escape onto a developer's real state dir.Refs: #583, builds on #613's Shutdown RPC
🤖 Generated with Claude Code
Summary by CodeRabbit
MOUNT_FAILEDreasons, and better run diagnostics viarun.log.