mod actor;
mod balloon;
mod boot;
mod health;
mod machine;
mod recovery;
#[cfg(target_os = "macos")]
mod serial;
#[cfg(test)]
mod tests;
mod types;
use crate::boot_assets::BootAssetProvider;
use crate::error::{CoreError, Result};
use crate::event::EventBus;
use crate::machine::{MachineInfo, MachineManager};
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Duration;
use tokio::sync::{mpsc, oneshot, watch};
use actor::{Command, LifecycleActor, LifecycleShared};
pub const DEFAULT_MACHINE_NAME: &str = "default";
const DEFAULT_STARTUP_TIMEOUT_SECS: u64 = 90;
const DEFAULT_HEALTH_CHECK_INTERVAL_SECS: u64 = 5;
const DEFAULT_IDLE_TIMEOUT_SECS: u64 = 300;
const DEFAULT_MAX_RETRIES: u32 = 3;
const BALLOON_SHRINK_DELAY_SECS: u64 = 10;
const DOCKER_DATA_IMAGE_NAME: &str = "docker.img";
const DOCKER_DATA_IMAGE_SIZE_BYTES: u64 = 8 * 1024 * 1024 * 1024 * 1024;
const DOCKER_METADATA_IMAGE_SIZE_BYTES: u64 = 2 * 1024 * 1024 * 1024;
pub(crate) use boot::ensure_sparse_block_image;
pub use health::HealthMonitor;
pub use recovery::{BackoffStrategy, RecoveryAction, RecoveryPolicy};
pub use types::{DefaultVmConfig, VmLifecycleConfig, VmLifecycleState};
pub struct ActivityScope {
shared: Arc<LifecycleShared>,
}
impl Drop for ActivityScope {
fn drop(&mut self) {
self.shared.active_ops.fetch_sub(1, Ordering::AcqRel);
self.shared.record_activity();
}
}
struct ActorSeed {
commands: mpsc::UnboundedReceiver<Command>,
state_tx: watch::Sender<VmLifecycleState>,
}
pub struct VmLifecycleManager {
shared: Arc<LifecycleShared>,
cmd_tx: mpsc::UnboundedSender<Command>,
state_rx: watch::Receiver<VmLifecycleState>,
actor: OnceLock<()>,
seed: Mutex<Option<ActorSeed>>,
}
impl VmLifecycleManager {
pub fn new(
machine_manager: Arc<MachineManager>,
event_bus: EventBus,
data_dir: PathBuf,
config: VmLifecycleConfig,
) -> Result<Self> {
Self::for_machine(
String::from(DEFAULT_MACHINE_NAME),
String::from(DOCKER_DATA_IMAGE_NAME),
machine_manager,
event_bus,
data_dir,
config,
)
}
pub fn for_machine(
machine_name: String,
data_image_filename: String,
machine_manager: Arc<MachineManager>,
event_bus: EventBus,
data_dir: PathBuf,
config: VmLifecycleConfig,
) -> Result<Self> {
let boot_assets = Arc::new(
BootAssetProvider::with_config(
crate::boot_assets::BootAssetConfig::with_cache_dir(data_dir.join("boot"))
.with_unpinned_manifest_allowed(config.allow_unpinned_boot_manifest),
)?
.with_kernel(config.default_vm.kernel.clone().unwrap_or_default())?,
);
let health_monitor = Arc::new(HealthMonitor::new(
config.health_check_interval,
config.max_retries,
));
let recovery = RecoveryPolicy::new(config.max_retries, BackoffStrategy::default());
let seeded_backend = machine_manager
.get(&machine_name)
.map_or(config.backend, |info| info.backend);
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
let shared = Arc::new(LifecycleShared {
machine_name,
data_image_filename,
data_dir,
machine_manager,
event_bus,
boot_assets,
recovery,
health_monitor,
config,
backend: AtomicU8::new(seeded_backend as u8),
restart_generation: AtomicU64::new(0),
last_activity_ms: AtomicU64::new(now_ms),
active_ops: AtomicUsize::new(0),
kubernetes_hold: AtomicBool::new(false),
});
let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
let (state_tx, state_rx) = watch::channel(VmLifecycleState::NotExist);
Ok(Self {
shared,
cmd_tx,
state_rx,
actor: OnceLock::new(),
seed: Mutex::new(Some(ActorSeed {
commands: cmd_rx,
state_tx,
})),
})
}
fn ensure_actor(&self) {
self.actor.get_or_init(|| {
let seed = self
.seed
.lock()
.expect("lifecycle actor seed lock poisoned")
.take();
if let Some(seed) = seed {
let actor = LifecycleActor::new(
Arc::clone(&self.shared),
seed.commands,
self.cmd_tx.clone(),
seed.state_tx,
);
drop(tokio::spawn(actor.run()));
}
});
}
async fn request<T>(
&self,
command: Command,
reply_rx: oneshot::Receiver<Result<T>>,
) -> Result<T> {
self.ensure_actor();
self.cmd_tx
.send(command)
.map_err(|_| CoreError::Vm("VM lifecycle actor terminated".to_string()))?;
reply_rx
.await
.map_err(|_| CoreError::Vm("VM lifecycle actor terminated".to_string()))?
}
#[must_use]
pub fn machine_name(&self) -> &str {
&self.shared.machine_name
}
#[must_use]
pub fn restart_generation(&self) -> u64 {
self.shared
.restart_generation
.load(std::sync::atomic::Ordering::Acquire)
}
#[must_use]
pub fn data_image_path(&self) -> PathBuf {
self.shared
.data_dir
.join(arcbox_constants::paths::host::DATA)
.join(&self.shared.data_image_filename)
}
#[allow(clippy::unused_async, reason = "public API compatibility")]
pub async fn state(&self) -> VmLifecycleState {
*self.state_rx.borrow()
}
#[must_use]
pub fn subscribe_state(&self) -> watch::Receiver<VmLifecycleState> {
self.state_rx.clone()
}
#[allow(clippy::unused_async, reason = "public API compatibility")]
pub async fn is_running(&self) -> bool {
self.state_rx.borrow().is_ready()
}
pub async fn ensure_ready(&self) -> Result<u32> {
self.ensure_ready_with_timeout(self.shared.config.startup_timeout)
.await
}
pub async fn ensure_ready_with_timeout(&self, timeout: Duration) -> Result<u32> {
if self.shared.config.skip_vm_check {
tracing::debug!("ensure_ready: skipping VM check (test mode)");
let mock_cid = 3;
self.shared
.machine_manager
.register_mock_machine(&self.shared.machine_name, mock_cid)?;
return Ok(mock_cid);
}
let (reply, reply_rx) = oneshot::channel();
self.request(Command::EnsureReady { timeout, reply }, reply_rx)
.await
}
pub fn note_activity(&self) {
self.shared.record_activity();
if *self.state_rx.borrow() == VmLifecycleState::Idle {
let _ = self.cmd_tx.send(Command::Activity);
}
}
pub fn begin_activity(&self) -> ActivityScope {
self.note_activity();
self.shared.active_ops.fetch_add(1, Ordering::AcqRel);
ActivityScope {
shared: Arc::clone(&self.shared),
}
}
#[allow(clippy::unused_async, reason = "public API compatibility")]
pub async fn set_kubernetes_hold(&self, active: bool) {
self.shared
.kubernetes_hold
.store(active, std::sync::atomic::Ordering::Relaxed);
self.shared.record_activity();
if active {
self.ensure_actor();
let _ = self.cmd_tx.send(Command::Activity);
}
}
pub async fn shutdown(&self) -> Result<()> {
let (reply, reply_rx) = oneshot::channel();
self.request(Command::Shutdown { reply }, reply_rx).await
}
pub async fn force_stop(&self) -> Result<()> {
let (reply, reply_rx) = oneshot::channel();
self.request(Command::ForceStop { reply }, reply_rx).await
}
#[must_use]
pub fn backend(&self) -> arcbox_vmm::VmBackend {
self.shared.backend()
}
pub fn set_backend(&self, backend: arcbox_vmm::VmBackend) {
self.shared
.backend
.store(backend as u8, std::sync::atomic::Ordering::Release);
}
#[must_use]
pub fn config(&self) -> &VmLifecycleConfig {
&self.shared.config
}
#[must_use]
pub fn boot_assets(&self) -> &Arc<BootAssetProvider> {
&self.shared.boot_assets
}
#[must_use]
pub fn default_vm_config(&self) -> DefaultVmConfig {
self.shared.config.default_vm.clone()
}
#[must_use]
pub fn health_monitor(&self) -> &Arc<HealthMonitor> {
&self.shared.health_monitor
}
pub fn default_machine_info(&self) -> Option<MachineInfo> {
self.shared.machine_manager.get(&self.shared.machine_name)
}
}