Skip to main content

consortium/dag/
pool.rs

1//! Worker pool management for DAG execution.
2//!
3//! Pools control how many tasks can execute concurrently, optionally
4//! with per-resource-tag slot management.
5
6use std::collections::HashMap;
7use std::sync::Mutex;
8
9/// Trait for managing available execution slots.
10///
11/// The executor calls `acquire` before dispatching a task and
12/// `release` when the task completes.
13pub trait WorkerPool: Send + Sync {
14    /// Try to acquire a slot for a task. Returns true if available.
15    fn acquire(&self, resource_tag: Option<&str>) -> bool;
16
17    /// Release a slot after task completion.
18    fn release(&self, resource_tag: Option<&str>);
19
20    /// Add a resource to the pool (for dynamic pools).
21    fn add_resource(&self, _tag: &str) {}
22
23    /// Remove a resource from the pool (for dynamic pools).
24    fn remove_resource(&self, _tag: &str) {}
25}
26
27/// Fixed-size pool with a global concurrency limit.
28pub struct FixedPool {
29    max: usize,
30    active: Mutex<usize>,
31}
32
33impl FixedPool {
34    pub fn new(max_concurrent: usize) -> Self {
35        Self {
36            max: max_concurrent,
37            active: Mutex::new(0),
38        }
39    }
40}
41
42impl WorkerPool for FixedPool {
43    fn acquire(&self, _resource_tag: Option<&str>) -> bool {
44        let mut active = self.active.lock().unwrap();
45        if *active < self.max {
46            *active += 1;
47            true
48        } else {
49            false
50        }
51    }
52
53    fn release(&self, _resource_tag: Option<&str>) {
54        let mut active = self.active.lock().unwrap();
55        *active = active.saturating_sub(1);
56    }
57}
58
59/// Unlimited pool — no concurrency restriction.
60pub struct UnlimitedPool;
61
62impl WorkerPool for UnlimitedPool {
63    fn acquire(&self, _resource_tag: Option<&str>) -> bool {
64        true
65    }
66
67    fn release(&self, _resource_tag: Option<&str>) {}
68}
69
70/// Dynamic pool where resources can be added/removed during execution.
71///
72/// Tracks both a global limit and per-tag slot counts. A task with a
73/// resource tag requires both a global slot AND a tag-specific slot.
74pub struct DynamicPool {
75    global_max: usize,
76    global_active: Mutex<usize>,
77    tag_slots: Mutex<HashMap<String, (usize, usize)>>, // tag -> (max, active)
78}
79
80impl DynamicPool {
81    pub fn new(global_max: usize) -> Self {
82        Self {
83            global_max,
84            global_active: Mutex::new(0),
85            tag_slots: Mutex::new(HashMap::new()),
86        }
87    }
88
89    /// Set the maximum slots for a specific resource tag.
90    pub fn set_tag_limit(&self, tag: &str, max: usize) {
91        let mut slots = self.tag_slots.lock().unwrap();
92        let entry = slots.entry(tag.to_string()).or_insert((0, 0));
93        entry.0 = max;
94    }
95}
96
97impl WorkerPool for DynamicPool {
98    fn acquire(&self, resource_tag: Option<&str>) -> bool {
99        let mut global = self.global_active.lock().unwrap();
100        if *global >= self.global_max {
101            return false;
102        }
103
104        if let Some(tag) = resource_tag {
105            let mut slots = self.tag_slots.lock().unwrap();
106            if let Some((max, active)) = slots.get_mut(tag) {
107                if *active >= *max {
108                    return false;
109                }
110                *active += 1;
111            }
112            // No limit defined for this tag — allow
113        }
114
115        *global += 1;
116        true
117    }
118
119    fn release(&self, resource_tag: Option<&str>) {
120        let mut global = self.global_active.lock().unwrap();
121        *global = global.saturating_sub(1);
122
123        if let Some(tag) = resource_tag {
124            let mut slots = self.tag_slots.lock().unwrap();
125            if let Some((_, active)) = slots.get_mut(tag) {
126                *active = active.saturating_sub(1);
127            }
128        }
129    }
130
131    fn add_resource(&self, tag: &str) {
132        let mut slots = self.tag_slots.lock().unwrap();
133        let entry = slots.entry(tag.to_string()).or_insert((0, 0));
134        entry.0 += 1;
135    }
136
137    fn remove_resource(&self, tag: &str) {
138        let mut slots = self.tag_slots.lock().unwrap();
139        if let Some((max, _)) = slots.get_mut(tag) {
140            *max = max.saturating_sub(1);
141        }
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn test_fixed_pool() {
151        let pool = FixedPool::new(2);
152        assert!(pool.acquire(None));
153        assert!(pool.acquire(None));
154        assert!(!pool.acquire(None)); // full
155        pool.release(None);
156        assert!(pool.acquire(None)); // slot freed
157    }
158
159    #[test]
160    fn test_unlimited_pool() {
161        let pool = UnlimitedPool;
162        for _ in 0..1000 {
163            assert!(pool.acquire(None));
164        }
165    }
166
167    #[test]
168    fn test_dynamic_pool_global() {
169        let pool = DynamicPool::new(2);
170        assert!(pool.acquire(None));
171        assert!(pool.acquire(None));
172        assert!(!pool.acquire(None));
173    }
174
175    #[test]
176    fn test_dynamic_pool_tags() {
177        let pool = DynamicPool::new(10);
178        pool.set_tag_limit("gpu", 1);
179
180        assert!(pool.acquire(Some("gpu")));
181        assert!(!pool.acquire(Some("gpu"))); // tag limit reached
182        assert!(pool.acquire(Some("cpu"))); // no limit on cpu
183        pool.release(Some("gpu"));
184        assert!(pool.acquire(Some("gpu"))); // slot freed
185    }
186
187    #[test]
188    fn test_dynamic_pool_add_remove() {
189        let pool = DynamicPool::new(10);
190        pool.set_tag_limit("builder", 0);
191        assert!(!pool.acquire(Some("builder"))); // no slots
192
193        pool.add_resource("builder"); // now 1 slot
194        assert!(pool.acquire(Some("builder")));
195        assert!(!pool.acquire(Some("builder")));
196
197        pool.add_resource("builder"); // now 2 slots (1 active)
198        assert!(pool.acquire(Some("builder")));
199    }
200}