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:
parent
6c654921a0
commit
5fe8008cce
5 changed files with 189 additions and 100 deletions
|
|
@ -212,21 +212,27 @@ fn lease_serializes_two_lifecycle_dags_for_same_agent() {
|
|||
None,
|
||||
),
|
||||
);
|
||||
// Restart's StopForUpdate acquires the lease; stop's Reconcile
|
||||
// must wait even though slots are free.
|
||||
// Restart's head SetWanted takes the lease; stop's Reconcile must
|
||||
// wait even though slots are free.
|
||||
let first = claim_one(&q);
|
||||
assert_eq!(first.dag_id, restart);
|
||||
assert_eq!(first.kind.as_str(), "set_wanted");
|
||||
assert!(first.lease_acquired);
|
||||
q.complete_node(restart, first.node_id, Ok(()));
|
||||
// Same DAG keeps the lease for its Reconcile.
|
||||
// Same DAG keeps the lease through StopForUpdate then Reconcile.
|
||||
let second = claim_one(&q);
|
||||
assert_eq!(second.dag_id, restart);
|
||||
assert_eq!(second.kind.as_str(), "stop_for_update");
|
||||
assert!(!second.lease_acquired, "lease already held by this DAG");
|
||||
q.complete_node(restart, second.node_id, Ok(()));
|
||||
// Restart terminal → lease released → stop's Reconcile runs.
|
||||
let third = claim_one(&q);
|
||||
assert_eq!(third.dag_id, stop);
|
||||
q.complete_node(stop, third.node_id, Ok(()));
|
||||
assert_eq!(third.dag_id, restart);
|
||||
assert_eq!(third.kind.as_str(), "reconcile");
|
||||
q.complete_node(restart, third.node_id, Ok(()));
|
||||
// Restart terminal → lease released → stop's Reconcile runs.
|
||||
let fourth = claim_one(&q);
|
||||
assert_eq!(fourth.dag_id, stop);
|
||||
q.complete_node(stop, fourth.node_id, Ok(()));
|
||||
assert_eq!(state_of(&q, restart), State::Done);
|
||||
assert_eq!(state_of(&q, stop), State::Done);
|
||||
}
|
||||
|
|
@ -510,6 +516,11 @@ fn terminal_dag_reported_exactly_once_and_lease_released() {
|
|||
&q,
|
||||
templates::restart("agent-a", Source::Manual, "r".to_owned()),
|
||||
);
|
||||
// restart = SetWanted → StopForUpdate → Reconcile; not terminal until
|
||||
// the last node completes.
|
||||
let set_wanted = claim_one(&q);
|
||||
q.complete_node(id, set_wanted.node_id, Ok(()));
|
||||
assert!(q.drain_terminal().is_empty(), "dag not terminal yet");
|
||||
let stop = claim_one(&q);
|
||||
q.complete_node(id, stop.node_id, Ok(()));
|
||||
assert!(q.drain_terminal().is_empty(), "dag not terminal yet");
|
||||
|
|
@ -727,7 +738,7 @@ fn graceful_stop_shape_signal_drain_reconcile() {
|
|||
&q,
|
||||
templates::graceful_stop("agent-a", Source::Manual, "graceful".to_owned()),
|
||||
);
|
||||
for expected in ["signal", "drain", "reconcile"] {
|
||||
for expected in ["set_wanted", "signal", "drain", "reconcile"] {
|
||||
let c = claim_one(&q);
|
||||
assert_eq!(c.kind.as_str(), expected);
|
||||
q.complete_node(id, c.node_id, Ok(()));
|
||||
|
|
@ -753,8 +764,9 @@ fn graceful_signal_and_drain_hold_no_build_slot() {
|
|||
let kinds: Vec<&str> = claims.iter().map(|c| c.kind.as_str()).collect();
|
||||
assert_eq!(
|
||||
kinds,
|
||||
vec!["prebuild", "signal", "signal"],
|
||||
"both agents' signals fire while the slot is held"
|
||||
vec!["prebuild", "set_wanted", "set_wanted"],
|
||||
"both agents' graceful-stop heads (SetWanted, build-slot-exempt) run \
|
||||
while the slot is held; their signals follow"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue