feat(#2772): make a dependency edge a set of accepted outcomes
`DepWhen` was two named cases, so every new combination wanted a new
variant. It is now a set over the terminal outcomes: a `u8` bitset
newtype, no dependency, with `AFTER_OK` / `AFTER_ANY` kept as the two
constants the templates actually use. "Run regardless" is all outcomes,
"anything that isn't a failure" is `{Done, Cancelled}`, a compensation
branch is `{Failed}` — closed under combination, so it never needs
another variant.
`TerminalState` is its own type rather than a subset of `State`, so an
edge cannot name `Pending` / `Running` / `Finishing`. Those are
meaningless in a dependency and are better unrepresentable than
validated against. The empty set is the one thing that can't be typed
away — nothing satisfies it, so `validate` rejects it next to the cycle
check.
Two consequences worth calling out:
- `cascade_cancel` collapses to one rule: a pending node is doomed once
any edge it names can no longer be satisfied. The hardcoded `AfterOk`
special case is gone, and a weak-edged node survives its dependency's
cancellation because of its own edge rather than by exemption.
- The cascade now runs on **any** terminal outcome, `Done` included.
With sets, success rules dependents out just as failure does — a
`{Failed}` branch is unsatisfiable the moment its dependency succeeds,
and leaving it `Pending` would wedge the subtree non-terminal forever.
That is a hang, not a wrong answer, so it is the load-bearing half of
this commit.
Edges are conjunctive, so "any of these N failed" is not directly
sayable. The composition that works is in the tests: the success branch
depends `AFTER_OK` on every root, so it is itself cancelled the moment
one of them doesn't succeed, and the failure branch hangs off *that*
with `{Cancelled}`. Exactly one of the two runs.
Also deletes hive-c0re's duplicate `DepWhen` enum and the
`to_crate_when` translation beside it. The copy bought nothing and had
to be widened in lockstep with the crate's edge model — it is the
in-between layer #2772 exists to remove, and it is what broke the build
when the crate's spelling changed.
All 34 jobq tests pass, including the four new ones covering both
directions of a failure-only branch, weak-edge survival of a cancelled
dependency, and the aggregator composition.
This commit is contained in:
parent
7110a25cf6
commit
affedecaa5
6 changed files with 303 additions and 82 deletions
|
|
@ -44,12 +44,12 @@ use chrono::{DateTime, Utc};
|
|||
use hive_host_sock::jobs::NodeView;
|
||||
use hive_jobq::resources::ResourceTable;
|
||||
use hive_jobq::scheduler::{Outcome, Scheduler};
|
||||
use hive_jobq::{Dep, DepWhen as JobDepWhen, Graph, NodeId, State as JobState};
|
||||
use hive_jobq::{Dep, Graph, NodeId, State as JobState};
|
||||
use hive_sh4re::wire_time::now_unix;
|
||||
use tokio::sync::Notify;
|
||||
|
||||
use crate::coordinator::TransientKind;
|
||||
pub use model::{DagSpec, DagView, DepWhen, NodeKind, NodeSpec, PermPayload, Source, State};
|
||||
pub use model::{DagSpec, DagView, NodeKind, NodeSpec, PermPayload, Source, State};
|
||||
use resource::Resource;
|
||||
|
||||
/// How many terminal DAGs (`Done` / `Failed` / `Cancelled`) the snapshot
|
||||
|
|
@ -190,14 +190,6 @@ impl Default for JobQueue {
|
|||
}
|
||||
}
|
||||
|
||||
/// Map a spec dependency edge kind onto the crate's.
|
||||
fn to_crate_when(when: DepWhen) -> JobDepWhen {
|
||||
match when {
|
||||
DepWhen::AfterOk => JobDepWhen::AfterOk,
|
||||
DepWhen::AfterAny => JobDepWhen::AfterAny,
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a crate node state onto the wire state (`Pending` ↔ `Queued`;
|
||||
/// `Finishing` — own logic done, sub-nodes still running — reads as `Running`).
|
||||
fn to_wire_state(state: JobState) -> State {
|
||||
|
|
@ -241,7 +233,7 @@ fn insert_group(
|
|||
for d in &ns.deps {
|
||||
deps.push(Dep::Node {
|
||||
id: ids[dep_index(d.on)],
|
||||
when: to_crate_when(d.when),
|
||||
when: d.when,
|
||||
});
|
||||
}
|
||||
let parent = match ns.parent {
|
||||
|
|
|
|||
|
|
@ -17,18 +17,11 @@ use serde::Serialize;
|
|||
|
||||
use crate::coordinator::TransientKind;
|
||||
|
||||
/// When a dependency edge is considered satisfied.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DepWhen {
|
||||
/// Dep must reach `Done`. A `Failed` / `Cancelled` dep cancels this
|
||||
/// node (cancel-downstream).
|
||||
AfterOk,
|
||||
/// Dep must merely reach a terminal state (ok *or* fail). Used only
|
||||
/// by `rebuild`'s tail `Reconcile` so the recovery-start runs even
|
||||
/// when `Swap` failed.
|
||||
AfterAny,
|
||||
}
|
||||
/// When a dependency edge is satisfied — re-exported from [`hive_jobq`] rather
|
||||
/// than mirrored here. It used to be a duplicate enum with a `to_crate_when`
|
||||
/// translation beside it; the copy bought nothing and had to be widened in
|
||||
/// lockstep every time the crate's edge model grew (#2772).
|
||||
pub use hive_jobq::DepWhen;
|
||||
|
||||
/// A dependency edge (intra-DAG only — cross-DAG ordering comes from
|
||||
/// the per-agent lease + dedup, never from edges between DAGs).
|
||||
|
|
@ -230,7 +223,7 @@ pub enum NodeKind {
|
|||
/// Tail node of an approval-carrying DAG (spawn / opaque deploy / config-PR
|
||||
/// merge): resolve the approval row from how the work actually ended.
|
||||
///
|
||||
/// Weak-edged (`DepWhen::AfterAny`) like [`NodeKind::DeployTail`], so it runs on
|
||||
/// Weak-edged (`DepWhen::AFTER_ANY`) like [`NodeKind::DeployTail`], so it runs on
|
||||
/// success, failure **and cancel** alike and decides internally. It reads its
|
||||
/// dependencies' terminal states off its own [`Claim::deps`] rather than
|
||||
/// re-deriving them from the world the way `DeployTail` reads git: a node is
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ use crate::coordinator::TransientKind;
|
|||
pub(crate) fn after_ok(on: u64) -> Vec<Dep> {
|
||||
vec![Dep {
|
||||
on,
|
||||
when: DepWhen::AfterOk,
|
||||
when: DepWhen::AFTER_OK,
|
||||
}]
|
||||
}
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ pub(crate) fn after_any_all(ons: &[u64]) -> Vec<Dep> {
|
|||
ons.iter()
|
||||
.map(|&on| Dep {
|
||||
on,
|
||||
when: DepWhen::AfterAny,
|
||||
when: DepWhen::AFTER_ANY,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
|
@ -135,7 +135,7 @@ pub(crate) fn rebuild_nodes(agent: &str, relock: bool, base: u64) -> Vec<NodeSpe
|
|||
NodeKind::Reconcile { agent: a() },
|
||||
vec![Dep {
|
||||
on: base + 1,
|
||||
when: DepWhen::AfterAny,
|
||||
when: DepWhen::AFTER_ANY,
|
||||
}],
|
||||
),
|
||||
]
|
||||
|
|
@ -174,11 +174,11 @@ pub(crate) fn deploy_rebuild_nodes(agent: &str) -> Vec<NodeSpec> {
|
|||
vec![
|
||||
Dep {
|
||||
on: 1,
|
||||
when: DepWhen::AfterOk,
|
||||
when: DepWhen::AFTER_OK,
|
||||
},
|
||||
Dep {
|
||||
on: 5,
|
||||
when: DepWhen::AfterOk,
|
||||
when: DepWhen::AFTER_OK,
|
||||
},
|
||||
],
|
||||
));
|
||||
|
|
@ -256,7 +256,7 @@ pub fn approval_deploy(agent: &str, approval_id: i64, reason: String) -> DagSpec
|
|||
NodeKind::DeployTail { agent: a() },
|
||||
vec![Dep {
|
||||
on: 2,
|
||||
when: DepWhen::AfterAny,
|
||||
when: DepWhen::AFTER_ANY,
|
||||
}],
|
||||
),
|
||||
node(
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ fn cyclic_dag_is_rejected_at_submit() {
|
|||
},
|
||||
deps: vec![Dep {
|
||||
on: 1,
|
||||
when: DepWhen::AfterOk,
|
||||
when: DepWhen::AFTER_OK,
|
||||
}],
|
||||
parent: None,
|
||||
},
|
||||
|
|
@ -164,7 +164,7 @@ fn cyclic_dag_is_rejected_at_submit() {
|
|||
},
|
||||
deps: vec![Dep {
|
||||
on: 0,
|
||||
when: DepWhen::AfterOk,
|
||||
when: DepWhen::AFTER_OK,
|
||||
}],
|
||||
parent: None,
|
||||
},
|
||||
|
|
@ -183,7 +183,7 @@ fn unknown_dep_is_rejected_at_submit() {
|
|||
},
|
||||
deps: vec![Dep {
|
||||
on: 9,
|
||||
when: DepWhen::AfterOk,
|
||||
when: DepWhen::AFTER_OK,
|
||||
}],
|
||||
parent: None,
|
||||
}];
|
||||
|
|
|
|||
Loading…
Reference in a new issue