jobq: one insertion entry point, and make it atomic
Three findings from the operator's review, all correct. 1. Two insert_job's. Graph::insert_job had no caller outside hive-jobq's own tests -- production only ever went through Scheduler::insert_job. It existed because the graph-level one got written first. Deleted; the tests moved onto a Scheduler, which is where insertion belongs anyway. 2. insert_job was not atomic, and the previous commit made that worse: a forward edge or forward parent surfaced mid-loop, leaving the nodes before it in the graph, and resolve_wanted ran after every insert, so an unknown handle failed once the whole job was already committed. The module documented this under "Partial insertion" instead of fixing it -- prose describing a hole is not a design. All three are decidable from what the builder holds, so check_declaration_order now runs before the first insert and the loop indexes ids directly. A malformed job leaves the graph untouched. What remains mid-insert is the graph's own rejection (out-of-group dep, empty DepWhen); closing that needs a dry-run validate on Graph, which is a separate change. 3. DagSpec no longer boxes its recipe: it is generic over the closure, which travels from the template that built it straight into submit. The box bought type inference, and paying for it costs annotations -- `|b: &Job|` at each declaration site (the field needs an HRTB, and an unannotated closure binds one lifetime) and `+ use<>` on each returning signature (or the opaque type captures the caller's borrows). Erasure is still needed where several recipe shapes share one type: the boxed Declare stays for the executor's append_subgraph, and a test table uses an erase() helper.
This commit is contained in:
parent
bf138ae79a
commit
f035b63b9a
8 changed files with 211 additions and 176 deletions
|
|
@ -241,7 +241,7 @@ pub(crate) fn rebuild_nodes<'a>(
|
|||
/// already holding it rather than deadlocking against it.
|
||||
pub(crate) fn deploy_rebuild_nodes(agent: &str, approval_id: i64) -> Declare {
|
||||
let agent = agent.to_owned();
|
||||
Box::new(move |b| {
|
||||
Box::new(move |b: &Job| {
|
||||
let roots = rebuild_nodes(
|
||||
b,
|
||||
&agent,
|
||||
|
|
@ -274,12 +274,17 @@ pub(crate) fn deploy_rebuild_nodes(agent: &str, approval_id: i64) -> Declare {
|
|||
/// whole `StopForUpdate`→`Swap`→`PostSwap` subtree, so those three cover every
|
||||
/// 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.
|
||||
pub fn rebuild(agent: &str, source: Source, reason: String, relock: bool) -> DagSpec {
|
||||
pub fn rebuild(
|
||||
agent: &str,
|
||||
source: Source,
|
||||
reason: String,
|
||||
relock: bool,
|
||||
) -> DagSpec<impl FnOnce(&Job) + use<>> {
|
||||
let agent = agent.to_owned();
|
||||
DagSpec {
|
||||
source,
|
||||
reason,
|
||||
declare: Box::new(move |b| {
|
||||
declare: Box::new(move |b: &Job| {
|
||||
let roots = rebuild_nodes(
|
||||
b,
|
||||
&agent,
|
||||
|
|
@ -319,12 +324,16 @@ pub fn rebuild(agent: &str, source: Source, reason: String, relock: bool) -> Dag
|
|||
///
|
||||
/// The window still spans the container build, as it must: `prepare_deploy`
|
||||
/// leaves `flake.lock` staged-uncommitted for the build's whole duration.
|
||||
pub fn approval_deploy(agent: &str, approval_id: i64, reason: String) -> DagSpec {
|
||||
pub fn approval_deploy(
|
||||
agent: &str,
|
||||
approval_id: i64,
|
||||
reason: String,
|
||||
) -> DagSpec<impl FnOnce(&Job) + use<>> {
|
||||
let agent = agent.to_owned();
|
||||
DagSpec {
|
||||
source: Source::Approval,
|
||||
reason,
|
||||
declare: Box::new(move |b| {
|
||||
declare: Box::new(move |b: &Job| {
|
||||
let a = || agent.clone();
|
||||
let window = node(
|
||||
b,
|
||||
|
|
@ -371,12 +380,16 @@ pub fn approval_deploy(agent: &str, approval_id: i64, reason: String) -> DagSpec
|
|||
/// single-node lifecycle DAGs that exercise per-agent lease serialization
|
||||
/// in the queue tests); production paths no longer emit a bare reconcile.
|
||||
#[cfg(test)]
|
||||
pub fn reconcile_only(agent: &str, source: Source, reason: String) -> DagSpec {
|
||||
pub fn reconcile_only(
|
||||
agent: &str,
|
||||
source: Source,
|
||||
reason: String,
|
||||
) -> DagSpec<impl FnOnce(&Job) + use<>> {
|
||||
let agent = agent.to_owned();
|
||||
DagSpec {
|
||||
source,
|
||||
reason,
|
||||
declare: Box::new(move |b| {
|
||||
declare: Box::new(move |b: &Job| {
|
||||
let _reconcile = node(b, NodeKind::Reconcile { agent });
|
||||
}),
|
||||
}
|
||||
|
|
@ -393,12 +406,12 @@ pub fn reconcile_only(agent: &str, source: Source, reason: String) -> DagSpec {
|
|||
/// container was never created). Closed by a `ResolveApproval` tail root edged
|
||||
/// `AfterAny` onto `Provision` — the DAG's only other group-root, so its roll-up
|
||||
/// already carries the whole cascade.
|
||||
pub fn spawn(agent: &str, approval_id: i64, reason: String) -> DagSpec {
|
||||
pub fn spawn(agent: &str, approval_id: i64, reason: String) -> DagSpec<impl FnOnce(&Job) + use<>> {
|
||||
let agent = agent.to_owned();
|
||||
DagSpec {
|
||||
source: Source::Approval,
|
||||
reason,
|
||||
declare: Box::new(move |b| {
|
||||
declare: Box::new(move |b: &Job| {
|
||||
let a = || agent.clone();
|
||||
let provision = node(b, NodeKind::Provision { agent: a() });
|
||||
let create = node(b, NodeKind::Create { agent: a() }).part_of(provision);
|
||||
|
|
@ -417,12 +430,17 @@ pub fn spawn(agent: &str, approval_id: i64, reason: String) -> DagSpec {
|
|||
/// effect in the container. Group-roots are `WritePermFile` plus the rebuild
|
||||
/// subgraph's `MetaSync` / `Prebuild` / `Reconcile`, so the `EmitRebuilt` tail
|
||||
/// edges all four.
|
||||
pub fn perm_change(agent: &str, source: Source, reason: String, payload: PermPayload) -> DagSpec {
|
||||
pub fn perm_change(
|
||||
agent: &str,
|
||||
source: Source,
|
||||
reason: String,
|
||||
payload: PermPayload,
|
||||
) -> DagSpec<impl FnOnce(&Job) + use<>> {
|
||||
let agent = agent.to_owned();
|
||||
DagSpec {
|
||||
source,
|
||||
reason,
|
||||
declare: Box::new(move |b| {
|
||||
declare: Box::new(move |b: &Job| {
|
||||
let write = node(
|
||||
b,
|
||||
NodeKind::WritePermFile {
|
||||
|
|
@ -463,11 +481,11 @@ pub fn meta_update(
|
|||
source: Source,
|
||||
reason: String,
|
||||
approval_id: Option<i64>,
|
||||
) -> DagSpec {
|
||||
) -> DagSpec<impl FnOnce(&Job) + use<>> {
|
||||
DagSpec {
|
||||
source,
|
||||
reason,
|
||||
declare: Box::new(move |b| {
|
||||
declare: Box::new(move |b: &Job| {
|
||||
let lock = node(
|
||||
b,
|
||||
NodeKind::MetaLock {
|
||||
|
|
@ -501,11 +519,11 @@ pub fn reparent(
|
|||
moves: Vec<(hive_types::Ident, Option<hive_types::Ident>)>,
|
||||
source: Source,
|
||||
reason: String,
|
||||
) -> DagSpec {
|
||||
) -> DagSpec<impl FnOnce(&Job) + use<>> {
|
||||
DagSpec {
|
||||
source,
|
||||
reason,
|
||||
declare: Box::new(move |b| {
|
||||
declare: Box::new(move |b: &Job| {
|
||||
let _reparent = node(b, NodeKind::Reparent { moves });
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue