use super::boot::{
StageError, chroot_root, kill_and_reap_fc_checked, stage_kernel_for_jailer,
stage_rootfs_cow_or_copy, stage_snapshot_files,
};
use super::reconcile::{self, POOL_SLOT_PREFIX, SandboxStateRecord};
use super::*;
use crate::config::JailerConfig;
use crate::snapshot::SnapshotMeta;
const MAX_POOLED_SNAPSHOTS: usize = 2;
pub(super) struct PreparedSlot {
pub slot_id: String,
pub process: fc_sdk::FirecrackerProcess,
pub cow_handle: Option<CowHandle>,
pub vmstate_path: String,
pub mem_path: Option<String>,
pub vsock_path: PathBuf,
pub vm_dir: PathBuf,
}
impl PreparedSlot {
pub fn process_alive(&self) -> bool {
self.process.pid().is_some_and(|pid| {
match std::fs::read_to_string(format!("/proc/{pid}/stat")) {
Ok(stat) => stat
.rsplit_once(')')
.and_then(|(_, rest)| rest.split_whitespace().next())
.is_some_and(|state| state != "Z" && state != "X"),
Err(_) => false,
}
})
}
}
pub(super) struct FillPlan<S> {
pub spawn: usize,
pub evicted: Vec<S>,
}
pub(super) struct SlotPool<S = PreparedSlot> {
inner: Mutex<PoolInner<S>>,
}
struct PoolInner<S> {
ready: HashMap<String, Vec<S>>,
filling: HashMap<String, usize>,
lru: Vec<String>,
}
impl<S> Default for SlotPool<S> {
fn default() -> Self {
Self {
inner: Mutex::new(PoolInner {
ready: HashMap::new(),
filling: HashMap::new(),
lru: Vec::new(),
}),
}
}
}
impl<S> SlotPool<S> {
pub fn claim(&self, snapshot_id: &str) -> Option<S> {
let mut inner = self.inner.lock().unwrap();
let slot = inner.ready.get_mut(snapshot_id)?.pop()?;
inner.touch(snapshot_id);
inner.prune(snapshot_id);
Some(slot)
}
pub fn begin_fill(&self, snapshot_id: &str, target: usize) -> FillPlan<S> {
if target == 0 {
return FillPlan {
spawn: 0,
evicted: Vec::new(),
};
}
let mut inner = self.inner.lock().unwrap();
inner.touch(snapshot_id);
let ready = inner.ready.get(snapshot_id).map_or(0, Vec::len);
let filling = inner.filling.get(snapshot_id).copied().unwrap_or(0);
let spawn = target.saturating_sub(ready + filling);
if spawn > 0 {
*inner.filling.entry(snapshot_id.to_owned()).or_default() += spawn;
}
let mut evicted = Vec::new();
while inner.lru.len() > MAX_POOLED_SNAPSHOTS {
let stale = inner.lru.remove(0);
evicted.extend(inner.ready.remove(&stale).unwrap_or_default());
}
FillPlan { spawn, evicted }
}
pub fn offer(&self, snapshot_id: &str, slot: S) -> Option<S> {
let mut inner = self.inner.lock().unwrap();
inner.finish_fill(snapshot_id);
if inner.lru.iter().any(|id| id == snapshot_id) {
inner
.ready
.entry(snapshot_id.to_owned())
.or_default()
.push(slot);
None
} else {
Some(slot)
}
}
pub fn abandon_fill(&self, snapshot_id: &str) {
let mut inner = self.inner.lock().unwrap();
inner.finish_fill(snapshot_id);
inner.prune(snapshot_id);
}
pub fn drain(&self, snapshot_id: Option<&str>) -> Vec<S> {
let mut inner = self.inner.lock().unwrap();
match snapshot_id {
Some(id) => {
inner.lru.retain(|entry| entry != id);
inner.ready.remove(id).unwrap_or_default()
}
None => {
inner.lru.clear();
inner.ready.drain().flat_map(|(_, slots)| slots).collect()
}
}
}
}
impl<S> PoolInner<S> {
fn touch(&mut self, snapshot_id: &str) {
self.lru.retain(|id| id != snapshot_id);
self.lru.push(snapshot_id.to_owned());
}
fn prune(&mut self, snapshot_id: &str) {
let ready = self.ready.get(snapshot_id).is_some_and(|s| !s.is_empty());
let filling = self.filling.get(snapshot_id).copied().unwrap_or(0) > 0;
if !ready && !filling {
self.ready.remove(snapshot_id);
self.lru.retain(|id| id != snapshot_id);
}
}
fn finish_fill(&mut self, snapshot_id: &str) {
if let Some(count) = self.filling.get_mut(snapshot_id) {
*count = count.saturating_sub(1);
if *count == 0 {
self.filling.remove(snapshot_id);
}
}
}
}
pub(super) async fn prepare_slot(
config: &VmmConfig,
cow_manager: &CowManager,
snapshot: &SnapshotMeta,
) -> Result<PreparedSlot> {
let fc_cfg = &config.firecracker;
let jc = fc_cfg
.jailer
.as_ref()
.ok_or_else(|| VmmError::Config("restore slot pooling requires jailer isolation".into()))?;
let slot_id = format!("{POOL_SLOT_PREFIX}{}", Uuid::new_v4());
let vm_dir = PathBuf::from(&fc_cfg.data_dir)
.join("sandboxes")
.join(&slot_id);
reconcile::create_runtime_dir(&vm_dir)?;
reconcile::write_state_record(
&vm_dir,
&SandboxStateRecord::new(&slot_id, None, None, None, true, None),
)?;
match stage_slot(fc_cfg, jc, cow_manager, snapshot, &slot_id, &vm_dir).await {
Ok((process, cow_handle, vmstate_path, mem_path, vsock_path)) => Ok(PreparedSlot {
slot_id,
process,
cow_handle,
vmstate_path,
mem_path,
vsock_path,
vm_dir,
}),
Err(mut failure) => {
let fc_unwound = match failure.process.take() {
Some(mut process) => match kill_and_reap_fc_checked(&mut process).await {
Ok(()) => true,
Err(error) => {
warn!(slot_id, error = %error, "failed slot prepare left its firecracker behind");
false
}
},
None => true,
};
let cow_unwound = match failure.cow_handle.take() {
Some(handle) => match cow_manager.teardown_checked(&handle).await {
Ok(()) => true,
Err(error) => {
warn!(slot_id, error = %error, "failed slot prepare left CoW resources behind");
false
}
},
None => true,
};
if fc_unwound && cow_unwound {
let base = jc.chroot_base_dir.as_deref().unwrap_or("/srv/jailer");
let chroot = chroot_root(&fc_cfg.binary, base, &slot_id);
if let Some(parent) = chroot.parent() {
let _ = tokio::fs::remove_dir_all(parent).await;
}
if let Err(error) = reconcile::clear_state_record(&vm_dir) {
warn!(slot_id, error = %error, "failed slot prepare kept its journal");
} else {
let _ = tokio::fs::remove_dir_all(&vm_dir).await;
}
}
Err(failure.error)
}
}
}
struct SlotFailure {
error: VmmError,
process: Option<fc_sdk::FirecrackerProcess>,
cow_handle: Option<CowHandle>,
}
impl From<VmmError> for SlotFailure {
fn from(error: VmmError) -> Self {
Self {
error,
process: None,
cow_handle: None,
}
}
}
type StagedSlot = (
fc_sdk::FirecrackerProcess,
Option<CowHandle>,
String,
Option<String>,
PathBuf,
);
async fn stage_slot(
fc_cfg: &crate::config::FirecrackerConfig,
jc: &JailerConfig,
cow_manager: &CowManager,
snapshot: &SnapshotMeta,
slot_id: &str,
vm_dir: &Path,
) -> std::result::Result<StagedSlot, SlotFailure> {
let base = jc.chroot_base_dir.as_deref().unwrap_or("/srv/jailer");
let cr = chroot_root(&fc_cfg.binary, base, slot_id);
std::fs::create_dir_all(cr.join("run")).map_err(VmmError::Io)?;
let vsock_path = cr.join("run/firecracker.vsock");
let process = spawn_jailer(jc, fc_cfg, slot_id).await?;
#[allow(
clippy::cast_possible_wrap,
reason = "Firecracker pid fits platform pid_t"
)]
let pid = process.pid().map(|pid| pid as i32);
let journal = |cow: Option<&CowHandle>| {
reconcile::write_state_record(
vm_dir,
&SandboxStateRecord::new(slot_id, pid, None, cow, true, None),
)
};
let carry = |error: VmmError, process, cow_handle| SlotFailure {
error,
process,
cow_handle,
};
if let Err(error) = journal(None) {
return Err(carry(error, Some(process), None));
}
if let Some(kernel) = snapshot.kernel_path.as_deref() {
if let Err(error) = stage_kernel_for_jailer(&cr, kernel, jc.uid, jc.gid).await {
return Err(carry(error, Some(process), None));
}
}
let mut cow_handle = None;
if let Some(rootfs) = snapshot.rootfs_path.as_deref() {
match stage_rootfs_cow_or_copy(cow_manager, &cr, slot_id, rootfs, jc.uid, jc.gid, &journal)
.await
{
Ok(handle) => cow_handle = handle,
Err(StageError {
error,
cow_handle: leaked,
}) => return Err(carry(error, Some(process), leaked)),
}
}
match stage_snapshot_files(&cr, snapshot, jc.uid, jc.gid).await {
Ok((vmstate_path, mem_path)) => {
Ok((process, cow_handle, vmstate_path, mem_path, vsock_path))
}
Err(error) => Err(carry(error, Some(process), cow_handle)),
}
}
pub(super) async fn drain_pool_slots(
pool: &SlotPool,
config: &VmmConfig,
cow_manager: &CowManager,
snapshot_id: Option<&str>,
) {
for slot in pool.drain(snapshot_id) {
let slot_id = slot.slot_id.clone();
if let Err(error) = destroy_slot(config, cow_manager, slot).await {
warn!(
slot_id,
error = %error,
"pool slot teardown incomplete; the startup sweep will retry"
);
}
}
}
pub(super) async fn destroy_slot(
config: &VmmConfig,
cow_manager: &CowManager,
mut slot: PreparedSlot,
) -> Result<()> {
kill_and_reap_fc_checked(&mut slot.process).await?;
if let Some(handle) = slot.cow_handle.take() {
cow_manager.teardown_checked(&handle).await?;
}
if let Some(ref jc) = config.firecracker.jailer {
let base = jc.chroot_base_dir.as_deref().unwrap_or("/srv/jailer");
let chroot = chroot_root(&config.firecracker.binary, base, &slot.slot_id);
if let Some(parent) = chroot.parent()
&& let Err(error) = tokio::fs::remove_dir_all(parent).await
&& error.kind() != std::io::ErrorKind::NotFound
{
return Err(VmmError::Io(error));
}
}
reconcile::clear_state_record(&slot.vm_dir)?;
match tokio::fs::remove_dir_all(&slot.vm_dir).await {
Ok(()) => Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(VmmError::Io(error)),
}
}
impl SandboxManager {
pub(super) fn claim_restore_slot(&self, snapshot_id: &str) -> Option<PreparedSlot> {
loop {
let slot = self.pool.claim(snapshot_id)?;
if slot.process_alive() {
return Some(slot);
}
warn!(
snapshot_id,
slot_id = %slot.slot_id,
"discarding pre-warmed slot whose firecracker died"
);
self.spawn_slot_teardown(slot);
}
}
fn spawn_slot_teardown(&self, slot: PreparedSlot) {
let config = Arc::clone(&self.config);
let cow_manager = Arc::clone(&self.cow_manager);
tokio::spawn(async move {
let slot_id = slot.slot_id.clone();
if let Err(error) = destroy_slot(&config, &cow_manager, slot).await {
warn!(
slot_id,
error = %error,
"pool slot teardown incomplete; the startup sweep will retry"
);
}
});
}
pub(super) fn spawn_pool_refill(&self, snapshot_id: &str) {
let plan = self
.pool
.begin_fill(snapshot_id, self.config.firecracker.pool_size);
for slot in plan.evicted {
self.spawn_slot_teardown(slot);
}
for _ in 0..plan.spawn {
let pool = Arc::clone(&self.pool);
let config = Arc::clone(&self.config);
let cow_manager = Arc::clone(&self.cow_manager);
let snapshots = Arc::clone(&self.snapshots);
let snapshot_id = snapshot_id.to_owned();
tokio::spawn(async move {
let staged = match snapshots.find_by_id(&snapshot_id) {
Ok(meta) => prepare_slot(&config, &cow_manager, &meta).await,
Err(error) => Err(error),
};
match staged {
Ok(slot) => match pool.offer(&snapshot_id, slot) {
None => debug!(snapshot_id, "restore slot pre-warmed"),
Some(rejected) => {
let slot_id = rejected.slot_id.clone();
if let Err(error) = destroy_slot(&config, &cow_manager, rejected).await
{
warn!(
slot_id,
error = %error,
"pool slot teardown incomplete; the startup sweep will retry"
);
}
}
},
Err(error) => {
pool.abandon_fill(&snapshot_id);
warn!(snapshot_id, error = %error, "restore slot pre-warm failed");
}
}
});
}
}
pub(super) async fn drain_pool(&self, snapshot_id: Option<&str>) {
drain_pool_slots(&self.pool, &self.config, &self.cow_manager, snapshot_id).await;
}
pub async fn shutdown(&self) {
self.drain_pool(None).await;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fill_and_offer(pool: &SlotPool<u32>, snapshot: &str, target: usize, base: u32) {
let plan = pool.begin_fill(snapshot, target);
assert!(plan.evicted.is_empty(), "unexpected eviction while filling");
for offset in 0..plan.spawn {
assert!(
pool.offer(snapshot, base + u32::try_from(offset).unwrap())
.is_none()
);
}
}
#[test]
fn claims_are_keyed_by_snapshot_id() {
let pool = SlotPool::<u32>::default();
fill_and_offer(&pool, "a", 1, 10);
assert_eq!(pool.claim("other"), None);
assert_eq!(pool.claim("a"), Some(10));
assert_eq!(pool.claim("a"), None);
}
#[test]
fn refill_accounts_for_ready_and_in_flight_slots() {
let pool = SlotPool::<u32>::default();
let plan = pool.begin_fill("a", 2);
assert_eq!(plan.spawn, 2);
assert_eq!(pool.begin_fill("a", 2).spawn, 0);
assert!(pool.offer("a", 10).is_none());
assert!(pool.offer("a", 11).is_none());
assert_eq!(pool.begin_fill("a", 2).spawn, 0);
assert_eq!(pool.claim("a"), Some(11));
assert_eq!(pool.begin_fill("a", 2).spawn, 1);
pool.abandon_fill("a");
assert_eq!(pool.begin_fill("a", 2).spawn, 1);
}
#[test]
fn pool_size_zero_disables_pooling() {
let pool = SlotPool::<u32>::default();
let plan = pool.begin_fill("a", 0);
assert_eq!(plan.spawn, 0);
assert!(plan.evicted.is_empty());
assert_eq!(pool.claim("a"), None);
}
#[test]
fn a_third_snapshot_evicts_the_least_recently_restored() {
let pool = SlotPool::<u32>::default();
fill_and_offer(&pool, "a", 1, 10);
fill_and_offer(&pool, "b", 1, 20);
let plan = pool.begin_fill("c", 1);
assert_eq!(plan.spawn, 1);
assert_eq!(plan.evicted, vec![10], "a's slot must be handed back");
assert_eq!(pool.claim("a"), None);
assert_eq!(pool.claim("b"), Some(20));
}
#[test]
fn claiming_refreshes_recency() {
let pool = SlotPool::<u32>::default();
fill_and_offer(&pool, "a", 2, 10);
fill_and_offer(&pool, "b", 1, 20);
assert_eq!(pool.claim("a"), Some(11));
let plan = pool.begin_fill("c", 1);
assert_eq!(plan.evicted, vec![20]);
assert_eq!(pool.claim("b"), None);
assert_eq!(pool.claim("a"), Some(10));
}
#[test]
fn late_offers_for_an_evicted_snapshot_are_rejected() {
let pool = SlotPool::<u32>::default();
let plan = pool.begin_fill("a", 1);
assert_eq!(plan.spawn, 1);
fill_and_offer(&pool, "b", 1, 20);
fill_and_offer(&pool, "c", 1, 30);
assert_eq!(pool.offer("a", 10), Some(10), "must come back for teardown");
assert_eq!(pool.claim("a"), None);
}
#[test]
fn drain_scopes_to_one_snapshot_or_all() {
let pool = SlotPool::<u32>::default();
fill_and_offer(&pool, "a", 2, 10);
fill_and_offer(&pool, "b", 1, 20);
let mut drained = pool.drain(Some("a"));
drained.sort_unstable();
assert_eq!(drained, vec![10, 11]);
assert_eq!(pool.claim("a"), None);
let plan = pool.begin_fill("a", 1);
assert_eq!(plan.spawn, 1);
assert!(pool.drain(Some("a")).is_empty());
assert_eq!(pool.offer("a", 12), Some(12));
assert_eq!(pool.drain(None), vec![20]);
assert_eq!(pool.claim("b"), None);
}
}