use super::boot::{StageError, stage_rootfs_cow_or_copy};
use super::reconcile::{self, POOL_SLOT_PREFIX, SandboxStateRecord};
use super::*;
use crate::config::JailerConfig;
use crate::snapshot::SnapshotMeta;
pub use super::policy::pool::SlotPool;
pub struct PreparedSlot {
pub slot_id: String,
pub prepared: Arc<dyn PreparedVm>,
pub cow_handle: Option<CowHandle>,
pub rootfs: PathBuf,
pub image: CheckpointImage,
pub vm_dir: PathBuf,
}
pub(super) async fn prepare_slot(
driver: &dyn VmDriver,
config: &RuntimeConfig,
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 mut suffix = Uuid::new_v4().simple().to_string();
suffix.truncate(16);
let slot_id = format!("{POOL_SLOT_PREFIX}{suffix}");
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, config, None)?,
)?;
match stage_slot(driver, config, jc, cow_manager, snapshot, &slot_id, &vm_dir).await {
Ok(staged) => Ok(PreparedSlot {
slot_id,
prepared: staged.prepared,
cow_handle: staged.cow_handle,
rootfs: staged.rootfs,
image: staged.image,
vm_dir,
}),
Err(mut failure) => {
let fc_unwound = match failure.prepared.take() {
Some(prepared) => match prepared.discard().await {
Ok(_) => true,
Err(error) => {
warn!(slot_id, error = %error, "failed slot prepare left its vmm 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 {
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,
prepared: Option<Arc<dyn PreparedVm>>,
cow_handle: Option<CowHandle>,
}
impl From<VmmError> for SlotFailure {
fn from(error: VmmError) -> Self {
Self {
error,
prepared: None,
cow_handle: None,
}
}
}
struct StagedSlot {
prepared: Arc<dyn PreparedVm>,
cow_handle: Option<CowHandle>,
rootfs: PathBuf,
image: CheckpointImage,
}
#[allow(
clippy::result_large_err,
reason = "the failure hands back the half-built sandbox's resources; boxing it is a refactor of its own"
)]
async fn stage_slot(
driver: &dyn VmDriver,
config: &RuntimeConfig,
jc: &JailerConfig,
cow_manager: &CowManager,
snapshot: &SnapshotMeta,
slot_id: &str,
vm_dir: &Path,
) -> std::result::Result<StagedSlot, SlotFailure> {
let prepared: Arc<dyn PreparedVm> = Arc::from(
super::prepare_capability(driver)
.prepare(
&VmId::new(slot_id).map_err(VmmError::from)?,
&IsolationSpec::try_from(jc)?,
vm_dir,
)
.await
.map_err(VmmError::from)?,
);
let pid = super::journaled_pid(&*prepared);
let vmm = super::journaled_vmm(&*prepared);
let journal = |cow: Option<&CowHandle>| {
reconcile::write_state_record(
vm_dir,
&SandboxStateRecord::new(slot_id, pid, None, cow, config, None)?.with_vmm(vmm.clone()),
)
};
let carry = |error: VmmError, prepared, cow_handle| SlotFailure {
error,
prepared,
cow_handle,
};
if let Err(error) = journal(None) {
return Err(carry(error, Some(prepared), None));
}
let staging = super::staging_capability(prepared.staging());
if let Some(kernel) = snapshot.kernel_path.as_deref() {
if let Err(error) = staging.stage_kernel(Path::new(kernel)).await {
return Err(carry(error.into(), Some(prepared), None));
}
}
let Some(rootfs) = snapshot.rootfs_path.as_deref() else {
return Err(carry(
VmmError::Snapshot(format!(
"checkpoint {} records no rootfs template; there is no disk to pre-warm a slot \
with",
snapshot.id
)),
Some(prepared),
None,
));
};
let staged =
match stage_rootfs_cow_or_copy(cow_manager, staging, slot_id, rootfs, &journal).await {
Ok(staged) => staged,
Err(StageError {
error,
cow_handle: leaked,
}) => return Err(carry(error, Some(prepared), leaked)),
};
let image = match super::catalogued_checkpoint(snapshot) {
Ok(catalogued) => staging
.stage_checkpoint(&catalogued)
.await
.map_err(Into::into),
Err(error) => Err(error),
};
match image {
Ok(image) => Ok(StagedSlot {
prepared,
cow_handle: staged.cow_handle,
rootfs: staged.path,
image,
}),
Err(error) => Err(carry(error, Some(prepared), staged.cow_handle)),
}
}
pub(super) async fn drain_pool_slots(
pool: &SlotPool,
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(cow_manager, slot).await {
warn!(
slot_id,
error = %error,
"pool slot teardown incomplete; the startup sweep will retry"
);
}
}
}
pub(super) async fn destroy_slot(cow_manager: &CowManager, mut slot: PreparedSlot) -> Result<()> {
slot.prepared.discard().await?;
if let Some(handle) = slot.cow_handle.take() {
cow_manager.teardown_checked(&handle).await?;
}
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)),
}
}
pub fn claim_restore_slot(
pool: &SlotPool,
cow_manager: &Arc<CowManager>,
snapshot_id: &str,
) -> Option<PreparedSlot> {
loop {
let slot = pool.claim(snapshot_id)?;
if slot.prepared.alive() {
return Some(slot);
}
warn!(
snapshot_id,
slot_id = %slot.slot_id,
"discarding pre-warmed slot whose vmm died"
);
spawn_slot_teardown(cow_manager, slot);
}
}
fn spawn_slot_teardown(cow_manager: &Arc<CowManager>, slot: PreparedSlot) {
{
let cow_manager = Arc::clone(cow_manager);
tokio::spawn(async move {
let slot_id = slot.slot_id.clone();
if let Err(error) = destroy_slot(&cow_manager, slot).await {
warn!(
slot_id,
error = %error,
"pool slot teardown incomplete; the startup sweep will retry"
);
}
});
}
}
pub fn spawn_pool_refill(
pool: &Arc<SlotPool>,
driver: &Arc<dyn VmDriver>,
config: &Arc<RuntimeConfig>,
cow_manager: &Arc<CowManager>,
snapshots: &Arc<SnapshotCatalog>,
snapshot_id: &str,
) {
{
let plan = pool.begin_fill(snapshot_id, config.firecracker.pool_size);
for slot in plan.evicted {
spawn_slot_teardown(cow_manager, slot);
}
for _ in 0..plan.spawn {
let pool = Arc::clone(pool);
let driver = Arc::clone(driver);
let config = Arc::clone(config);
let cow_manager = Arc::clone(cow_manager);
let snapshots = Arc::clone(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(driver.as_ref(), &config, &cow_manager, &meta).await,
Err(error) => Err(error.into()),
};
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(&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");
}
}
});
}
}
}
impl SandboxManager {
pub(super) async fn drain_pool(&self, snapshot_id: Option<&str>) {
drain_pool_slots(&self.pool, &self.cow_manager, snapshot_id).await;
}
pub async fn shutdown(&self) {
self.drain_pool(None).await;
}
}