use std::sync::Arc;
use std::time::Duration;
use arcbox_computer::cleanup;
use arcbox_core::{Runtime, VmBackend, VmLifecycleState};
use arcbox_engine::EngineError;
use tokio::sync::watch;
use tokio_util::sync::CancellationToken;
use tracing::{debug, info, warn};
use crate::context::DaemonContext;
const RECONNECT_DELAY: Duration = Duration::from_secs(1);
pub fn spawn(ctx: &DaemonContext, runtime: &Arc<Runtime>) {
let vm = runtime.subscribe_system_vm_state();
let shutdown = ctx.shutdown.clone();
let stream = RuntimeStream {
runtime: Arc::clone(runtime),
};
drop(tokio::spawn(async move {
reconcile_loop(&stream, vm, shutdown).await;
}));
}
trait CleanupStream: Send + Sync {
fn backend(&self) -> VmBackend;
fn watch(&self) -> impl Future<Output = EngineError> + Send;
}
struct RuntimeStream {
runtime: Arc<Runtime>,
}
impl CleanupStream for RuntimeStream {
fn backend(&self) -> VmBackend {
self.runtime.system_vm_backend()
}
async fn watch(&self) -> EngineError {
cleanup::watch(self.runtime.as_ref()).await
}
}
async fn reconcile_loop(
stream: &impl CleanupStream,
mut vm: watch::Receiver<VmLifecycleState>,
shutdown: CancellationToken,
) {
let mut idle = false;
loop {
let ready = vm.borrow_and_update().is_ready();
let mut reconnect = false;
if !ready {
idle = false;
} else if !idle {
let backend = stream.backend();
if backend.supports_nested_virt() {
let error = tokio::select! {
biased;
() = shutdown.cancelled() => return,
error = stream.watch() => error,
};
if cleanup::sandbox_unavailable(&error) {
info!(%error, "sandbox cleanup watch idle: the guest runs no sandboxes");
idle = true;
} else if vm.borrow().is_ready() {
warn!(%error, "sandbox cleanup watch disconnected; reconnecting");
reconnect = true;
} else {
debug!(%error, "sandbox cleanup watch ended with the VM");
}
} else {
info!(
backend = backend.as_str(),
"sandbox cleanup watch idle: the backend runs no sandboxes"
);
idle = true;
}
}
tokio::select! {
() = shutdown.cancelled() => break,
changed = vm.changed() => if changed.is_err() { break },
() = tokio::time::sleep(RECONNECT_DELAY), if reconnect => {}
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use super::*;
struct FakeStream {
backend: Mutex<VmBackend>,
connections: AtomicUsize,
end: fn() -> EngineError,
}
impl FakeStream {
fn new(backend: VmBackend, end: fn() -> EngineError) -> Self {
Self {
backend: Mutex::new(backend),
connections: AtomicUsize::new(0),
end,
}
}
fn set_backend(&self, backend: VmBackend) {
*self.backend.lock().unwrap() = backend;
}
fn connections(&self) -> usize {
self.connections.load(Ordering::SeqCst)
}
}
impl CleanupStream for FakeStream {
fn backend(&self) -> VmBackend {
*self.backend.lock().unwrap()
}
async fn watch(&self) -> EngineError {
self.connections.fetch_add(1, Ordering::SeqCst);
(self.end)()
}
}
fn disconnected() -> EngineError {
EngineError::Machine("stream reset".into())
}
fn no_sandboxes() -> EngineError {
EngineError::Agent {
code: 412,
message: "nested virtualization unavailable".into(),
}
}
async fn settle(elapsed: Duration) {
tokio::time::sleep(elapsed).await;
tokio::task::yield_now().await;
}
fn spawn_loop(
stream: &Arc<FakeStream>,
vm: watch::Receiver<VmLifecycleState>,
shutdown: &CancellationToken,
) -> tokio::task::JoinHandle<()> {
let (stream, shutdown) = (Arc::clone(stream), shutdown.clone());
tokio::spawn(async move { reconcile_loop(stream.as_ref(), vm, shutdown).await })
}
#[tokio::test(start_paused = true)]
async fn watches_only_while_the_vm_is_ready_on_a_nesting_backend() {
let stream = Arc::new(FakeStream::new(VmBackend::Hv, disconnected));
let (vm_tx, vm) = watch::channel(VmLifecycleState::Running);
let shutdown = CancellationToken::new();
let task = spawn_loop(&stream, vm, &shutdown);
settle(RECONNECT_DELAY * 3).await;
assert_eq!(
stream.connections(),
0,
"no stream on a backend without sandboxes"
);
stream.set_backend(VmBackend::Vz);
vm_tx.send_replace(VmLifecycleState::Stopping);
settle(Duration::ZERO).await;
assert_eq!(stream.connections(), 0, "no stream while the VM is down");
vm_tx.send_replace(VmLifecycleState::Running);
settle(RECONNECT_DELAY * 3).await;
let while_up = stream.connections();
assert!(
while_up >= 3,
"reconnects every {RECONNECT_DELAY:?} while the guest is up, got {while_up}"
);
vm_tx.send_replace(VmLifecycleState::Stopping);
settle(Duration::ZERO).await;
let at_stop = stream.connections();
settle(RECONNECT_DELAY * 3).await;
assert_eq!(
stream.connections(),
at_stop,
"no stream while the VM is down"
);
stream.set_backend(VmBackend::Hv);
vm_tx.send_replace(VmLifecycleState::Running);
settle(RECONNECT_DELAY * 3).await;
assert_eq!(
stream.connections(),
at_stop,
"no stream after switching to a backend without sandboxes"
);
shutdown.cancel();
task.await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn a_guest_without_sandboxes_is_asked_once_per_incarnation() {
let stream = Arc::new(FakeStream::new(VmBackend::Vz, no_sandboxes));
let (vm_tx, vm) = watch::channel(VmLifecycleState::Running);
let shutdown = CancellationToken::new();
let task = spawn_loop(&stream, vm, &shutdown);
settle(RECONNECT_DELAY * 3).await;
assert_eq!(stream.connections(), 1, "asked once, then left alone");
vm_tx.send_replace(VmLifecycleState::Idle);
settle(RECONNECT_DELAY * 3).await;
assert_eq!(stream.connections(), 1);
vm_tx.send_replace(VmLifecycleState::Stopping);
settle(Duration::ZERO).await;
vm_tx.send_replace(VmLifecycleState::Running);
settle(RECONNECT_DELAY * 3).await;
assert_eq!(stream.connections(), 2);
shutdown.cancel();
task.await.unwrap();
}
}