use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use arcbox_vm_driver::{AfterCheckpoint, CheckpointKind, CheckpointOptions, VmHandle};
use tracing::info;
use crate::error::{ComputerError, Result};
use crate::lifecycle::runtime::ComputerRuntime;
use crate::sandbox::policy::settle::{self, Capture, GuestHold, Settlement};
use crate::sandbox::{CheckpointInfo, ComputerId, ComputerState};
use crate::snapshot::{SnapshotCatalog, SnapshotDraft};
pub struct CheckpointRequest {
pub name: String,
pub labels: HashMap<String, String>,
pub expected_state: ComputerState,
pub resume_after: bool,
}
pub enum CheckpointFailure {
Recoverable(ComputerError),
Frozen(ComputerError),
}
impl CheckpointFailure {
pub fn into_error(self) -> ComputerError {
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: &ComputerId,
expected_state: ComputerState,
) -> Result<CheckpointSource> {
let inst = computer.lock().unwrap();
if inst.state != expected_state {
return Err(ComputerError::WrongState {
id: sandbox_id.clone(),
expected: expected_state.to_string(),
actual: inst.state.to_string(),
});
}
if inst.spec.kernel.is_empty() {
return Err(ComputerError::FailedPrecondition(format!(
"sandbox {sandbox_id} cannot be checkpointed: its durable record has no kernel path"
)));
}
if inst.spec.rootfs.is_empty() {
return Err(ComputerError::FailedPrecondition(format!(
"sandbox {sandbox_id} cannot be checkpointed: its durable record has no rootfs path"
)));
}
let handle = inst
.handle
.clone()
.ok_or_else(|| ComputerError::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: &ComputerId,
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(ComputerError::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: &ComputerId,
source: CheckpointSource,
request: CheckpointRequest,
) -> Result<CheckpointInfo> {
let CheckpointRequest {
name,
labels,
resume_after,
..
} = request;
let checkpoint = source.handle.checkpoint().ok_or_else(|| {
ComputerError::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(),
})
}