use std::path::Path;
use super::*;
use crate::snapshot::SnapshotCatalog;
pub(super) use super::policy::warm::{WarmCache, WarmKey, reject_reserved_labels, warm_eligible};
use super::policy::warm::{FileFingerprint, WARM_KEY_LABEL, warm_key};
fn fingerprint(path: &Path) -> std::io::Result<FileFingerprint> {
use std::os::unix::fs::MetadataExt;
let meta = std::fs::metadata(path)?;
Ok(FileFingerprint {
dev: meta.dev(),
ino: meta.ino(),
mtime: meta.mtime(),
mtime_nsec: meta.mtime_nsec(),
size: meta.size(),
})
}
pub(super) fn derive_warm_key(spec: &ComputerSpec) -> std::io::Result<WarmKey> {
Ok(warm_key(
spec,
fingerprint(Path::new(&spec.kernel))?,
fingerprint(Path::new(&spec.rootfs))?,
))
}
pub(super) struct WarmEntry {
pub(super) snapshot_id: String,
pub(super) key: String,
pub(super) created_at: DateTime<Utc>,
}
pub(super) fn warm_entries(catalog: &SnapshotCatalog) -> Result<Vec<WarmEntry>> {
Ok(catalog
.list_all()?
.into_iter()
.filter_map(|snapshot| {
snapshot.labels.get(WARM_KEY_LABEL).map(|key| WarmEntry {
snapshot_id: snapshot.id,
key: key.clone(),
created_at: snapshot.created_at,
})
})
.collect())
}
pub(super) fn find_warm_snapshot(
catalog: &SnapshotCatalog,
key: &WarmKey,
) -> Result<Option<String>> {
Ok(warm_entries(catalog)?
.into_iter()
.filter(|entry| entry.key == key.hex())
.max_by_key(|entry| entry.created_at)
.map(|entry| entry.snapshot_id))
}
pub struct WarmPublishTicket {
pub(super) key: WarmKey,
pub(super) cache: Arc<WarmCache>,
pub(super) snapshots: Arc<SnapshotCatalog>,
pub(super) pool: Arc<super::pool::SlotPool>,
}
pub async fn publish_after_boot(
sandbox_id: &ComputerId,
ticket: &WarmPublishTicket,
computer: &Arc<Mutex<ComputerRuntime>>,
cow_manager: &CowManager,
expected_state: ComputerState,
) -> Result<()> {
if !ticket.cache.begin_publish(&ticket.key) {
debug!(
sandbox_id,
"a warm snapshot publish for this key is already in flight"
);
return Ok(());
}
let started = std::time::Instant::now();
let published =
publish_warm_snapshot(sandbox_id, ticket, computer, cow_manager, expected_state).await;
ticket.cache.end_publish(&ticket.key);
match published {
Ok(Some(snapshot_id)) => {
let checkpoint_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX);
info!(
sandbox_id,
snapshot_id, checkpoint_ms, "warm template snapshot published"
);
Ok(())
}
Ok(None) => Ok(()),
Err(PublishFailure::Frozen(error)) => Err(error),
Err(PublishFailure::Recoverable(error)) => {
warn!(
sandbox_id,
%error,
"warm snapshot publish failed; later creates keep cold-booting"
);
Ok(())
}
}
}
enum PublishFailure {
Recoverable(ComputerError),
Frozen(ComputerError),
}
impl From<ComputerError> for PublishFailure {
fn from(error: ComputerError) -> Self {
Self::Recoverable(error)
}
}
impl From<crate::lifecycle::tasks::checkpoint::CheckpointFailure> for PublishFailure {
fn from(failure: crate::lifecycle::tasks::checkpoint::CheckpointFailure) -> Self {
use crate::lifecycle::tasks::checkpoint::CheckpointFailure;
match failure {
CheckpointFailure::Recoverable(error) => Self::Recoverable(error),
CheckpointFailure::Frozen(error) => Self::Frozen(error),
}
}
}
async fn publish_warm_snapshot(
sandbox_id: &ComputerId,
ticket: &WarmPublishTicket,
computer: &Arc<Mutex<ComputerRuntime>>,
cow_manager: &CowManager,
expected_state: ComputerState,
) -> std::result::Result<Option<String>, PublishFailure> {
if warm_entries(&ticket.snapshots)?
.iter()
.any(|entry| entry.key == ticket.key.hex())
{
return Ok(None);
}
let name = format!("warm-{}", &ticket.key.hex()[..12]);
let labels = HashMap::from([(WARM_KEY_LABEL.to_owned(), ticket.key.hex().to_owned())]);
let info = crate::lifecycle::tasks::checkpoint::checkpoint_impl(
computer,
&ticket.snapshots,
sandbox_id,
crate::lifecycle::tasks::checkpoint::CheckpointRequest {
name,
labels,
expected_state,
resume_after: true,
},
)
.await?;
ticket.cache.touch(&ticket.key);
let entries = warm_entries(&ticket.snapshots)?;
let mut newest: HashMap<String, DateTime<Utc>> = HashMap::new();
for entry in &entries {
newest
.entry(entry.key.clone())
.and_modify(|at| *at = (*at).max(entry.created_at))
.or_insert(entry.created_at);
}
let keys: Vec<(String, DateTime<Utc>)> = newest.into_iter().collect();
let evicted = ticket.cache.plan_evictions(&keys);
for entry in entries {
let replaced = entry.key == ticket.key.hex() && entry.snapshot_id != info.snapshot_id;
if replaced || evicted.contains(&entry.key) {
super::pool::drain_pool_slots(&ticket.pool, cow_manager, Some(&entry.snapshot_id))
.await;
if let Err(error) = ticket.snapshots.delete_by_id(&entry.snapshot_id) {
warn!(
snapshot_id = %entry.snapshot_id,
%error,
"failed to delete a superseded warm snapshot"
);
}
}
}
Ok(Some(info.snapshot_id))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::snapshot::SnapshotDraft;
fn fingerprint_of(ino: u64) -> FileFingerprint {
FileFingerprint {
dev: 5,
ino,
mtime: 1_700_000_000,
mtime_nsec: 123,
size: 64 << 20,
}
}
fn some_key() -> WarmKey {
warm_key(&base_spec(), fingerprint_of(7), fingerprint_of(42))
}
fn base_spec() -> ComputerSpec {
ComputerSpec {
kernel: "/run/kernel/vmlinux".into(),
rootfs: "/data/rootfs.ext4".into(),
boot_args: "console=ttyS0 quiet".into(),
vcpus: 2,
memory_mib: 512,
network: ComputerNetworkSpec { mode: "tap".into() },
..Default::default()
}
}
#[test]
fn rebuilding_a_boot_input_in_place_changes_the_key() {
let dir = tempfile::tempdir().unwrap();
let kernel = dir.path().join("vmlinux");
let rootfs = dir.path().join("rootfs.ext4");
std::fs::write(&kernel, b"kernel v1").unwrap();
std::fs::write(&rootfs, b"template v1").unwrap();
let mut spec = base_spec();
spec.kernel = kernel.to_str().unwrap().to_owned();
spec.rootfs = rootfs.to_str().unwrap().to_owned();
let first = derive_warm_key(&spec).unwrap();
assert_eq!(derive_warm_key(&spec).unwrap(), first, "stat is stable");
let staging = dir.path().join(".rootfs.tmp");
std::fs::write(&staging, b"template v2").unwrap();
std::fs::rename(&staging, &rootfs).unwrap();
let second = derive_warm_key(&spec).unwrap();
assert_ne!(second, first);
let staging = dir.path().join(".vmlinux.tmp");
std::fs::write(&staging, b"kernel v2").unwrap();
std::fs::rename(&staging, &kernel).unwrap();
assert_ne!(derive_warm_key(&spec).unwrap(), second);
}
fn publish_labeled(catalog: &SnapshotCatalog, vm_id: &str, key: Option<&str>) -> String {
let pending = catalog.begin(vm_id).unwrap();
std::fs::write(pending.dir().join("vmstate"), b"vmstate").unwrap();
let labels = key
.map(|key| HashMap::from([(WARM_KEY_LABEL.to_owned(), key.to_owned())]))
.unwrap_or_default();
pending
.commit(SnapshotDraft {
name: None,
labels,
snapshot_type: crate::config::SnapshotType::Full,
parent_id: None,
kernel_path: None,
rootfs_path: None,
net_invariant: true,
geometry: None,
format: crate::sandbox::checkpoint::CHECKPOINT_FORMAT.to_owned(),
})
.unwrap()
.id
}
#[test]
fn lookup_matches_the_label_and_prefers_the_newest() {
let dir = tempfile::tempdir().unwrap();
let catalog = SnapshotCatalog::new(dir.path().to_str().unwrap());
let key = some_key();
let hex = key.hex().to_owned();
assert_eq!(find_warm_snapshot(&catalog, &key).unwrap(), None);
publish_labeled(&catalog, "box-1", None);
publish_labeled(&catalog, "box-1", Some("other-key"));
assert_eq!(find_warm_snapshot(&catalog, &key).unwrap(), None);
let older = publish_labeled(&catalog, "box-1", Some(&hex));
let newer = publish_labeled(&catalog, "box-2", Some(&hex));
let found = find_warm_snapshot(&catalog, &key).unwrap().unwrap();
assert_eq!(found, newer);
assert_ne!(found, older);
}
}