haz-exec 0.1.0

Async task execution engine for haz.
Documentation
//! Async task scheduler and process executor for `haz`.
//!
//! The crate currently exposes the following public modules:
//!
//! - [`process`]: the [`process::ProcessSpawner`] /
//!   [`process::Process`] trait pair, the
//!   [`std_impl::StdProcessSpawner`] production backend over
//!   [`tokio::process::Command`], and the value types every
//!   implementation produces. A scriptable mock implementation lives
//!   alongside but is gated to test builds only and is not part of
//!   the crate's public surface.
//! - [`cache_key`]: the [`cache_key::build_cache_key`] derivation,
//!   gluing the workspace state, the validated dependency graph,
//!   the host-env snapshot, and the per-predecessor stream hashes
//!   into a [`haz_cache::CacheKey`].
//! - [`run_task`]: the single-task lifecycle ([`run_task::run_task`],
//!   [`run_task::cache_lookup_phase`], [`run_task::restore_from_hit`],
//!   [`run_task::run_fresh`]) and the [`run_task::RunObserver`]
//!   port. The [`run_task::RunOutcome`] sum type carries
//!   [`run_task::CompletedRecord`] (a task that reached an
//!   `EXEC-009` classification through the lookup-then-spawn
//!   pipeline), [`run_task::SkipRecord`] (a task the cascade
//!   marked do-not-schedule per `EXEC-010` / `EXEC-011`), and
//!   [`run_task::CancelledRecord`] (a task the cancellation
//!   flow caught per `EXEC-012..015`, in three structural
//!   shapes: signalled in flight, drained from the ready set,
//!   or cascade-cancelled from an upstream cancellation).
//! - [`run_graph`]: the workspace-wide [`run_graph::run_graph`]
//!   scheduler. It walks a validated [`haz_dag::graph::TaskGraph`]
//!   in canonical order, admits ready tasks subject to the
//!   global and per-tag concurrency caps (`EXEC-004` /
//!   `EXEC-005`), evaluates mutex compatibility post-lookup
//!   against a live hold set (`EXEC-006` condition 3 /
//!   `EXEC-007` / `MUTEX-001..007`), threads predecessor stream
//!   hashes into every downstream [`run_task::run_task`] call,
//!   and cascade-skips hard descendants of a failed task while
//!   letting unrelated subgraphs continue (`EXEC-010`). Per
//!   `EXEC-011` the scheduler emits a
//!   [`run_task::RunOutcome::Skipped`] for every cascade-skipped
//!   descendant and fires
//!   [`run_task::RunObserver::on_task_skipped`] at cascade time.
//!
//! Cancellation (`EXEC-009` cancelled state,
//! `EXEC-012..015`) is wired end-to-end: the spawn-step future
//! observes [`run_task::RunContext::cancel`] and runs the
//! per-future SIGTERM / grace / SIGKILL dance against the child's
//! process group on Unix; the scheduler polls the same token,
//! stops admitting new tasks, drains [`run_task::RunOutcome`]s
//! into the `RunCancelled` shape for every task still in the
//! ready set, and reclassifies late-arriving lookup-step results
//! as `RunCancelled`. Cancellation cascades along hard edges via
//! the same `complete_failed` machinery as the failure cascade,
//! emitting [`run_task::CancelledRecord::UpstreamCancelled`]
//! per descendant.
//!
//! Output presentation (`EXEC-016` / `EXEC-017`) is delivered as
//! two distinct [`run_task::RunObserver`] implementations under
//! the [`output`] module:
//! [`output::LiveOutputObserver`] tag-prefixes each emitted line
//! with `[project:task] ` and writes lines atomically under a
//! single per-observer mutex, so multiple in-flight tasks
//! interleave at line granularity only;
//! [`output::BufferedOutputObserver`] accumulates each task's
//! bytes and writes them as two contiguous blocks (`stdout` then
//! `stderr`) on task completion. Cache-hit emission travels the
//! same observer methods as a fresh run, satisfying `EXEC-017`
//! structurally.
//!
//! Runtime DAG validation (`EXEC-019`, `EXEC-020`) is wired:
//! [`run_task::CompletedRecord`] carries each succeeded task's
//! [`run_task::CompletedRecord::materialised_outputs`] (sourced
//! from the output-resolution pass for fresh runs and
//! [`haz_cache::Manifest::outputs`] for cache hits); the
//! [`run_graph::run_graph`] scheduler maintains a per-run claim
//! map for `EXEC-020` and a kind-erased augmented edge set for
//! `EXEC-019`. Detected violations are appended to
//! [`run_graph::RunGraphOutcome::invariant_violations`] as
//! [`run_graph::RuntimeInvariantViolation`] entries
//! ([`run_graph::RuntimeInvariantViolation::OutputOverlap`] and
//! [`run_graph::RuntimeInvariantViolation::RuntimeCycle`]
//! variants). `EXEC-019` additionally trips an
//! internal child token (a child of [`run_task::RunContext::cancel`])
//! so in-flight non-cycle tasks receive SIGTERM via the
//! `EXEC-014` grace-and-escalate flow; the user-supplied parent
//! token stays
//! uncancelled. Pending cycle members in the ready set surface
//! as [`run_task::RunOutcome::Skipped`] with
//! [`run_task::SkipCause::RuntimeCycle`]. `EXEC-020` only stops
//! admission and lets in-flight tasks complete naturally per the
//! spec's silence on cancellation for output overlaps.
//!
//! Exit-code mapping (`EXEC-021`) is delivered by the
//! [`exit_code`] module: [`exit_code::exit_code_for`] takes a
//! finished [`run_graph::RunGraphOutcome`] plus an optional
//! [`exit_code::CancellationSignal`] (recorded by the binary's
//! OS signal handler) and returns the numeric exit status the
//! `haz run` process MUST report. The helper computes the
//! number; the binary that consumes haz-exec performs the
//! actual `process::exit` call (per the workspace-wide "pure
//! library" decision: signal handlers and stdio writes live at
//! the binary boundary, not inside any haz-exec module).

#![deny(missing_docs)]

pub mod cache_key;
pub mod exit_code;
pub(crate) mod hold_set;
#[cfg(any(test, feature = "test-util"))]
pub mod mock_impl;
pub mod output;
pub(crate) mod pattern_walk;
pub mod presenter;
pub mod process;
pub mod run_graph;
pub mod run_task;
pub mod std_impl;