From 32af7e06394fd87e0726f19a6280adb5195355c8 Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 6 Nov 2026 03:07:50 +0100 Subject: [PATCH] lifecycle: leak the stop-failed warning kind once per agent Closes #6096 --- hive-c0re/src/lifecycle/mod.rs | 21 +++++++++++++++------ hive-c0re/src/lifecycle/tests.rs | 24 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/hive-c0re/src/lifecycle/mod.rs b/hive-c0re/src/lifecycle/mod.rs index 2b1a442f..609cf2c0 100644 --- a/hive-c0re/src/lifecycle/mod.rs +++ b/hive-c0re/src/lifecycle/mod.rs @@ -325,11 +325,20 @@ fn stop_failed_guards() REG.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new())) } -/// Leak a per-agent warning `kind`. Agent names are a small, bounded set — -/// leaking one short string per distinct agent that ever hits a stop -/// failure is cheap. -fn static_stop_kind(name: &str) -> &'static str { - Box::leak(format!("agent_stop_failed_{name}").into_boxed_str()) +/// The per-agent warning `kind`, leaked on the first call for a name and +/// returned unchanged afterwards, whether or not the warning was cleared in +/// between. The leak is one short string per distinct agent name. +fn stop_kind(name: &str) -> &'static str { + static KINDS: std::sync::OnceLock< + std::sync::Mutex>, + > = std::sync::OnceLock::new(); + let mut kinds = KINDS + .get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new())) + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + kinds + .entry(name.to_owned()) + .or_insert_with(|| Box::leak(format!("agent_stop_failed_{name}").into_boxed_str())) } /// Stop `name`'s container. See `hive-priv`'s `stop_and_release` for why @@ -356,7 +365,7 @@ pub async fn kill(name: &str) -> Result<()> { guards.insert( name.to_owned(), crate::warnings::set_warning( - static_stop_kind(name), + stop_kind(name), "crit", format!("{name}: stop failed — {e}"), ), diff --git a/hive-c0re/src/lifecycle/tests.rs b/hive-c0re/src/lifecycle/tests.rs index 7f7b5680..2e635925 100644 --- a/hive-c0re/src/lifecycle/tests.rs +++ b/hive-c0re/src/lifecycle/tests.rs @@ -422,3 +422,27 @@ fn a_build_without_a_log_row_has_an_empty_build_id() { assert_eq!(field(&logged[0].1, "build_id"), Some("")); assert_eq!(field(&logged[0].1, "agent"), Some("alice")); } + +#[test] +fn stop_kind_is_leaked_once_per_agent() { + let first = stop_kind("stopkind-a"); + assert_eq!(first, "agent_stop_failed_stopkind-a"); + assert!(std::ptr::eq(first, stop_kind("stopkind-a"))); + assert!(!std::ptr::eq(first, stop_kind("stopkind-b"))); +} + +#[test] +fn stop_kind_survives_a_clean_stop_between_failures() { + let failed = |guards: &mut std::collections::HashMap<_, _>| { + let kind = stop_kind("stopkind-c"); + guards.insert( + "stopkind-c".to_owned(), + crate::warnings::set_warning(kind, "crit", "stop failed"), + ); + kind + }; + let mut guards = std::collections::HashMap::new(); + let first = failed(&mut guards); + guards.remove("stopkind-c"); + assert!(std::ptr::eq(first, failed(&mut guards))); +}