use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use arcbox_vm_driver::{AfterCheckpoint, CheckpointKind, CheckpointOptions, VmHandle};
use tracing::info;
use crate::error::{Result, VmmError};
use crate::lifecycle::runtime::ComputerRuntime;
use crate::sandbox::policy::settle::{self, Capture, GuestHold, Settlement};
use crate::sandbox::{CheckpointInfo, SandboxId, SandboxState};
use crate::snapshot::{SnapshotCatalog, SnapshotDraft};
pub struct CheckpointRequest {
pub name: String,
pub labels: HashMap<String, String>,
pub expected_state: SandboxState,
pub resume_after: bool,
}
pub enum CheckpointFailure {
Recoverable(VmmError),
Frozen(VmmError),
}
impl CheckpointFailure {
pub fn into_error(self) -> VmmError {
match self {
Self::Recoverable(error) | Self::Frozen(error) => error,
}
}
}
struct CheckpointSource {
kernel_path: String,
rootfs_path: String,
net_invariant: bool,
geometry: crate::snapshot::SnapshotGeometry,
handle: Arc<dyn VmHandle>,
}
fn checkpoint_source(
computer: &Arc<Mutex<ComputerRuntime>>,
sandbox_id: &SandboxId,
expected_state: SandboxState,
) -> Result<CheckpointSource> {
let inst = computer.lock().unwrap();
if inst.state != expected_state {
return Err(VmmError::WrongState {
id: sandbox_id.clone(),
expected: expected_state.to_string(),
actual: inst.state.to_string(),
});
}
let handle = inst.handle.clone().ok_or_else(|| VmmError::WrongState {
id: sandbox_id.clone(),
expected: format!("{expected_state} (VM handle available)"),
actual: inst.state.to_string(),
})?;
Ok(CheckpointSource {
kernel_path: inst.spec.kernel.clone(),
rootfs_path: inst.spec.rootfs.clone(),
net_invariant: inst.net_invariant,
geometry: crate::snapshot::SnapshotGeometry {
vcpus: inst.spec.vcpus,
memory_mib: inst.spec.memory_mib,
},
handle,
})
}
pub async fn checkpoint_impl(
computer: &Arc<Mutex<ComputerRuntime>>,
snapshots: &SnapshotCatalog,
sandbox_id: &SandboxId,
request: CheckpointRequest,
) -> std::result::Result<CheckpointInfo, CheckpointFailure> {
let resume_after = request.resume_after;
let source = checkpoint_source(computer, sandbox_id, request.expected_state)
.map_err(CheckpointFailure::Recoverable)?;
let handle = Arc::clone(&source.handle);
let outcome = capture(snapshots, sandbox_id, source, request).await;
let settlement = settle::settlement(
handle.state(),
if resume_after {
GuestHold::Resume
} else {
GuestHold::Hold
},
if outcome.is_ok() {
Capture::Succeeded
} else {
Capture::Failed
},
);
match (outcome, settlement) {
(Ok(info), Settlement::AsRequested) => Ok(info),
(Ok(info), Settlement::Frozen) => {
Err(CheckpointFailure::Frozen(VmmError::Process(format!(
"sandbox {sandbox_id} stayed frozen after checkpoint {}: the driver could not \
resume the guest",
info.snapshot_id
))))
}
(Err(error), Settlement::Frozen) => Err(CheckpointFailure::Frozen(error)),
(Err(error), Settlement::AsRequested) => Err(CheckpointFailure::Recoverable(error)),
}
}
async fn capture(
snapshots: &SnapshotCatalog,
sandbox_id: &SandboxId,
source: CheckpointSource,
request: CheckpointRequest,
) -> Result<CheckpointInfo> {
let CheckpointRequest {
name,
labels,
resume_after,
..
} = request;
let checkpoint = source.handle.checkpoint().ok_or_else(|| {
VmmError::FailedPrecondition(format!(
"sandbox {sandbox_id} cannot be checkpointed: checkpoints require jailer \
isolation, and this VM runs without it"
))
})?;
let pending = snapshots.begin(sandbox_id)?;
let image = checkpoint
.checkpoint(
&pending.dir(),
CheckpointOptions {
after: if resume_after {
AfterCheckpoint::Resume
} else {
AfterCheckpoint::HoldQuiesced
},
kind: CheckpointKind::Full,
},
)
.await?;
let meta = pending.commit(SnapshotDraft {
name: Some(name),
labels,
snapshot_type: crate::config::SnapshotType::Full,
parent_id: None,
kernel_path: Some(source.kernel_path),
rootfs_path: Some(source.rootfs_path),
net_invariant: source.net_invariant,
geometry: Some(source.geometry),
format: image.format.as_str().to_owned(),
})?;
let snap_dir_path = meta
.vmstate_path
.parent()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default();
info!(sandbox_id, snapshot_id = %meta.id, "sandbox checkpointed");
Ok(CheckpointInfo {
snapshot_id: meta.id,
snapshot_dir: snap_dir_path,
created_at: meta.created_at.to_rfc3339(),
})
}