refactor(#2756): declare the terminal hook instead of inferring it
`Template` was a DAG-level enum that three different things read back out: `terminal_hook()` mapped it to a side effect, the retention pass bucketed history by it, and a tracing field printed it. None of those needed a *label* — they needed the two facts the label happened to encode. So the enum was a lossy stand-in for intent, and every new DAG shape had to pick the variant whose inferred behaviour matched, whether or not the name fit (`reparent` rode `MetaUpdate` for exactly this reason, with a 10-line comment apologising for it). Replace the inference with a declaration: `DagSpec.hook: Option<HookKind>`. Only the builder assembling a DAG knows why it did so, so only the builder can say what should happen when it settles. `run_terminal_hook` becomes a field read, and `reparent`'s apology becomes `hook: None`. Hook assignment is byte-identical to the old precedence rule (`approval_id.is_some()` wins, then `Rebuild | PermChange`), checked site by site; `meta_update` is the only builder with a variable approval id and so the only remaining conditional. Retention loses the per-template bucket with the enum that keyed it. The dashboard renders one recent-builds list, so one flat newest-first cap (`MAX_HISTORY_DAGS`) bounds it. `HISTORY_GRACE_SECS` goes too — it existed to stop a burst of same-template DAGs evicting each other inside one poll interval, which is not a failure mode a flat cap has. That takes `snapshot_capped()` and the `snapshot_no_grace()` test hook with it. The queue is runtime-only (empty graph on boot), so the serde changes carry no migration risk.
This commit is contained in:
parent
a8728ac532
commit
ca7146e4f0
8 changed files with 113 additions and 241 deletions
|
|
@ -17,55 +17,25 @@ use serde::Serialize;
|
|||
|
||||
use crate::coordinator::TransientKind;
|
||||
|
||||
/// What a DAG *means* — the request-level shape. Internal to the queue now:
|
||||
/// it drives the terminal-hook dispatch ([`crate::job_queue`]'s `dag_hook`)
|
||||
/// and the meta-update dedup key, and is **no longer sent on the wire** — the
|
||||
/// dashboard derives a DAG's label from its node kinds (see `DagView`). The
|
||||
/// `NodeKind::Dag` container carries it in its payload.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
|
||||
/// The inline side effect a settled DAG fires when its container node rolls
|
||||
/// up terminal (there is no hook *node*). Stated explicitly by the builder in
|
||||
/// `templates.rs` / `submit.rs` rather than inferred from a DAG-level enum:
|
||||
/// only the builder knows why it assembled the DAG, so only the builder can
|
||||
/// say what should happen at the end of it.
|
||||
///
|
||||
/// A cancelled DAG deliberately gets **no** compensating hook. [`super::JobQueue::cancel`]
|
||||
/// refuses unless every work node is still `Pending`, and a cancel *cascade*
|
||||
/// rolls up `Failed` (see `dag_rollup`), never `Cancelled` — so on a
|
||||
/// `Cancelled` DAG no node ever executed and there is nothing to undo. A power
|
||||
/// op's `SetWanted` head provably never ran, so its intent is still whatever
|
||||
/// the operator last set it to.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Template {
|
||||
/// Rebuild one agent's container (prebuild → stop → profile-swap →
|
||||
/// reconcile).
|
||||
Rebuild,
|
||||
/// Bump meta flake locks; grows a rebuild subgraph per affected
|
||||
/// agent into the same DAG on completion.
|
||||
MetaUpdate,
|
||||
/// First-deploy spawn (approval-driven).
|
||||
Spawn,
|
||||
/// Mechanical stop + converge to `wanted = Up` (a restart).
|
||||
Restart,
|
||||
/// Signal → drain → mechanical stop → converge to `wanted = Up` — a
|
||||
/// graceful restart as one atomic DAG.
|
||||
GracefulRestart,
|
||||
/// Perm-file commit followed by the rebuild subgraph.
|
||||
PermChange,
|
||||
/// Quiesce the harness, drain, then stop (`wanted = Offline`).
|
||||
GracefulStop,
|
||||
/// Converge to `wanted = Up`.
|
||||
Start,
|
||||
/// Converge to `wanted = Offline`.
|
||||
Stop,
|
||||
/// Boot-time config sweep as one DAG.
|
||||
Boot,
|
||||
}
|
||||
|
||||
impl Template {
|
||||
#[must_use]
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Template::Rebuild => "rebuild",
|
||||
Template::MetaUpdate => "meta_update",
|
||||
Template::Spawn => "spawn",
|
||||
Template::Restart => "restart",
|
||||
Template::GracefulRestart => "graceful_restart",
|
||||
Template::PermChange => "perm_change",
|
||||
Template::GracefulStop => "graceful_stop",
|
||||
Template::Start => "start",
|
||||
Template::Stop => "stop",
|
||||
Template::Boot => "boot",
|
||||
}
|
||||
}
|
||||
pub enum HookKind {
|
||||
/// Approval-driven DAG (spawn / opaque deploy): resolve the approval row.
|
||||
ResolveApproval,
|
||||
/// Rebuild / perm-change: emit one `Rebuilt` manager event per agent.
|
||||
EmitRebuilt,
|
||||
}
|
||||
|
||||
/// When a dependency edge is considered satisfied.
|
||||
|
|
@ -293,15 +263,16 @@ pub enum NodeKind {
|
|||
/// is skipped.)
|
||||
SetWanted { agent: String, up: bool },
|
||||
/// The **DAG container** node: one per submitted DAG, carrying the group's
|
||||
/// domain metadata. Every template node hangs *under* it (its subtree), so
|
||||
/// domain metadata. Every node hangs *under* it (its subtree), so
|
||||
/// the container's `NodeId` **is** the DAG id, its rolled-up state **is** the
|
||||
/// DAG state, and it reaching terminal **is** the completion signal that
|
||||
/// fires the DAG's inline hook (approval-resolve / rebuilt-emit /
|
||||
/// intent-revert, dispatched off `template`). Pure grouping — lease- and
|
||||
/// fires the DAG's inline `hook`. Pure grouping — lease- and
|
||||
/// build-slot-exempt; the executor instant-completes it (`Done`) so it
|
||||
/// reaches `Finishing` and its children start.
|
||||
Dag {
|
||||
template: Template,
|
||||
/// The side effect to run when this DAG settles, or `None` for a DAG
|
||||
/// with none (power op, meta-update, boot).
|
||||
hook: Option<HookKind>,
|
||||
source: Source,
|
||||
reason: String,
|
||||
transient: Option<TransientKind>,
|
||||
|
|
@ -475,14 +446,16 @@ pub struct NodeSpec {
|
|||
/// ([`NodeKind::WritePermFile`]), not this generic spec.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DagSpec {
|
||||
pub template: Template,
|
||||
/// The inline side effect to fire when this DAG settles. Explicit — the
|
||||
/// builder assembling the DAG is the only thing that knows its intent.
|
||||
pub hook: Option<HookKind>,
|
||||
pub source: Source,
|
||||
/// Free-form "why".
|
||||
pub reason: String,
|
||||
/// Fires the approval-resolution hook on DAG terminal.
|
||||
/// The approval row [`HookKind::ResolveApproval`] resolves. Set together
|
||||
/// with that hook; carried separately because the hook needs the id.
|
||||
pub approval_id: Option<i64>,
|
||||
/// `MetaUpdate`-only: the inputs to bump (also part of the dedup
|
||||
/// key for that template). Display copy lives on the DAG.
|
||||
/// Meta-update only: the inputs to bump. Display copy lives on the DAG.
|
||||
pub inputs: Vec<String>,
|
||||
/// Dashboard transient pill (and crash-watch suppression) held for
|
||||
/// the lease window — from lease acquisition to DAG terminal.
|
||||
|
|
|
|||
Loading…
Reference in a new issue