Commit graph hyperhive/hive-jobq
Author SHA1 Message Date
atlas
4017a57350 docs(#2627): add READMEs for hive-jobq + the socket wire crates
Adds crate READMEs (matching the hive-claude precedent) and wires
readme = "README.md" into each Cargo.toml [package] for hive-jobq,
hive-host-sock, and hive-priv-sock — the crates squarely in the infra
lane. Each README leads with purpose + when-to-use and points at the
crate-root //! docs for depth rather than duplicating them.

First increment of the per-crate-README effort; the shape here is the
proposed template for the remaining crates (see issue discussion).
2026-07-23 12:34:22 +02:00
atlas
03eb64cb5c feat(#2591): hive-jobq Node lifecycle — started/finished timestamps + failure reason
Node gains started_at/finished_at (chrono DateTime<Utc>, serialized
RFC 3339 on the wire per hive_sh4re::wire_time) plus error (String).
Graph::set_state self-stamps started_at on the first Running transition
and finished_at on the first terminal one, via an internal now_utc()
clock (keeps settle/complete signatures stable). Outcome::Failed(String)
carries the failure reason, set on the terminal transition.

hive-c0re complete_node builds Outcome::Failed(msg); its node_rt
side-table stays i64 for now (double-write) until #2637 reads the Node.

Toward #2637: the jobq graph becomes the source of truth for per-node
lifecycle so the queue can be sent to the client as-is.
2026-07-22 23:58:30 +02:00
atlas
5906cc2f2b feat(#2591): hive-jobq parent-axis grouping + borrow + roll-up scheduler
Rework the crate's scheduling model onto an explicit parent (grouping)
axis, separate from the dep (ordering) axis.

- Node gains a structural `parent: Option<NodeId>`, set by the caller
  independent of its `Dep::Node` edges. Grouping is not ordering. A
  `Dep::Node` edge must stay inside the depender's own parent group
  (validated) — never crossing to another group or onto the parent.
- Resource holding walks the parent tree: acquire fresh when no ancestor
  holds it (the acquirer owns it, held for its whole subtree); borrow an
  ancestor's grant (one branch at a time; nodes inside are covered); take
  an extra unit when the grant is lent to a sibling branch, else wait. A
  grant releases only once the owner and its whole subtree are terminal.
- Completion rolls up the parent tree: a node's sub-nodes run after its
  own logic, and it is not terminal until they finish — it parks in
  `State::Finishing`, rolling up to Done (every child Done) or Failed
  (any child Failed/Cancelled). A child is gated on its parent reaching
  Finishing; a downstream dep on a node therefore waits for that node's
  dynamically-appended children with no explicit edge. A failed node
  cancels its pending sub-nodes.

Deletes the SharedResources/ResourceGuard layer (guard.rs) and the
add_dep graph-growth hook (no longer needed). The scheduler stays
single-threaded, owning the ResourceTable directly. Early release of a
grant once no subtree node still needs it is a deferred optimization
(unsafe under dynamically-appended subnodes, #2611).

Base for the hive-c0re job_queue port (#2605), split out so that PR can
rebase onto it.
2026-07-20 20:54:21 +02:00
atlas
02167caf60 refactor(#2500): skip the empty acquire for fully re-entrant nodes
When every resource dep of a node re-enters an ancestor's lock, `owned_reqs`
is empty; the old code still called `acquire(vec![])` and stored a no-op
empty guard in `owned`. Gate the acquire + guard insertion on
`!owned_reqs.is_empty()` — one fewer `borrow_mut` + `HashMap` entry per
fully-re-entrant node in the settle loop. `node_owns` already treats a
missing `owned` entry as non-owning, so behaviour is unchanged.
2026-07-19 16:10:56 +02:00
atlas
f64ab47de0 refactor(#2500): encapsulate the jobq lock, drop the dead borrowed-guard layer
Make the resource lock unmisusable from outside the crate: the public
surface is now purely declarative (build a Graph with Dep::Resource edges,
configure capacities, run the Scheduler), and the scheduler owns every
acquire/release — a consumer never holds a guard, so it cannot hold the
lock wrong.

- `guard` module + `ResourceTable::try_acquire_all`/`release_all` +
  `Graph::set_state` are now `pub(crate)`.
- Remove the dead borrowed-guard layer (`ResourceGuard::borrowed`,
  `Acq::Borrowed`, `is_owning`): the scheduler tracks re-entrancy via its
  own single borrow slot per (holder, resource) and never constructs a
  borrowed guard, so re-entrancy lives in exactly one place. `Acq`
  collapses into the owning `ResourceGuard` struct.
- `#[must_use]` on `Scheduler::settle` — ignoring its ids silently drops
  runnable work.
- `SharedResources::with` (test-only table observability) is `#[cfg(test)]`.
- Drop the moot borrowed-guard tests; retained owning tests are black-box,
  and the redundant `set_state` test helper is gone.
2026-07-19 15:24:11 +02:00
atlas
b4bcf8b6e4 refactor(jobq): make the crate generic over the resource type R
Replace the concrete ResourceName(String) with a type parameter
R: Clone + Eq + Hash threaded end-to-end (Dep<R>, Node<N,R>, Graph<N,R>,
ResourceTable<R>, ResourceGuard<R>/SharedResources<R>, Scheduler<N,R>).
The crate no longer hard-codes the resource identity; the consumer picks
the concrete type (a String, or an enum like BuildSlot/Agent(name)) at
the port. Tests use String as the concrete R. Pure type-parameter
thread-through, no logic change. 25 tests green, clippy pedantic clean.
2026-07-19 15:24:11 +02:00
atlas
7ffc13dc86 feat(#2500): hive-jobq scheduler re-entrancy + eager AfterOk cascade 2026-07-19 15:24:11 +02:00
atlas
12e618097a feat(#2500): hive-jobq scheduler settle loop (owned resources + subtree-hold) 2026-07-19 15:24:11 +02:00
atlas
01ff8071f6 feat(#2500): add hive-jobq RAII resource guards with recursive re-entrancy 2026-07-19 15:24:11 +02:00
atlas
29ceca4323 feat(#2500): add hive-jobq ResourceTable with atomic all-or-nothing acquire
Named counting-semaphore resources — the Dep::Resource side of the v2
model. A ResourceTable tracks per-name capacity + held counts; unconfigured
names default to capacity 1 (created lazily). try_acquire_all grants every
requested unit or none, leaving the table untouched on failure — so a node
never holds one resource while waiting for another, which is what makes the
scheduler deadlock-free without cycle detection. Duplicate names in a
request are summed; over-capacity requests can never acquire. release_all
saturates rather than underflowing. Runtime scheduler state, not persisted:
held counts are rederived from running nodes on restart. Guard objects
(RAII release, recursive re-entrancy) wrap this in a follow-up.
2026-07-17 14:46:25 +02:00
atlas
11df4a1bf5 feat(#2500): validate NodeId references on insert and deserialize
Per mara's direction — validate ids as they enter the graph so internal
iteration can trust every id the graph holds; the generational route for
removal comes later. Adds GraphError; insert() now rejects a dangling
Dep::Node / parent id (it is fallible); validate() checks all internal id
references resolve and that next_id is past the largest existing id;
deserialization runs validate() via #[serde(try_from = "GraphData<N>")], so
a loaded graph can never carry a dangling reference. 5 new tests; serde_json
added as a dev-dependency for the round-trip cases.
2026-07-17 12:54:19 +02:00
atlas
570188fa1a refactor(#2500): make NodeId genuinely opaque via a crate-private field
argus flagged that `NodeId(pub u64)` contradicted the "opaque" doc — a pub
inner field lets callers fabricate `NodeId(42)`. Make the field `pub(crate)`
so an id can only originate from the graph's monotonic counter or serde
deserialization, never a caller. Tests construct ids in-crate (unaffected);
the derived Serialize/Deserialize round-trips fine. Doc keeps "opaque" — now
accurate — with a line explaining the enforcement.
2026-07-17 12:54:19 +02:00
atlas
581737583e fix(#2500): group_terminal must require the group node itself terminal
group_terminal checked only that every child was terminal, never the group
node's own state — so an empty group whose node is still Running returned
true (empty .all()), making a running node that has yet to append its
subgraph look already-finished. Now it requires the group node itself
terminal AND every child recursively terminal. Deciding when to settle a
group node to terminal once its children are done stays a scheduler concern;
this answers the dependents' question — is the whole group, node included,
finished. Adds group-node-pending-with-child-done + empty-running-group tests.
2026-07-17 12:54:19 +02:00
atlas
8f5ccb2882 feat(#2500): scaffold hive-jobq crate with the core graph data model
First step of extracting the job-DAG queue into a domain-agnostic
`hive-jobq` library, per the operator's v2 design: one persistent
graph, named-counter resources, recursive node groups, opaque stable
node ids, guard-object locks, a slot-filling scheduler.

This commit lands only the data model, so the shape can be reviewed
before the machinery is built on it:

- NodeId: opaque, stable, monotonic; group membership is a parent
  edge, not encoded in the id (the 1/1/2 hierarchy is a derived UI
  label).
- ResourceName, Dep (Node | Resource{name,count}), State.
- Node<N>: caller-defined payload N so the library stays
  container-agnostic.
- Graph<N>: insert (mints stable ids), node lookup, children,
  recursive group-terminal check. Retains completed groups (no
  pruning in v1).

The resource-acquisition machinery (atomic all-or-nothing acquire),
the recursive-lock guards, and the scheduler loop are follow-ups.
Tests cover id minting, group terminality, and state terminality;
clippy + rustdoc clean.
2026-07-17 12:54:19 +02:00