Skip to main content

actor_kit/
queue.rs

1//! Lock-free work queue with work stealing support.
2//!
3//! Uses crossbeam-deque for efficient work stealing between workers.
4
5use crossbeam_deque::{Injector, Steal, Stealer, Worker};
6
7use crate::{ActorId, Message, Priority};
8
9/// A task to be executed by a worker.
10///
11/// When `additional_messages` is non-empty the task represents a batch: the
12/// worker processes `message` first, then each entry in `additional_messages`.
13/// This avoids pushing N separate `Task`s onto the work-stealing queue.
14#[derive(Debug)]
15pub struct Task {
16    /// Target actor ID
17    pub actor_id: ActorId,
18    /// Message to process
19    pub message: Message,
20    /// Priority level
21    pub priority: Priority,
22    /// Extra messages that belong to the same batch (processed after `message`).
23    pub additional_messages: Vec<Message>,
24}
25
26/// Global work queue for task injection.
27pub struct WorkQueue {
28    /// Global injector for new tasks
29    injector: Injector<Task>,
30    /// Number of workers
31    worker_count: std::sync::atomic::AtomicUsize,
32}
33
34impl WorkQueue {
35    /// Create a new work queue.
36    pub fn new() -> Self {
37        Self {
38            injector: Injector::new(),
39            worker_count: std::sync::atomic::AtomicUsize::new(0),
40        }
41    }
42
43    /// Push a task to the global queue.
44    pub fn push(&self, task: Task) {
45        self.injector.push(task);
46    }
47
48    /// Push multiple tasks to the global queue.
49    pub fn push_batch(&self, tasks: Vec<Task>) {
50        for task in tasks {
51            self.injector.push(task);
52        }
53    }
54
55    /// Steal a task from the global queue.
56    pub fn steal_global(&self) -> Option<Task> {
57        loop {
58            match self.injector.steal() {
59                Steal::Success(task) => return Some(task),
60                Steal::Empty => return None,
61                Steal::Retry => continue,
62            }
63        }
64    }
65
66    /// Get the global injector reference.
67    pub fn injector(&self) -> &Injector<Task> {
68        &self.injector
69    }
70
71    /// Register a new worker.
72    pub fn register_worker(&self) {
73        self.worker_count
74            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
75    }
76
77    /// Unregister a worker.
78    pub fn unregister_worker(&self) {
79        self.worker_count
80            .fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
81    }
82}
83
84impl Default for WorkQueue {
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90/// Create a new local worker queue and return both the worker and stealer.
91pub fn create_local_queue() -> (Worker<Task>, Stealer<Task>) {
92    let worker = Worker::new_fifo();
93    let stealer = worker.stealer();
94    (worker, stealer)
95}
96
97/// Work stealer for stealing from other workers.
98pub struct WorkStealer {
99    /// Stealers for all worker queues
100    stealers: Vec<Stealer<Task>>,
101    /// Index for round-robin stealing
102    index: std::sync::atomic::AtomicUsize,
103}
104
105impl WorkStealer {
106    /// Create a new work stealer with the given stealers.
107    pub fn new(stealers: Vec<Stealer<Task>>) -> Self {
108        Self {
109            stealers,
110            index: std::sync::atomic::AtomicUsize::new(0),
111        }
112    }
113
114    /// Steal a task from another worker using work stealing.
115    pub fn steal(&self) -> Option<Task> {
116        if self.stealers.is_empty() {
117            return None;
118        }
119
120        let start = self
121            .index
122            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
123        let len = self.stealers.len();
124
125        for i in 0..len {
126            let idx = (start + i) % len;
127            loop {
128                match self.stealers[idx].steal() {
129                    Steal::Success(task) => {
130                        return Some(task);
131                    }
132                    Steal::Empty => break,
133                    Steal::Retry => continue,
134                }
135            }
136        }
137        None
138    }
139
140    /// Steal multiple tasks (batch steal).
141    pub fn steal_batch(&self, dest: &Worker<Task>, max: usize) -> usize {
142        if self.stealers.is_empty() {
143            return 0;
144        }
145
146        let start = self
147            .index
148            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
149        let len = self.stealers.len();
150        let mut stolen = 0;
151
152        for i in 0..len {
153            if stolen >= max {
154                break;
155            }
156            let idx = (start + i) % len;
157            loop {
158                match self.stealers[idx].steal_batch_and_pop(dest) {
159                    Steal::Success(task) => {
160                        dest.push(task);
161                        stolen += 1;
162                    }
163                    Steal::Empty => break,
164                    Steal::Retry => continue,
165                }
166            }
167        }
168        stolen
169    }
170}
171
172/// Priority-aware work queue wrapper.
173pub struct PriorityQueue {
174    /// Critical priority tasks
175    critical: Injector<Task>,
176    /// High priority tasks
177    high: Injector<Task>,
178    /// Normal priority tasks
179    normal: Injector<Task>,
180    /// Low priority tasks
181    low: Injector<Task>,
182}
183
184impl PriorityQueue {
185    /// Create a new priority queue.
186    pub fn new() -> Self {
187        Self {
188            critical: Injector::new(),
189            high: Injector::new(),
190            normal: Injector::new(),
191            low: Injector::new(),
192        }
193    }
194
195    /// Push a task with the given priority.
196    pub fn push(&self, task: Task) {
197        match task.priority {
198            Priority::Critical => self.critical.push(task),
199            Priority::High => self.high.push(task),
200            Priority::Normal => self.normal.push(task),
201            Priority::Low => self.low.push(task),
202        }
203    }
204
205    /// Pop the highest priority task available.
206    pub fn pop(&self) -> Option<Task> {
207        fn try_steal(injector: &Injector<Task>) -> Option<Task> {
208            loop {
209                match injector.steal() {
210                    Steal::Success(task) => return Some(task),
211                    Steal::Empty => return None,
212                    Steal::Retry => continue,
213                }
214            }
215        }
216
217        try_steal(&self.critical)
218            .or_else(|| try_steal(&self.high))
219            .or_else(|| try_steal(&self.normal))
220            .or_else(|| try_steal(&self.low))
221    }
222}
223
224impl Default for PriorityQueue {
225    fn default() -> Self {
226        Self::new()
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use crate::MessagePayload;
234
235    #[test]
236    fn test_work_queue_basic() {
237        let queue = WorkQueue::new();
238        let task = Task {
239            actor_id: ActorId::new(),
240            message: Message {
241                sender: None,
242                payload: MessagePayload::Start,
243                priority: Priority::Normal,
244            },
245            priority: Priority::Normal,
246            additional_messages: Vec::new(),
247        };
248
249        queue.push(task);
250        let stolen = queue.steal_global();
251        assert!(stolen.is_some());
252    }
253
254    #[test]
255    fn test_local_queue() {
256        let (worker, _stealer) = create_local_queue();
257        let task = Task {
258            actor_id: ActorId::new(),
259            message: Message {
260                sender: None,
261                payload: MessagePayload::Start,
262                priority: Priority::Normal,
263            },
264            priority: Priority::Normal,
265            additional_messages: Vec::new(),
266        };
267
268        worker.push(task);
269        assert_eq!(worker.len(), 1);
270        let popped = worker.pop();
271        assert!(popped.is_some());
272        assert!(worker.is_empty());
273    }
274
275    #[test]
276    fn test_priority_queue() {
277        let queue = PriorityQueue::new();
278
279        let low_task = Task {
280            actor_id: ActorId::new(),
281            message: Message {
282                sender: None,
283                payload: MessagePayload::Start,
284                priority: Priority::Low,
285            },
286            priority: Priority::Low,
287            additional_messages: Vec::new(),
288        };
289        let high_task = Task {
290            actor_id: ActorId::new(),
291            message: Message {
292                sender: None,
293                payload: MessagePayload::Start,
294                priority: Priority::High,
295            },
296            priority: Priority::High,
297            additional_messages: Vec::new(),
298        };
299
300        queue.push(low_task);
301        queue.push(high_task);
302
303        let first = queue.pop().unwrap();
304        assert_eq!(first.priority, Priority::High);
305
306        let second = queue.pop().unwrap();
307        assert_eq!(second.priority, Priority::Low);
308    }
309}