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

@ -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]