test(hypervisor): add incremental pagemap_anon snapshot restore tests - #1576
Conversation
8c9bf58 to
d9033dc
Compare
| .ssh_command("dd if=/dev/urandom of=/dev/null bs=1M count=64") | ||
| .unwrap(); | ||
|
|
||
| incremental_snapshot_and_check_events( |
There was a problem hiding this comment.
This incremental snapshot targets the same snapshot_dir the full snapshot above just wrote to, so the snapshot API call will fail. The VMM still runs Vm::send on the incremental path, and it creates config.json and state.json with OpenOptions::new().create_new(true) (hypervisor/vmm/src/vm.rs:2825 and :2849). Those files already exist from the full snapshot, so the open returns AlreadyExists, ch-remote exits non-zero, remote_command_w_args returns false, and the assert! in incremental_snapshot_and_check_events panics.
An incremental snapshot only needs the base memory-ranges file to pre-exist at the destination (send_pagemap_anon_memory, hypervisor/vmm/src/memory_manager.rs:2418-2431); it cannot tolerate pre-existing config.json/state.json. Give this incremental snapshot a fresh destination seeded with a copy of the base memory-ranges — exactly what test_snapshot_restore_incremental_after_restore does at lines 6896-6906.
There was a problem hiding this comment.
Agreed. Same-dir incremental after a full snapshot hits create_new(true) on config.json/state.json (Vm::send), so that test would fail at runtime.
Dropped test_snapshot_restore_incremental. Incremental is only covered by:
test_incremental_snapshot_requires_base_memory_ranges— empty dest must failtest_snapshot_restore_incremental_after_restore— Cubelet Tier 2 (restore → copymemory-ranges→ incremental → restore)
Also switched the guest dirty step to write /root/dirty.bin so the second test actually CoWs pages.
Review:
|
d9033dc to
50e754f
Compare
| guest.check_devices_common(Some(&socket), Some(&console_text), None); | ||
|
|
||
| let dirty_size = guest | ||
| .ssh_command("sudo stat -c %s /root/dirty.bin") |
There was a problem hiding this comment.
This assertion doesn't actually verify that the incremental (pagemap_anon) snapshot captured the CoW pages — the data it checks is persisted on disk, not in the snapshot.
/root/dirty.bin is written with conv=fsync, so it's durably on the shared disk image (cloud-hypervisor snapshots don't include disks; VM1/VM2/VM3 all share the same qcow2). After the second restore, stat reads the file size from disk metadata, which survives independently of the memory snapshot. A pagemap_anon filter that silently dropped the dirty page-cache pages would still pass this check, because the guest can re-read the file from disk.
To genuinely exercise the CoW-capture path, dirty a memory-backed filesystem instead (e.g. sudo mount -t tmpfs tmpfs /mnt or /dev/shm) and verify content with a checksum (md5sum) before and after restore. That content only survives if the CoW pages are actually captured in the incremental memory file.
There was a problem hiding this comment.
Agreed — /root/dirty.bin lives on the shared disk image, so stat after restore does not prove pagemap_anon captured the CoW pages.
Changed the payload to /dev/shm/dirty.bin (tmpfs) and compare md5sum before vs after the incremental restore. That content only survives if the dirty RAM pages are in the memory snapshot.
|
|
||
| fn remote_command_w_args(api_socket: &str, command: &str, args: &[&str]) -> bool { | ||
| let mut cmd = Command::new(clh_command("ch-remote")); | ||
| cmd.args([&format!("--api-socket={}", api_socket), command]); |
There was a problem hiding this comment.
This new helper is a near-verbatim copy of the existing remote_command above (same ch-remote invocation, same error-printing block). Since the file already has three near-identical helpers (remote_command, remote_command_w_output, and now remote_command_w_args), consider having remote_command delegate to remote_command_w_args with a one-element slice (or vice-versa) so the command-execution/error-reporting logic lives in one place.
There was a problem hiding this comment.
Done — remote_command now delegates to remote_command_w_args.
50e754f to
c0ac932
Compare
| assert!(remote_command(&api_socket, "pause", None)); | ||
|
|
||
| let url = format!("file://{}", snapshot_dir); | ||
| assert!( |
There was a problem hiding this comment.
This assertion can pass for the wrong reason. remote_command_w_args returns false on any ch-remote failure, and stderr is discarded. In send_pagemap_anon_memory, the filter_memory_ranges_by_pagemap_anon call (memory_manager.rs) runs before the base-file existence check, so if pagemap filtering errors on the host (permissions, kernel restrictions), the command fails and no memory-ranges is created — and this test passes without ever exercising the "incremental requires an existing base" path it is meant to pin.
Consider asserting on the actual error message, e.g. add a remote_command_w_args_output variant that returns stderr and check it mentions the missing base file (the VMM error is "Base snapshot file not found at ...").
There was a problem hiding this comment.
Good catch — filter_memory_ranges_by_pagemap_anon runs before the dest existence check, so any pagemap error would also fail the command.
The negative test now uses remote_command_w_args_output and asserts stderr contains Base snapshot file not found.
|
|
||
| // Incremental (pagemap_anon) snapshot: dest must already contain a | ||
| // memory-ranges base file. Only CoW anonymous pages are overwritten. | ||
| fn incremental_snapshot_and_check_events(api_socket: &str, snapshot_dir: &str, event_path: &str) { |
There was a problem hiding this comment.
This helper is a near-verbatim copy of snapshot_and_check_events — the only difference is the extra --snapshot-type incremental argument to ch-remote snapshot. Consider parameterizing the existing helper by snapshot type (or by the extra args slice) instead of duplicating the pause + event-check + sleep logic. Since you already introduced remote_command_w_args, snapshot_and_check_events could delegate to it with &[] for the full path and ["--snapshot-type", "incremental"] for this one, keeping a single source of truth for the event assertions.
There was a problem hiding this comment.
Done — snapshot_and_check_events and incremental_snapshot_and_check_events now share snapshot_with_extra_args_and_check_events.
Add integration tests covering incremental (pagemap_anon) snapshots, where the incremental snapshot overlays CoW anonymous pages onto an existing dest memory-ranges base file: - test_snapshot_restore_incremental: take a full snapshot first, then an incremental one onto the same dest directory, restore and verify events. - test_snapshot_restore_incremental_after_restore: restore a VM from a full snapshot, dirty more pages, copy the base memory-ranges into a new dest (simulating Cubelet reflink), then incremental snapshot and restore again. Also add a remote_command_w_args helper for ch-remote commands with extra arguments, and assert_memory_ranges_full_size to check the memory-ranges file keeps the full guest RAM logical size. Signed-off-by: Songqian Li <sionli@tencent.com> Signed-off-by: jinlong <jinlong@tencent.com>
c0ac932 to
ca0893d
Compare
| "incremental snapshot did not produce {}", | ||
| memory_ranges.display() | ||
| ); | ||
| let mem_config = MemoryConfig::parse(mem_params, None).unwrap(); |
There was a problem hiding this comment.
assert_memory_ranges_full_size compares the on-disk memory-ranges length against MemoryConfig::parse(mem_params, None).size, re-deriving the expected size from the CLI string. That equality only holds when guest RAM is a single, unzoned, non-hotplug region — which is exactly this test's config (size=1G), so the assertion is correct here. But the helper reads like a general invariant, and it would silently mis-validate for configs with hotplug_size/zones, where the sum of the snapshot's RAM regions (what actually goes into memory-ranges) is not equal to config.size. Consider deriving the expected length from the actual snapshot memory ranges (or the snapshot's config.json) and/or documenting the single-zone assumption in the helper.
| "incremental snapshot without dest memory-ranges must fail" | ||
| ); | ||
| assert!( | ||
| stderr.contains("Base snapshot file not found"), |
There was a problem hiding this comment.
This test pins the current VMM behavior that an incremental snapshot with no base memory-ranges fails with Base snapshot file not found. Worth noting that send_pagemap_anon_memory's own doc comment (hypervisor/vmm/src/memory_manager.rs:2382-2384) still describes the opposite behavior — "When no base snapshot exists (cold start), a new file is created and only anonymous pages are written; non-anonymous regions are left as holes (zeros)". The code (memory_manager.rs:2420-2425) instead errors out, so the doc is stale. Since this test now locks in the error behavior, it's worth reconciling the doc vs. the code so the intended cold-start semantics (if any) are clear.
| guest | ||
| .ssh_command("dd if=/dev/urandom of=/dev/shm/dirty.bin bs=1M count=64") | ||
| .unwrap(); | ||
| let sum = guest.ssh_command("md5sum /dev/shm/dirty.bin").unwrap(); |
There was a problem hiding this comment.
Flake risk to be aware of: the md5 comparison at the end depends on the 64 MiB of freshly written /dev/shm/dirty.bin staying resident in the guest page cache between this md5sum and the incremental snapshot. If the guest reclaims those (clean) tmpfs pages before the pause/snapshot, the second restore's md5 would legitimately differ even though the snapshot feature works. Unlikely here (1 GiB guest, 64 MiB file, snapshot taken immediately after), but it's the main source of non-determinism in this otherwise well-constructed test.
Capture the remote channel's stderr via `channel.stderr()` and print it alongside stdout when `ssh_command_ip_with_auth` gives up after exhausting retries. Both buffers are reset before each retry so the reported output reflects the final failed attempt only. Signed-off-by: Songqian Li <sionli@tencent.com>
| device_id: None, | ||
| }, | ||
| ]; | ||
| assert!(check_sequential_events_exact( |
There was a problem hiding this comment.
This assertion is the only one in this test that runs outside catch_unwind. If check_sequential_events_exact panics (e.g. the restore hasn't emitted starting/restoring within the 10s wait), the cloud-hypervisor child is dropped without kill_child/wait_with_output, leaving an orphaned VM process and no child output dumped for diagnosis. Every other assertion in this test wraps the child in catch_unwind + kill_child + handle_child_output. Consider moving this check inside the catch_unwind block below.
| "incremental snapshot did not produce {}", | ||
| memory_ranges.display() | ||
| ); | ||
| let mem_config = MemoryConfig::parse(mem_params, None).unwrap(); |
There was a problem hiding this comment.
This asserts memory-ranges file length == MemoryConfig.size. That equality holds for the plain size=1G config used here (the full snapshot writes every RAM region), but it's an implicit assumption: it would break for file=-backed memory, NUMA zones, or hotplug, where the sum of region lengths differs from the logical size. Worth a one-line comment documenting the assumption, or deriving the expected length from the snapshot's memory ranges instead of re-parsing the CLI memory string.
| let (ok, stderr) = remote_command_w_args_output( | ||
| &api_socket, | ||
| "snapshot", | ||
| &[&url, "--snapshot-type", "incremental"], |
There was a problem hiding this comment.
Style nit: this array literal relies on &String → &str deref coercion for &url. url.as_str() would be more explicit and matches how the same PR builds the arg list in snapshot_with_extra_args_and_check_events (args.push(url.as_str())).
|
@fslongjin Please click "Resolve conversation" to ensure that all comments from cubesandboxbot are either approved or rejected. |
Summary
Adds integration tests covering incremental (
pagemap_anon) snapshots, where the incremental snapshot overlays CoW anonymous pages onto an existing destmemory-rangesbase file.Changes
Base snapshot file not found, and must not creatememory-ranges./dev/shm) pages, copy the basememory-rangesinto a new dest (simulating Cubelet reflink), incremental snapshot, restore again, and verify the tmpfsmd5sum.remote_commanddelegates toremote_command_w_args; snapshot event helpers share one implementation.assert_memory_ranges_full_sizechecks thatmemory-rangeskeeps the full guest RAM logical size.Testing
Covered by the two new integration tests in
hypervisor/tests/integration.rs.