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:
parent
7ffc13dc86
commit
b4bcf8b6e4
4 changed files with 105 additions and 100 deletions
|
|
@ -18,18 +18,25 @@
|
|||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::ResourceName;
|
||||
use std::hash::Hash;
|
||||
|
||||
use crate::resources::ResourceTable;
|
||||
|
||||
/// A [`ResourceTable`] shared between the scheduler and the live guards that
|
||||
/// release back into it on drop. Cheap to clone — an `Rc` refcount bump.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SharedResources(Rc<RefCell<ResourceTable>>);
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SharedResources<R>(Rc<RefCell<ResourceTable<R>>>);
|
||||
|
||||
impl SharedResources {
|
||||
impl<R: Clone + Eq + Hash> Default for SharedResources<R> {
|
||||
fn default() -> Self {
|
||||
Self::new(ResourceTable::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Clone + Eq + Hash> SharedResources<R> {
|
||||
/// Wrap an existing table so guards can release into it.
|
||||
#[must_use]
|
||||
pub fn new(table: ResourceTable) -> Self {
|
||||
pub fn new(table: ResourceTable<R>) -> Self {
|
||||
Self(Rc::new(RefCell::new(table)))
|
||||
}
|
||||
|
||||
|
|
@ -41,7 +48,7 @@ impl SharedResources {
|
|||
/// an over-capacity request can never succeed — same all-or-nothing
|
||||
/// semantics as [`ResourceTable::try_acquire_all`].
|
||||
#[must_use]
|
||||
pub fn acquire(&self, reqs: Vec<(ResourceName, u32)>) -> Option<ResourceGuard> {
|
||||
pub fn acquire(&self, reqs: Vec<(R, u32)>) -> Option<ResourceGuard<R>> {
|
||||
if self.0.borrow_mut().try_acquire_all(&reqs) {
|
||||
Some(ResourceGuard(Acq::Owned {
|
||||
table: self.clone(),
|
||||
|
|
@ -57,18 +64,18 @@ impl SharedResources {
|
|||
/// Keep the closure short: it holds a shared borrow, so calling
|
||||
/// [`SharedResources::acquire`] (a mutable borrow) from inside it would
|
||||
/// panic on the overlapping `RefCell` borrow.
|
||||
pub fn with<R>(&self, f: impl FnOnce(&ResourceTable) -> R) -> R {
|
||||
pub fn with<T>(&self, f: impl FnOnce(&ResourceTable<R>) -> T) -> T {
|
||||
f(&self.0.borrow())
|
||||
}
|
||||
}
|
||||
|
||||
/// How a [`ResourceGuard`] relates to the units it represents.
|
||||
#[derive(Debug)]
|
||||
enum Acq {
|
||||
enum Acq<R> {
|
||||
/// Owns real units; drop releases them back into the shared table.
|
||||
Owned {
|
||||
table: SharedResources,
|
||||
reqs: Vec<(ResourceName, u32)>,
|
||||
table: SharedResources<R>,
|
||||
reqs: Vec<(R, u32)>,
|
||||
},
|
||||
/// Re-entrant reuse of a resource an ancestor group already holds; drop
|
||||
/// releases nothing.
|
||||
|
|
@ -78,9 +85,9 @@ enum Acq {
|
|||
/// An RAII grant of resources. Dropping it releases exactly what was acquired
|
||||
/// (nothing, for a borrowed re-entrant guard).
|
||||
#[derive(Debug)]
|
||||
pub struct ResourceGuard(Acq);
|
||||
pub struct ResourceGuard<R: Clone + Eq + Hash>(Acq<R>);
|
||||
|
||||
impl ResourceGuard {
|
||||
impl<R: Clone + Eq + Hash> ResourceGuard<R> {
|
||||
/// A borrowed (re-entrant) guard that owns no units and releases nothing on
|
||||
/// drop. The scheduler hands one to a sub-node that depends on a resource
|
||||
/// its ancestor group already holds, so the shared unit is released once —
|
||||
|
|
@ -93,7 +100,7 @@ impl ResourceGuard {
|
|||
/// The `(name, count)` units this guard releases on drop — empty when it is
|
||||
/// a borrowed re-entrant guard.
|
||||
#[must_use]
|
||||
pub fn held(&self) -> &[(ResourceName, u32)] {
|
||||
pub fn held(&self) -> &[(R, u32)] {
|
||||
match &self.0 {
|
||||
Acq::Owned { reqs, .. } => reqs,
|
||||
Acq::Borrowed => &[],
|
||||
|
|
@ -108,7 +115,7 @@ impl ResourceGuard {
|
|||
}
|
||||
}
|
||||
|
||||
impl Drop for ResourceGuard {
|
||||
impl<R: Clone + Eq + Hash> Drop for ResourceGuard<R> {
|
||||
fn drop(&mut self) {
|
||||
if let Acq::Owned { table, reqs } = &self.0 {
|
||||
table.0.borrow_mut().release_all(reqs);
|
||||
|
|
@ -120,11 +127,11 @@ impl Drop for ResourceGuard {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn res(name: &str) -> ResourceName {
|
||||
ResourceName(name.to_owned())
|
||||
fn res(name: &str) -> String {
|
||||
name.to_owned()
|
||||
}
|
||||
|
||||
fn shared_with(slots: u32) -> SharedResources {
|
||||
fn shared_with(slots: u32) -> SharedResources<String> {
|
||||
let mut t = ResourceTable::new();
|
||||
t.set_capacity(res("build-slot"), slots);
|
||||
SharedResources::new(t)
|
||||
|
|
@ -161,7 +168,7 @@ mod tests {
|
|||
let _owner = sr.acquire(vec![(agent.clone(), 1)]).expect("fits");
|
||||
sr.with(|t| assert_eq!(t.available(&agent), 0));
|
||||
{
|
||||
let b = ResourceGuard::borrowed();
|
||||
let b = ResourceGuard::<String>::borrowed();
|
||||
assert!(!b.is_owning());
|
||||
assert!(b.held().is_empty());
|
||||
} // borrowed drop is a no-op
|
||||
|
|
@ -179,7 +186,7 @@ mod tests {
|
|||
.expect("group takes the lock");
|
||||
{
|
||||
// A sub-node reuses the group's lock: borrowed, no re-acquire.
|
||||
let _sub = ResourceGuard::borrowed();
|
||||
let _sub = ResourceGuard::<String>::borrowed();
|
||||
sr.with(|t| assert_eq!(t.available(&agent), 0));
|
||||
} // sub-node done — must NOT free the shared lock
|
||||
sr.with(|t| assert_eq!(t.available(&agent), 0));
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@
|
|||
//! inserts a self-contained **node group** and returns its id; the scheduler
|
||||
//! runs a continuous loop, starting every node whose [`Dep`]s are satisfied:
|
||||
//!
|
||||
//! - **Resource** deps are named counting semaphores ([`ResourceName`]):
|
||||
//! `build-slot` (capacity N), `agent/<name>` (capacity 1), or any name
|
||||
//! (capacity 1, created on use). A node acquires *all* its resource deps
|
||||
//! atomically at start (all-or-nothing) — no hold-and-wait, so no deadlock
|
||||
//! and no cycle detection needed.
|
||||
//! - **Resource** deps are named counting semaphores over a caller-chosen
|
||||
//! type `R` (a `String` or an enum): `build-slot` (cap N), `agent/<name>`
|
||||
//! (cap 1), or any name (cap 1, created on use). A node acquires *all* its
|
||||
//! resource deps atomically at start (all-or-nothing) — no hold-and-wait,
|
||||
//! so no deadlock and no cycle detection needed.
|
||||
//! - **Node** deps wait on a node/group per [`DepWhen`]: `AfterOk` needs
|
||||
//! success (a failed dep cancels the dependent), `AfterAny` only terminal.
|
||||
//!
|
||||
|
|
@ -49,16 +49,6 @@ pub mod scheduler;
|
|||
)]
|
||||
pub struct NodeId(pub(crate) u64);
|
||||
|
||||
/// A named counting semaphore.
|
||||
///
|
||||
/// Examples: `build-slot` (capacity configured to the number of build slots),
|
||||
/// `agent/<name>` (capacity 1 — the per-agent lifecycle lock), or any other
|
||||
/// name, which is assumed to have capacity 1 and is created on first use.
|
||||
#[derive(
|
||||
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
pub struct ResourceName(pub String);
|
||||
|
||||
/// When a [`Dep::Node`] edge is satisfied — the strong/weak distinction the
|
||||
/// current queue carries as `DepWhen`, load-bearing for failure safety.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
|
|
@ -89,7 +79,7 @@ impl DepWhen {
|
|||
/// edge it names is satisfied (per its [`DepWhen`]) *and* every [`Dep::Resource`]
|
||||
/// it names can be acquired (all of them, atomically).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Dep {
|
||||
pub enum Dep<R> {
|
||||
/// Depend on another node (or a group, by its group node's id). Whether a
|
||||
/// *failed* dependency satisfies the edge is decided by `when`: `AfterOk`
|
||||
/// requires success (and cancels this node if the dep fails), `AfterAny`
|
||||
|
|
@ -105,7 +95,7 @@ pub enum Dep {
|
|||
/// released when the node completes.
|
||||
Resource {
|
||||
/// The resource to acquire.
|
||||
name: ResourceName,
|
||||
name: R,
|
||||
/// How many units to hold (usually 1).
|
||||
count: u32,
|
||||
},
|
||||
|
|
@ -143,7 +133,7 @@ impl State {
|
|||
/// payload; the caller supplies `N` (its own node kind) and a runner to execute
|
||||
/// a claimed node.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Node<N> {
|
||||
pub struct Node<N, R> {
|
||||
/// Stable identity, assigned on insert.
|
||||
pub id: NodeId,
|
||||
/// The group this node belongs to, if any. `None` for a top-level group
|
||||
|
|
@ -152,7 +142,7 @@ pub struct Node<N> {
|
|||
/// Caller-defined payload (the node's kind / work description).
|
||||
pub payload: N,
|
||||
/// What must hold before this node runs (other nodes + resources).
|
||||
pub deps: Vec<Dep>,
|
||||
pub deps: Vec<Dep<R>>,
|
||||
/// Lifecycle state.
|
||||
pub state: State,
|
||||
}
|
||||
|
|
@ -188,9 +178,15 @@ pub enum GraphError {
|
|||
/// walks this graph filling open slots. Completed groups are retained (no
|
||||
/// pruning in v1).
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(try_from = "GraphData<N>")]
|
||||
pub struct Graph<N> {
|
||||
nodes: Vec<Node<N>>,
|
||||
#[serde(
|
||||
try_from = "GraphData<N, R>",
|
||||
bound(
|
||||
serialize = "N: serde::Serialize, R: serde::Serialize",
|
||||
deserialize = "N: serde::Deserialize<'de>, R: serde::Deserialize<'de>"
|
||||
)
|
||||
)]
|
||||
pub struct Graph<N, R> {
|
||||
nodes: Vec<Node<N, R>>,
|
||||
next_id: u64,
|
||||
}
|
||||
|
||||
|
|
@ -198,15 +194,16 @@ pub struct Graph<N> {
|
|||
// below — which runs [`Graph::validate`], so a loaded graph can never carry a
|
||||
// dangling id reference (Serialize does not validate; Deserialize always does).
|
||||
#[derive(serde::Deserialize)]
|
||||
struct GraphData<N> {
|
||||
nodes: Vec<Node<N>>,
|
||||
#[serde(bound(deserialize = "N: serde::Deserialize<'de>, R: serde::Deserialize<'de>"))]
|
||||
struct GraphData<N, R> {
|
||||
nodes: Vec<Node<N, R>>,
|
||||
next_id: u64,
|
||||
}
|
||||
|
||||
impl<N> TryFrom<GraphData<N>> for Graph<N> {
|
||||
impl<N, R> TryFrom<GraphData<N, R>> for Graph<N, R> {
|
||||
type Error = GraphError;
|
||||
|
||||
fn try_from(data: GraphData<N>) -> Result<Self, Self::Error> {
|
||||
fn try_from(data: GraphData<N, R>) -> Result<Self, Self::Error> {
|
||||
let graph = Graph {
|
||||
nodes: data.nodes,
|
||||
next_id: data.next_id,
|
||||
|
|
@ -218,13 +215,13 @@ impl<N> TryFrom<GraphData<N>> for Graph<N> {
|
|||
|
||||
// A `derive(Default)` would wrongly require `N: Default` (an empty graph holds
|
||||
// no payload); an empty `Vec<Node<N>>` needs no such bound, so impl it directly.
|
||||
impl<N> Default for Graph<N> {
|
||||
impl<N, R> Default for Graph<N, R> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<N> Graph<N> {
|
||||
impl<N, R> Graph<N, R> {
|
||||
/// An empty graph.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
|
|
@ -255,7 +252,7 @@ impl<N> Graph<N> {
|
|||
pub fn insert(
|
||||
&mut self,
|
||||
payload: N,
|
||||
deps: Vec<Dep>,
|
||||
deps: Vec<Dep<R>>,
|
||||
parent: Option<NodeId>,
|
||||
) -> Result<NodeId, GraphError> {
|
||||
if let Some(parent_id) = parent
|
||||
|
|
@ -283,18 +280,18 @@ impl<N> Graph<N> {
|
|||
|
||||
/// Borrow a node by id.
|
||||
#[must_use]
|
||||
pub fn node(&self, id: NodeId) -> Option<&Node<N>> {
|
||||
pub fn node(&self, id: NodeId) -> Option<&Node<N, R>> {
|
||||
self.nodes.iter().find(|n| n.id == id)
|
||||
}
|
||||
|
||||
/// The direct children of a group node (nodes whose `parent` is `id`).
|
||||
pub fn children(&self, id: NodeId) -> impl Iterator<Item = &Node<N>> {
|
||||
pub fn children(&self, id: NodeId) -> impl Iterator<Item = &Node<N, R>> {
|
||||
self.nodes.iter().filter(move |n| n.parent == Some(id))
|
||||
}
|
||||
|
||||
/// Every node in the graph, in insertion order. The scheduler iterates
|
||||
/// this to find runnable pending nodes.
|
||||
pub fn nodes(&self) -> impl Iterator<Item = &Node<N>> {
|
||||
pub fn nodes(&self) -> impl Iterator<Item = &Node<N, R>> {
|
||||
self.nodes.iter()
|
||||
}
|
||||
|
||||
|
|
@ -365,7 +362,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn insert_mints_stable_monotonic_ids() {
|
||||
let mut g: Graph<&str> = Graph::new();
|
||||
let mut g: Graph<&str, String> = Graph::new();
|
||||
let a = g.insert("sweep", vec![], None).unwrap();
|
||||
let b = g
|
||||
.insert(
|
||||
|
|
@ -384,14 +381,14 @@ mod tests {
|
|||
assert_eq!(g.node(a).unwrap().parent, None);
|
||||
}
|
||||
|
||||
fn set_state<N>(g: &mut Graph<N>, id: NodeId, state: State) {
|
||||
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> = Graph::new();
|
||||
let mut g: Graph<&str, String> = Graph::new();
|
||||
let group = g.insert("group", vec![], None).unwrap();
|
||||
let child = g.insert("child", vec![], Some(group)).unwrap();
|
||||
// Both pending → not terminal.
|
||||
|
|
@ -409,7 +406,7 @@ mod tests {
|
|||
fn empty_running_group_is_not_terminal() {
|
||||
// A running node with no children yet may still append some, so it must
|
||||
// not read as terminal just because its child set is currently empty.
|
||||
let mut g: Graph<&str> = Graph::new();
|
||||
let mut g: Graph<&str, String> = Graph::new();
|
||||
let group = g.insert("group", vec![], None).unwrap();
|
||||
set_state(&mut g, group, State::Running);
|
||||
assert!(!g.group_terminal(group));
|
||||
|
|
@ -444,7 +441,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn insert_rejects_unknown_parent() {
|
||||
let mut g: Graph<&str> = Graph::new();
|
||||
let mut g: Graph<&str, String> = Graph::new();
|
||||
let bogus = NodeId(7);
|
||||
assert_eq!(
|
||||
g.insert("x", vec![], Some(bogus)).unwrap_err(),
|
||||
|
|
@ -454,7 +451,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn insert_rejects_unknown_dep() {
|
||||
let mut g: Graph<&str> = Graph::new();
|
||||
let mut g: Graph<&str, String> = Graph::new();
|
||||
let bogus = NodeId(42);
|
||||
let deps = vec![Dep::Node {
|
||||
id: bogus,
|
||||
|
|
@ -468,7 +465,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn valid_graph_round_trips_through_serde() {
|
||||
let mut g: Graph<String> = Graph::new();
|
||||
let mut g: Graph<String, String> = Graph::new();
|
||||
let a = g.insert("a".to_owned(), vec![], None).unwrap();
|
||||
g.insert(
|
||||
"b".to_owned(),
|
||||
|
|
@ -480,7 +477,7 @@ mod tests {
|
|||
)
|
||||
.unwrap();
|
||||
let json = serde_json::to_string(&g).unwrap();
|
||||
let back: Graph<String> = serde_json::from_str(&json).unwrap();
|
||||
let back: Graph<String, String> = serde_json::from_str(&json).unwrap();
|
||||
assert!(back.validate().is_ok());
|
||||
assert_eq!(back.node(a).unwrap().payload, "a");
|
||||
}
|
||||
|
|
@ -489,7 +486,7 @@ mod tests {
|
|||
fn deserialize_rejects_a_dangling_dependency() {
|
||||
// Build a graph whose only node depends on a non-existent id, serialize
|
||||
// it (Serialize does not validate), and confirm deserialize rejects it.
|
||||
let bad = Graph::<String> {
|
||||
let bad = Graph::<String, String> {
|
||||
nodes: vec![Node {
|
||||
id: NodeId(0),
|
||||
parent: None,
|
||||
|
|
@ -503,13 +500,13 @@ mod tests {
|
|||
next_id: 1,
|
||||
};
|
||||
let json = serde_json::to_string(&bad).unwrap();
|
||||
let err = serde_json::from_str::<Graph<String>>(&json).unwrap_err();
|
||||
let err = serde_json::from_str::<Graph<String, String>>(&json).unwrap_err();
|
||||
assert!(err.to_string().contains("unknown node"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_next_id_that_would_remint() {
|
||||
let bad = Graph::<&str> {
|
||||
let bad = Graph::<&str, String> {
|
||||
nodes: vec![Node {
|
||||
id: NodeId(5),
|
||||
parent: None,
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@
|
|||
//!
|
||||
//! [`Dep::Resource`]: crate::Dep::Resource
|
||||
|
||||
use crate::ResourceName;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hash;
|
||||
|
||||
/// A set of named counting semaphores.
|
||||
///
|
||||
|
|
@ -30,22 +30,22 @@ use std::collections::HashMap;
|
|||
/// units atomically via [`ResourceTable::try_acquire_all`] /
|
||||
/// [`ResourceTable::release_all`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResourceTable {
|
||||
/// Configured capacities, keyed by name. Missing ⇒ `default_capacity`.
|
||||
capacities: HashMap<ResourceName, u32>,
|
||||
/// Units currently held, keyed by name. Missing ⇒ 0.
|
||||
held: HashMap<ResourceName, u32>,
|
||||
/// Capacity assumed for a name with no configured entry.
|
||||
pub struct ResourceTable<R> {
|
||||
/// Configured capacities, keyed by resource. Missing ⇒ `default_capacity`.
|
||||
capacities: HashMap<R, u32>,
|
||||
/// Units currently held, keyed by resource. Missing ⇒ 0.
|
||||
held: HashMap<R, u32>,
|
||||
/// Capacity assumed for a resource with no configured entry.
|
||||
default_capacity: u32,
|
||||
}
|
||||
|
||||
impl Default for ResourceTable {
|
||||
impl<R: Clone + Eq + Hash> Default for ResourceTable<R> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceTable {
|
||||
impl<R: Clone + Eq + Hash> ResourceTable<R> {
|
||||
/// An empty table whose unconfigured names default to capacity 1.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
|
|
@ -61,14 +61,14 @@ impl ResourceTable {
|
|||
/// Overwrites any previous capacity for that name. Lowering capacity below
|
||||
/// the currently-held count is allowed — the table simply reports zero
|
||||
/// available until enough is released; it never rejects a config change.
|
||||
pub fn set_capacity(&mut self, name: ResourceName, capacity: u32) {
|
||||
pub fn set_capacity(&mut self, name: R, capacity: u32) {
|
||||
self.capacities.insert(name, capacity);
|
||||
}
|
||||
|
||||
/// The capacity of a name — its configured value, or the default (1) if it
|
||||
/// was never configured.
|
||||
#[must_use]
|
||||
pub fn capacity(&self, name: &ResourceName) -> u32 {
|
||||
pub fn capacity(&self, name: &R) -> u32 {
|
||||
self.capacities
|
||||
.get(name)
|
||||
.copied()
|
||||
|
|
@ -77,13 +77,13 @@ impl ResourceTable {
|
|||
|
||||
/// Units of a name currently held (0 if none).
|
||||
#[must_use]
|
||||
pub fn held(&self, name: &ResourceName) -> u32 {
|
||||
pub fn held(&self, name: &R) -> u32 {
|
||||
self.held.get(name).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Units of a name available to acquire right now (`capacity - held`).
|
||||
#[must_use]
|
||||
pub fn available(&self, name: &ResourceName) -> u32 {
|
||||
pub fn available(&self, name: &R) -> u32 {
|
||||
self.capacity(name).saturating_sub(self.held(name))
|
||||
}
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ impl ResourceTable {
|
|||
/// `false` and leaves the table completely untouched. A request for more
|
||||
/// units than a name's capacity can therefore never succeed — the scheduler
|
||||
/// should reject such a node at insert time so it does not wait forever.
|
||||
pub fn try_acquire_all(&mut self, reqs: &[(ResourceName, u32)]) -> bool {
|
||||
pub fn try_acquire_all(&mut self, reqs: &[(R, u32)]) -> bool {
|
||||
let wanted = aggregate(reqs);
|
||||
// All-or-nothing: bail before mutating if any request cannot be met.
|
||||
for (name, &count) in &wanted {
|
||||
|
|
@ -113,7 +113,7 @@ impl ResourceTable {
|
|||
///
|
||||
/// Duplicate names are summed. Releasing more than is held saturates at zero
|
||||
/// rather than underflowing, so a double release is harmless.
|
||||
pub fn release_all(&mut self, reqs: &[(ResourceName, u32)]) {
|
||||
pub fn release_all(&mut self, reqs: &[(R, u32)]) {
|
||||
for (name, count) in aggregate(reqs) {
|
||||
if let Some(h) = self.held.get_mut(name) {
|
||||
*h = h.saturating_sub(count);
|
||||
|
|
@ -123,8 +123,8 @@ impl ResourceTable {
|
|||
}
|
||||
|
||||
/// Sum a request list into per-name totals so duplicate names are one entry.
|
||||
fn aggregate(reqs: &[(ResourceName, u32)]) -> HashMap<&ResourceName, u32> {
|
||||
let mut wanted: HashMap<&ResourceName, u32> = HashMap::new();
|
||||
fn aggregate<R: Eq + Hash>(reqs: &[(R, u32)]) -> HashMap<&R, u32> {
|
||||
let mut wanted: HashMap<&R, u32> = HashMap::new();
|
||||
for (name, count) in reqs {
|
||||
*wanted.entry(name).or_insert(0) += *count;
|
||||
}
|
||||
|
|
@ -135,8 +135,8 @@ fn aggregate(reqs: &[(ResourceName, u32)]) -> HashMap<&ResourceName, u32> {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn res(name: &str) -> ResourceName {
|
||||
ResourceName(name.to_owned())
|
||||
fn res(name: &str) -> String {
|
||||
name.to_owned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue