refactor(#2286): make transient set/clear private — RAII guard is the only door

This commit is contained in:
damocles 2026-07-08 23:40:58 +02:00 committed by mara
commit 05c91245c7

View file

@ -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<Coordinator>` 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<Coordinator>,
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