Skip to main content

oximedia_distributed/
work_stealing.rs

1//! Work-stealing queue for task distribution.
2//!
3//! Implements a work-stealing scheduler where each worker thread has
4//! its own double-ended queue. The owner pops tasks from the back
5//! (LIFO) while idle workers steal from the front (FIFO) of other queues.
6
7#![allow(dead_code)]
8
9/// A task that can be stolen by other workers.
10#[derive(Debug, Clone)]
11pub struct StealableTask {
12    /// Unique task identifier.
13    pub id: u64,
14    /// Priority value (higher = more important).
15    pub priority: u32,
16    /// Task payload (serialized command or descriptor).
17    pub payload: String,
18}
19
20impl StealableTask {
21    /// Create a new stealable task.
22    #[must_use]
23    pub fn new(id: u64, priority: u32, payload: impl Into<String>) -> Self {
24        Self {
25            id,
26            priority,
27            payload: payload.into(),
28        }
29    }
30
31    /// Returns true if this task has a priority of 10 or higher.
32    #[must_use]
33    pub fn is_high_priority(&self) -> bool {
34        self.priority >= 10
35    }
36}
37
38/// A double-ended work queue owned by a single worker.
39///
40/// The owner pushes and pops from the back (LIFO); thieves steal
41/// from the front (FIFO) to minimise cache invalidation.
42#[derive(Debug)]
43pub struct WorkQueue {
44    /// The internal deque of tasks (front = steal end, back = owner end).
45    pub deque: Vec<StealableTask>,
46    /// Identifier of the owning worker.
47    pub owner_id: u32,
48}
49
50impl WorkQueue {
51    /// Create a new empty work queue for the given owner.
52    #[must_use]
53    pub fn new(owner_id: u32) -> Self {
54        Self {
55            deque: Vec::new(),
56            owner_id,
57        }
58    }
59
60    /// Push a task onto the owner's end of the queue (back).
61    pub fn push(&mut self, task: StealableTask) {
62        self.deque.push(task);
63    }
64
65    /// Pop a task from the owner's end of the queue (back, LIFO).
66    ///
67    /// Returns `None` if the queue is empty.
68    pub fn pop(&mut self) -> Option<StealableTask> {
69        self.deque.pop()
70    }
71
72    /// Steal a task from the thief's end of the queue (front, FIFO).
73    ///
74    /// Returns `None` if the queue is empty.
75    pub fn steal(&mut self) -> Option<StealableTask> {
76        if self.deque.is_empty() {
77            None
78        } else {
79            Some(self.deque.remove(0))
80        }
81    }
82
83    /// Returns the number of tasks in the queue.
84    #[must_use]
85    pub fn len(&self) -> usize {
86        self.deque.len()
87    }
88
89    /// Returns true if the queue has no tasks.
90    #[must_use]
91    pub fn is_empty(&self) -> bool {
92        self.deque.is_empty()
93    }
94
95    /// Returns all high-priority tasks (without removing them).
96    #[must_use]
97    pub fn high_priority_tasks(&self) -> Vec<&StealableTask> {
98        self.deque.iter().filter(|t| t.is_high_priority()).collect()
99    }
100}
101
102/// A work-stealing scheduler managing multiple worker queues.
103#[derive(Debug, Default)]
104pub struct WorkStealingScheduler {
105    /// One queue per worker, indexed by position.
106    pub queues: Vec<WorkQueue>,
107}
108
109impl WorkStealingScheduler {
110    /// Create a new empty scheduler.
111    #[must_use]
112    pub fn new() -> Self {
113        Self { queues: Vec::new() }
114    }
115
116    /// Add a new queue for the given `owner_id`.
117    ///
118    /// If a queue for `owner_id` already exists this is a no-op.
119    pub fn add_queue(&mut self, owner_id: u32) {
120        if !self.queues.iter().any(|q| q.owner_id == owner_id) {
121            self.queues.push(WorkQueue::new(owner_id));
122        }
123    }
124
125    /// Submit a task to the queue owned by `owner_id`.
126    ///
127    /// Returns `false` if no queue for `owner_id` exists.
128    pub fn submit_task(&mut self, owner_id: u32, task: StealableTask) -> bool {
129        if let Some(queue) = self.queues.iter_mut().find(|q| q.owner_id == owner_id) {
130            queue.push(task);
131            true
132        } else {
133            false
134        }
135    }
136
137    /// Steal a task from the queue owned by `target_id`.
138    ///
139    /// Returns `None` if the queue doesn't exist or is empty.
140    pub fn steal_from(&mut self, target_id: u32) -> Option<StealableTask> {
141        self.queues
142            .iter_mut()
143            .find(|q| q.owner_id == target_id)
144            .and_then(WorkQueue::steal)
145    }
146
147    /// Pop a task for the given `owner_id` (owner's own pop, LIFO).
148    pub fn pop_for(&mut self, owner_id: u32) -> Option<StealableTask> {
149        self.queues
150            .iter_mut()
151            .find(|q| q.owner_id == owner_id)
152            .and_then(WorkQueue::pop)
153    }
154
155    /// Returns the total number of pending tasks across all queues.
156    #[must_use]
157    pub fn total_pending(&self) -> usize {
158        self.queues.iter().map(WorkQueue::len).sum()
159    }
160
161    /// Returns the number of queues registered.
162    #[must_use]
163    pub fn queue_count(&self) -> usize {
164        self.queues.len()
165    }
166
167    /// Find the busiest queue (most tasks) and return its owner ID.
168    #[must_use]
169    pub fn busiest_owner(&self) -> Option<u32> {
170        self.queues
171            .iter()
172            .max_by_key(|q| q.len())
173            .filter(|q| !q.is_empty())
174            .map(|q| q.owner_id)
175    }
176
177    /// Find the most idle queue (fewest tasks) and return its owner ID.
178    #[must_use]
179    pub fn idlest_owner(&self) -> Option<u32> {
180        self.queues
181            .iter()
182            .min_by_key(|q| q.len())
183            .map(|q| q.owner_id)
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    fn task(id: u64, priority: u32) -> StealableTask {
192        StealableTask::new(id, priority, format!("payload_{}", id))
193    }
194
195    #[test]
196    fn test_stealable_task_is_high_priority() {
197        assert!(task(1, 10).is_high_priority());
198        assert!(task(2, 15).is_high_priority());
199        assert!(!task(3, 9).is_high_priority());
200        assert!(!task(4, 0).is_high_priority());
201    }
202
203    #[test]
204    fn test_work_queue_push_pop_lifo() {
205        let mut q = WorkQueue::new(1);
206        q.push(task(1, 5));
207        q.push(task(2, 5));
208        q.push(task(3, 5));
209
210        // Owner pops LIFO (last pushed = first out)
211        assert_eq!(q.pop().expect("pop should return a value").id, 3);
212        assert_eq!(q.pop().expect("pop should return a value").id, 2);
213        assert_eq!(q.pop().expect("pop should return a value").id, 1);
214        assert!(q.pop().is_none());
215    }
216
217    #[test]
218    fn test_work_queue_steal_fifo() {
219        let mut q = WorkQueue::new(1);
220        q.push(task(1, 5));
221        q.push(task(2, 5));
222        q.push(task(3, 5));
223
224        // Thief steals FIFO (first pushed = first stolen)
225        assert_eq!(q.steal().expect("steal should return a task").id, 1);
226        assert_eq!(q.steal().expect("steal should return a task").id, 2);
227        assert_eq!(q.steal().expect("steal should return a task").id, 3);
228        assert!(q.steal().is_none());
229    }
230
231    #[test]
232    fn test_work_queue_len_and_is_empty() {
233        let mut q = WorkQueue::new(1);
234        assert!(q.is_empty());
235        assert_eq!(q.len(), 0);
236
237        q.push(task(1, 5));
238        q.push(task(2, 5));
239        assert_eq!(q.len(), 2);
240        assert!(!q.is_empty());
241    }
242
243    #[test]
244    fn test_work_queue_high_priority_tasks() {
245        let mut q = WorkQueue::new(1);
246        q.push(task(1, 5));
247        q.push(task(2, 10));
248        q.push(task(3, 15));
249        q.push(task(4, 3));
250
251        let hi = q.high_priority_tasks();
252        assert_eq!(hi.len(), 2);
253    }
254
255    #[test]
256    fn test_scheduler_add_queue() {
257        let mut sched = WorkStealingScheduler::new();
258        sched.add_queue(0);
259        sched.add_queue(1);
260        sched.add_queue(2);
261        assert_eq!(sched.queue_count(), 3);
262
263        // Duplicate should be ignored
264        sched.add_queue(1);
265        assert_eq!(sched.queue_count(), 3);
266    }
267
268    #[test]
269    fn test_scheduler_submit_task() {
270        let mut sched = WorkStealingScheduler::new();
271        sched.add_queue(0);
272
273        assert!(sched.submit_task(0, task(1, 5)));
274        assert_eq!(sched.total_pending(), 1);
275
276        // Owner 99 does not exist
277        assert!(!sched.submit_task(99, task(2, 5)));
278        assert_eq!(sched.total_pending(), 1);
279    }
280
281    #[test]
282    fn test_scheduler_steal_from() {
283        let mut sched = WorkStealingScheduler::new();
284        sched.add_queue(0);
285        sched.submit_task(0, task(1, 5));
286        sched.submit_task(0, task(2, 5));
287
288        // Steal FIFO (task 1 was pushed first)
289        let stolen = sched.steal_from(0).expect("steal_from should succeed");
290        assert_eq!(stolen.id, 1);
291        assert_eq!(sched.total_pending(), 1);
292    }
293
294    #[test]
295    fn test_scheduler_steal_from_empty() {
296        let mut sched = WorkStealingScheduler::new();
297        sched.add_queue(0);
298        assert!(sched.steal_from(0).is_none());
299    }
300
301    #[test]
302    fn test_scheduler_steal_from_missing_queue() {
303        let mut sched = WorkStealingScheduler::new();
304        assert!(sched.steal_from(42).is_none());
305    }
306
307    #[test]
308    fn test_scheduler_pop_for() {
309        let mut sched = WorkStealingScheduler::new();
310        sched.add_queue(0);
311        sched.submit_task(0, task(1, 5));
312        sched.submit_task(0, task(2, 5));
313
314        // Owner pops LIFO (task 2 was pushed last)
315        let t = sched.pop_for(0).expect("pop_for should return a task");
316        assert_eq!(t.id, 2);
317    }
318
319    #[test]
320    fn test_scheduler_total_pending() {
321        let mut sched = WorkStealingScheduler::new();
322        sched.add_queue(0);
323        sched.add_queue(1);
324        sched.submit_task(0, task(1, 5));
325        sched.submit_task(0, task(2, 5));
326        sched.submit_task(1, task(3, 5));
327
328        assert_eq!(sched.total_pending(), 3);
329    }
330
331    #[test]
332    fn test_scheduler_busiest_owner() {
333        let mut sched = WorkStealingScheduler::new();
334        sched.add_queue(0);
335        sched.add_queue(1);
336        sched.submit_task(0, task(1, 5));
337        sched.submit_task(0, task(2, 5));
338        sched.submit_task(1, task(3, 5));
339
340        assert_eq!(sched.busiest_owner(), Some(0));
341    }
342
343    #[test]
344    fn test_scheduler_idlest_owner() {
345        let mut sched = WorkStealingScheduler::new();
346        sched.add_queue(0);
347        sched.add_queue(1);
348        sched.submit_task(0, task(1, 5));
349        sched.submit_task(0, task(2, 5));
350
351        assert_eq!(sched.idlest_owner(), Some(1));
352    }
353}