use std::sync::Arc;
use std::time::Duration;
use chrono::Utc;
use tracing::{info, warn};
use super::{ComputerFlows, Launch, RestoreLaunch, warm_create};
use crate::agent::GuestAgent;
use crate::error::ComputerError;
use crate::lifecycle::event::RestoreOrigin;
use crate::lifecycle::tasks::restore::{RestoreTimings, RestoreVm, RestoredVm, restore_vm};
use crate::lifecycle::tasks::resume::restore_paused;
use crate::lifecycle::tasks::{TaskFailure, TaskResult};
use crate::sandbox::reconcile::{ComputerStateRecord, JournaledLease, write_state_record};
use crate::sandbox::record::ComputerProvisionOutcome;
use crate::sandbox::{journaled_pid, journaled_vmm, pool};
impl ComputerFlows {
pub(super) async fn restore_vm(
&self,
origin: RestoreOrigin,
) -> TaskResult<(Arc<dyn GuestAgent>, ComputerProvisionOutcome)> {
let Launch::Restore(launch) = self.take_launch() else {
return Err(self.wrong_launch("restore"));
};
let RestoreLaunch {
snapshot_id,
snap_meta,
lease,
nic,
started,
} = *launch;
let services = &self.services;
let ip_address = lease
.as_ref()
.map(|lease| lease.ip.to_string())
.unwrap_or_default();
let vm_dir = self.computer.lock().unwrap().vm_dir.clone();
let Some(jailer) = services.config.firecracker.jailer.clone() else {
return Err(TaskFailure::recoverable(ComputerError::Config(
"checkpoint restore requires jailer isolation; direct mode embeds shared origin \
paths"
.into(),
)));
};
let claimed = pool::claim_restore_slot(&services.pool, &services.cow_manager, &snapshot_id);
let restored = restore_vm(RestoreVm {
new_id: &self.id,
snap_meta: &snap_meta,
lease: lease.as_ref(),
nic,
net_invariant: snap_meta.net_invariant,
vm_dir: &vm_dir,
jailer: &jailer,
instance: &self.computer,
claimed,
config: &services.config,
cow_manager: &services.cow_manager,
driver: &*services.driver,
network: &*services.network,
agents: &*services.agents,
})
.await;
let RestoredVm {
prepared,
handle,
agent,
identity,
cow_handle,
pool_hit,
timings,
} = match restored {
Ok(restored) => restored,
Err(failure) => {
let pool_slot_id = self.computer.lock().unwrap().pool_slot_id.clone();
let journal = ComputerStateRecord::new(
&self.id,
failure.prepared.as_deref().and_then(journaled_pid),
lease
.as_ref()
.map(|lease| JournaledLease::from_snapshot(lease, snap_meta.net_invariant)),
failure.cow_handle.as_ref(),
&services.config,
None,
)
.map(|record| {
record
.with_vmm(failure.prepared.as_deref().and_then(journaled_vmm))
.with_pool_slot(pool_slot_id.as_deref())
})
.and_then(|record| write_state_record(&vm_dir, &record));
if let Err(error) = journal {
warn!(sandbox_id = %self.id, %error, "the failed restore's journal was not written");
}
let mut computer = self.computer.lock().unwrap();
computer.prepared = failure.prepared;
computer.network = lease;
computer.cow_handle = failure.cow_handle;
computer.net_invariant = snap_meta.net_invariant;
return Err(TaskFailure::recoverable(failure.error));
}
};
{
let mut computer = self.computer.lock().unwrap();
computer.network = lease;
computer.prepared = Some(prepared);
computer.handle = Some(handle);
computer.net_identity = identity;
computer.cow_handle = cow_handle;
computer.net_invariant = snap_meta.net_invariant;
computer.ready_at = Some(Utc::now());
}
let RestoreTimings {
prepared: t_prepared,
staged: t_staged,
loaded: t_loaded,
guest_cfg: t_guest_cfg,
} = timings;
let ms = |d: Duration| u64::try_from(d.as_millis()).unwrap_or(u64::MAX);
info!(
sandbox_id = %self.id,
snapshot_id = %snapshot_id,
pool_hit,
warm_create = warm_create(origin),
spawn_ms = ms(t_prepared.duration_since(started)),
stage_ms = ms(t_staged.duration_since(t_prepared)),
load_ms = ms(t_loaded.duration_since(t_staged)),
guest_cfg_ms = ms(t_guest_cfg.duration_since(t_loaded)),
total_ms = ms(started.elapsed()),
"computer restored from checkpoint"
);
pool::spawn_pool_refill(
&services.pool,
&services.driver,
&services.config,
&services.cow_manager,
&services.snapshots,
&snapshot_id,
);
Ok((agent, ComputerProvisionOutcome { ip_address }))
}
pub(super) async fn resume_vm(&self) -> TaskResult<Arc<dyn GuestAgent>> {
let (snapshot_id, vm_dir, networked) = {
let computer = self.computer.lock().unwrap();
(
computer.pause_snapshot_id.clone(),
computer.vm_dir.clone(),
computer.spec.network.mode != "none",
)
};
let recoverable = TaskFailure::recoverable;
let snapshot_id = snapshot_id.ok_or_else(|| {
recoverable(ComputerError::Snapshot(format!(
"paused computer {} has no pause checkpoint recorded",
self.id
)))
})?;
let services = &self.services;
let jailer = services.config.firecracker.jailer.clone().ok_or_else(|| {
recoverable(ComputerError::Config(
"computer resume requires jailer isolation".into(),
))
})?;
let snap_meta = services
.snapshots
.find_by_id(&snapshot_id)
.map_err(|error| recoverable(error.into()))?;
let started = std::time::Instant::now();
let resumed = restore_paused(
&self.id,
&jailer,
&snap_meta,
&vm_dir,
networked,
&services.config,
&services.cow_manager,
&*services.driver,
&*services.network,
&*services.agents,
)
.await
.map_err(|failure| {
if failure.unwound {
TaskFailure::recoverable(failure.error)
} else {
TaskFailure::stranded(failure.error)
}
})?;
{
let mut computer = self.computer.lock().unwrap();
computer.prepared = Some(resumed.prepared);
computer.handle = Some(resumed.handle);
computer.net_identity = resumed.net_identity;
computer.network = resumed.network;
computer.cow_handle = resumed.cow_handle;
computer.net_invariant = snap_meta.net_invariant;
computer.paused_at = None;
computer.pause_snapshot_id = None;
computer.ready_at = Some(Utc::now());
}
if let Err(error) = services.snapshots.delete_by_id(&snapshot_id) {
warn!(
sandbox_id = %self.id,
snapshot_id = %snapshot_id,
%error,
"resumed, but deleting the pause checkpoint failed"
);
}
info!(
sandbox_id = %self.id,
total_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX),
"computer resumed from pause checkpoint"
);
let agent = self.agent().map_err(TaskFailure::recoverable)?;
Ok(agent)
}
}