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.
This commit is contained in:
atlas 2026-07-19 14:40:25 +02:00 committed by mara
commit f64ab47de0
4 changed files with 41 additions and 111 deletions

View file

@ -27,9 +27,9 @@
//! completion via guard objects, recursive within a group.
//!
//! The [`scheduler`] settle loop drives execution; the resource machinery
//! lives in [`resources`] and the RAII lock guards over it in [`guard`].
//! lives in [`resources`] and the RAII lock guards over it in `guard`.
pub mod guard;
pub(crate) mod guard;
pub mod resources;
pub mod scheduler;
@ -298,7 +298,7 @@ impl<N, R> Graph<N, R> {
/// Set a node's lifecycle state, returning `false` for an unknown id. The
/// scheduler drives every state transition — nothing else mutates state,
/// which is what keeps the resource guards + terminality in sync.
pub fn set_state(&mut self, id: NodeId, state: State) -> bool {
pub(crate) fn set_state(&mut self, id: NodeId, state: State) -> bool {
if let Some(node) = self.nodes.iter_mut().find(|n| n.id == id) {
node.state = state;
true
@ -381,11 +381,6 @@ mod tests {
assert_eq!(g.node(a).unwrap().parent, None);
}
fn set_state<N, R>(g: &mut Graph<N, R>, id: NodeId, state: State) {
let idx = g.nodes.iter().position(|n| n.id == id).unwrap();
g.nodes[idx].state = state;
}
#[test]
fn group_terminal_requires_the_group_node_and_all_children_terminal() {
let mut g: Graph<&str, String> = Graph::new();
@ -395,10 +390,10 @@ mod tests {
assert!(!g.group_terminal(group));
// Child done, but the group node itself is still pending → NOT terminal:
// the group node's own state is load-bearing, not just its children.
set_state(&mut g, child, State::Done);
g.set_state(child, State::Done);
assert!(!g.group_terminal(group));
// Group node terminal too → the whole group is terminal.
set_state(&mut g, group, State::Done);
g.set_state(group, State::Done);
assert!(g.group_terminal(group));
}
@ -408,10 +403,10 @@ mod tests {
// not read as terminal just because its child set is currently empty.
let mut g: Graph<&str, String> = Graph::new();
let group = g.insert("group", vec![], None).unwrap();
set_state(&mut g, group, State::Running);
g.set_state(group, State::Running);
assert!(!g.group_terminal(group));
// Once it finishes (having grown no children), it is terminal.
set_state(&mut g, group, State::Done);
g.set_state(group, State::Done);
assert!(g.group_terminal(group));
}