refactor(#2949): kill Declare — a running node declares onto its own builder
A node no longer hands back a recipe for the scheduler to replay later. It declares straight onto a builder it was given, and that builder is inserted as part of completing the node. Deleted: `pub type Declare`, `struct NodeOutput` (+ its hand-written `Debug`), `JobQueue::append_subgraph`. Nothing added to `Dag` / `DagView`. jobq gains `Scheduler::new_job()` (the only way to obtain a `JobBuilder`) and `complete_growing(id, outcome, grown)`, which inserts under `id` and *then* completes it, so a DAG cannot roll terminal while grown work is still pending. `complete()` and `complete_growing()` share a private `finish()` rather than one redirecting through the other. The DAG-gone guard lives beside the graph now, where it cannot be skipped, instead of being a caller-side lookup. The growth executors return data (`run_meta_lock -> (Vec<String>, RebuildOpts)`, `run_reconcile -> Option<NodeKind>`) rather than taking the builder: a `&Job` parameter is live for the whole function body, and `&RefCell<T>` is never `Send`, so an async fn taking one cannot be spawned. `run_node` threads the builder by value and hands it back. A node can now declare work and then fail, which was previously inexpressible. `grown` is dropped in that case — failure cancel-cascades downstream, so inserting it would only add nodes to immediately cancel — and the log line carries `grown_nodes` so the drop is visible.
This commit is contained in:
parent
2454a1ea6a
commit
82ef06f445
8 changed files with 388 additions and 344 deletions
|
|
@ -251,6 +251,72 @@ impl<N, R: Clone + Eq + Hash> Scheduler<N, R> {
|
|||
/// propagates up the parent chain. Call [`Scheduler::settle`] again afterwards
|
||||
/// to start newly-unblocked work.
|
||||
pub fn complete(&mut self, id: NodeId, outcome: Outcome) {
|
||||
self.finish(id, outcome);
|
||||
}
|
||||
|
||||
/// A fresh builder for a **running** node to declare more work into.
|
||||
///
|
||||
/// The node runs outside this scheduler's lock — often for minutes — so it
|
||||
/// cannot hold a graph reference while it works. It doesn't need one: a
|
||||
/// builder is pure local state (locally-minted guids, resolved to
|
||||
/// [`NodeId`]s only at insert), so it can be filled in freely and handed
|
||||
/// back to [`Scheduler::complete_growing`], which inserts it under the lock.
|
||||
///
|
||||
/// This is the only way to get one — [`JobBuilder::new`] is `pub(crate)` and
|
||||
/// there is no `Default` impl — so a caller can declare work but never
|
||||
/// insert it itself.
|
||||
#[must_use]
|
||||
pub fn new_job(&self) -> JobBuilder<N, R> {
|
||||
JobBuilder::new()
|
||||
}
|
||||
|
||||
/// [`Scheduler::complete`], plus whatever the node declared into the builder
|
||||
/// it was handed while running.
|
||||
///
|
||||
/// `grown`'s nodes are inserted **under `id`** and *before* the completion,
|
||||
/// so the node cannot roll terminal with its own appended work still
|
||||
/// pending — the same ordering the caller previously had to arrange by
|
||||
/// hand. A job that declares nothing costs nothing: the insert is skipped
|
||||
/// outright, which is the overwhelmingly common case (most nodes grow no
|
||||
/// work at all).
|
||||
///
|
||||
/// # Errors
|
||||
/// [`BuildError`] if `grown` is malformed — **and the node is still
|
||||
/// completed**. Its own work already happened; refusing to complete it
|
||||
/// would misreport that, and leaving it `Running` forever would wedge the
|
||||
/// DAG. So the error is returned for the caller to log, not used to abort
|
||||
/// the completion. This crate has no logger of its own; the caller does.
|
||||
pub fn complete_growing(
|
||||
&mut self,
|
||||
id: NodeId,
|
||||
outcome: Outcome,
|
||||
grown: JobBuilder<N, R>,
|
||||
) -> Result<(), BuildError> {
|
||||
// A node that is no longer in the graph grows nothing. The DAG it
|
||||
// belonged to can be cancelled or evicted while it runs, and the insert
|
||||
// below is *unchecked* — rooting on a departed parent would plant a
|
||||
// dangling `parent` edge rather than being rejected. The host used to
|
||||
// carry this guard itself, as a lookup before a separate append call;
|
||||
// it belongs here, where the graph is and where it cannot be skipped.
|
||||
let grew = if grown.is_empty() || self.graph.node(id).is_none() {
|
||||
Ok(())
|
||||
} else {
|
||||
let graph = &mut self.graph;
|
||||
grown
|
||||
.insert_with(Some(id), &[], |payload, deps, parent| {
|
||||
graph.insert_unchecked(payload, deps, parent)
|
||||
})
|
||||
.map(|_ids| ())
|
||||
};
|
||||
self.finish(id, outcome);
|
||||
grew
|
||||
}
|
||||
|
||||
/// The completion half, shared by [`Scheduler::complete`] and
|
||||
/// [`Scheduler::complete_growing`] so neither is a redirect through the
|
||||
/// other: the growing form must insert *before* this runs, and the plain
|
||||
/// form must not pay for an empty job.
|
||||
fn finish(&mut self, id: NodeId, outcome: Outcome) {
|
||||
match outcome {
|
||||
Outcome::Failed(error) => {
|
||||
// Record the reason before the terminal transition so it's set
|
||||
|
|
|
|||
Loading…
Reference in a new issue