use std::collections::{HashMap, HashSet};
use std::sync::Mutex;
use chrono::{DateTime, Utc};
use sha2::{Digest, Sha256};
use crate::config::RuntimeConfig;
use crate::error::{ComputerError, Result};
use crate::sandbox::ComputerSpec;
const MAX_WARM_KEYS: usize = 2;
pub(in crate::sandbox) const WARM_KEY_LABEL: &str = "arcbox.warm_key";
#[derive(Debug, Clone, PartialEq, Eq)]
pub(in crate::sandbox) struct WarmKey(String);
impl WarmKey {
pub(in crate::sandbox) fn hex(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::sandbox) struct FileFingerprint {
pub(in crate::sandbox) dev: u64,
pub(in crate::sandbox) ino: u64,
pub(in crate::sandbox) mtime: i64,
pub(in crate::sandbox) mtime_nsec: i64,
pub(in crate::sandbox) size: u64,
}
pub(in crate::sandbox) fn warm_key(
spec: &ComputerSpec,
kernel: FileFingerprint,
rootfs: FileFingerprint,
) -> WarmKey {
let mut hasher = Sha256::new();
for text in [&spec.kernel, &spec.rootfs, &spec.boot_args] {
hasher.update(text.as_bytes());
hasher.update([0u8]);
}
for number in [u64::from(spec.vcpus), spec.memory_mib] {
hasher.update(number.to_le_bytes());
}
for fingerprint in [kernel, rootfs] {
for number in [fingerprint.dev, fingerprint.ino, fingerprint.size] {
hasher.update(number.to_le_bytes());
}
for number in [fingerprint.mtime, fingerprint.mtime_nsec] {
hasher.update(number.to_le_bytes());
}
}
WarmKey(format!("{:x}", hasher.finalize()))
}
pub(in crate::sandbox) fn warm_eligible(
config: &RuntimeConfig,
spec: &ComputerSpec,
caller_supplied_boot: bool,
) -> bool {
config.firecracker.warm_create
&& config.firecracker.jailer.is_some()
&& !caller_supplied_boot
&& spec.network.mode == "tap"
&& !spec.boot_args.contains("ip=")
&& spec.mounts.is_empty()
&& spec.ssh_public_key.is_none()
}
pub(in crate::sandbox) fn reject_reserved_labels(labels: &HashMap<String, String>) -> Result<()> {
if labels.contains_key(WARM_KEY_LABEL) {
return Err(ComputerError::Config(format!(
"snapshot label {WARM_KEY_LABEL} is reserved for the warm-create cache"
)));
}
if labels.contains_key(crate::template_catalog::TEMPLATE_LABEL) {
return Err(ComputerError::Config(format!(
"snapshot label {} is reserved for the template catalog",
crate::template_catalog::TEMPLATE_LABEL
)));
}
Ok(())
}
#[derive(Default)]
pub(in crate::sandbox) struct WarmCache {
inner: Mutex<WarmCacheInner>,
}
#[derive(Default)]
struct WarmCacheInner {
recency: Vec<String>,
publishing: HashSet<String>,
}
impl WarmCache {
pub(in crate::sandbox) fn touch(&self, key: &WarmKey) {
let mut inner = self.inner.lock().unwrap();
inner.recency.retain(|entry| entry != key.hex());
inner.recency.push(key.hex().to_owned());
}
pub(in crate::sandbox) fn begin_publish(&self, key: &WarmKey) -> bool {
self.inner
.lock()
.unwrap()
.publishing
.insert(key.hex().to_owned())
}
pub(in crate::sandbox) fn end_publish(&self, key: &WarmKey) {
self.inner.lock().unwrap().publishing.remove(key.hex());
}
pub(in crate::sandbox) fn plan_evictions(
&self,
catalog: &[(String, DateTime<Utc>)],
) -> Vec<String> {
let mut inner = self.inner.lock().unwrap();
inner
.recency
.retain(|entry| catalog.iter().any(|(key, _)| key == entry));
let Some(excess) = catalog.len().checked_sub(MAX_WARM_KEYS).filter(|n| *n > 0) else {
return Vec::new();
};
let mut ranked: Vec<&(String, DateTime<Utc>)> = catalog.iter().collect();
ranked.sort_by_key(|(key, created_at)| {
let recency = inner
.recency
.iter()
.position(|entry| entry == key)
.map_or(-1, |position| i64::try_from(position).unwrap_or(i64::MAX));
(recency, *created_at)
});
ranked
.into_iter()
.take(excess)
.map(|(key, _)| key.clone())
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::JailerConfig;
use crate::sandbox::{ComputerMountSpec, ComputerNetworkSpec};
fn kernel_fingerprint() -> FileFingerprint {
FileFingerprint {
dev: 5,
ino: 7,
mtime: 1_690_000_000,
mtime_nsec: 456,
size: 9 << 20,
}
}
fn base_fingerprint() -> FileFingerprint {
FileFingerprint {
dev: 5,
ino: 42,
mtime: 1_700_000_000,
mtime_nsec: 123,
size: 64 << 20,
}
}
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()
}
}
fn warm_config() -> RuntimeConfig {
let mut config = RuntimeConfig::default();
config.firecracker.jailer = Some(JailerConfig {
uid: 0,
gid: 0,
chroot_base_dir: None,
netns: None,
new_pid_ns: false,
cgroup_version: None,
parent_cgroup: None,
});
config
}
#[test]
fn key_is_stable_for_an_identical_shape() {
assert_eq!(
warm_key(&base_spec(), kernel_fingerprint(), base_fingerprint()),
warm_key(&base_spec(), kernel_fingerprint(), base_fingerprint())
);
}
type SpecEdit = Box<dyn Fn(&mut ComputerSpec)>;
type FingerprintEdit = Box<dyn Fn(&mut FileFingerprint)>;
#[test]
fn key_tracks_every_boot_recipe_and_geometry_field() {
let base = warm_key(&base_spec(), kernel_fingerprint(), base_fingerprint());
let variants: Vec<SpecEdit> = vec![
Box::new(|s| s.kernel = "/run/kernel/vmlinux-new".into()),
Box::new(|s| s.rootfs = "/data/rootfs-other.ext4".into()),
Box::new(|s| s.boot_args.push_str(" debug")),
Box::new(|s| s.vcpus = 4),
Box::new(|s| s.memory_mib = 1024),
];
for mutate in variants {
let mut spec = base_spec();
mutate(&mut spec);
assert_ne!(
warm_key(&spec, kernel_fingerprint(), base_fingerprint()),
base,
"{spec:?}"
);
}
let mut spec = base_spec();
spec.labels.insert("team".into(), "x".into());
spec.cmd = vec!["sleep".into()];
spec.ttl_seconds = 60;
assert_eq!(
warm_key(&spec, kernel_fingerprint(), base_fingerprint()),
base
);
}
#[test]
fn key_tracks_every_fingerprint_field_of_both_files() {
let base = warm_key(&base_spec(), kernel_fingerprint(), base_fingerprint());
let variants: Vec<FingerprintEdit> = vec![
Box::new(|f| f.dev += 1),
Box::new(|f| f.ino += 1),
Box::new(|f| f.mtime += 1),
Box::new(|f| f.mtime_nsec += 1),
Box::new(|f| f.size += 1),
];
for mutate in &variants {
let mut fingerprint = base_fingerprint();
mutate(&mut fingerprint);
assert_ne!(
warm_key(&base_spec(), kernel_fingerprint(), fingerprint),
base,
"rootfs {fingerprint:?}"
);
}
for mutate in &variants {
let mut fingerprint = kernel_fingerprint();
mutate(&mut fingerprint);
assert_ne!(
warm_key(&base_spec(), fingerprint, base_fingerprint()),
base,
"kernel {fingerprint:?}"
);
}
assert_ne!(
warm_key(&base_spec(), base_fingerprint(), kernel_fingerprint()),
base
);
}
#[test]
fn eligibility_requires_the_template_shape() {
let config = warm_config();
assert!(warm_eligible(&config, &base_spec(), false));
assert!(!warm_eligible(&config, &base_spec(), true));
let mut no_warm = warm_config();
no_warm.firecracker.warm_create = false;
assert!(!warm_eligible(&no_warm, &base_spec(), false));
assert!(!warm_eligible(
&RuntimeConfig::default(),
&base_spec(),
false
));
let mut no_net = base_spec();
no_net.network.mode = "none".into();
assert!(!warm_eligible(&config, &no_net, false));
let mut explicit_ip = base_spec();
explicit_ip.boot_args.push_str(" ip=10.0.0.9::10.0.0.1");
assert!(!warm_eligible(&config, &explicit_ip, false));
let mut mounted = base_spec();
mounted.mounts.push(ComputerMountSpec {
source: "/src".into(),
target: "/dst".into(),
readonly: true,
});
assert!(!warm_eligible(&config, &mounted, false));
let mut ssh = base_spec();
ssh.ssh_public_key = Some("ssh-ed25519 AAAA".into());
assert!(!warm_eligible(&config, &ssh, false));
}
#[test]
fn reserved_label_is_rejected() {
assert!(reject_reserved_labels(&HashMap::new()).is_ok());
let mut labels = HashMap::new();
labels.insert("env".to_owned(), "prod".to_owned());
assert!(reject_reserved_labels(&labels).is_ok());
labels.insert(WARM_KEY_LABEL.to_owned(), "deadbeef".to_owned());
assert!(reject_reserved_labels(&labels).is_err());
let mut labels = HashMap::new();
labels.insert(
crate::template_catalog::TEMPLATE_LABEL.to_owned(),
"code".to_owned(),
);
assert!(reject_reserved_labels(&labels).is_err());
}
#[test]
fn publish_is_single_flighted_per_key() {
let cache = WarmCache::default();
let key_a = WarmKey("aa".into());
let key_b = WarmKey("bb".into());
assert!(cache.begin_publish(&key_a));
assert!(!cache.begin_publish(&key_a), "second publisher must skip");
assert!(cache.begin_publish(&key_b), "keys single-flight separately");
cache.end_publish(&key_a);
assert!(cache.begin_publish(&key_a), "slot frees on end_publish");
}
fn at(seconds: i64) -> DateTime<Utc> {
DateTime::from_timestamp(seconds, 0).unwrap()
}
#[test]
fn evictions_keep_the_cap_and_respect_recency() {
let cache = WarmCache::default();
let catalog = vec![
("a".to_owned(), at(100)),
("b".to_owned(), at(200)),
("c".to_owned(), at(300)),
];
assert_eq!(cache.plan_evictions(&catalog[..2]), Vec::<String>::new());
assert_eq!(cache.plan_evictions(&catalog), vec!["a".to_owned()]);
cache.touch(&WarmKey("a".into()));
assert_eq!(cache.plan_evictions(&catalog), vec!["b".to_owned()]);
cache.touch(&WarmKey("b".into()));
cache.touch(&WarmKey("c".into()));
assert_eq!(cache.plan_evictions(&catalog), vec!["a".to_owned()]);
}
#[test]
fn evictions_ignore_recency_of_deleted_keys() {
let cache = WarmCache::default();
cache.touch(&WarmKey("gone".into()));
let catalog = vec![
("a".to_owned(), at(100)),
("b".to_owned(), at(200)),
("c".to_owned(), at(300)),
];
assert_eq!(cache.plan_evictions(&catalog), vec!["a".to_owned()]);
}
}