jobq: a job asks for the ids it wants back

The operator's instruction on the issue was "the closure returns an array
of guids, and enqueue_job returns the node ids in that order". What was
here instead returned a HashMap of everything inserted, and no caller used
the keys: submit dropped the return, insert_group did into_values(), and
the scheduler ignored what append_subgraph handed back. The guid-keyed
lookup was dead weight, and into_values() made that Vec arbitrarily
ordered -- harmless only because nothing read it.

insert_job now takes FnOnce(&JobBuilder) -> Vec<NodeGuid> and returns the
matching ids positionally. A handle from another job is UnknownNode rather
than a silent omission: the return is positional, so a short vector would
misalign every id after it.

c0re's Declare stays FnOnce(&Job) and the wrapper names no handles in one
place, rather than ending seven templates in an empty vector -- a DAG is
addressed by its container node, which submit inserts itself. That frees
insert_group from needing every id, so the node_rt pre-seeding goes too:
NodeRuntime is one Option field and every reader already tolerated a
missing entry (entry().or_default(), get().and_then(), iter().find()).

The tests are the argument for the shape: capturing a handle through a
mutable binding to look it up in the map afterwards collapses into
returning it and destructuring the result.
This commit is contained in:
atlas 2026-08-02 14:03:37 +02:00 committed by mara
commit bf138ae79a
7 changed files with 147 additions and 76 deletions

View file

@ -103,11 +103,12 @@ impl<N, R: Clone + Eq + Hash> Scheduler<N, R> {
/// Insert a whole job under `root_parent`, returning the id each handle's
/// node was minted as.
///
/// `declare` receives a fresh [`JobBuilder`] and names the job's nodes on
/// it; the builder never leaves this call. That is the whole insertion
/// API — a caller cannot construct a builder, hold one, or insert one
/// itself, so there is no way to end up with a job-shaped value being
/// passed around as a spec.
/// `declare` receives a fresh [`JobBuilder`], names the job's nodes on it,
/// and returns the handles whose ids it wants back — they come back in
/// that order. The builder never leaves this call. That is the whole
/// insertion API — a caller cannot construct a builder, hold one, or
/// insert one itself, so there is no way to end up with a job-shaped value
/// being passed around as a spec.
///
/// The scheduler-side counterpart of [`Graph::insert_job`]: same
/// resolution, but each node goes through [`Scheduler::append`], so a
@ -121,13 +122,14 @@ impl<N, R: Clone + Eq + Hash> Scheduler<N, R> {
pub fn insert_job(
&mut self,
root_parent: Option<NodeId>,
declare: impl FnOnce(&JobBuilder<N, R>),
) -> Result<HashMap<NodeGuid, NodeId>, BuildError> {
declare: impl FnOnce(&JobBuilder<N, R>) -> Vec<NodeGuid>,
) -> Result<Vec<NodeId>, BuildError> {
let job = JobBuilder::new();
declare(&job);
job.insert_with(root_parent, |payload, deps, parent| {
let wanted = declare(&job);
let ids = job.insert_with(root_parent, |payload, deps, parent| {
self.append(payload, deps, parent)
})
})?;
crate::builder::resolve_wanted(&wanted, &ids)
}
/// Claim every currently-runnable pending node and start it: node-deps