mod child;
mod config;
mod error;
mod probe;
#[cfg(test)]
#[path = "tests.rs"]
mod tests;
pub use crate::uds::probe::{SocketVerdict, probe_socket_verdict, socket_is_serving};
pub use child::over_rss_limit;
pub use config::{
DEFAULT_CONNECT_PROBE_TIMEOUT, DEFAULT_INITIAL_PROBE_INTERVAL, DEFAULT_MAX_PROBE_INTERVAL,
ServiceTimeouts, SpawnSpec, SupervisorConfig,
};
pub use error::SupervisorError;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::sync::Mutex;
use child::{ChildHandle, remove_socket_file, spawn_child, terminate_child};
pub struct UdsServiceSupervisor {
config: SupervisorConfig,
children: Mutex<HashMap<String, ChildHandle>>,
spawn_gate: Mutex<()>,
clock: AtomicU64,
reaped: AtomicU64,
spawned: AtomicU64,
doomed: Mutex<Vec<(String, ChildHandle)>>,
}
impl UdsServiceSupervisor {
pub fn new(config: SupervisorConfig) -> Self {
Self {
config,
children: Mutex::new(HashMap::new()),
spawn_gate: Mutex::new(()),
clock: AtomicU64::new(0),
reaped: AtomicU64::new(0),
spawned: AtomicU64::new(0),
doomed: Mutex::new(Vec::new()),
}
}
pub fn config(&self) -> &SupervisorConfig {
&self.config
}
pub fn max_live(&self) -> usize {
self.config.max_live
}
pub fn rss_limit_mb(&self) -> Option<u64> {
self.config.rss_limit_mb
}
pub fn reaped_count(&self) -> u64 {
self.reaped.load(Ordering::Relaxed)
}
pub fn spawned_count(&self) -> u64 {
self.spawned.load(Ordering::Relaxed)
}
pub async fn supervised_count(&self) -> usize {
self.children.lock().await.len()
}
pub async fn ensure_running<F>(
&self,
key: &str,
socket_path: &Path,
spec: F,
) -> Result<PathBuf, SupervisorError>
where
F: FnOnce() -> Result<SpawnSpec, Box<dyn std::error::Error + Send + Sync + 'static>>,
{
let service = self.config.service.as_str();
if self.config.external_mode_enabled() {
tracing::debug!(
service = %service,
instance = %key,
socket = %socket_path.display(),
"external mode — skipping spawn supervision"
);
return Ok(socket_path.to_path_buf());
}
if let Some(path) = self.lookup_live(key).await {
return Ok(path);
}
let _spawn = self.spawn_gate.lock().await;
if let Some(path) = self.lookup_live(key).await {
return Ok(path);
}
self.reap_doomed().await;
if socket_is_serving(socket_path, self.config.timeouts.connect_probe).await {
crate::uds::verify_socket_for_connect(socket_path).map_err(|source| {
SupervisorError::UntrustedSocket {
service: service.to_string(),
key: key.to_string(),
socket: socket_path.to_path_buf(),
source: Box::new(source),
}
})?;
tracing::info!(
service = %service,
instance = %key,
socket = %socket_path.display(),
"socket already responding — not spawning a new child"
);
return Ok(socket_path.to_path_buf());
}
self.enforce_limits(1).await;
crate::uds::check_sun_path_budget(socket_path).map_err(|source| {
SupervisorError::SocketPath {
service: service.to_string(),
key: key.to_string(),
source: Box::new(source),
}
})?;
let spec = spec().map_err(|source| SupervisorError::SpawnSpec {
service: service.to_string(),
key: key.to_string(),
source,
})?;
let detached = self.config.detached;
let mut spawned = spawn_child(service, key, &spec, detached).await?;
self.spawned.fetch_add(1, Ordering::Relaxed);
match probe::wait_for_spawn(socket_path, &self.config.timeouts, &mut spawned.child).await {
probe::SpawnWait::Bound => {}
probe::SpawnWait::Exited(status) => {
let stderr = spawned.stderr_tail().await;
tracing::warn!(
service = %service,
instance = %key,
socket = %socket_path.display(),
?status,
"spawned child exited before binding its socket"
);
drop(spawned);
return Err(SupervisorError::ChildExited {
service: service.to_string(),
key: key.to_string(),
socket: socket_path.to_path_buf(),
status,
stderr,
});
}
probe::SpawnWait::TimedOut => {
if detached {
let _ =
terminate_child(&mut spawned.child, self.config.timeouts.sigterm_patience)
.await;
} else {
let _ = spawned.child.kill().await;
}
let stderr = spawned.stderr_tail().await;
drop(spawned);
return Err(SupervisorError::SpawnTimeout {
service: service.to_string(),
key: key.to_string(),
socket: socket_path.to_path_buf(),
budget: self.config.timeouts.spawn_probe,
stderr,
});
}
}
let mut child = spawned.child;
tracing::info!(
service = %service,
instance = %key,
socket = %socket_path.display(),
program = %spec.program.display(),
detached,
"spawned supervised child"
);
if detached {
tokio::spawn(async move {
let _ = child.wait().await;
});
return Ok(socket_path.to_path_buf());
}
let mut guard = self.children.lock().await;
guard.insert(
key.to_string(),
ChildHandle {
child,
socket_path: socket_path.to_path_buf(),
last_used: self.tick(),
},
);
let live = guard.len();
drop(guard);
tracing::debug!(service = %service, live, cap = self.config.max_live, "child count");
Ok(socket_path.to_path_buf())
}
fn tick(&self) -> u64 {
self.clock.fetch_add(1, Ordering::Relaxed).wrapping_add(1)
}
async fn lookup_live(&self, key: &str) -> Option<PathBuf> {
let (pid, path) = {
let stamp = self.tick();
let mut guard = self.children.lock().await;
let entry = guard.get_mut(key)?;
match entry.child.try_wait() {
Ok(None) => {
entry.last_used = stamp;
(entry.child.id(), entry.socket_path.clone())
}
Ok(Some(status)) => {
tracing::warn!(
service = %self.config.service,
instance = %key,
?status,
"supervised child exited unexpectedly — attempting one restart"
);
guard.remove(key);
return None;
}
Err(e) => {
tracing::warn!(
service = %self.config.service,
instance = %key,
"try_wait failed: {e:#} — evicting and retrying"
);
guard.remove(key);
return None;
}
}
};
if probe_socket_verdict(&path, self.config.timeouts.connect_probe).await
!= SocketVerdict::NotServing
{
return Some(path);
}
tracing::warn!(
service = %self.config.service,
instance = %key,
socket = %path.display(),
pid = ?pid,
"supervised child is not serving its socket — evicting and respawning"
);
let evicted = {
let mut guard = self.children.lock().await;
let same_child = matches!(
(pid, guard.get(key).and_then(|h| h.child.id())),
(Some(ours), Some(current)) if ours == current
);
if same_child { guard.remove(key) } else { None }
};
if let Some(handle) = evicted {
self.doomed.lock().await.push((key.to_string(), handle));
}
None
}
async fn reap_doomed(&self) {
let doomed: Vec<(String, ChildHandle)> = {
let mut guard = self.doomed.lock().await;
if guard.is_empty() {
return;
}
std::mem::take(&mut *guard)
};
for (key, mut handle) in doomed {
tracing::info!(
service = %self.config.service,
instance = %key,
pid = ?handle.child.id(),
"gracefully terminating an evicted child so it can flush"
);
if let Err(e) =
terminate_child(&mut handle.child, self.config.timeouts.sigterm_patience).await
{
tracing::warn!(instance = %key, "graceful termination error: {e:#}");
}
remove_socket_file(&self.config.service, &key, &handle.socket_path).await;
}
}
async fn enforce_limits(&self, headroom: usize) {
let mut victims: Vec<(String, ChildHandle, &'static str)> = Vec::new();
{
let mut guard = self.children.lock().await;
let over: Vec<String> = guard
.iter()
.filter(|(_, h)| over_rss_limit(h.child.id(), self.config.rss_limit_mb))
.map(|(k, _)| k.clone())
.collect();
for key in over {
if let Some(h) = guard.remove(&key) {
victims.push((key, h, "rss"));
}
}
while guard.len() + headroom > self.config.max_live {
let Some(lru) = guard
.iter()
.min_by_key(|(_, h)| h.last_used)
.map(|(k, _)| k.clone())
else {
break;
};
if let Some(h) = guard.remove(&lru) {
victims.push((lru, h, "cap"));
}
}
}
for (key, mut handle, reason) in victims {
tracing::info!(
service = %self.config.service,
instance = %key,
reason,
cap = self.config.max_live,
rss_limit_mb = ?self.config.rss_limit_mb,
pid = ?handle.child.id(),
"reaping supervised child to stay within limits"
);
if let Err(e) =
terminate_child(&mut handle.child, self.config.timeouts.sigterm_patience).await
{
tracing::warn!(instance = %key, "reap error: {e:#}");
}
remove_socket_file(&self.config.service, &key, &handle.socket_path).await;
self.reaped.fetch_add(1, Ordering::Relaxed);
}
}
pub async fn shutdown(&self) {
self.reap_doomed().await;
let handles: Vec<(String, ChildHandle)> = self.children.lock().await.drain().collect();
for (key, mut entry) in handles {
tracing::info!(
service = %self.config.service,
instance = %key,
pid = ?entry.child.id(),
"shutting down supervised child"
);
if let Err(e) =
terminate_child(&mut entry.child, self.config.timeouts.sigterm_patience).await
{
tracing::warn!(instance = %key, "shutdown encountered an error: {e:#}");
}
remove_socket_file(&self.config.service, &key, &entry.socket_path).await;
}
}
}
impl std::fmt::Debug for UdsServiceSupervisor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UdsServiceSupervisor")
.field("service", &self.config.service)
.field("children", &"<locked>")
.finish()
}
}