use std::collections::{HashMap, VecDeque};
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use chrono::{DateTime, Utc};
use statig::blocking::IntoStateMachineExt;
use tokio::sync::{mpsc, oneshot, watch};
use tokio::task::JoinHandle;
use tracing::debug;
use uuid::Uuid;
use super::effect::{
Answer, Durability, Effect, Effects, Notify, RecordEnd, ReleaseScope, Timer, Unconfirmed,
};
pub use super::event::PauseReason;
use super::event::{Event, Provision};
use super::machine::{ComputerLifecycle, State};
use super::tasks::{CaptureSpec, ComputerTasks, Drain, TaskFailure};
use arcbox_vm_driver::VmHandle;
use arcbox_vm_driver::net::NetworkLease;
use crate::agent::{ExitStatus, GuestAgent};
use crate::error::{ComputerError, Result};
use crate::lifecycle::runtime::ComputerRuntime;
use crate::sandbox::policy::deadlines;
use crate::sandbox::record::{
ComputerProvisionOutcome, ComputerRecordStore, ComputerTransition, PersistPhase,
};
use crate::sandbox::types::action;
use crate::sandbox::workload::WorkloadClaim;
use crate::sandbox::{
CheckpointInfo, ComputerEvent, ComputerId, ComputerState, IdleAction, LifecycleUpdate,
};
mod commands;
mod effects;
mod inflight;
const HANDOFF_TIMEOUT: Duration = Duration::from_secs(10);
const DETACH_TIMEOUT: Duration = Duration::from_secs(5);
const RETRY_INITIAL: Duration = Duration::from_millis(250);
const RETRY_MAX: Duration = Duration::from_secs(5);
type Reply = oneshot::Sender<Result<()>>;
#[derive(Clone)]
pub struct Mailbox(mpsc::UnboundedSender<Command>);
impl Mailbox {
pub(crate) fn new(sender: mpsc::UnboundedSender<Command>) -> Self {
Self(sender)
}
pub(crate) async fn ask<T>(
&self,
id: &ComputerId,
command: impl FnOnce(oneshot::Sender<Result<T>>) -> Command,
) -> Result<T> {
let (reply, answer) = oneshot::channel();
self.0
.send(command(reply))
.map_err(|_| ComputerError::NotFound(id.clone()))?;
answer
.await
.map_err(|_| ComputerError::NotFound(id.clone()))?
}
pub(crate) fn tell(&self, command: Command) {
let _ = self.0.send(command);
}
pub(crate) fn downgrade(&self) -> WeakMailbox {
WeakMailbox(self.0.downgrade())
}
}
#[derive(Clone)]
pub struct WeakMailbox(mpsc::WeakUnboundedSender<Command>);
impl WeakMailbox {
pub(crate) fn upgrade(&self) -> Option<Mailbox> {
self.0.upgrade().map(Mailbox)
}
}
pub enum Command {
Provision {
provision: Provision,
outcome: ComputerProvisionOutcome,
reply: Reply,
},
Checkpoint {
spec: CaptureSpec,
reply: oneshot::Sender<Result<CheckpointInfo>>,
},
Pause {
reason: PauseReason,
reply: Reply,
},
Resume {
reason: String,
reply: Reply,
},
Stop {
budget: Duration,
reply: Reply,
},
Remove {
force: bool,
reply: Reply,
},
Detach {
reply: Reply,
},
ClaimWorkload {
claim: WorkloadClaim,
reply: Reply,
},
WorkloadExited {
outcome: WorkloadOutcome,
},
ReleaseWorkload,
SetLifecycle {
update: LifecycleUpdate,
reply: Reply,
},
VmExited,
}
#[derive(Debug, Clone)]
pub enum WorkloadOutcome {
Exited(ExitStatus),
Broke(String),
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Deadlines {
pub ttl: Option<DateTime<Utc>>,
pub idle_timeout_seconds: u32,
pub on_idle: IdleAction,
}
#[derive(Clone)]
pub struct ComputerSnapshot {
pub state: ComputerState,
pub agent: Option<Arc<dyn GuestAgent>>,
pub handle: Option<Arc<dyn VmHandle>>,
pub error: Option<String>,
pub labels: HashMap<String, String>,
pub vcpus: u32,
pub memory_mib: u64,
pub lease: Option<NetworkLease>,
pub vm_dir: PathBuf,
pub created_at: DateTime<Utc>,
pub ready_at: Option<DateTime<Utc>>,
pub last_exited_at: Option<DateTime<Utc>>,
pub last_exit_status: Option<ExitStatus>,
pub paused_at: Option<DateTime<Utc>>,
pub pause_snapshot_id: Option<String>,
pub cow_file: Option<PathBuf>,
pub deadlines: Deadlines,
}
impl ComputerSnapshot {
pub fn project(runtime: &ComputerRuntime, state: ComputerState, deadlines: Deadlines) -> Self {
Self {
state,
agent: None,
handle: runtime.handle.clone(),
error: runtime.error.clone(),
labels: runtime.labels.clone(),
vcpus: runtime.spec.vcpus,
memory_mib: runtime.spec.memory_mib,
lease: runtime.network.clone(),
vm_dir: runtime.vm_dir.clone(),
created_at: runtime.created_at,
ready_at: runtime.ready_at,
last_exited_at: runtime.last_exited_at,
last_exit_status: runtime.last_exit_status,
paused_at: runtime.paused_at,
pause_snapshot_id: runtime.pause_snapshot_id.clone(),
cow_file: runtime
.cow_handle
.as_ref()
.map(|handle| handle.cow_file.clone()),
deadlines,
}
}
}
struct Completion {
epoch: u64,
report: Report,
}
enum Report {
Booted(Arc<dyn GuestAgent>),
Restored(Arc<dyn GuestAgent>, ComputerProvisionOutcome),
Resumed(Arc<dyn GuestAgent>),
Gated,
Captured(CheckpointInfo),
Released(ReleaseScope),
Stopped,
Failed(TaskFailure),
}
struct Preemptible {
handle: JoinHandle<()>,
handoff: Handoff,
}
enum Handoff {
Awaited(oneshot::Receiver<()>),
Abortable,
JoinOnly,
}
struct Stalled {
retry: StalledStep,
state: State,
effects: Vec<Effect>,
}
enum StalledStep {
Handoff,
Record(RecordEnd),
}
#[derive(PartialEq, Eq)]
enum Flow {
Continue,
Stalled,
Refused,
}
pub struct ComputerActor {
id: ComputerId,
runtime: Arc<Mutex<ComputerRuntime>>,
unregister: Arc<dyn Fn() + Send + Sync>,
generation: Option<Uuid>,
vm_dir: PathBuf,
records: Arc<ComputerRecordStore>,
events: Arc<crate::sandbox::events::EventBus>,
tasks: Arc<dyn ComputerTasks>,
seeded: Seeded,
commands: mpsc::UnboundedReceiver<Command>,
completions_tx: mpsc::UnboundedSender<Completion>,
completions: mpsc::UnboundedReceiver<Completion>,
snapshot_tx: watch::Sender<ComputerSnapshot>,
effects: Effects,
queued: VecDeque<Event>,
waiters: Vec<(Answer, Reply)>,
capture_reply: Option<oneshot::Sender<Result<CheckpointInfo>>>,
pending_stop: Option<Duration>,
inflight: Option<Preemptible>,
epoch: u64,
stalled: Option<Stalled>,
stalled_on: StalledStep,
retry_backoff: Duration,
error: Option<String>,
pause_snapshot_id: Option<String>,
capture: Option<CaptureSpec>,
outcome: ComputerProvisionOutcome,
pause_reason: PauseReason,
resume_reason: String,
exit: Option<WorkloadOutcome>,
unconfirmed: Option<String>,
answer_error: Option<String>,
unwinding: Option<ComputerError>,
clear_journal_after_release: bool,
journal_blocked: bool,
deadlines: Deadlines,
ttl: Option<Pin<Box<tokio::time::Sleep>>>,
idle: Option<Pin<Box<tokio::time::Sleep>>>,
retry: Option<Pin<Box<tokio::time::Sleep>>>,
timers_enabled: watch::Receiver<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Seeded {
Fresh,
Recovered(PersistPhase),
Adopted,
}
pub struct ComputerSeed {
pub id: ComputerId,
pub runtime: Arc<Mutex<ComputerRuntime>>,
pub unregister: Arc<dyn Fn() + Send + Sync>,
pub generation: Option<Uuid>,
pub vm_dir: PathBuf,
pub records: Arc<ComputerRecordStore>,
pub events: Arc<crate::sandbox::events::EventBus>,
pub tasks: Arc<dyn ComputerTasks>,
pub deadlines: Deadlines,
pub timers_enabled: watch::Receiver<bool>,
pub seeded: Seeded,
}
impl ComputerActor {
pub fn new(
seed: ComputerSeed,
commands: mpsc::UnboundedReceiver<Command>,
snapshot_tx: watch::Sender<ComputerSnapshot>,
) -> Self {
let (completions_tx, completions) = mpsc::unbounded_channel();
Self {
id: seed.id,
runtime: seed.runtime,
unregister: seed.unregister,
generation: seed.generation,
vm_dir: seed.vm_dir,
records: seed.records,
events: seed.events,
tasks: seed.tasks,
seeded: seed.seeded,
commands,
completions_tx,
completions,
snapshot_tx,
effects: Effects::default(),
queued: VecDeque::new(),
waiters: Vec::new(),
capture_reply: None,
pending_stop: None,
inflight: None,
epoch: 0,
stalled: None,
stalled_on: StalledStep::Handoff,
retry_backoff: RETRY_INITIAL,
error: None,
pause_snapshot_id: None,
capture: None,
outcome: ComputerProvisionOutcome::default(),
pause_reason: PauseReason::Requested,
resume_reason: crate::sandbox::pause_reason::RESUME.to_owned(),
exit: None,
unconfirmed: None,
answer_error: None,
unwinding: None,
clear_journal_after_release: false,
journal_blocked: false,
deadlines: seed.deadlines,
ttl: None,
idle: None,
retry: None,
timers_enabled: seed.timers_enabled,
}
}
pub async fn run(mut self) {
let mut machine = ComputerLifecycle
.uninitialized_state_machine()
.init_with_context(&mut self.effects);
drop(self.effects.take());
self.publish_state(*machine.state());
match self.seeded {
Seeded::Fresh => {}
Seeded::Recovered(phase) => {
self.dispatch(&mut machine, Event::Recovered { phase })
.await;
}
Seeded::Adopted => {
if let Some(agent) = self.tasks.adopted_agent() {
self.publish_agent(agent);
}
self.dispatch(&mut machine, Event::Adopted).await;
}
}
self.rearm(*machine.state());
loop {
tokio::select! {
command = self.commands.recv() => {
let Some(command) = command else { break };
self.on_command(&mut machine, command).await;
}
Some(completion) = self.completions.recv() => {
self.on_completion(&mut machine, completion).await;
}
landed = handoff(&mut self.inflight) => {
if let Some(task) = self.inflight.as_mut() {
task.handoff = if landed {
Handoff::Abortable
} else {
Handoff::JoinOnly
};
}
if landed {
self.dispatch(&mut machine, Event::ResourcesHandedOff).await;
}
}
() = due(&mut self.ttl) => {
self.ttl = None;
self.dispatch(&mut machine, Event::TtlExpired).await;
}
() = due(&mut self.idle) => {
self.idle = None;
let action = self.deadlines.on_idle;
if matches!(action, IdleAction::Pause) {
self.pause_reason = PauseReason::IdleTimeout;
}
self.dispatch(&mut machine, Event::IdleExpired { action }).await;
}
() = due(&mut self.retry) => {
self.retry = None;
self.resume_stalled().await;
}
Ok(()) = self.timers_enabled.changed() => self.rearm(*machine.state()),
}
if matches!(machine.state(), State::Gone {}) && self.stalled.is_none() {
break;
}
}
if let Some(task) = self.inflight.take() {
task.handle.abort();
}
(self.unregister)();
for (_, reply) in self.waiters.drain(..) {
let _ = reply.send(Err(ComputerError::NotFound(self.id.clone())));
}
if let Some(reply) = self.capture_reply.take() {
let _ = reply.send(Err(ComputerError::NotFound(self.id.clone())));
}
}
async fn dispatch(&mut self, machine: &mut Machine, event: Event) -> bool {
let before = *machine.state();
machine.handle_with_context(&event, &mut self.effects);
let after = *machine.state();
let effects = self.effects.take();
let acted = !effects.is_empty() || before != after;
if before.to_public() != after.to_public() {
debug!(
sandbox_id = %self.id,
from = %before.to_public(),
to = %after.to_public(),
"computer lifecycle transition"
);
}
self.publish_state(after);
let mut effects = effects.into_iter();
while let Some(effect) = effects.next() {
match self.apply(effect, after).await {
Flow::Continue => {}
Flow::Stalled => {
self.park_stalled(after, effects.collect());
break;
}
Flow::Refused => break,
}
}
self.publish_state(after);
while let Some(queued) = self.queued.pop_front() {
Box::pin(self.dispatch(machine, queued)).await;
}
acted
}
}
type Machine = statig::blocking::InitializedStateMachine<ComputerLifecycle>;
async fn handoff(inflight: &mut Option<Preemptible>) -> bool {
match inflight.as_mut().map(|task| &mut task.handoff) {
Some(Handoff::Awaited(signal)) => signal.await.is_ok(),
_ => std::future::pending().await,
}
}
const fn launching(state: State) -> bool {
matches!(
state,
State::Provisioning {}
| State::Staging {}
| State::Booting {}
| State::Restoring { .. }
| State::Gating { .. }
| State::Resuming {}
)
}
async fn due(timer: &mut Option<Pin<Box<tokio::time::Sleep>>>) {
match timer {
Some(sleep) => sleep.as_mut().await,
None => std::future::pending().await,
}
}