fix(#2398): graceful restart as one atomic DAG, not compose-and-await

mara's review on #2436: no submit-await-submit composition, even
server-side. Adds Template::GracefulRestart (Signal -> Drain ->
StopForUpdate -> Reconcile, wanted=Up) mirroring how Restart already
does StopForUpdate -> Reconcile, plus submit::graceful_restart and
templates::graceful_restart. handle_restart_scoped now submits exactly
one DAG per agent up front for both the graceful and non-graceful
case -- no await_dags in the loop anymore.
This commit is contained in:
atlas 2026-07-14 20:21:06 +02:00 committed by mara
commit 901ab6a779
8 changed files with 103 additions and 40 deletions

View file

@ -7,6 +7,7 @@
//! rebuild(a): Prebuild(a) → StopForUpdate(a) → Swap(a) →(any) Reconcile(a)
//! graceful-stop(a): [wanted=Offline] Signal(a) → Drain(a) → Reconcile(a)
//! restart(a): [wanted=Up] StopForUpdate(a) → Reconcile(a)
//! graceful-restart(a): [wanted=Up] Signal(a) → Drain(a) → StopForUpdate(a) → Reconcile(a)
//! start(a): [wanted=Up] Reconcile(a)
//! stop(a): [wanted=Offline] Reconcile(a)
//! spawn(a): [wanted=Up] Provision(a) → Create(a) → WriteDropin(a) → Reconcile(a)
@ -168,6 +169,46 @@ pub fn restart(agent: &str, source: Source, reason: String) -> DagSpec {
}
}
/// Graceful restart: signal → drain → mechanical stop → converge to
/// `wanted` — the caller writes `wanted = Up` first, same as `restart`.
/// One atomic DAG start to finish (no client- or server-side "submit
/// one DAG, await it, submit the next" composition): the `Drain` node
/// is the same bounded harness-checkpoint wait `graceful_stop` uses,
/// then `StopForUpdate` (mechanical, ignores `wanted`) and the tail
/// `Reconcile` (converges to `wanted = Up`, i.e. starts it back up)
/// chain exactly like `restart`'s tail.
pub fn graceful_restart(agent: &str, source: Source, reason: String) -> DagSpec {
DagSpec {
template: Template::GracefulRestart,
agent: agent.to_owned(),
source,
reason,
parent_id: None,
approval_id: None,
inputs: Vec::new(),
perm_payload: None,
transient: Some(TransientKind::Restarting),
nodes: vec![
NodeSpec {
kind: NodeKind::Signal,
deps: Vec::new(),
},
NodeSpec {
kind: NodeKind::Drain,
deps: after_ok(0),
},
NodeSpec {
kind: NodeKind::StopForUpdate,
deps: after_ok(1),
},
NodeSpec {
kind: NodeKind::Reconcile,
deps: after_ok(2),
},
],
}
}
/// Single-`Reconcile` DAG: `Start` / `Stop` (caller writes `wanted`
/// first) and the boot-time `Reconcile` converge (wanted untouched).
pub fn reconcile_only(