Share one snapshot between runs that differ only in the balloon target - #1060
Conversation
`podman run --balloon N` put N in the snapshot key, so every target cold-booted once and kept its own pre-start and startup snapshots (#1053). The key now says whether a balloon device exists and nothing about its target, and a restore sets the caller's target on the loaded VM before the guest resumes. Key. `FirecrackerConfig::balloon` is `Option<BalloonDevice>`, and `BalloonDevice` keeps its target out of the JSON the key hashes, so the config serializes as `"balloon":{}`. Whether the device exists stays in the key because a device cannot be added to a restored VM. Keys without --balloon are unchanged (`test_snapshot_key_golden` is untouched). Keys with --balloon change once, so each such configuration misses once; `fcvm snapshots prune` removes the old entries. Neither type derives `Deserialize` any more: nothing reads them back, and a reader would get target 0 for every device. What only that derive used is gone with it: two serde default helpers, and `default` in the 28 `skip_serializing_if` attributes. Restore. `podman run` hands its --balloon to the restore through an internal `SnapshotRunArgs` field. `restore_from_snapshot` sends `PATCH /balloon` after the snapshot load and before the resume, raising or lowering the target the snapshot was made with. A target that cannot be set ends the run: the error is not a snapshot load failure, so there is no fallback to a cold boot. Every such failure says what was being done; when Firecracker answered 400 it also says what Firecracker refuses and names --no-snapshot. A target above --mem is refused before the cache is looked up, so a hit and a miss refuse the same run the same way. A Cloud Hypervisor restore refuses a target before it prepares the clone; that backend is not in the snapshot cache and no caller names one. Record. The VMM is the authority on its balloon device: the target can be changed on the VM's API socket after boot, and nothing writes that to the VM's state. So the record is what `GET /vm/config` reports, not a copy of another record. That call answers 200 with or without a device, so a VM with no balloon is an answer and any refusal is a failure. A memory snapshot reads it inside `create_snapshot_core`, with the VM paused and right before the save, so `snapshot create` and podman's own pre-start and startup snapshots record the target the saved device holds. A disk-only capture has no pause and reads it under the per-VM snapshot lock. Every Firecracker restore reads it before the resume, after the PATCH if there is one. `balloon_stats()` is for the size the guest has reached, and `BalloonStats` carries it. A disk-only clone has no restored VM to ask and cold-boots from the snapshot's record. A Cloud Hypervisor clone keeps the snapshot's record, and its snapshots copy it. Limits, stated in DESIGN.md. Firecracker refuses a target for a device the guest never activated (a kernel without the virtio balloon driver), so such a configuration cold-boots once and then fails on every cache hit. On an NV2 profile its first run fails too, because a miss there tears the cold-booted VM down and restores its own snapshot with the caller's target. A startup-snapshot hit hands over a guest whose workload started under the target of the run that made the snapshot. Tested (x86_64, default kernel profile, rootless networking): make fmt leaves the tree unchanged make clippy clean make _test-unit FILTER="-E 'binary(/^test_(hypervisor_api|balloon_call_sites|documented_make_targets|bench_fixtures|nested_harness_invariants|readme_examples|log_scan|ci_workflow_coverage|ami_hash_inputs|default_kernel_release|fuzz_chaos)/) | test(/^firecracker::|^hypervisor::|^commands::|^cli::|^state::|^storage::/)'" 543 tests run: 543 passed make _test-root FILTER="-E 'test(=test_balloon_target_honored_on_snapshot_cache_hit) | test(=test_snapshot_and_restored_clone_record_the_balloon_the_vmm_has) | test(=test_restored_clone_reboot_keeps_its_balloon) | test(=test_disk_only_clone_keeps_the_balloon) | test(=test_restored_clone_reboot_comes_back_healthy)'" STREAM=1 5 tests run: 5 passed, each on its first try Each test was watched failing without its subject. On a tree with the types, parameters and tests and none of the behaviour: the_snapshot_key_says_whether_a_balloon_exists_not_its_target keys differ: "5f8216e1747d" and "7d46cc9d7eb6" the_key_json_names_a_balloon_device_without_its_target Some(Object {"target_mib": Number(0)}) is not Some(Object {}) a_cache_restore_asks_for_this_runs_balloon_target None is not Some(512) a_balloon_target_above_the_guests_memory_is_refused a 1025 MiB balloon in a 1024 MiB guest was accepted firecracker_balloon_target_update_sends_the_target_alone red by mutation (the client sending the three-member `Balloon`) test_balloon_target_honored_on_snapshot_cache_hit (VM) "run 2 differs from run 1 in --balloon alone and must restore from run 1's pre-start snapshot ...; its lineage is None, so it cold-booted" On the source before the record was read from `GET /vm/config`, with the tests for that: snapshot_create_reads_the_balloon_under_the_vm_snapshot_lock snapshot create reads the VM's balloon before it holds the per-VM snapshot lock the_restore_sets_the_balloon_target_between_load_and_resume the Firecracker restore has no `.balloon_target_mib()` a_cloud_hypervisor_restore_refuses_a_balloon_target_before_it_prepares_the_clone the Cloud Hypervisor restore does not refuse a balloon target by its number On the source before a memory snapshot read the balloon inside `create_snapshot_core`, with the tests for that (4 run: 2 failed, 2 passed): a_memory_snapshot_reads_the_balloon_between_the_pause_and_the_save create_snapshot_core has no `.balloon_target_mib()` every_statement_that_sets_or_reads_a_restored_vms_balloon_still_does 1 of 4 statements gone: a memory snapshot records the device the VMM has On this tree with one thing mutated: the PATCH failure explained for every failure: a_failed_balloon_target_explains_a_refusal_and_nothing_else a 500 came back with the never-activated explanation and --no-snapshot the client's 400 not recognisable as a refusal: a_failed_balloon_target_explains_a_refusal_and_nothing_else a 400 came back with the neutral context only firecracker_balloon_target_update_sends_the_target_alone "a 400 is not recognisable as a refusal" the PATCH removed (VM): test_balloon_target_honored_on_snapshot_cache_hit "run 2 asked for a 128 MiB balloon; 120s after its device could first be read, the target is 64 MiB and the size 64 MiB" and "run 3 asked for a 32 MiB balloon; ... the target is 64 MiB and the size 64 MiB" Run before the last round of changes, on statements that round did not touch: the client reading any failure of GET /vm/config as "no device": firecracker_vm_config_reports_the_balloon_device_or_none called `Result::unwrap_err()` on an `Ok` value: None the restore's read of the VMM removed (VM): test_balloon_target_honored_on_snapshot_cache_hit "run 2's state records balloon target Some(64), not the Some(128) it asked for" test_snapshot_and_restored_clone_record_the_balloon_the_vmm_has the clone's state records Some(64), not the Some(96) the VMM has (and, with the snapshot's read removed where it then was, so does the snapshot) In the passing cache-hit run the PATCH and the read together took 256 and 230 us on the paused VM. The guest inflated the balloon from 64 to 128 MiB within about 0.4 s of the resume and deflated it from 64 to 32 MiB within about 0.25 s (load average about 45 on 192 CPUs). Five earlier hits, at load averages of 120 to 350, took 1.19 to 1.99 s to inflate. The read alone took 139 to 258 us on restores that named no target. Firecracker's log shows Paused, GET /vm/config, PUT /snapshot/create in that order for `snapshot create` and for podman's pre-start snapshot, and a restored VM with no balloon has one INFO line for the read and no error line. Not run: arm64, the nested profile's Firecracker and with it the NV2 relaunch path (every --balloon run patched while the implicit UFFD server replays), the PATCH on any UFFD-served restore, Cloud Hypervisor, the full `make test-root`, `cargo audit` and `cargo deny` (the Cargo files are unchanged).
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (2)
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe change updates balloon configuration, snapshot identity, and restore handling. Pre-start snapshots can be shared across balloon targets. Startup snapshots use target-specific keys. Firecracker records the VMM-reported target and applies requested targets to paused restored VMs. ChangesBalloon Snapshot and Restore Flow
Priority: ➖ Normal Estimated code review effort: 4 (Complex) | ~45 minutes Change: Feature Sequence Diagram(s)sequenceDiagram
participant Podman as podman run
participant SnapshotCache as Snapshot cache
participant FirecrackerRestore as Firecracker restore
participant FirecrackerClient
participant FirecrackerVMM as Firecracker VMM
Podman->>SnapshotCache: Find snapshot using device-presence key
SnapshotCache->>FirecrackerRestore: Load matching snapshot
FirecrackerRestore->>FirecrackerClient: Set requested balloon target
FirecrackerClient->>FirecrackerVMM: PATCH /balloon
FirecrackerRestore->>FirecrackerClient: Read balloon target
FirecrackerClient->>FirecrackerVMM: GET /vm/config
FirecrackerRestore->>FirecrackerVMM: Resume guest
Merge Risk: ⚪ Minimal · up to No concrete merge-blocking regression is established. The unrun platform and test-matrix items remain validation follow-ups, not evidence of a defect. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to Requested balloon targets are applied before restored guests resume, and failures abort rather than silently using a different target. No introduced security vulnerability was established. Remaining uncertainty concerns concurrent direct control-socket updates and interruption outside the normal command lifecycle. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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 |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b323f1ec07
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… shared
Runs that differ only in --balloon share one snapshot key, and a cache hit
sets its own target on the loaded VM. That is right for the pre-start
snapshot, which is taken before the workload starts. It was wrong for the
startup snapshot (--health-check): a run at another target restored a workload
that had initialized under the creator's target, sized by the memory that guest
had, and setting the target after the load does not replay that. The startup
snapshot behaves differently for such a run and restore cannot reconcile it,
so by the reuse rule it must not be shared.
The startup snapshot's name now carries the target:
<key>-balloon<MIB>-startup a run with --balloon <MIB>
<key>-startup a run without --balloon, as before
`startup_snapshot_key` takes the run's balloon, so each place that derives the
name has to say which target it means: the lookup in `podman run` and
`podman prepare`, the save in the `podman run` loop (a cold boot that turned
healthy) and the save in the restore loop (a pre-start hit that turned
healthy). A run at a target with no startup snapshot restores the shared
pre-start snapshot, sets its target, starts the workload under it and saves a
startup snapshot for that target. No cold boot is added, and the snapshot key
is unchanged (`test_snapshot_key_golden` is untouched).
`podman prepare` installs a startup snapshot, so its content key carries the
target the same way, and a --tag whose generation was prepared at another
target is rebuilt like any other changed content key. That follows from the
code (the content key is the startup name); no prepare was run.
Nothing else derives the name. `snapshots prune` selects by snapshot type, the
diff parent is read from the VM's state, and the one matcher on the suffix
(benches/hugepages.rs, `ends_with("-startup")`) still matches. The cache-hit
test's helper made up a `<key>-startup` name for runs that take no health
check; it now returns the pre-start key those runs record.
Tested, each test first without the change:
make _test-unit FILTER="-E 'test(/two_balloon_targets_share_the_pre_start|test_snapshot_key_golden|the_snapshot_key_says_whether_a_balloon/)'"
with the name ignoring the target:
FAIL commands::podman::tests::two_balloon_targets_share_the_pre_start_snapshot_and_not_the_startup_one
a 96 MiB run must not find the startup snapshot a 64 MiB run left
left: "849babc9270a-startup" right: "849babc9270a-startup"
on this commit:
Summary [ 0.016s] 3 tests run: 3 passed, 1431 skipped
make _test-root FILTER="--retries 0 -E 'test(=test_startup_snapshot_is_per_balloon_target)'" STREAM=1
on the parent commit with the test added:
Error: run 2 differs from run 1 in --balloon alone (96 MiB, not 64) and has to restore the pre-start snapshot 58db22792d0e; it chose Startup("58db22792d0e-startup") (run 1's startup snapshot is 58db22792d0e-startup)
run 2 at 96 MiB and run 1 at 64 MiB name one startup snapshot, 58db22792d0e-startup
FAIL test_startup_snapshot_is_per_balloon_target
on this commit, together with test_balloon_target_honored_on_snapshot_cache_hit
and test_startup_snapshot_key_generation:
PASS [ 24.346s] fcvm::test_snapshot_clone test_balloon_target_honored_on_snapshot_cache_hit
PASS [ 27.605s] fcvm::test_snapshot_clone test_startup_snapshot_is_per_balloon_target
PASS [ 0.025s] fcvm::test_startup_snapshot test_startup_snapshot_key_generation
Summary [ 51.985s] 3 tests run: 3 passed, 1785 skipped
and what the four runs logged:
run 1 (64 MiB): Snapshot miss, will create snapshot after image load snapshot_key=9ef576e3083b
run 1 (64 MiB): Startup snapshot created successfully snapshot_key=9ef576e3083b-balloon64-startup
run 2 (96 MiB): Pre-start snapshot hit! Restoring from cached snapshot snapshot_key=9ef576e3083b
run 2 (96 MiB): Startup snapshot created successfully snapshot_key=9ef576e3083b-balloon96-startup
run 3 (64 MiB): Startup snapshot hit! Restoring from fully-initialized snapshot snapshot_key=9ef576e3083b-balloon64-startup
run 4 (96 MiB): Startup snapshot hit! Restoring from fully-initialized snapshot snapshot_key=9ef576e3083b-balloon96-startup
make _test-unit on the changed modules and the test binaries that read the
files this change edits:
Summary [ 8.084s] 544 tests run: 544 passed, 890 skipped
make fmt leaves the tree unchanged. make clippy is clean.
Not run: the existing startup snapshot tests (they pull an image this host
cannot reach, and their rootless health check does not pass on a host with no
IPv4, #1056), arm64, the nested profile, `podman prepare --balloon`, and the
full `make test-root`. The new VM test uses routed networking, which is the
mode whose health check passes on the host it was written on.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fcd65badbf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
`balloon_target_mib()` reads `GET /vm/config` on every snapshot and every
restore. Its `balloon` field was declared so that a reply without the member
was an error ("missing field `balloon`"): only `null` meant "no device". A
Firecracker that leaves the member out for a VM with no balloon would have
failed every snapshot and restore of an ordinary VM.
The two Firecracker builds this repository pins do not leave it out. In both,
`VmmConfig::balloon` is an `Option` with no `skip_serializing_if`, filled from
`resources.balloon.get_config().ok()` and written with `serde_json::to_string`,
so a VM with no balloon gets `"balloon":null`. Upstream v1.14.0 declares it the
same way. That is why the VM tests with no balloon passed: with the old
declaration an absent member would have failed their restores.
The client now takes both. The field is a plain `Option` with
`#[serde(default)]`, so a null member and an absent one read as None. What
stays an error: a refusal (any status that is not success), and a device that
does not say its target.
Tested, first without the change:
make _test-unit FILTER="-E 'test(/a_vm_config_reply_with_the_balloon_absent_or_null/) | test(=firecracker_vm_config_reports_the_balloon_device_or_none)'"
with the old declaration (2 tests run: 0 passed, 2 failed, 1433 skipped):
FAIL firecracker::api::tests::a_vm_config_reply_with_the_balloon_absent_or_null_means_no_device
left: Err("missing field `balloon` at line 1 column 13") right: Ok(None)
FAIL test_hypervisor_api firecracker_vm_config_reports_the_balloon_device_or_none
missing field `balloon` at line 1 column 13
on this commit:
Summary [ 0.014s] 2 tests run: 2 passed, 1433 skipped
make _test-root FILTER="--retries 0 -E 'test(=test_snapshot_and_restored_clone_record_the_balloon_the_vmm_has) | test(=test_restored_clone_reboot_comes_back_healthy) | test(=test_disk_only_clone_keeps_the_balloon)'" STREAM=1
(one VM with a balloon and one without, each snapshotted and restored)
PASS [ 31.640s] fcvm::test_disk_only_snapshot test_disk_only_clone_keeps_the_balloon
PASS [ 33.511s] fcvm::test_reboot test_restored_clone_reboot_comes_back_healthy
PASS [ 23.851s] fcvm::test_snapshot_clone test_snapshot_and_restored_clone_record_the_balloon_the_vmm_has
Summary [ 89.010s] 3 tests run: 3 passed, 1786 skipped
make _test-unit on the changed modules and the test binaries that read the
files this change edits:
Summary [ 8.182s] 545 tests run: 545 passed, 890 skipped
make fmt leaves the tree unchanged. make clippy is clean.
Not run: arm64, the nested profile's Firecracker, and the full make test-root.
No Firecracker that omits the member was run; none is pinned.
fcvm balloon (--pid PID | --name NAME) [MIB]
prints one JSON line, `{"target_mib":96,"actual_mib":64}`: the target the
device was asked to reach and the size the guest has given it so far. With MIB
it sets the target first (`PATCH /balloon`) and prints right after, so the size
is still on its way. Until now the target of a running VM could be changed only
by talking to Firecracker's API socket by hand.
Refusals. A Cloud Hypervisor VM is refused by name and a target above the VM's
memory with both numbers, each from the VM's state and before any request.
Whether the VM has a balloon device is read from `GET /vm/config`, which
answers 200 either way, so a VM booted without --balloon is told it has no
device and that one is attached only at boot. A set that Firecracker answers
with a 400 (the typed `ApiRefusal`) says what Firecracker refuses; any other
failure says only what was being done.
The command writes no state. What follows the new target and what does not,
from the code:
- The next snapshot of the VM does. A memory snapshot (`snapshot create` and
`podman run`'s own) and a disk-only capture read the device from the VMM. A
clone of that snapshot records the target in its state, and its relaunch
after a guest reboot attaches the device at it. The name of `podman run`'s
own startup snapshot does not move: it carries the run's --balloon.
- The VM's own state does not: `config.balloon_mib` keeps the target the VM
booted with or, for a clone, the one its restore read. Nothing else writes
that field.
- The VM's own relaunch after a guest reboot does not: a cold-booted VM
relaunches from its launch config and a restored clone from its state.
Firecracker has exited by then, so there is nothing to ask.
Also: `exec`'s lookup of a VM by --pid or --name moves to
`commands::common::load_vm_state` and both commands use it; the text of what
Firecracker refuses and the test for a 400 are shared with the restore's PATCH;
`BalloonStats` serializes, since it is the command's output.
Tested, each test first without its subject. The reds are from this change over
the first commit of #1060; every green step ran again after the change moved
onto its second commit.
make _test-unit FILTER="-E 'test(/balloon_names_one_vm|balloon_mib_is_optional|a_cloud_hypervisor_vm_is_refused|a_target_above_the_vms_memory|a_vm_with_no_balloon_device|a_failed_set_explains|the_report_is_one_json_line|a_failed_balloon_target_explains/)'"
the command's skeleton with none of its checks (8 tests run: 2 passed, 6 failed, 1432 skipped):
FAIL cli::args::tests::balloon_names_one_vm_by_pid_or_by_name
--pid and --name were accepted together
FAIL commands::balloon::tests::a_cloud_hypervisor_vm_is_refused_by_name_before_any_request
FAIL commands::balloon::tests::a_target_above_the_vms_memory_is_refused_before_any_request
FAIL commands::balloon::tests::a_vm_with_no_balloon_device_is_told_where_one_comes_from
FAIL commands::balloon::tests::a_failed_set_explains_a_refusal_and_nothing_else
FAIL commands::balloon::tests::the_report_is_one_json_line
MIB made a required argument:
FAIL cli::args::tests::balloon_mib_is_optional
a report needs no MIB: ErrorInner { kind: MissingRequiredArgument, ...
on this commit:
Summary [ 0.012s] 8 tests run: 8 passed, 1433 skipped
make _test-root FILTER="--retries 0 -E 'test(=test_balloon_command_sets_and_reports_the_target)'" STREAM=1
with the PATCH and the device check removed:
Error: `fcvm balloon --pid P 96` printed target 64 MiB
after `fcvm balloon --pid P 96` Firecracker reports the device at target 64 MiB
a snapshot taken after the target was set to 96 MiB records Some(64)
and for the VM booted without --balloon, with and without MIB, Firecracker's own
400 Bad Request ... "Failed perform action on device: Device not found"
FAIL test_balloon_command_sets_and_reports_the_target
on this commit, with test_restored_clone_reboot_keeps_its_balloon and
test_snapshot_and_restored_clone_record_the_balloon_the_vmm_has:
PASS [ 23.275s] fcvm::test_balloon test_balloon_command_sets_and_reports_the_target
PASS [ 32.589s] fcvm::test_reboot test_restored_clone_reboot_keeps_its_balloon
PASS [ 33.016s] fcvm::test_snapshot_clone test_snapshot_and_restored_clone_record_the_balloon_the_vmm_has
Summary [ 88.889s] 3 tests run: 3 passed, 1793 skipped
make _test-unit on the changed modules and the test binaries that read the
files this change edits:
Summary [ 8.320s] 562 tests run: 562 passed, 879 skipped
make fmt leaves the tree unchanged. make clippy is clean.
Not run: arm64, the nested profile's Firecracker, a VM whose guest never
activated the device (the 400 explanation is covered by unit tests), Cloud
Hypervisor (its refusal is a unit test on the VM's state), and the full
make test-root.
ejc3
left a comment
There was a problem hiding this comment.
NOT-A-DEFECT: the review bodies on this pull request carry no finding of their own. Each finding they announce is an inline thread, and each thread is answered there with the test that was watched failing.
|
@codex review |
|
Codex Review: Didn't find any major issues. Swish! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
ejc3
left a comment
There was a problem hiding this comment.
NOT-A-DEFECT: the review bot's summary comment was rewritten when this pull request's description changed, and it carries no finding. The findings on this pull request are inline threads, and each is answered there with the test that was watched failing.
Runs that differ only in
--balloonnow share the pre-start snapshot, and a cache hit runs at its own target (#1053). The startup snapshot is kept per target.Follows: #1055, which is merged. This branch is based on its head and targets main.
The Problem
#1054 put the
--balloonvalue in the snapshot key, because nothing set a target on a restored VM. That made the cache correct and fragmented it: every target cold-boots once and keeps its own pre-start and startup snapshots, although the guests differ in one number the VMM can change at any time.The Solution
FirecrackerConfig::balloonisOption<BalloonDevice>, andBalloonDevicekeeps its target out of the JSON the key hashes ("balloon":{}). Whether a device exists stays in the key, because one cannot be added to a restored VM. Keys without--balloondo not move:test_snapshot_key_goldenis untouched. Keys with--balloonchange once, so each such configuration misses once, andfcvm snapshots pruneremoves the old entries. Neither type derivesDeserializeany more (nothing reads them back, and a reader would get target 0 for every device). What only that derive used is gone with it: two serde default helpers, anddefaultin the 28skip_serializing_ifattributes.podman runhands its--balloonto the restore through an internalSnapshotRunArgsfield (no new flag onfcvm snapshot run).restore_from_snapshotsendsPATCH /balloonafter the snapshot load and before the resume, raising or lowering the target the snapshot was made with. A target that cannot be set ends the run. It is not a snapshot load failure, so there is no fallback to a cold boot. Every such failure says what was being done; when Firecracker answered 400 it also says what Firecracker refuses and names--no-snapshot. A target above--memis refused before the cache is looked up, so a hit and a miss refuse the same run the same way.--health-check) holds a workload that initialized under the target of the run that made it, sized by the memory that guest had. Setting the target after the load does not replay that, so restore cannot reconcile it and the snapshot is not shared. Its name carries the target:<key>-balloon<MIB>-startup, and<key>-startupfor a run without--balloon, as before.startup_snapshot_keytakes the run's balloon, so the lookup and both places that save (thepodman runloop after a cold boot, the restore loop after a pre-start hit) name the same snapshot. A run at a target that has none restores the shared pre-start snapshot, sets its target, starts the workload under it and saves a startup snapshot for that target. No cold boot is added.podman prepareinstalls a startup snapshot, so its content key carries the target the same way, and a--tagprepared at another target is rebuilt like any other changed content key.fcvm ballooncommand does exactly that and writes no state), so a record copied from a state file can differ from the device. The record is whatGET /vm/configreports. That call answers 200 with or without a device. "No device" is the reply's"balloon": null, which both pinned Firecracker builds send (their reply serializes theOptionwith no skip), or a reply without the member, which the client reads the same way since the third commit. Any refusal is a failure, and a VM without a balloon gets no error line in itsfirecracker.logfor the read.create_snapshot_core, with the VM paused and right before the save. That coverssnapshot createandpodman run's own pre-start and startup snapshots, and the record is the target the saved device holds.restore_from_snapshot_chrefuses one, by its number, before it prepares the clone, so a later change cannot drop a target without a word. A Cloud Hypervisor clone keeps the snapshot's record, and its snapshots copy it.One limit (also in DESIGN.md):
--no-snapshot, which runs it.Not changed:
run_args_from_snapshot_metadatakeeps itsballoonparameter. Finding 6 of the A rebooted clone and a disk-only clone keep the source VM's balloon device #1055 review said it always equalsmeta.balloon_mib. After this PR it does not: the reboot site passes the clone's state, which is what the VMM reported at restore and can differ from the snapshot's record.snapshot_cache_opt_outentry is added, and fc-agent is not touched.Test Results
Each test was watched failing without its subject. The trees for the first commit: a scaffold with the types, parameters and tests and none of the behaviour; the source as it stood before a round of changes, with that round's tests added; and the commit with one statement mutated. For the second commit: the first commit with the new VM test added, and the second commit with the startup name ignoring the target.
VM tests,
make _test-root FILTER="--retries 0 -E 'test(=...)'" STREAM=1:Green, on the third commit (one VM with a balloon and one without, each snapshotted and restored through the changed read):
Green, on the second commit:
Green, on the first commit:
What
GET /vm/configreports was checked in both Firecracker refs fcvm pins (the default profile's and the nested profile's): the runtime handler builds the reply from the live devices, and the balloon'samount_mibis its current target, which a restore fills from the saved state andPATCH /balloonchanges. The VM tests show it on the default profile's Firecracker: the record test reads 96 MiB, from a VM that booted at 64 and was set to 96, into its snapshot (read while the VM is paused for the save) and into the state of the clone restored from it; the cache-hit test's state is the read made right after each PATCH (128, then 32);test_restored_clone_reboot_comes_back_healthyrestores a VM with no balloon and records none. Thefirecracker.logof a restored VM with no balloon holds one INFO line for the read (The API server received a Get request on "/vm/config".) and no error line.Convergence after a
PATCHon a loaded, paused VM, from the passing cache-hit run (x86_64, default profile's Firecracker, pre-start snapshot restored through the File backend, host load average about 45 on 192 CPUs):Each first reading is within about 0.05 s of the resume, so the guest had inflated the balloon within about 0.4 s of first running and deflated it within about 0.25 s. Five earlier hits on the same host, at load averages of 120 to 350, took 1.19 to 1.99 s to inflate.
Firecracker's own log shows the snapshot's read where the code puts it:
Paused,GET /vm/config,PUT /snapshot/create, in that order, forsnapshot createand forpodman run's pre-start snapshot.Not run: arm64 and the nested profile's Firecracker, and with it the NV2 relaunch path, where every
--balloonrun is patched while the implicit UFFD server replays. Also not run: the PATCH on any UFFD-served restore (a hugepage restore uses the same implicit server), Cloud Hypervisor, the fullmake test-root, andcargo auditandcargo deny(the Cargo files are unchanged). The cache-hit test takes no--health-check: it restores from the pre-start snapshot, and nothing in it reads the container's HTTP port. The startup snapshot test takes one and uses routed networking. Also not run for the second commit: the existing startup snapshot tests (their image could not be pulled where this was written, and their rootless health check does not pass on a host with no IPv4, #1056), andpodman prepare --balloon(its content key is the startup name, from the code).Summary by CodeRabbit
--balloon <MIB>support for restored virtual machines. Cache hits apply the requested target before the guest resumes, so runs with different targets can share a pre-start snapshot.