refactor(#2916): destroy submits a DAG instead of an imperative teardown
Destroy was a straight-line async fn with no queue node behind it, so
nothing in the graph could answer "is this container going down on
purpose?". That gap is why an imperative crash-watch suppression guard
existed: an RAII handle held for the operation's duration, a second way
to say what every other lifecycle op already says through its node.
Reuse the existing Stop node rather than teaching a new node to stop
things:
Stop -> DestroyContainer -> (PurgeState) -> DestroyBookkeeping
Stop already declares takes_container_down honestly, so the suppression
is now derived from the graph like every other op's. It also turns the
precondition into an edge: DestroyContainer runs only under a completed
Stop, so it operates on an already-stopped container and carries
takes_container_down = false permanently. A container still alive at
that point is a real bug and stays loud instead of being absorbed by a
flag -- which matters because a wrong true silently swallows a crash
while a wrong false only costs a spurious event.
Removes suppress_crash_watch, CrashWatchSuppression, crash_suppressed,
crash_watch_suppressed and NO_NODE_LABEL. The migration call sites went
with the obsolete startup migrations, so destroy was the last caller and
intent now has exactly one home.
destroy() becomes a submit-and-return, matching every sibling endpoint
(rebuild, kill, restart, start, pause, resume) -- it was the only
lifecycle op that awaited its work. The container rescan moves into the
bookkeeping tail, so ContainerRemoved now arrives after the 200 rather
than before it.
Also drops an orphaned doc-comment in coordinator.rs: two stacked blocks
where only the second described crash_suppressed, the first documenting
a field that no longer exists. Removing the field would have re-pointed
it at recent_transient.
This commit is contained in:
parent
2c7872841a
commit
6338939657
9 changed files with 326 additions and 194 deletions
|
|
@ -474,6 +474,59 @@ pub fn spawn(builder: &JobBuilder, agent: &str, approval_id: i64) {
|
|||
resolve_approval_tails(builder, approval_id, provision);
|
||||
}
|
||||
|
||||
/// Teardown: `Stop` → `DestroyContainer` → (`PurgeState`) → `DestroyBookkeeping`.
|
||||
///
|
||||
/// The chain is the point, not a decomposition for its own sake. Destroy used
|
||||
/// to be a straight-line async fn with no queue node behind it, so nothing in
|
||||
/// the graph could answer "is this container going down on purpose?" — which is
|
||||
/// why an imperative crash-watch suppression guard existed at all. Reusing the
|
||||
/// existing [`NodeKind::Stop`] answers it structurally: `Stop` already declares
|
||||
/// `takes_container_down`, so the suppression is derived from the graph like
|
||||
/// every other lifecycle op's.
|
||||
///
|
||||
/// That also makes the precondition an edge rather than an assertion.
|
||||
/// `DestroyContainer` runs only after `Stop` succeeded, so it operates on an
|
||||
/// already-stopped container and carries `takes_container_down = false`
|
||||
/// permanently — a container still alive at that point is a real bug and stays
|
||||
/// loud instead of being absorbed by a flag.
|
||||
///
|
||||
/// `Stop` is idempotent against an already-down container, so the common
|
||||
/// "destroy something that isn't running" path costs nothing extra.
|
||||
///
|
||||
/// `Stop` is the group root and holds the agent lease for the whole teardown;
|
||||
/// the rest are `part_of` children that borrow it, so no other op can interleave
|
||||
/// with a half-destroyed agent. `PurgeState` is inserted only when asked for —
|
||||
/// the graph shows the irreversible step as its own row when it happens, and
|
||||
/// omits it entirely when it doesn't.
|
||||
pub fn destroy(builder: &JobBuilder, agent: &str, purge: bool) {
|
||||
let a = || agent.to_owned();
|
||||
let stop = builder
|
||||
.node(NodeKind::Stop { agent: a() })
|
||||
.needs(Resource::Agent(a()));
|
||||
// `part_of` IS the ordering: a child runs once its parent reaches
|
||||
// `Finishing`, and a node may not also declare a dep on its own parent
|
||||
// (dep-scope validation rejects it — it would deadlock). So the
|
||||
// "container is already stopped" precondition is the group edge itself,
|
||||
// with no explicit `after_ok(stop)` to add.
|
||||
let destroy = builder
|
||||
.node(NodeKind::DestroyContainer { agent: a() })
|
||||
.part_of(stop);
|
||||
// The bookkeeping tail hangs off the purge when there is one, so the
|
||||
// irreversible delete lands before the meta sync that stops referencing it.
|
||||
let last = if purge {
|
||||
builder
|
||||
.node(NodeKind::PurgeState { agent: a() })
|
||||
.part_of(stop)
|
||||
.after_ok(destroy)
|
||||
} else {
|
||||
destroy
|
||||
};
|
||||
let _tail = builder
|
||||
.node(NodeKind::DestroyBookkeeping { agent: a(), purge })
|
||||
.part_of(stop)
|
||||
.after_ok(last);
|
||||
}
|
||||
|
||||
/// Perm change: commit the JSON file(s), then the rebuild subgraph so
|
||||
/// the updated `HIVE_TOOL_GROUPS` / `HIVE_CAPABILITIES` env var takes
|
||||
/// effect in the container. Group-roots are `WritePermFile` plus the rebuild
|
||||
|
|
|
|||
Loading…
Reference in a new issue