refactor(#3034): rename PostSwap to RebuildBookkeeping

It reads as a swap thing but is post-*rebuild* bookkeeping: rev marker, forge and
matrix sync, manager kick, container rescan, meta-inputs snapshot. Wire label
follows (`post_swap` -> `rebuild_bookkeeping`); the graph view renders whatever
label it is sent, so nothing keys on the old string.

Also corrects a doc the previous commit falsified: this node's comment still said
it declares the agent lease as a re-entrant borrow, after that declaration moved
to the brace. Clippy and the whole suite pass over a stale doc comment, so it
took reading the file to find.

Verified by grepping the new name for places it has no business being, which
caught the sed rewriting a *historical* test name in a `// Replaces ...` comment
- reverted, since prose about the past must keep its old spelling.
This commit is contained in:
atlas 2026-08-04 12:55:52 +02:00
commit a0790e4e49
5 changed files with 56 additions and 38 deletions

View file

@ -867,7 +867,7 @@ async fn prepare_applied_target(
/// [`run_deploy_tail`] must not roll `main` back. Ordering that ahead of the tag /// [`run_deploy_tail`] must not roll `main` back. Ordering that ahead of the tag
/// plant is what makes the tail's `deployed/<id>` cross-check a second line of /// plant is what makes the tail's `deployed/<id>` cross-check a second line of
/// defence rather than the only one. No agent kick — the rebuild's own /// defence rather than the only one. No agent kick — the rebuild's own
/// `PostSwap` already did it. /// `RebuildBookkeeping` already did it.
/// ///
/// # Errors /// # Errors
/// ///

View file

@ -63,7 +63,7 @@ pub(super) async fn run_node(
NodeKind::MetaSync { relock, .. } => run_meta_sync(coord, agent, *relock).await, NodeKind::MetaSync { relock, .. } => run_meta_sync(coord, agent, *relock).await,
NodeKind::Prebuild { .. } => run_prebuild(agent, id).await, NodeKind::Prebuild { .. } => run_prebuild(agent, id).await,
NodeKind::Swap { .. } => run_swap(coord, agent, id).await, NodeKind::Swap { .. } => run_swap(coord, agent, id).await,
NodeKind::PostSwap { .. } => run_post_swap(coord, agent).await, NodeKind::RebuildBookkeeping { .. } => run_rebuild_bookkeeping(coord, agent).await,
NodeKind::Provision { .. } => run_provision(coord, agent).await, NodeKind::Provision { .. } => run_provision(coord, agent).await,
NodeKind::Create { .. } => run_create(agent).await, NodeKind::Create { .. } => run_create(agent).await,
NodeKind::MetaLock { NodeKind::MetaLock {
@ -263,9 +263,9 @@ async fn run_swap(coord: &Arc<Coordinator>, name: &str, id: NodeId) -> Result<()
let paths = Coordinator::agent_paths(name, agent_dir); let paths = Coordinator::agent_paths(name, agent_dir);
let result = crate::lifecycle::swap_update(name, &hive, &paths, Some(id.get())).await; let result = crate::lifecycle::swap_update(name, &hive, &paths, Some(id.get())).await;
// On success the Ok-only bookkeeping tail (rev marker, forge/matrix // On success the Ok-only bookkeeping tail (rev marker, forge/matrix
// sync, kick, rescan, snapshot) runs in the sibling `PostSwap` node, // sync, kick, rescan, snapshot) runs in the sibling `RebuildBookkeeping` node,
// which deps `AfterOk(Swap)`. On failure `PostSwap` is cancel-cascaded // which deps `AfterOk(Swap)`. On failure `RebuildBookkeeping` is cancel-cascaded
// and the tail `Reconcile` (`AfterAny(PostSwap)`) handles recovery; here // and the tail `Reconcile` (`AfterAny(RebuildBookkeeping)`) handles recovery; here
// we only refresh the observed state so dashboards reflect the failed // we only refresh the observed state so dashboards reflect the failed
// swap immediately. The `Rebuilt { ok: false }` manager event is emitted by // swap immediately. The `Rebuilt { ok: false }` manager event is emitted by
// the DAG's `EmitRebuilt` tail (any node may be the one that failed). // the DAG's `EmitRebuilt` tail (any node may be the one that failed).
@ -280,7 +280,7 @@ async fn run_swap(coord: &Arc<Coordinator>, name: &str, id: NodeId) -> Result<()
/// means the profile swap succeeded. Store/forge/matrix work only — no nix /// means the profile swap succeeded. Store/forge/matrix work only — no nix
/// build (build-slot-exempt); the agent lease taken at `Swap` is still held /// build (build-slot-exempt); the agent lease taken at `Swap` is still held
/// (the whole chain up to `Reconcile` is one agent's subgraph). /// (the whole chain up to `Reconcile` is one agent's subgraph).
async fn run_post_swap(coord: &Arc<Coordinator>, name: &str) -> Result<()> { async fn run_rebuild_bookkeeping(coord: &Arc<Coordinator>, name: &str) -> Result<()> {
if let Some(rev) = crate::auto_update::current_flake_rev(&coord.hyperhive_flake) if let Some(rev) = crate::auto_update::current_flake_rev(&coord.hyperhive_flake)
&& let Err(e) = std::fs::write(crate::paths::applied_rev_marker(name), rev) && let Err(e) = std::fs::write(crate::paths::applied_rev_marker(name), rev)
{ {

View file

@ -53,20 +53,22 @@ pub enum NodeKind {
/// `nixos-container update` profile-swap (requires the container /// `nixos-container update` profile-swap (requires the container
/// stopped). Re-applies nspawn flags + resource limits first — /// stopped). Re-applies nspawn flags + resource limits first —
/// rebuild is the reconcile verb. The post-rebuild bookkeeping tail /// rebuild is the reconcile verb. The post-rebuild bookkeeping tail
/// lives in the sibling `PostSwap` node. /// lives in the sibling `RebuildBookkeeping` node.
Swap { agent: String }, Swap { agent: String },
/// The post-`Swap` bookkeeping tail as a first-class node: rev marker, /// The post-`Swap` bookkeeping tail as a first-class node: rev marker,
/// forge + matrix sync, manager kick, container rescan, meta-inputs /// forge + matrix sync, manager kick, container rescan, meta-inputs
/// snapshot. Split out of `Swap` for dashboard visibility + retry /// snapshot. Split out of `Swap` for dashboard visibility + retry
/// granularity. Deps `AfterOk(Swap)`, so it runs only when the profile /// granularity. Deps `AfterOk(Swap)`, so it runs only when the profile
/// swap succeeded; the tail `Reconcile` deps `AfterAny(PostSwap)`, so on /// swap succeeded; the tail `Reconcile` deps `AfterAny(RebuildBookkeeping)`, so on
/// swap failure this node is cancel-cascaded (a terminal state) and /// swap failure this node is cancel-cascaded (a terminal state) and
/// recovery still runs. Store/forge/matrix work only — no nix build, so /// recovery still runs. Store/forge/matrix work only — no nix build.
/// build-slot-exempt. It *does* declare the agent lease: an ancestor in the ///
/// stop chain already holds it, so this is a re-entrant borrow rather than a /// Declares **no resources**: it is a coordinated child of
/// second unit — declaring it keeps the requirement true of this node rather /// [`NodeKind::AgentWindow`], which holds the agent lease (and the build
/// than of the one DAG shape it happens to be used in. /// slot) for the whole rebuild subtree. See `templates.rs`'s module doc for
PostSwap { agent: String }, /// why the brace is the one place a resource is declared on behalf of
/// others.
RebuildBookkeeping { agent: String },
/// First-spawn pre-create provisioning: proposed/applied repos, /// First-spawn pre-create provisioning: proposed/applied repos,
/// state subvolume, and meta registration (`sync_agents`). Runs /// state subvolume, and meta registration (`sync_agents`). Runs
/// ahead of `Create` so the `nixos-container create --flake /// ahead of `Create` so the `nixos-container create --flake
@ -360,7 +362,7 @@ impl NodeKind {
NodeKind::MetaSync { .. } => "meta_sync", NodeKind::MetaSync { .. } => "meta_sync",
NodeKind::Prebuild { .. } => "prebuild", NodeKind::Prebuild { .. } => "prebuild",
NodeKind::Swap { .. } => "swap", NodeKind::Swap { .. } => "swap",
NodeKind::PostSwap { .. } => "post_swap", NodeKind::RebuildBookkeeping { .. } => "rebuild_bookkeeping",
NodeKind::Provision { .. } => "provision", NodeKind::Provision { .. } => "provision",
NodeKind::Create { .. } => "create", NodeKind::Create { .. } => "create",
NodeKind::MetaLock { .. } => "meta_lock", NodeKind::MetaLock { .. } => "meta_lock",
@ -396,7 +398,7 @@ impl NodeKind {
NodeKind::MetaSync { agent, .. } NodeKind::MetaSync { agent, .. }
| NodeKind::Prebuild { agent } | NodeKind::Prebuild { agent }
| NodeKind::Swap { agent } | NodeKind::Swap { agent }
| NodeKind::PostSwap { agent } | NodeKind::RebuildBookkeeping { agent }
| NodeKind::Provision { agent } | NodeKind::Provision { agent }
| NodeKind::Create { agent } | NodeKind::Create { agent }
| NodeKind::Reconcile { agent } | NodeKind::Reconcile { agent }

View file

@ -22,7 +22,7 @@
//! shape says "this subtree is coordinated, one holder speaks for it". //! shape says "this subtree is coordinated, one holder speaks for it".
//! //!
//! ```text //! ```text
//! rebuild(a): MetaSync(a) → AgentWindow(a){ Prebuild(a) → StopForUpdate(a) → Swap(a) →(ok) PostSwap(a) } →(any) Reconcile(a) //! rebuild(a): MetaSync(a) → AgentWindow(a){ Prebuild(a) → StopForUpdate(a) → Swap(a) →(ok) RebuildBookkeeping(a) } →(any) Reconcile(a)
//! rebuild(a) graceful: … AgentWindow(a){ Prebuild(a) ∥ Signal(a) → Drain(a); both →(ok) StopForUpdate(a) → … } [boot sweep only] //! rebuild(a) graceful: … AgentWindow(a){ Prebuild(a) ∥ Signal(a) → Drain(a); both →(ok) StopForUpdate(a) → … } [boot sweep only]
//! spawn(a): Provision(a) → Create(a) → WriteDropin(a) → Reconcile(a) [wanted=Up at approve] //! spawn(a): Provision(a) → Create(a) → WriteDropin(a) → Reconcile(a) [wanted=Up at approve]
//! perm-change(a): WritePermFile(a) → «rebuild subgraph» //! perm-change(a): WritePermFile(a) → «rebuild subgraph»
@ -149,7 +149,7 @@ pub(crate) struct RebuildRoots<'a> {
pub meta_sync: Handle<'a>, pub meta_sync: Handle<'a>,
/// The brace holding the agent lease and the build slot — its roll-up /// The brace holding the agent lease and the build slot — its roll-up
/// carries the whole mechanical subtree (`Prebuild`, the quiesce chain, /// carries the whole mechanical subtree (`Prebuild`, the quiesce chain,
/// `StopForUpdate` → `Swap` → `PostSwap`). /// `StopForUpdate` → `Swap` → `RebuildBookkeeping`).
pub agent_window: Handle<'a>, pub agent_window: Handle<'a>,
/// The recovery/convergence tail root. /// The recovery/convergence tail root.
pub reconcile: Handle<'a>, pub reconcile: Handle<'a>,
@ -186,7 +186,7 @@ impl<'a> RebuildRoots<'a> {
/// `Drain`. Waiting on the build is deliberate — running the drain window /// `Drain`. Waiting on the build is deliberate — running the drain window
/// early is the win, taking the container *down* early would be pure /// early is the win, taking the container *down* early would be pure
/// downtime. This is the node that actually stops the container. /// downtime. This is the node that actually stops the container.
/// - `Swap` (child of `StopForUpdate`), then `PostSwap` (`AfterOk` its sibling /// - `Swap` (child of `StopForUpdate`), then `RebuildBookkeeping` (`AfterOk` its sibling
/// `Swap`): the Ok-only bookkeeping tail (rev marker, forge/matrix sync, /// `Swap`): the Ok-only bookkeeping tail (rev marker, forge/matrix sync,
/// kick, rescan). /// kick, rescan).
/// - `Reconcile` (**last, root**): `AfterAny` `AgentWindow`, which rolls up /// - `Reconcile` (**last, root**): `AfterAny` `AgentWindow`, which rolls up
@ -260,8 +260,8 @@ fn rebuild_subtree<'a>(
let swap = builder let swap = builder
.node(NodeKind::Swap { agent: a() }) .node(NodeKind::Swap { agent: a() })
.part_of(stop_for_update); .part_of(stop_for_update);
let _post_swap = builder let _rebuild_bookkeeping = builder
.node(NodeKind::PostSwap { agent: a() }) .node(NodeKind::RebuildBookkeeping { agent: a() })
.part_of(stop_for_update) .part_of(stop_for_update)
.after_ok(swap); .after_ok(swap);
@ -317,7 +317,7 @@ pub(crate) fn graceful_rebuild_nodes<'a>(
/// `FinalizeDeploy` waits on **two** roots, which together reproduce the gate /// `FinalizeDeploy` waits on **two** roots, which together reproduce the gate
/// the old fused node had around its inline `rebuild_no_meta` call: /// the old fused node had around its inline `rebuild_no_meta` call:
/// - `AfterOk` `Prebuild` — a parent's state is its roll-up, so this is `Done` /// - `AfterOk` `Prebuild` — a parent's state is its roll-up, so this is `Done`
/// only once `StopForUpdate` → `Swap` → `PostSwap` all are (a failed *or* /// only once `StopForUpdate` → `Swap` → `RebuildBookkeeping` all are (a failed *or*
/// cancelled child rolls the parent up `Failed`). That's the old /// cancelled child rolls the parent up `Failed`). That's the old
/// `build_result`. /// `build_result`.
/// - `AfterOk` `Reconcile` — the old call passed `deferred_start = false` on /// - `AfterOk` `Reconcile` — the old call passed `deferred_start = false` on
@ -350,7 +350,7 @@ pub(crate) fn deploy_rebuild_nodes(builder: &JobBuilder, agent: &str, approval_i
/// ///
/// Closed by an [`NodeKind::EmitRebuilt`] tail edged onto all three group-roots /// Closed by an [`NodeKind::EmitRebuilt`] tail edged onto all three group-roots
/// (`MetaSync`, `Prebuild`, `Reconcile`) — `Prebuild`'s roll-up carries the /// (`MetaSync`, `Prebuild`, `Reconcile`) — `Prebuild`'s roll-up carries the
/// whole `StopForUpdate`→`Swap`→`PostSwap` subtree, so those three cover every /// whole `StopForUpdate`→`Swap`→`RebuildBookkeeping` subtree, so those three cover every
/// node. Edging `Reconcile` alone would not do: it is `AfterAny` `Prebuild`, so /// node. Edging `Reconcile` alone would not do: it is `AfterAny` `Prebuild`, so
/// it reaches `Done` even after a failed swap and the tail would report success. /// it reaches `Done` even after a failed swap and the tail would report success.
pub fn rebuild(builder: &JobBuilder, agent: &str, relock: bool) { pub fn rebuild(builder: &JobBuilder, agent: &str, relock: bool) {

View file

@ -418,7 +418,11 @@ fn rebuild_chain_is_declared_serial() {
&[("prebuild", "done")] &[("prebuild", "done")]
), ),
row("swap", Some("stop_for_update"), &[]), row("swap", Some("stop_for_update"), &[]),
row("post_swap", Some("stop_for_update"), &[("swap", "done")]), row(
"rebuild_bookkeeping",
Some("stop_for_update"),
&[("swap", "done")]
),
row( row(
"reconcile", "reconcile",
None, None,
@ -500,7 +504,11 @@ fn graceful_rebuild_chain_drains_before_stopping() {
&[("prebuild", "done"), ("drain", "done")] &[("prebuild", "done"), ("drain", "done")]
), ),
row("swap", Some("stop_for_update"), &[]), row("swap", Some("stop_for_update"), &[]),
row("post_swap", Some("stop_for_update"), &[("swap", "done")]), row(
"rebuild_bookkeeping",
Some("stop_for_update"),
&[("swap", "done")]
),
row( row(
"reconcile", "reconcile",
None, None,
@ -534,7 +542,7 @@ fn non_graceful_rebuild_has_no_signal_or_drain() {
"prebuild", "prebuild",
"stop_for_update", "stop_for_update",
"swap", "swap",
"post_swap", "rebuild_bookkeeping",
"reconcile" "reconcile"
], ],
"exactly seven nodes, and none of them is signal or drain" "exactly seven nodes, and none of them is signal or drain"
@ -713,7 +721,11 @@ fn multi_agent_start_one_dag_folds_per_agent_stale_rebuild() {
&[("prebuild", "done")] &[("prebuild", "done")]
), ),
row("swap", Some("stop_for_update"), &[]), row("swap", Some("stop_for_update"), &[]),
row("post_swap", Some("stop_for_update"), &[("swap", "done")]), row(
"rebuild_bookkeeping",
Some("stop_for_update"),
&[("swap", "done")]
),
row( row(
"reconcile", "reconcile",
None, None,
@ -869,21 +881,21 @@ fn rebuild_chain_nodes_suppress_crash_watch() {
// individually legible, and the arrangement was the only reason this module // individually legible, and the arrangement was the only reason this module
// needed to claim and complete nodes. // needed to claim and complete nodes.
/// The swap-success path: `Swap` ok → the `AfterOk` `PostSwap` (bookkeeping /// The swap-success path: `Swap` ok → the `AfterOk` `RebuildBookkeeping` (bookkeeping
/// tail) runs, and only then does `Reconcile` fire — serialized behind /// tail) runs, and only then does `Reconcile` fire — serialized behind
/// `PostSwap` (not racing it) because `Reconcile` deps `AfterAny(PostSwap)`. /// `RebuildBookkeeping` (not racing it) because `Reconcile` deps `AfterAny(RebuildBookkeeping)`.
#[test] #[test]
fn rebuild_reconcile_waits_for_the_whole_build_subtree() { fn rebuild_reconcile_waits_for_the_whole_build_subtree() {
// Replaces `swap_ok_runs_post_swap_before_reconcile` and // Replaces `swap_ok_runs_post_swap_before_reconcile` and
// `swap_failure_still_runs_reconcile`, which walked the same DAG with the // `swap_failure_still_runs_reconcile`, which walked the same DAG with the
// swap succeeding in one and failing in the other. // swap succeeding in one and failing in the other.
// //
// The interesting claim was "Reconcile must wait for PostSwap, not race // The interesting claim was "Reconcile must wait for RebuildBookkeeping, not race
// it" — and it does *not* come from an edge between them. `reconcile` deps // it" — and it does *not* come from an edge between them. `reconcile` deps
// `AfterAny(agent_window)`, while `post_swap` sits inside the brace's // `AfterAny(agent_window)`, while `rebuild_bookkeeping` sits inside the brace's
// subtree (post_swap → stop_for_update → agent_window). A parent is not // subtree (rebuild_bookkeeping → stop_for_update → agent_window). A parent is not
// terminal until its subtree is, so the brace cannot satisfy that edge while // terminal until its subtree is, so the brace cannot satisfy that edge while
// post_swap is outstanding. **The ordering is the parent chain, not a // rebuild_bookkeeping is outstanding. **The ordering is the parent chain, not a
// dependency.** // dependency.**
// //
// Both facts are asserted in `rebuild_chain_is_declared_serial`; this test // Both facts are asserted in `rebuild_chain_is_declared_serial`; this test
@ -907,7 +919,7 @@ fn rebuild_reconcile_waits_for_the_whole_build_subtree() {
.unwrap_or_else(|| panic!("{kind} node")) .unwrap_or_else(|| panic!("{kind} node"))
.parent .parent
}; };
assert_eq!(parent_of("post_swap"), Some("stop_for_update")); assert_eq!(parent_of("rebuild_bookkeeping"), Some("stop_for_update"));
assert_eq!(parent_of("stop_for_update"), Some("agent_window")); assert_eq!(parent_of("stop_for_update"), Some("agent_window"));
assert_eq!( assert_eq!(
shape shape
@ -917,13 +929,13 @@ fn rebuild_reconcile_waits_for_the_whole_build_subtree() {
.after, .after,
vec![("agent_window", "done|failed|skipped".to_owned())], vec![("agent_window", "done|failed|skipped".to_owned())],
"reconcile gates on the brace's roll-up, which covers the whole \ "reconcile gates on the brace's roll-up, which covers the whole \
subtree including post_swap and runs on failure too" subtree including rebuild_bookkeeping and runs on failure too"
); );
} }
// `swap_failure_still_runs_reconcile` lived here. // `swap_failure_still_runs_reconcile` lived here.
// //
// It asserted that a failed swap leaves `post_swap` `Skipped` and `swap` // It asserted that a failed swap leaves `rebuild_bookkeeping` `Skipped` and `swap`
// `Failed`, and that reconcile still runs. All three are hive_jobq's cascade // `Failed`, and that reconcile still runs. All three are hive_jobq's cascade
// (`failed_after_ok_dep_cancels_dependents_but_after_any_still_runs`), and the // (`failed_after_ok_dep_cancels_dependents_but_after_any_still_runs`), and the
// "says so on the wire" half turned out to be nothing: `snapshot` fills // "says so on the wire" half turned out to be nothing: `snapshot` fills
@ -1300,7 +1312,11 @@ fn deploy_apply_grows_rebuild_subgraph_and_finalizes_after_it() {
&[("prebuild", "done")] &[("prebuild", "done")]
), ),
row("swap", Some("stop_for_update"), &[]), row("swap", Some("stop_for_update"), &[]),
row("post_swap", Some("stop_for_update"), &[("swap", "done")]), row(
"rebuild_bookkeeping",
Some("stop_for_update"),
&[("swap", "done")]
),
row( row(
"reconcile", "reconcile",
None, None,
@ -1539,7 +1555,7 @@ fn perm_change_shape_prefixes_rebuild_chain() {
"prebuild", "prebuild",
"stop_for_update", "stop_for_update",
"swap", "swap",
"post_swap", "rebuild_bookkeeping",
"reconcile", "reconcile",
// the ok / !ok tail pair // the ok / !ok tail pair
"emit_rebuilt", "emit_rebuilt",