From 05c91245c7a933e02551500f97cf203bead152cf Mon Sep 17 00:00:00 2001 From: damocles Date: Wed, 8 Jul 2026 23:40:58 +0200 Subject: [PATCH] =?UTF-8?q?refactor(#2286):=20make=20transient=20set/clear?= =?UTF-8?q?=20private=20=E2=80=94=20RAII=20guard=20is=20the=20only=20door?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hive-c0re/src/coordinator.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index 868e97af..97d984e9 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -338,6 +338,9 @@ pub struct TransientState { /// drop — including drop-via-cancellation, the path that bare /// `set_transient` / `clear_transient` pairs leaked through. Holds an /// `Arc` so the guard is freely returnable / movable. +#[must_use = "the guard clears the transient when dropped; bind it for the operation's \ + duration (`let _guard = coord.transient_guard(...)`). An unbound call drops \ + it immediately and un-sets the transient at once — the exact footgun this guards against."] pub struct TransientGuard { coord: Arc, name: String, @@ -1158,13 +1161,13 @@ impl Coordinator { /// Mark an agent as in-progress (only one state per agent for now). /// - /// Prefer `transient_guard` when possible — it auto-clears on drop - /// even if the surrounding future is cancelled (HTTP request - /// aborted, runtime shutdown mid-rebuild, panic between set and - /// clear). The bare `set_transient` / `clear_transient` pair leaks - /// the transient on any of those paths and the dashboard then - /// shows the agent stuck in "rebuilding…" forever. - pub fn set_transient(&self, name: &str, kind: TransientKind) { + /// Private on purpose: the RAII [`TransientGuard`] (via + /// [`Coordinator::transient_guard`]) is the only door, so the paired + /// `clear_transient` always runs on drop even if the surrounding future + /// is cancelled (HTTP request aborted, runtime shutdown mid-rebuild, + /// panic). A bare set with no guaranteed clear would leak the transient + /// and leave the dashboard stuck in "rebuilding…" forever. + fn set_transient(&self, name: &str, kind: TransientKind) { self.transient.lock().unwrap().insert( name.to_owned(), TransientState { @@ -1188,7 +1191,10 @@ impl Coordinator { }); } - pub fn clear_transient(&self, name: &str) { + /// Clear an agent's transient state. Private: only reachable through + /// [`TransientGuard`]'s `Drop`, which guarantees it runs (see + /// [`Coordinator::set_transient`]). + fn clear_transient(&self, name: &str) { let removed = self.transient.lock().unwrap().remove(name); if let Some(state) = removed { // Stamp the tombstone so the crash watcher can still see