Skip to main content

haz_exec/
lib.rs

1//! Async task scheduler and process executor for `haz`.
2//!
3//! The crate currently exposes the following public modules:
4//!
5//! - [`process`]: the [`process::ProcessSpawner`] /
6//!   [`process::Process`] trait pair, the
7//!   [`std_impl::StdProcessSpawner`] production backend over
8//!   [`tokio::process::Command`], and the value types every
9//!   implementation produces. A scriptable mock implementation lives
10//!   alongside but is gated to test builds only and is not part of
11//!   the crate's public surface.
12//! - [`run_task`]: the single-task lifecycle ([`run_task::run_task`],
13//!   [`run_task::cache_lookup_phase`], [`run_task::restore_from_hit`],
14//!   [`run_task::run_fresh`]) and the [`run_task::RunObserver`]
15//!   port. The [`run_task::RunOutcome`] sum type carries
16//!   [`run_task::CompletedRecord`] (a task that reached an
17//!   `EXEC-009` classification through the lookup-then-spawn
18//!   pipeline), [`run_task::SkipRecord`] (a task the cascade
19//!   marked do-not-schedule per `EXEC-010` / `EXEC-011`), and
20//!   [`run_task::CancelledRecord`] (a task the cancellation
21//!   flow caught per `EXEC-012..015`, in three structural
22//!   shapes: signalled in flight, drained from the ready set,
23//!   or cascade-cancelled from an upstream cancellation).
24//! - [`run_graph`]: the workspace-wide [`run_graph::run_graph`]
25//!   scheduler. It walks a validated [`haz_dag::graph::TaskGraph`]
26//!   in canonical order, admits ready tasks subject to the
27//!   global and per-tag concurrency caps (`EXEC-004` /
28//!   `EXEC-005`), evaluates mutex compatibility post-lookup
29//!   against a live hold set (`EXEC-006` condition 3 /
30//!   `EXEC-007` / `MUTEX-001..007`), threads predecessor stream
31//!   hashes into every downstream [`run_task::run_task`] call,
32//!   and cascade-skips hard descendants of a failed task while
33//!   letting unrelated subgraphs continue (`EXEC-010`). Per
34//!   `EXEC-011` the scheduler emits a
35//!   [`run_task::RunOutcome::Skipped`] for every cascade-skipped
36//!   descendant and fires
37//!   [`run_task::RunObserver::on_task_skipped`] at cascade time.
38//!
39//! Cancellation (`EXEC-009` cancelled state,
40//! `EXEC-012..015`) is wired end-to-end: the spawn-step future
41//! observes [`run_task::RunContext::cancel`] and runs the
42//! per-future SIGTERM / grace / SIGKILL dance against the child's
43//! process group on Unix; the scheduler polls the same token,
44//! stops admitting new tasks, drains [`run_task::RunOutcome`]s
45//! into the `RunCancelled` shape for every task still in the
46//! ready set, and reclassifies late-arriving lookup-step results
47//! as `RunCancelled`. Cancellation cascades along hard edges via
48//! the same `complete_failed` machinery as the failure cascade,
49//! emitting [`run_task::CancelledRecord::UpstreamCancelled`]
50//! per descendant.
51//!
52//! Output presentation (`EXEC-016` / `EXEC-017`) is delivered as
53//! two distinct [`run_task::RunObserver`] implementations under
54//! the [`output`] module:
55//! [`output::LiveOutputObserver`] tag-prefixes each emitted line
56//! with `[project:task] ` and writes lines atomically under a
57//! single per-observer mutex, so multiple in-flight tasks
58//! interleave at line granularity only;
59//! [`output::BufferedOutputObserver`] accumulates each task's
60//! bytes and writes them as two contiguous blocks (`stdout` then
61//! `stderr`) on task completion. Cache-hit emission travels the
62//! same observer methods as a fresh run, satisfying `EXEC-017`
63//! structurally.
64//!
65//! Runtime DAG validation (`EXEC-019`, `EXEC-020`) is wired:
66//! [`run_task::CompletedRecord`] carries each succeeded task's
67//! [`run_task::CompletedRecord::materialised_outputs`] (sourced
68//! from the output-resolution pass for fresh runs and
69//! [`haz_cache::Manifest::outputs`] for cache hits); the
70//! [`run_graph::run_graph`] scheduler maintains a per-run claim
71//! map for `EXEC-020` and a kind-erased augmented edge set for
72//! `EXEC-019`. Detected violations are appended to
73//! [`run_graph::RunGraphOutcome::invariant_violations`] as
74//! [`run_graph::RuntimeInvariantViolation`] entries
75//! ([`run_graph::RuntimeInvariantViolation::OutputOverlap`] and
76//! [`run_graph::RuntimeInvariantViolation::RuntimeCycle`]
77//! variants). `EXEC-019` additionally trips an
78//! internal child token (a child of [`run_task::RunContext::cancel`])
79//! so in-flight non-cycle tasks receive SIGTERM via the
80//! `EXEC-014` grace-and-escalate flow; the user-supplied parent
81//! token stays
82//! uncancelled. Pending cycle members in the ready set surface
83//! as [`run_task::RunOutcome::Skipped`] with
84//! [`run_task::SkipCause::RuntimeCycle`]. `EXEC-020` only stops
85//! admission and lets in-flight tasks complete naturally per the
86//! spec's silence on cancellation for output overlaps.
87//!
88//! Exit-code mapping (`EXEC-021`) is delivered by the
89//! [`exit_code`] module: [`exit_code::exit_code_for`] takes a
90//! finished [`run_graph::RunGraphOutcome`] plus an optional
91//! [`exit_code::CancellationSignal`] (recorded by the binary's
92//! OS signal handler) and returns the numeric exit status the
93//! `haz run` process MUST report. The helper computes the
94//! number; the binary that consumes haz-exec performs the
95//! actual `process::exit` call (per the workspace-wide "pure
96//! library" decision: signal handlers and stdio writes live at
97//! the binary boundary, not inside any haz-exec module).
98
99#![deny(missing_docs)]
100
101pub mod exit_code;
102pub(crate) mod hold_set;
103#[cfg(any(test, feature = "test-util"))]
104pub mod mock_impl;
105pub mod output;
106pub mod presenter;
107pub mod process;
108pub mod run_graph;
109pub mod run_task;
110pub mod std_impl;