refactor(#2449): write power intent via a SetWanted DAG node, not a pre-submit side effect

The durable 'wanted' power intent was written by submit::{start,stop,
restart,graceful_restart,graceful_stop} as a synchronous pre-submit side
effect, then read by the DAG's tail Reconcile. That's not crash-safe
(a crash between the write and the enqueue loses it) and, with agent now
per-node, can't be per-agent in a DAG that spans agents.

Move it into the DAG as a head SetWanted node:
- NodeKind::SetWanted { up } + run_set_wanted executor (fails the node on
  a write error, unlike the old warn-and-continue, so a stale intent
  never reaches Reconcile).
- LEASE-NEEDING, not lease-exempt: it takes the agent lease so a power-op
  DAG's intent-write + reconcile is atomic per-agent. If it were exempt,
  two racing ops (restart vs stop) would run both intent-writes up front
  and clobber each other before either reconciled — defeating the point
  of moving the write into the DAG. (In stale_start the lease is thus held
  across the head Prebuild, but that's a no-op there: the agent is down so
  prebuild is skipped.)
- templates: explicit SetWanted node 0 on restart/graceful_restart/
  graceful_stop, plus dedicated start/stop templates (SetWanted -> Reconcile)
  and stale_start (SetWanted(Up) -> rebuild subgraph, reusing rebuild_nodes).
  No compose helper / rebuild variant. reconcile_only is now boot-only.
- submit.rs: drop the set_wanted side effect; the stale-rev shape decision
  (start vs stale_start) stays submit-side.

All 33 job_queue tests pass (shape/lease tests updated for the head node).
This commit is contained in:
atlas 2026-07-14 22:28:59 +02:00 committed by mara
commit 5fe8008cce
5 changed files with 189 additions and 100 deletions

View file

@ -121,6 +121,20 @@ pub enum NodeKind {
/// dashboard. Holds no lease and no build slot; the child DAGs it anchors
/// still run concurrently — the grouping is a display link, not a dep edge.
Noop,
/// Write the agent's durable power intent (`wanted = Up` when `up`, else
/// `Offline`) as a first-class DAG node, at the head of a power-op
/// template so the downstream `Reconcile` reads it. Replaces the old
/// pre-submit `set_wanted` side effect: the intent write is now part of
/// the atomic DAG (crash-safe, per-agent — a multi-agent DAG carries one
/// `SetWanted` per agent). Build-slot-exempt (a store write), but
/// **lease-needing**: it takes the agent's lifecycle lease so the whole
/// power-op DAG (intent write → reconcile) is atomic per-agent — two
/// racing ops (e.g. restart vs stop) can't clobber each other's intent
/// before either reconciles, which is the point of moving the write into
/// the DAG. (In `stale_start` the lease is thus held across the head
/// `Prebuild`, but that's a no-op there — the agent is down, so prebuild
/// is skipped.)
SetWanted { up: bool },
}
impl NodeKind {
@ -142,6 +156,7 @@ impl NodeKind {
NodeKind::WritePermFile => "write_perm_file",
NodeKind::ApprovalDeploy => "approval_deploy",
NodeKind::Noop => "noop",
NodeKind::SetWanted { .. } => "set_wanted",
}
}
@ -177,6 +192,7 @@ impl NodeKind {
| NodeKind::Drain
| NodeKind::WriteDropin
| NodeKind::ApprovalDeploy
| NodeKind::SetWanted { .. }
)
}
}