use std::path::PathBuf;
use std::process::Stdio;
use std::time::Duration;
use tokio::process::{Child, Command};
use super::child::{ChildHandle, terminate_child};
use super::*;
const TEST_TIMEOUTS: ServiceTimeouts = ServiceTimeouts::new(
Duration::from_millis(400),
Duration::from_millis(50),
Duration::from_secs(5),
);
fn config(max_live: usize, rss_limit_mb: Option<u64>) -> SupervisorConfig {
SupervisorConfig::new("test-service", max_live, TEST_TIMEOUTS).with_rss_limit_mb(rss_limit_mb)
}
fn supervisor(max_live: usize, rss_limit_mb: Option<u64>) -> UdsServiceSupervisor {
UdsServiceSupervisor::new(config(max_live, rss_limit_mb))
}
fn stub_child() -> Child {
Command::new("sleep")
.arg("60")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.kill_on_drop(true)
.spawn()
.expect("spawn stub child")
}
async fn register_stub(sup: &UdsServiceSupervisor, key: &str, socket: PathBuf, last_used: u64) {
sup.children.lock().await.insert(
key.to_string(),
ChildHandle {
child: stub_child(),
socket_path: socket,
last_used,
},
);
}
fn never_spawn() -> Result<SpawnSpec, Box<dyn std::error::Error + Send + Sync + 'static>> {
panic!("the supervisor must not resolve a spawn spec on this path");
}
struct EnvGuard {
key: String,
prev: Option<String>,
}
impl EnvGuard {
fn set(key: &str, value: &str) -> Self {
let prev = std::env::var(key).ok();
unsafe { std::env::set_var(key, value) }
Self {
key: key.to_string(),
prev,
}
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
unsafe {
match &self.prev {
Some(v) => std::env::set_var(&self.key, v),
None => std::env::remove_var(&self.key),
}
}
}
}
#[serial_test::serial]
#[test]
#[should_panic(expected = "SIGTERM patience must strictly exceed")]
fn service_timeouts_reject_patience_equal_to_the_flush() {
let _ = ServiceTimeouts::new(
Duration::from_secs(1),
Duration::from_secs(2),
Duration::from_secs(2),
);
}
#[serial_test::serial]
#[test]
#[should_panic(expected = "SIGTERM patience must strictly exceed")]
fn service_timeouts_reject_patience_below_the_flush() {
let _ = ServiceTimeouts::new(
Duration::from_secs(1),
Duration::from_secs(2),
Duration::from_secs(1),
);
}
#[serial_test::serial]
#[test]
fn service_timeouts_accept_a_subsecond_margin() {
let t = ServiceTimeouts::new(
Duration::from_millis(100),
Duration::from_millis(500),
Duration::from_millis(600),
);
assert_eq!(t.sigterm_patience, Duration::from_millis(600));
}
#[serial_test::serial]
#[test]
fn try_new_rejects_an_inverted_pair_without_panicking() {
let err = ServiceTimeouts::try_new(
Duration::from_secs(1),
Duration::from_secs(2),
Duration::from_secs(2),
)
.expect_err("an equal pair must be rejected");
assert!(
matches!(err, SupervisorError::InvalidTimeouts { .. }),
"expected InvalidTimeouts, got {err:?}"
);
}
#[serial_test::serial]
#[test]
fn try_new_matches_new_for_a_valid_pair() {
let built = ServiceTimeouts::try_new(
Duration::from_millis(400),
Duration::from_millis(50),
Duration::from_secs(5),
)
.expect("a valid pair must be accepted");
assert_eq!(built, TEST_TIMEOUTS);
}
#[serial_test::serial]
#[test]
fn service_timeouts_carry_probe_defaults_and_honour_overrides() {
assert_eq!(
TEST_TIMEOUTS.initial_probe_interval,
DEFAULT_INITIAL_PROBE_INTERVAL
);
assert_eq!(TEST_TIMEOUTS.max_probe_interval, DEFAULT_MAX_PROBE_INTERVAL);
assert_eq!(TEST_TIMEOUTS.connect_probe, DEFAULT_CONNECT_PROBE_TIMEOUT);
let tuned = TEST_TIMEOUTS
.with_probe_intervals(Duration::from_millis(1), Duration::from_millis(2))
.with_connect_probe(Duration::from_millis(3));
assert_eq!(tuned.initial_probe_interval, Duration::from_millis(1));
assert_eq!(tuned.max_probe_interval, Duration::from_millis(2));
assert_eq!(tuned.connect_probe, Duration::from_millis(3));
assert_eq!(
tuned.spawn_probe, TEST_TIMEOUTS.spawn_probe,
"an override must not disturb the service's own budget"
);
}
#[serial_test::serial]
#[test]
fn supervisor_config_clamps_max_live_to_one() {
assert_eq!(supervisor(0, None).max_live(), 1);
}
#[serial_test::serial]
#[test]
fn external_env_only_honours_exactly_one() {
const VAR: &str = "TRUSTY_TEST_SUPERVISOR_EXTERNAL";
let cfg = config(2, None).with_external_env(VAR);
let _g = EnvGuard::set(VAR, "1");
assert!(cfg.external_mode_enabled());
let _g = EnvGuard::set(VAR, "0");
assert!(!cfg.external_mode_enabled());
let _g = EnvGuard::set(VAR, "true");
assert!(!cfg.external_mode_enabled());
assert!(
!config(2, None).external_mode_enabled(),
"a config with no external var can never be opted out"
);
}
#[serial_test::serial]
#[test]
fn spawn_spec_builder_accumulates_args_and_dirs() {
let spec = SpawnSpec::new("/bin/echo")
.arg("--palace")
.arg("alpha")
.create_dir("/tmp/a")
.create_dir("/tmp/b");
assert_eq!(spec.program, PathBuf::from("/bin/echo"));
assert_eq!(spec.args, vec!["--palace", "alpha"]);
assert_eq!(spec.create_dirs.len(), 2);
}
#[serial_test::serial]
#[tokio::test]
async fn probe_verdict_reports_not_serving_for_a_missing_socket() {
let tmp = tempfile::tempdir().expect("tempdir");
let missing = tmp.path().join("nonexistent.sock");
assert_eq!(
probe_socket_verdict(&missing, DEFAULT_CONNECT_PROBE_TIMEOUT).await,
SocketVerdict::NotServing
);
assert!(!socket_is_serving(&missing, DEFAULT_CONNECT_PROBE_TIMEOUT).await);
}
#[serial_test::serial]
#[tokio::test]
async fn probe_verdict_reports_not_serving_for_a_stale_socket_file() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = tmp.path().join("stale.sock");
let listener = tokio::net::UnixListener::bind(&sock).expect("bind listener");
drop(listener);
assert!(sock.exists(), "the socket file must outlive the listener");
assert_eq!(
probe_socket_verdict(&sock, DEFAULT_CONNECT_PROBE_TIMEOUT).await,
SocketVerdict::NotServing
);
}
#[serial_test::serial]
#[tokio::test]
async fn probe_verdict_reports_serving_for_a_bound_socket() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = tmp.path().join("listen.sock");
let _listener = tokio::net::UnixListener::bind(&sock).expect("bind listener");
assert_eq!(
probe_socket_verdict(&sock, DEFAULT_CONNECT_PROBE_TIMEOUT).await,
SocketVerdict::Serving
);
assert!(socket_is_serving(&sock, DEFAULT_CONNECT_PROBE_TIMEOUT).await);
}
#[serial_test::serial]
#[tokio::test]
async fn wait_for_spawn_gives_up_within_the_spawn_budget() {
let tmp = tempfile::tempdir().expect("tempdir");
let missing = tmp.path().join("never.sock");
let budget = Duration::from_millis(120);
let timeouts = ServiceTimeouts::new(budget, Duration::from_millis(10), Duration::from_secs(1));
let mut child = stub_child();
let started = std::time::Instant::now();
assert!(matches!(
super::probe::wait_for_spawn(&missing, &timeouts, &mut child).await,
super::probe::SpawnWait::TimedOut
));
let elapsed = started.elapsed();
assert!(
elapsed >= budget,
"must not give up before the budget: {elapsed:?}"
);
assert!(
elapsed < budget * 8,
"must not overshoot the budget by an order of magnitude: {elapsed:?}"
);
}
#[serial_test::serial]
#[tokio::test]
async fn wait_for_spawn_returns_once_the_socket_binds() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = tmp.path().join("bound.sock");
let _listener = tokio::net::UnixListener::bind(&sock).expect("bind listener");
let mut child = stub_child();
assert!(matches!(
super::probe::wait_for_spawn(&sock, &TEST_TIMEOUTS, &mut child).await,
super::probe::SpawnWait::Bound
));
}
#[serial_test::serial]
#[tokio::test]
async fn wait_for_spawn_reports_a_child_that_exited() {
let tmp = tempfile::tempdir().expect("tempdir");
let missing = tmp.path().join("never.sock");
let budget = Duration::from_secs(3);
let timeouts = ServiceTimeouts::new(budget, Duration::from_millis(10), Duration::from_secs(1));
let mut child = Command::new("/bin/sh")
.args(["-c", "exit 3"])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.kill_on_drop(true)
.spawn()
.expect("spawn a child that exits at once");
let started = std::time::Instant::now();
let outcome = super::probe::wait_for_spawn(&missing, &timeouts, &mut child).await;
let elapsed = started.elapsed();
match outcome {
super::probe::SpawnWait::Exited(status) => {
assert_eq!(status.code(), Some(3), "the real exit status must survive")
}
_ => panic!("a child that exited must not be reported as a timeout"),
}
assert!(
elapsed < budget / 3,
"the answer must not wait out the spawn budget: {elapsed:?}"
);
}
#[serial_test::serial]
#[test]
fn over_rss_limit_is_false_without_a_measurement() {
assert!(
!over_rss_limit(Some(0), Some(0)),
"unmeasurable must not reap"
);
assert!(!over_rss_limit(None, Some(0)), "exited child must not reap");
}
#[serial_test::serial]
#[test]
fn over_rss_limit_is_false_when_disabled() {
assert!(!over_rss_limit(Some(std::process::id()), None));
}
#[serial_test::serial]
#[tokio::test]
async fn terminate_child_reaps_a_live_child() {
let mut child = stub_child();
terminate_child(&mut child, Duration::from_secs(5))
.await
.expect("terminate a live child");
assert!(
child.try_wait().expect("try_wait").is_some(),
"the child must be reaped by the time terminate returns"
);
}
#[serial_test::serial]
#[tokio::test]
async fn spawn_child_creates_requested_directories() {
let tmp = tempfile::tempdir().expect("tempdir");
let data = tmp.path().join("nested/data");
let spec = SpawnSpec::new("/bin/echo").create_dir(&data);
let mut spawned = super::child::spawn_child("test-service", "k", &spec, false)
.await
.expect("spawn");
let _ = spawned.child.wait().await;
assert!(data.is_dir(), "the spec's directory must exist after spawn");
}
#[serial_test::serial]
#[tokio::test]
async fn spawn_child_reports_a_missing_binary() {
let tmp = tempfile::tempdir().expect("tempdir");
let spec = SpawnSpec::new(tmp.path().join("no-such-binary"));
let err = super::child::spawn_child("test-service", "k", &spec, false)
.await
.expect_err("a missing binary must fail");
assert!(
matches!(err, SupervisorError::Spawn { .. }),
"expected Spawn, got {err:?}"
);
}
#[serial_test::serial]
#[tokio::test]
async fn supervisor_starts_empty() {
let sup = supervisor(3, None);
assert_eq!(sup.supervised_count().await, 0);
assert_eq!(sup.spawned_count(), 0);
assert_eq!(sup.reaped_count(), 0);
}
#[serial_test::serial]
#[test]
fn supervisor_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<UdsServiceSupervisor>();
}
#[serial_test::serial]
#[tokio::test]
async fn shutdown_with_no_children_is_noop() {
let sup = supervisor(3, None);
sup.shutdown().await;
assert_eq!(sup.supervised_count().await, 0);
}
#[serial_test::serial]
#[tokio::test]
async fn external_mode_skips_spawn() {
const VAR: &str = "TRUSTY_TEST_SUPERVISOR_EXTERNAL";
let _g = EnvGuard::set(VAR, "1");
let tmp = tempfile::tempdir().expect("tempdir");
let socket = tmp.path().join("external.sock");
let sup = UdsServiceSupervisor::new(config(3, None).with_external_env(VAR));
let path = sup
.ensure_running("inst", &socket, never_spawn)
.await
.expect("external mode must return the socket path without spawning");
assert_eq!(path, socket);
assert_eq!(sup.supervised_count().await, 0);
assert_eq!(sup.spawned_count(), 0);
}
#[serial_test::serial]
#[tokio::test]
async fn a_serving_child_is_reused_without_a_spawn() {
let tmp = tempfile::tempdir().expect("tempdir");
let socket = tmp.path().join("live.sock");
let _listener = tokio::net::UnixListener::bind(&socket).expect("bind");
let sup = supervisor(3, None);
register_stub(&sup, "inst", socket.clone(), 1).await;
let path = sup
.ensure_running("inst", &socket, never_spawn)
.await
.expect("a serving child must be reused");
assert_eq!(path, socket);
assert_eq!(sup.spawned_count(), 0);
assert_eq!(sup.supervised_count().await, 1);
sup.shutdown().await;
}
#[serial_test::serial]
#[tokio::test]
async fn a_dead_child_is_evicted_without_counting_as_a_reap() {
let tmp = tempfile::tempdir().expect("tempdir");
let socket = tmp.path().join("dead.sock");
let sup = supervisor(3, None);
{
let mut child = Command::new("true")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.kill_on_drop(true)
.spawn()
.expect("spawn a child that exits immediately");
child.wait().await.expect("wait");
sup.children.lock().await.insert(
"inst".to_string(),
ChildHandle {
child,
socket_path: socket.clone(),
last_used: 1,
},
);
}
assert!(sup.lookup_live("inst").await.is_none());
assert_eq!(sup.supervised_count().await, 0);
assert_eq!(sup.reaped_count(), 0);
assert_eq!(
sup.doomed.lock().await.len(),
0,
"a corpse owes no flush and must not enter the doomed queue"
);
}
#[serial_test::serial]
#[tokio::test]
async fn an_unserved_live_child_is_queued_for_graceful_termination() {
let tmp = tempfile::tempdir().expect("tempdir");
let socket = tmp.path().join("gone.sock");
let sup = supervisor(3, None);
register_stub(&sup, "inst", socket, 1).await;
assert!(sup.lookup_live("inst").await.is_none());
assert_eq!(sup.supervised_count().await, 0);
assert_eq!(
sup.doomed.lock().await.len(),
1,
"a live evictee owes a flush and must be queued, not dropped"
);
sup.reap_doomed().await;
assert_eq!(sup.doomed.lock().await.len(), 0);
assert_eq!(
sup.reaped_count(),
0,
"a restart is not a reclamation and must not inflate the reap count"
);
}
#[serial_test::serial]
#[tokio::test]
async fn exceeding_the_cap_reaps_the_least_recently_used() {
let tmp = tempfile::tempdir().expect("tempdir");
let sup = supervisor(3, None);
for (i, key) in ["oldest", "older", "newer", "newest"].iter().enumerate() {
register_stub(&sup, key, tmp.path().join(key), i as u64 + 1).await;
}
assert_eq!(sup.supervised_count().await, 4);
sup.enforce_limits(1).await;
assert_eq!(
sup.supervised_count().await,
2,
"cap 3 with headroom 1 must leave room for the incoming child"
);
let live = sup.children.lock().await;
assert!(live.contains_key("newest"), "most recent must survive");
assert!(
live.contains_key("newer"),
"second most recent must survive"
);
assert!(!live.contains_key("oldest"), "LRU must be reaped first");
drop(live);
assert_eq!(
sup.reaped_count(),
2,
"reaps must be counted, not just done"
);
}
#[serial_test::serial]
#[tokio::test]
async fn cap_of_one_keeps_a_single_child_live() {
let tmp = tempfile::tempdir().expect("tempdir");
let sup = supervisor(1, None);
register_stub(&sup, "a", tmp.path().join("a"), 1).await;
sup.enforce_limits(0).await;
assert_eq!(sup.supervised_count().await, 1);
sup.enforce_limits(1).await;
assert_eq!(sup.supervised_count().await, 0);
}
#[serial_test::serial]
#[tokio::test]
async fn over_rss_limit_children_are_reaped() {
let tmp = tempfile::tempdir().expect("tempdir");
let sup = supervisor(16, Some(0));
register_stub(&sup, "fat-a", tmp.path().join("a"), 1).await;
register_stub(&sup, "fat-b", tmp.path().join("b"), 2).await;
assert_eq!(sup.supervised_count().await, 2);
sup.enforce_limits(0).await;
assert_eq!(
sup.supervised_count().await,
0,
"children at or above the RSS ceiling must be reaped even under the cap"
);
assert_eq!(sup.reaped_count(), 2);
}
#[serial_test::serial]
#[tokio::test]
async fn under_rss_limit_children_are_left_alone() {
let tmp = tempfile::tempdir().expect("tempdir");
let sup = supervisor(16, Some(1_000_000));
register_stub(&sup, "lean", tmp.path().join("lean"), 1).await;
sup.enforce_limits(0).await;
assert_eq!(sup.supervised_count().await, 1);
assert_eq!(sup.reaped_count(), 0);
}
#[serial_test::serial]
#[tokio::test]
async fn shutdown_does_not_count_as_a_limit_reap() {
let tmp = tempfile::tempdir().expect("tempdir");
let sup = supervisor(16, None);
register_stub(&sup, "bye", tmp.path().join("bye"), 1).await;
sup.shutdown().await;
assert_eq!(sup.supervised_count().await, 0);
assert_eq!(sup.reaped_count(), 0);
}
#[serial_test::serial]
#[tokio::test]
async fn adoption_refuses_a_world_writable_socket() {
use std::os::unix::fs::PermissionsExt;
let tmp = tempfile::tempdir().expect("tempdir");
let socket = tmp.path().join("wide.sock");
let _listener = tokio::net::UnixListener::bind(&socket).expect("bind");
std::fs::set_permissions(&socket, std::fs::Permissions::from_mode(0o666))
.expect("widen the socket");
let sup = supervisor(3, None);
let err = sup
.ensure_running("inst", &socket, never_spawn)
.await
.expect_err("an unhardened socket must not be adopted");
assert!(
matches!(err, SupervisorError::UntrustedSocket { .. }),
"expected UntrustedSocket, got {err:?}"
);
assert_eq!(sup.spawned_count(), 0);
}
#[serial_test::serial]
#[tokio::test]
async fn adoption_accepts_a_hardened_socket_without_spawning() {
let tmp = tempfile::tempdir().expect("tempdir");
let socket = tmp.path().join("hardened.sock");
let _listener = crate::uds::bind_hardened(&socket).expect("bind_hardened");
let sup = supervisor(3, None);
let path = sup
.ensure_running("inst", &socket, never_spawn)
.await
.expect("a hardened serving socket must be adopted");
assert_eq!(path, socket);
assert_eq!(sup.spawned_count(), 0);
assert_eq!(
sup.supervised_count().await,
0,
"adoption must not register a child this supervisor does not own"
);
}
#[serial_test::serial]
#[tokio::test]
async fn a_child_that_never_binds_fails_with_the_service_budget() {
let tmp = tempfile::tempdir().expect("tempdir");
let socket = tmp.path().join("never.sock");
let budget = Duration::from_millis(150);
let cfg = SupervisorConfig::new(
"test-service",
3,
ServiceTimeouts::new(budget, Duration::from_millis(10), Duration::from_secs(1)),
);
let sup = UdsServiceSupervisor::new(cfg);
let err = sup
.ensure_running("inst", &socket, || Ok(SpawnSpec::new("sleep").arg("30")))
.await
.expect_err("a child that never binds must not be reported as running");
match err {
SupervisorError::SpawnTimeout {
budget: reported, ..
} => assert_eq!(
reported, budget,
"the error must carry the service's budget"
),
other => panic!("expected SpawnTimeout, got {other:?}"),
}
assert_eq!(sup.spawned_count(), 1, "the launch itself did happen");
assert_eq!(
sup.supervised_count().await,
0,
"a child that never bound must not be registered"
);
}
#[serial_test::serial]
#[tokio::test]
async fn a_child_that_never_binds_reports_the_stderr_it_did_write() {
let tmp = tempfile::tempdir().expect("tempdir");
let socket = tmp.path().join("stuck.sock");
let cfg = SupervisorConfig::new(
"test-service",
3,
ServiceTimeouts::new(
Duration::from_millis(300),
Duration::from_millis(10),
Duration::from_secs(1),
),
);
let sup = UdsServiceSupervisor::new(cfg);
let err = sup
.ensure_running("inst", &socket, || {
Ok(SpawnSpec::new("/bin/sh")
.arg("-c")
.arg("echo 'still loading the model' >&2; sleep 30"))
})
.await
.expect_err("a child that never binds must not be reported as running");
let SupervisorError::SpawnTimeout { stderr, .. } = &err else {
panic!("expected SpawnTimeout, got {err:?}");
};
assert!(
stderr.iter().any(|l| l.contains("still loading the model")),
"the child's own words must reach the caller, got {stderr:?}"
);
assert!(
err.to_string().contains("still loading the model"),
"the operator-facing message must quote the stderr tail: {err}"
);
}
#[serial_test::serial]
#[tokio::test]
async fn a_child_that_exits_before_binding_reports_its_status_and_stderr() {
let tmp = tempfile::tempdir().expect("tempdir");
let socket = tmp.path().join("dead.sock");
let budget = Duration::from_secs(3);
let cfg = SupervisorConfig::new(
"test-service",
3,
ServiceTimeouts::new(budget, Duration::from_millis(10), Duration::from_secs(1)),
);
let sup = UdsServiceSupervisor::new(cfg);
let started = std::time::Instant::now();
let err = sup
.ensure_running("inst", &socket, || {
Ok(SpawnSpec::new("/bin/sh")
.arg("-c")
.arg("echo 'Database already open. Cannot acquire lock.' >&2; exit 3"))
})
.await
.expect_err("a child that exited must not be reported as running");
let elapsed = started.elapsed();
match &err {
SupervisorError::ChildExited { status, stderr, .. } => {
assert_eq!(status.code(), Some(3), "the real exit status must survive");
assert!(
stderr.iter().any(|l| l.contains("Cannot acquire lock")),
"the child's own diagnosis must reach the caller, got {stderr:?}"
);
}
other => panic!("expected ChildExited, got {other:?}"),
}
assert!(
elapsed < budget / 3,
"a dead child must be reported within a poll interval, not at the \
spawn budget: {elapsed:?}"
);
assert!(
err.to_string().contains("Cannot acquire lock"),
"the operator-facing message must quote the stderr tail: {err}"
);
assert_eq!(sup.spawned_count(), 1, "the launch itself did happen");
assert_eq!(
sup.supervised_count().await,
0,
"a child that never bound must not be registered"
);
}
#[serial_test::serial]
#[tokio::test]
async fn a_real_line_survives_the_over_cap_line_that_follows_it() {
use super::child::{STDERR_LINE_CAP, STDERR_TAIL_LINES, relay_stderr_into};
const PADDING: usize = 1024 * 1024;
const DIAGNOSIS: &str = "Database already open. Cannot acquire lock.";
let mut child = Command::new("/bin/sh")
.arg("-c")
.arg(format!(
"echo '{DIAGNOSIS}' >&2; printf '%0{PADDING}d\\n' 0 >&2; echo 'after' >&2"
))
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.expect("spawn the shouty child");
let pipe = child.stderr.take().expect("stderr was piped");
let (buffer, relay) = relay_stderr_into(pipe, tokio::io::sink());
relay.await.expect("the relay must finish at EOF");
child.wait().await.expect("reap the child");
let tail = buffer.tail(STDERR_TAIL_LINES);
assert_eq!(
tail.len(),
3,
"three logical lines were written; the ring must hold three, not one \
per capped read: {:?}",
tail.iter().map(|l| l.len()).collect::<Vec<_>>()
);
assert!(
tail[0].contains(DIAGNOSIS),
"the line written BEFORE the giant one must survive it, got {:?}",
tail[0]
);
assert!(
tail[1].ends_with("bytes truncated]"),
"the over-cap line must be retained once, marked, got {:?}",
&tail[1][tail[1].len().saturating_sub(40)..]
);
assert_eq!(tail[2], "after", "the line after it must survive too");
let longest = tail.iter().map(String::len).max().unwrap_or(0);
assert!(
longest as u64 <= STDERR_LINE_CAP,
"the marker must fit inside the cap, not extend past it; longest was \
{longest}"
);
}
#[serial_test::serial]
#[tokio::test]
async fn a_detached_child_that_exits_before_binding_reports_an_empty_tail() {
let tmp = tempfile::tempdir().expect("tempdir");
let socket = tmp.path().join("dead-detached.sock");
let budget = Duration::from_secs(3);
let cfg = SupervisorConfig::new(
"test-service",
3,
ServiceTimeouts::new(budget, Duration::from_millis(10), Duration::from_secs(1)),
)
.with_detached(true);
let sup = UdsServiceSupervisor::new(cfg);
let started = std::time::Instant::now();
let err = sup
.ensure_running("inst", &socket, || {
Ok(SpawnSpec::new("/bin/sh")
.arg("-c")
.arg("echo 'Database already open. Cannot acquire lock.' >&2; exit 3"))
})
.await
.expect_err("a detached child that exited must not be reported as running");
let elapsed = started.elapsed();
match &err {
SupervisorError::ChildExited { status, stderr, .. } => {
assert_eq!(
status.code(),
Some(3),
"detaching must not cost the exit status"
);
assert!(
stderr.is_empty(),
"a detached child's stderr is inherited, so there is nothing to \
quote; got {stderr:?}"
);
}
other => panic!("expected ChildExited, got {other:?}"),
}
assert!(
elapsed < budget / 3,
"the #6600 latency fix must hold for a detached child too: {elapsed:?}"
);
assert_eq!(
sup.supervised_count().await,
0,
"a detached child that never bound must not be registered"
);
}
#[tokio::test]
async fn a_child_writing_an_enormous_line_does_not_grow_the_relay_buffer() {
use super::child::{STDERR_LINE_CAP, STDERR_TAIL_LINES, relay_stderr_into};
const WRITTEN: usize = 1024 * 1024;
let mut child = Command::new("/bin/sh")
.arg("-c")
.arg(format!("printf '%0{WRITTEN}d' 0 >&2"))
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.expect("spawn the shouty child");
let pipe = child.stderr.take().expect("stderr was piped");
let (buffer, relay) = relay_stderr_into(pipe, tokio::io::sink());
relay.await.expect("the relay must finish at EOF");
child.wait().await.expect("reap the child");
let tail = buffer.tail(STDERR_TAIL_LINES);
let longest = tail.iter().map(String::len).max().unwrap_or(0);
assert!(
longest as u64 <= STDERR_LINE_CAP,
"no retained line may exceed the per-line cap ({STDERR_LINE_CAP} bytes); \
longest was {longest}"
);
let retained: usize = tail.iter().map(String::len).sum();
let ceiling = STDERR_TAIL_LINES as u64 * STDERR_LINE_CAP;
assert!(
retained as u64 <= ceiling,
"the whole retained tail must fit {ceiling} bytes; it held {retained} \
out of the {WRITTEN} the child wrote"
);
assert!(
!tail.is_empty(),
"bounding the line must not throw the child's output away entirely"
);
}
#[serial_test::serial]
#[tokio::test]
async fn a_failing_spawn_spec_is_reported_as_such() {
let tmp = tempfile::tempdir().expect("tempdir");
let socket = tmp.path().join("nospec.sock");
let sup = supervisor(3, None);
let err = sup
.ensure_running("inst", &socket, || {
Err("no binary on PATH".to_string().into())
})
.await
.expect_err("an unresolvable spec must fail");
assert!(
matches!(err, SupervisorError::SpawnSpec { .. }),
"expected SpawnSpec, got {err:?}"
);
assert_eq!(sup.spawned_count(), 0);
}
#[serial_test::serial]
#[tokio::test]
async fn an_over_long_socket_path_is_rejected_before_spawning() {
let tmp = tempfile::tempdir().expect("tempdir");
let socket = tmp.path().join("x".repeat(crate::uds::sun_path_capacity()));
let sup = supervisor(3, None);
let err = sup
.ensure_running("inst", &socket, never_spawn)
.await
.expect_err("an unbindable path must fail before the spawn");
assert!(
matches!(err, SupervisorError::SocketPath { .. }),
"expected SocketPath, got {err:?}"
);
assert_eq!(sup.spawned_count(), 0);
}
#[test]
fn supervisor_config_carries_the_detached_flag() {
assert!(
!config(1, None).detached,
"the default must stay `kill_on_drop`: a resident owner reclaims its children"
);
assert!(config(1, None).with_detached(true).detached);
assert!(
!config(1, None)
.with_detached(true)
.with_detached(false)
.detached
);
}
#[serial_test::serial]
#[tokio::test]
async fn detached_children_are_not_retained_in_the_population() {
let tmp = tempfile::tempdir().expect("tempdir");
let socket = tmp.path().join("detached.sock");
let sup = UdsServiceSupervisor::new(config(3, None).with_detached(true));
let err = sup
.ensure_running("inst", &socket, || Ok(SpawnSpec::new("sleep").arg("60")))
.await
.expect_err("a child that never binds must not be reported as running");
assert!(
matches!(err, SupervisorError::SpawnTimeout { .. }),
"unexpected error: {err:?}"
);
assert_eq!(sup.spawned_count(), 1, "the spawn did happen");
assert_eq!(
sup.supervised_count().await,
0,
"a detached child must never enter the population map"
);
}
#[serial_test::serial]
#[tokio::test]
async fn a_detached_caller_adopts_a_socket_that_is_already_serving() {
let tmp = tempfile::tempdir().expect("tempdir");
let socket = tmp.path().join("live.sock");
let _listener = crate::uds::bind_hardened(&socket).expect("bind");
let sup = UdsServiceSupervisor::new(config(3, None).with_detached(true));
let path = sup
.ensure_running("inst", &socket, never_spawn)
.await
.expect("a serving socket must be adopted, not raced");
assert_eq!(path, socket);
assert_eq!(
sup.spawned_count(),
0,
"nothing may be spawned over a live socket"
);
assert_eq!(sup.supervised_count().await, 0);
}