envd (0.6.0): - Run blocking syscalls (sync/drop_caches, directory walks and removals, stdin/pty writes) on the blocking pool so health checks and exec streams don't stall during snapshot quiesce or large-tree operations - Stop writing sandbox metadata into envd's own environment at init; it was undefined behavior with the async runtime running - Retry a memory preload that previously failed or was cancelled instead of blocking every future pause until the next init - Match RAM ranges against kernel core segments on page boundaries so preload no longer rejects kernels with unaligned low-memory ranges - Tolerate a concurrent request creating a directory first host agent: - Detach the copy-on-write loop device when snapshot removal fails so deleting its backing file can no longer orphan it permanently - Clean the sandbox directory on pause rollback, and skip paused restore for directories still owned by a live VM that failed to re-attach
68 lines
2.8 KiB
Rust
68 lines
2.8 KiB
Rust
use std::sync::Arc;
|
|
|
|
use axum::extract::State;
|
|
use axum::http::{StatusCode, header};
|
|
use axum::response::IntoResponse;
|
|
use nix::unistd::sync;
|
|
|
|
use crate::state::AppState;
|
|
|
|
/// POST /snapshot/prepare — called by the host agent immediately before it
|
|
/// invokes vm.pause + vm.snapshot. The handler quiesces guest state so the
|
|
/// resulting snapshot is clean: outstanding writes are flushed to disk, the
|
|
/// VFS page cache is dropped (so the dm-snapshot CoW is the source of truth),
|
|
/// and the port forwarder is stopped to prevent socat children from being
|
|
/// frozen mid-handshake.
|
|
pub async fn post_snapshot_prepare(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
|
// Stop port forwarder + scanner so no socat process is captured in the
|
|
// snapshot with a half-open TCP connection. /init on resume restarts it.
|
|
if let Some(ref port_sub) = state.port_subsystem {
|
|
port_sub.stop();
|
|
}
|
|
|
|
// Flush in-memory FS state, then drop the VFS page cache +
|
|
// dentries/inodes. sync first so the pages we drop are clean; dropping
|
|
// reduces snapshot size by ensuring CH only persists memory pages the
|
|
// guest actually needs.
|
|
flush_and_drop_caches("first pass").await;
|
|
|
|
// Best-effort fstrim on the rootfs so unused blocks are returned to the
|
|
// dm-snapshot, keeping CoW size minimal.
|
|
let _ = tokio::process::Command::new("fstrim")
|
|
.arg("/")
|
|
.output()
|
|
.await;
|
|
|
|
// Second pass after fstrim: fstrim re-reads superblock / group descriptor
|
|
// pages that we just evicted, putting them back in the page cache. This
|
|
// drops those and any other late readers (e.g. sync flushers).
|
|
flush_and_drop_caches("second pass").await;
|
|
|
|
// No balloon settle window here: free-page reporting drains asynchronously,
|
|
// so any pages not yet hole-punched by the host at snapshot time are written
|
|
// verbatim — but with init_on_free=1 the guest zeroes them on free, and the
|
|
// host-side background zero-page punch reclaims them off the pause critical
|
|
// path. Trading a fixed ~1s of pause latency for a slightly larger artifact
|
|
// that the async punch later shrinks anyway.
|
|
|
|
tracing::info!("snapshot/prepare: quiesced");
|
|
(
|
|
StatusCode::NO_CONTENT,
|
|
[(header::CACHE_CONTROL, "no-store")],
|
|
)
|
|
}
|
|
|
|
// sync(2) can block for seconds flushing dirty pages, and the drop_caches
|
|
// write blocks while the kernel evicts — both run on the blocking pool or
|
|
// they starve every async task sharing the worker thread (health probes,
|
|
// exec streams) for the whole quiesce window.
|
|
async fn flush_and_drop_caches(pass: &'static str) {
|
|
let _ = tokio::task::spawn_blocking(move || {
|
|
sync();
|
|
if let Err(e) = std::fs::write("/proc/sys/vm/drop_caches", "3") {
|
|
tracing::warn!(error = %e, pass, "drop_caches failed (continuing)");
|
|
}
|
|
})
|
|
.await;
|
|
}
|