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.
This commit is contained in:
atlas 2026-07-18 21:49:27 +02:00 committed by mara
commit b4bcf8b6e4
4 changed files with 105 additions and 100 deletions

View file

@ -18,10 +18,11 @@
//! the eager `AfterOk` failure cascade are layered on top of this owned core.
use std::collections::HashMap;
use std::hash::Hash;
use crate::guard::{ResourceGuard, SharedResources};
use crate::resources::ResourceTable;
use crate::{Dep, DepWhen, Graph, GraphError, NodeId, ResourceName, State};
use crate::{Dep, DepWhen, Graph, GraphError, NodeId, State};
/// The result of a node's own execution, reported to [`Scheduler::complete`].
///
@ -37,23 +38,23 @@ pub enum Outcome {
/// Drives a [`Graph`] over a shared resource pool: claim runnable nodes, hold
/// their resources for the subtree's lifetime, release on subtree-terminal.
pub struct Scheduler<N> {
graph: Graph<N>,
resources: SharedResources,
pub struct Scheduler<N, R: Clone + Eq + Hash> {
graph: Graph<N, R>,
resources: SharedResources<R>,
/// Owned resource guards, keyed by the node that acquired them. Dropped
/// (releasing the units) when that node's whole subtree is terminal.
owned: HashMap<NodeId, Vec<ResourceGuard>>,
owned: HashMap<NodeId, Vec<ResourceGuard<R>>>,
/// The single re-entrancy slot per `(ancestor-holder, resource)`: the id of
/// the descendant currently *borrowing* that ancestor's lock. Present ⇒ the
/// slot is taken, so no other descendant may re-enter the same lock until
/// the borrower's subtree is terminal — "only one node at a time within".
borrow_slots: HashMap<(NodeId, ResourceName), NodeId>,
borrow_slots: HashMap<(NodeId, R), NodeId>,
}
impl<N> Scheduler<N> {
impl<N, R: Clone + Eq + Hash> Scheduler<N, R> {
/// A scheduler over `graph` with `resources` as the capacity pool.
#[must_use]
pub fn new(graph: Graph<N>, resources: ResourceTable) -> Self {
pub fn new(graph: Graph<N, R>, resources: ResourceTable<R>) -> Self {
Self {
graph,
resources: SharedResources::new(resources),
@ -64,7 +65,7 @@ impl<N> Scheduler<N> {
/// The graph, for inspection (state, hierarchy, UI rendering).
#[must_use]
pub fn graph(&self) -> &Graph<N> {
pub fn graph(&self) -> &Graph<N, R> {
&self.graph
}
@ -77,7 +78,7 @@ impl<N> Scheduler<N> {
pub fn append(
&mut self,
payload: N,
deps: Vec<Dep>,
deps: Vec<Dep<R>>,
parent: Option<NodeId>,
) -> Result<NodeId, GraphError> {
self.graph.insert(payload, deps, parent)
@ -138,7 +139,7 @@ impl<N> Scheduler<N> {
/// The nearest ancestor of `id` that *owns* (holds real units of) `name`,
/// or `None` if no ancestor holds it (⇒ `id` must own-acquire it itself).
fn ancestor_owning(&self, id: NodeId, name: &ResourceName) -> Option<NodeId> {
fn ancestor_owning(&self, id: NodeId, name: &R) -> Option<NodeId> {
let mut cursor = self.graph.node(id)?.parent;
while let Some(ancestor) = cursor {
if self.node_owns(ancestor, name) {
@ -150,7 +151,7 @@ impl<N> Scheduler<N> {
}
/// Whether node `holder` holds an owned guard covering resource `name`.
fn node_owns(&self, holder: NodeId, name: &ResourceName) -> bool {
fn node_owns(&self, holder: NodeId, name: &R) -> bool {
self.owned.get(&holder).is_some_and(|guards| {
guards
.iter()
@ -246,7 +247,7 @@ impl<N> Scheduler<N> {
}
/// The `(name, count)` resource units `id` must hold to run.
fn resource_reqs(&self, id: NodeId) -> Vec<(ResourceName, u32)> {
fn resource_reqs(&self, id: NodeId) -> Vec<(R, u32)> {
let Some(node) = self.graph.node(id) else {
return Vec::new();
};
@ -264,25 +265,25 @@ impl<N> Scheduler<N> {
mod tests {
use super::*;
fn res(name: &str) -> ResourceName {
ResourceName(name.to_owned())
fn res(name: &str) -> String {
name.to_owned()
}
/// A graph + a resource table with `build-slot` set to `slots`.
fn scheduler_with_slots(slots: u32) -> Scheduler<&'static str> {
fn scheduler_with_slots(slots: u32) -> Scheduler<&'static str, String> {
let mut table = ResourceTable::new();
table.set_capacity(res("build-slot"), slots);
Scheduler::new(Graph::new(), table)
}
fn slot_dep() -> Vec<Dep> {
fn slot_dep() -> Vec<Dep<String>> {
vec![Dep::Resource {
name: res("build-slot"),
count: 1,
}]
}
fn resource_dep(name: &str) -> Vec<Dep> {
fn resource_dep(name: &str) -> Vec<Dep<String>> {
vec![Dep::Resource {
name: res(name),
count: 1,
@ -379,7 +380,7 @@ mod tests {
#[test]
fn failed_after_ok_dep_cancels_dependents_but_after_any_still_runs() {
let mut s = Scheduler::new(Graph::new(), ResourceTable::new());
let mut s: Scheduler<&str, String> = Scheduler::new(Graph::new(), ResourceTable::new());
let root = s.append("root", vec![], None).expect("root");
let strong1 = s
.append(