use std::net::IpAddr;
use std::sync::Arc;
use arcbox_core::Runtime;
use arcbox_core::event::Event;
use arcbox_core::machine::MachineState;
use tokio::sync::broadcast;
use tokio::sync::broadcast::error::RecvError;
use tokio_util::sync::CancellationToken;
use crate::context::DaemonContext;
pub fn spawn(ctx: &DaemonContext, runtime: &Arc<Runtime>) {
let events = runtime.event_bus().subscribe();
let shutdown = ctx.shutdown.clone();
let runtime = Arc::clone(runtime);
drop(tokio::spawn(async move {
run(&runtime, events, shutdown).await;
}));
}
async fn run(
runtime: &Runtime,
mut events: broadcast::Receiver<Event>,
shutdown: CancellationToken,
) {
sync_all(runtime).await;
loop {
tokio::select! {
() = shutdown.cancelled() => break,
event = events.recv() => match event {
Ok(
Event::MachineStarted { name }
| Event::MachineStopped { name }
| Event::MachineRemoved { name },
) => sync(runtime, &name).await,
Ok(_) => {}
Err(RecvError::Lagged(_)) => sync_all(runtime).await,
Err(RecvError::Closed) => break,
},
}
}
}
async fn sync(runtime: &Runtime, name: &str) {
let machine = runtime.machine_manager().get(name);
let target = machine
.as_ref()
.and_then(|machine| published_address(machine.state, machine.bridge_ip_address.as_deref()));
match target {
Some(ip) => runtime.register_machine_dns(name, ip).await,
None => runtime.deregister_machine_dns(name).await,
}
}
async fn sync_all(runtime: &Runtime) {
let mut names: Vec<String> = runtime
.machine_manager()
.list()
.into_iter()
.map(|machine| machine.name)
.collect();
names.extend(runtime.registered_machine_dns_names().await);
names.sort_unstable();
names.dedup();
for name in names {
sync(runtime, &name).await;
}
}
fn published_address(state: MachineState, bridge_ip: Option<&str>) -> Option<IpAddr> {
if state != MachineState::Running {
return None;
}
bridge_ip?.parse().ok()
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use arcbox_core::{Config, event::EventBus};
use super::*;
#[test]
fn only_a_running_machine_with_a_bridge_address_is_published() {
let ip: IpAddr = "192.168.64.5".parse().unwrap();
assert_eq!(
published_address(MachineState::Running, Some("192.168.64.5")),
Some(ip)
);
assert_eq!(
published_address(MachineState::Stopped, Some("192.168.64.5")),
None
);
assert_eq!(published_address(MachineState::Running, None), None);
assert_eq!(
published_address(MachineState::Running, Some("not-an-address")),
None
);
}
async fn wait_until(mut condition: impl AsyncFnMut() -> bool) -> bool {
for _ in 0..100 {
if condition().await {
return true;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
false
}
#[tokio::test]
async fn the_loop_follows_machine_records_through_the_event_bus() {
let tmp = tempfile::TempDir::new().unwrap();
let runtime = Arc::new(
Runtime::new(Config {
data_dir: tmp.path().to_path_buf(),
..Default::default()
})
.expect("runtime"),
);
let bus: &EventBus = runtime.event_bus();
runtime
.register_machine_dns("gone", "192.168.64.9".parse().unwrap())
.await;
let shutdown = CancellationToken::new();
let task = {
let runtime = Arc::clone(&runtime);
let events = bus.subscribe();
let shutdown = shutdown.clone();
tokio::spawn(async move { run(&runtime, events, shutdown).await })
};
assert!(
wait_until(async || runtime.registered_machine_dns_names().await.is_empty()).await,
"the initial pass keeps a name no machine owns"
);
runtime
.machine_manager()
.register_mock_machine("no-bridge", 3)
.unwrap();
bus.publish(Event::MachineStarted {
name: "no-bridge".into(),
});
tokio::time::sleep(Duration::from_millis(50)).await;
assert_eq!(
runtime.registered_machine_dns_names().await,
Vec::<String>::new()
);
shutdown.cancel();
tokio::time::timeout(Duration::from_secs(1), task)
.await
.expect("loop exits on shutdown")
.expect("loop task panicked");
}
}