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
|
|
@ -30,7 +30,7 @@
|
|||
|
||||
use anyhow::{Result, bail};
|
||||
|
||||
use super::model::{DagSpec, Dep, DepWhen, NodeKind, NodeSpec, PermPayload, Source, Template};
|
||||
use super::model::{DagSpec, Dep, DepWhen, HookKind, NodeKind, NodeSpec, PermPayload, Source};
|
||||
use crate::coordinator::TransientKind;
|
||||
|
||||
/// After-ok edge on the previous node — the common chain link. Shared with
|
||||
|
|
@ -175,7 +175,7 @@ pub(crate) fn deploy_rebuild_nodes(agent: &str) -> Vec<NodeSpec> {
|
|||
/// children.
|
||||
pub fn rebuild(agent: &str, source: Source, reason: String, relock: bool) -> DagSpec {
|
||||
DagSpec {
|
||||
template: Template::Rebuild,
|
||||
hook: Some(HookKind::EmitRebuilt),
|
||||
source,
|
||||
reason,
|
||||
approval_id: None,
|
||||
|
|
@ -206,7 +206,7 @@ pub fn rebuild(agent: &str, source: Source, reason: String, relock: bool) -> Dag
|
|||
pub fn approval_deploy(agent: &str, approval_id: i64, reason: String) -> DagSpec {
|
||||
let a = || agent.to_owned();
|
||||
DagSpec {
|
||||
template: Template::Rebuild,
|
||||
hook: Some(HookKind::ResolveApproval),
|
||||
source: Source::Approval,
|
||||
reason,
|
||||
approval_id: Some(approval_id),
|
||||
|
|
@ -235,14 +235,13 @@ pub fn approval_deploy(agent: &str, approval_id: i64, reason: String) -> DagSpec
|
|||
/// in the queue tests); production paths no longer emit a bare reconcile.
|
||||
#[cfg(test)]
|
||||
pub fn reconcile_only(
|
||||
template: Template,
|
||||
agent: &str,
|
||||
source: Source,
|
||||
reason: String,
|
||||
transient: Option<TransientKind>,
|
||||
) -> DagSpec {
|
||||
DagSpec {
|
||||
template,
|
||||
hook: None,
|
||||
source,
|
||||
reason,
|
||||
approval_id: None,
|
||||
|
|
@ -268,7 +267,7 @@ pub fn reconcile_only(
|
|||
/// container was never created).
|
||||
pub fn spawn(agent: &str, approval_id: i64, reason: String) -> DagSpec {
|
||||
DagSpec {
|
||||
template: Template::Spawn,
|
||||
hook: Some(HookKind::ResolveApproval),
|
||||
source: Source::Approval,
|
||||
reason,
|
||||
approval_id: Some(approval_id),
|
||||
|
|
@ -299,7 +298,7 @@ pub fn perm_change(agent: &str, source: Source, reason: String, payload: PermPay
|
|||
)];
|
||||
nodes.extend(rebuild_nodes(agent, true, 1));
|
||||
DagSpec {
|
||||
template: Template::PermChange,
|
||||
hook: Some(HookKind::EmitRebuilt),
|
||||
source,
|
||||
reason,
|
||||
approval_id: None,
|
||||
|
|
@ -326,7 +325,9 @@ pub fn meta_update(
|
|||
approval_id: Option<i64>,
|
||||
) -> DagSpec {
|
||||
DagSpec {
|
||||
template: Template::MetaUpdate,
|
||||
// The bump itself has no side effect; an approval-driven one still has
|
||||
// its row to resolve.
|
||||
hook: approval_id.map(|_| HookKind::ResolveApproval),
|
||||
source,
|
||||
reason,
|
||||
approval_id,
|
||||
|
|
@ -350,24 +351,14 @@ pub fn meta_update(
|
|||
/// (dashboard tree, `<parent>`/`<children>` sentinel routing, permission
|
||||
/// checks), so a parent move needs no container rebuild to take effect.
|
||||
/// No transient pill either — the node is agentless (no lease to hang one
|
||||
/// off of) and near-instant.
|
||||
///
|
||||
/// Rides `Template::MetaUpdate` rather than a dedicated variant because
|
||||
/// `Template` is being removed and nothing should dispatch on it — a fresh
|
||||
/// variant would just be more surface to delete later. `Template` is already
|
||||
/// internal-only (not on `DagView`'s wire shape — the dashboard derives its
|
||||
/// label from `nodes`), so the choice of stand-in variant only affects
|
||||
/// `terminal_hook` dispatch (`MetaUpdate` resolves to `None`, same as a
|
||||
/// dedicated variant would) and the per-template history-retention bucket —
|
||||
/// both cosmetic. Swap this to whatever the eventual node-kind-derived
|
||||
/// dispatch lands with, whenever it lands.
|
||||
/// off of) and near-instant. No terminal hook: the write is the whole effect.
|
||||
pub fn reparent(
|
||||
moves: Vec<(hive_types::Ident, Option<hive_types::Ident>)>,
|
||||
source: Source,
|
||||
reason: String,
|
||||
) -> DagSpec {
|
||||
DagSpec {
|
||||
template: Template::MetaUpdate,
|
||||
hook: None,
|
||||
source,
|
||||
reason,
|
||||
approval_id: None,
|
||||
|
|
@ -388,7 +379,7 @@ pub fn reparent(
|
|||
/// old queue's documented "circular dep silently deadlocks forever" caveat.
|
||||
pub fn validate(spec: &DagSpec) -> Result<()> {
|
||||
if spec.nodes.is_empty() {
|
||||
bail!("dag spec {:?} has no nodes", spec.template);
|
||||
bail!("dag spec {:?} has no nodes", spec.reason);
|
||||
}
|
||||
let n = spec.nodes.len();
|
||||
let mut graph = petgraph::graph::DiGraph::<u32, ()>::new();
|
||||
|
|
@ -404,14 +395,14 @@ pub fn validate(spec: &DagSpec) -> Result<()> {
|
|||
{
|
||||
bail!(
|
||||
"dag spec {:?} node {i} has invalid parent {p} (must be an earlier node)",
|
||||
spec.template
|
||||
spec.reason
|
||||
);
|
||||
}
|
||||
for dep in &node.deps {
|
||||
let Some(&dep_idx) = usize::try_from(dep.on).ok().and_then(|i| idx.get(i)) else {
|
||||
bail!(
|
||||
"dag spec {:?} node {i} depends on unknown node {}",
|
||||
spec.template,
|
||||
spec.reason,
|
||||
dep.on
|
||||
);
|
||||
};
|
||||
|
|
@ -419,7 +410,7 @@ pub fn validate(spec: &DagSpec) -> Result<()> {
|
|||
}
|
||||
}
|
||||
if petgraph::algo::toposort(&graph, None).is_err() {
|
||||
bail!("dag spec {:?} contains a dependency cycle", spec.template);
|
||||
bail!("dag spec {:?} contains a dependency cycle", spec.reason);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue