Skip to main content

actor_kit/
mailbox.rs

1//! MPSC mailbox for actors with bounded capacity and backpressure.
2//!
3//! Each actor has its own mailbox for receiving messages.
4//! Uses lock-free `crossbeam_queue::ArrayQueue` for the message queues
5//! and `tokio::sync::Notify` for efficient async waiting.
6
7use crossbeam_queue::ArrayQueue;
8use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
9use std::sync::Arc;
10use tokio::sync::{Notify, Semaphore};
11
12use crate::Error;
13use crate::{ActorId, Message, Priority};
14
15/// Configuration for an actor mailbox.
16#[derive(Debug, Clone)]
17pub struct MailboxConfig {
18    /// Maximum capacity of the mailbox
19    pub capacity: usize,
20    /// Enable priority queuing
21    pub priority_queue: bool,
22    /// Backpressure threshold (percentage of capacity)
23    pub backpressure_threshold: f32,
24}
25
26impl Default for MailboxConfig {
27    fn default() -> Self {
28        Self {
29            capacity: 10_000,
30            priority_queue: true,
31            backpressure_threshold: 0.8,
32        }
33    }
34}
35
36impl MailboxConfig {
37    /// Create a new mailbox config with the given capacity.
38    pub fn new(capacity: usize) -> Self {
39        Self {
40            capacity,
41            ..Default::default()
42        }
43    }
44
45    /// Calculate the backpressure threshold in message count.
46    pub fn backpressure_count(&self) -> usize {
47        (self.capacity as f32 * self.backpressure_threshold) as usize
48    }
49}
50
51/// MPSC mailbox for an actor.
52///
53/// Uses lock-free bounded queues internally.
54/// Backpressure is coordinated via a semaphore; async wakeup via `Notify`.
55pub struct Mailbox {
56    /// Actor ID this mailbox belongs to
57    actor_id: ActorId,
58    /// Configuration
59    config: MailboxConfig,
60    /// Lock-free message queue
61    queue: ArrayQueue<Message>,
62    /// Current queue size
63    size: AtomicUsize,
64    /// Semaphore for bounded capacity
65    semaphore: Arc<Semaphore>,
66    /// Whether the mailbox is in backpressure state
67    backpressure: AtomicBool,
68    /// Lock-free critical priority queue
69    critical_queue: ArrayQueue<Message>,
70    /// Notify waiters when a message is enqueued
71    notify: Arc<Notify>,
72}
73
74impl Mailbox {
75    /// Create a new mailbox.
76    pub fn new(actor_id: ActorId, config: MailboxConfig) -> Self {
77        let capacity = config.capacity;
78        Self {
79            actor_id,
80            config,
81            queue: ArrayQueue::new(capacity),
82            size: AtomicUsize::new(0),
83            semaphore: Arc::new(Semaphore::new(capacity)),
84            backpressure: AtomicBool::new(false),
85            critical_queue: ArrayQueue::new(capacity),
86            notify: Arc::new(Notify::new()),
87        }
88    }
89
90    /// Try to send a message to the mailbox (non-blocking).
91    pub fn try_send(&self, message: Message) -> Result<(), (Message, Error)> {
92        let semaphore = self.semaphore.clone();
93        let permit = semaphore.try_acquire_owned();
94        match permit {
95            Ok(permit) => {
96                permit.forget();
97            }
98            Err(_) => {
99                return Err((
100                    message,
101                    Error::resource_exhausted(format!(
102                        "mailbox for actor {:?} is full",
103                        self.actor_id
104                    )),
105                ));
106            }
107        }
108
109        let size = self.size.fetch_add(1, Ordering::Relaxed) + 1;
110
111        if let Err(rejected) =
112            if self.config.priority_queue && message.priority == Priority::Critical {
113                self.critical_queue.push(message)
114            } else {
115                self.queue.push(message)
116            }
117        {
118            self.size.fetch_sub(1, Ordering::Relaxed);
119            self.semaphore.add_permits(1);
120            return Err((
121                rejected,
122                Error::resource_exhausted(format!(
123                    "mailbox for actor {:?} internal queue is full",
124                    self.actor_id
125                )),
126            ));
127        }
128
129        if size >= self.config.backpressure_count() {
130            self.backpressure.store(true, Ordering::Relaxed);
131        }
132
133        self.notify.notify_waiters();
134
135        Ok(())
136    }
137
138    /// Send a message to the mailbox (async, waits if full).
139    pub async fn send(&self, message: Message) -> crate::Result<()> {
140        let semaphore = self.semaphore.clone();
141        let permit = semaphore
142            .acquire()
143            .await
144            .map_err(|e| Error::resource_exhausted(e.to_string()))?;
145        permit.forget();
146
147        let size = self.size.fetch_add(1, Ordering::Relaxed) + 1;
148
149        if if self.config.priority_queue && message.priority == Priority::Critical {
150            self.critical_queue.push(message)
151        } else {
152            self.queue.push(message)
153        }
154        .is_err()
155        {
156            self.size.fetch_sub(1, Ordering::Relaxed);
157            self.semaphore.add_permits(1);
158            return Err(Error::resource_exhausted(format!(
159                "mailbox for actor {:?} internal queue is full",
160                self.actor_id
161            )));
162        }
163
164        if size >= self.config.backpressure_count() {
165            self.backpressure.store(true, Ordering::Relaxed);
166        }
167
168        self.notify.notify_waiters();
169
170        Ok(())
171    }
172
173    /// Receive a message from the mailbox (non-blocking).
174    pub fn try_recv(&self) -> Option<Message> {
175        if self.config.priority_queue {
176            if let Some(msg) = self.critical_queue.pop() {
177                self.size.fetch_sub(1, Ordering::Relaxed);
178                self.semaphore.add_permits(1);
179                self.check_backpressure();
180                return Some(msg);
181            }
182        }
183
184        if let Some(msg) = self.queue.pop() {
185            self.size.fetch_sub(1, Ordering::Relaxed);
186            self.semaphore.add_permits(1);
187            self.check_backpressure();
188            return Some(msg);
189        }
190
191        None
192    }
193
194    /// Receive a message from the mailbox (async, waits if empty).
195    ///
196    /// Uses `tokio::sync::Notify` for efficient wakeup instead of busy-polling.
197    pub async fn recv(&self) -> Message {
198        loop {
199            if let Some(msg) = self.try_recv() {
200                return msg;
201            }
202            self.notify.notified().await;
203        }
204    }
205
206    /// Get the current queue size.
207    pub fn len(&self) -> usize {
208        self.size.load(Ordering::Relaxed)
209    }
210
211    /// Check if the mailbox is empty.
212    pub fn is_empty(&self) -> bool {
213        self.len() == 0
214    }
215
216    /// Check if the mailbox is in backpressure state.
217    pub fn is_backpressured(&self) -> bool {
218        self.backpressure.load(Ordering::Relaxed)
219    }
220
221    /// Get the mailbox capacity.
222    pub fn capacity(&self) -> usize {
223        self.config.capacity
224    }
225
226    /// Get remaining capacity.
227    pub fn remaining_capacity(&self) -> usize {
228        self.config.capacity.saturating_sub(self.len())
229    }
230
231    /// Clear all messages from the mailbox.
232    pub fn clear(&self) {
233        let mut total = 0;
234
235        while self.critical_queue.pop().is_some() {
236            total += 1;
237        }
238        while self.queue.pop().is_some() {
239            total += 1;
240        }
241
242        if total > 0 {
243            self.size.fetch_sub(total, Ordering::Relaxed);
244            self.semaphore.add_permits(total);
245        }
246        self.backpressure.store(false, Ordering::Relaxed);
247    }
248
249    fn check_backpressure(&self) {
250        let size = self.size.load(Ordering::Relaxed);
251        if size < (self.config.backpressure_count() / 2) {
252            self.backpressure.store(false, Ordering::Relaxed);
253        }
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use crate::MessagePayload;
261
262    #[test]
263    fn test_mailbox_basic() {
264        let mailbox = Mailbox::new(ActorId::new(), MailboxConfig::default());
265
266        let msg = Message {
267            sender: None,
268            payload: MessagePayload::Start,
269            priority: Priority::Normal,
270        };
271
272        mailbox.try_send(msg).unwrap();
273        assert_eq!(mailbox.len(), 1);
274
275        let received = mailbox.try_recv();
276        assert!(received.is_some());
277        assert!(mailbox.is_empty());
278    }
279
280    #[test]
281    fn test_mailbox_backpressure() {
282        let config = MailboxConfig {
283            capacity: 10,
284            backpressure_threshold: 0.8,
285            ..Default::default()
286        };
287        let mailbox = Mailbox::new(ActorId::new(), config);
288
289        for i in 0..8 {
290            let msg = Message {
291                sender: None,
292                payload: MessagePayload::Custom(vec![i as u8]),
293                priority: Priority::Normal,
294            };
295            mailbox.try_send(msg).unwrap();
296        }
297
298        assert!(mailbox.is_backpressured());
299    }
300
301    #[test]
302    fn test_mailbox_priority() {
303        let config = MailboxConfig {
304            priority_queue: true,
305            ..Default::default()
306        };
307        let mailbox = Mailbox::new(ActorId::new(), config);
308
309        let normal_msg = Message {
310            sender: None,
311            payload: MessagePayload::Custom(vec![1]),
312            priority: Priority::Normal,
313        };
314        let critical_msg = Message {
315            sender: None,
316            payload: MessagePayload::Custom(vec![2]),
317            priority: Priority::Critical,
318        };
319
320        mailbox.try_send(normal_msg).unwrap();
321        mailbox.try_send(critical_msg).unwrap();
322
323        let first = mailbox.try_recv().unwrap();
324        assert_eq!(first.priority, Priority::Critical);
325    }
326
327    #[tokio::test]
328    async fn test_mailbox_async() {
329        let mailbox = Mailbox::new(ActorId::new(), MailboxConfig::default());
330
331        let msg = Message {
332            sender: None,
333            payload: MessagePayload::Start,
334            priority: Priority::Normal,
335        };
336
337        mailbox.send(msg).await.unwrap();
338        let received = mailbox.recv().await;
339        assert!(matches!(received.payload, MessagePayload::Start));
340    }
341
342    #[tokio::test]
343    async fn test_recv_waits_then_wakes() {
344        let mailbox = Arc::new(Mailbox::new(ActorId::new(), MailboxConfig::default()));
345        let mailbox2 = mailbox.clone();
346
347        let handle = tokio::spawn(async move { mailbox2.recv().await });
348
349        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
350
351        let msg = Message {
352            sender: None,
353            payload: MessagePayload::Custom(vec![42]),
354            priority: Priority::Normal,
355        };
356        mailbox.try_send(msg).unwrap();
357
358        let received = handle.await.expect("recv task should not panic");
359        assert!(matches!(received.payload, MessagePayload::Custom(_)));
360    }
361
362    #[test]
363    fn test_mailbox_clear() {
364        let config = MailboxConfig {
365            capacity: 10,
366            priority_queue: true,
367            ..Default::default()
368        };
369        let mailbox = Mailbox::new(ActorId::new(), config);
370
371        for i in 0..5 {
372            let msg = Message {
373                sender: None,
374                payload: MessagePayload::Custom(vec![i]),
375                priority: Priority::Normal,
376            };
377            mailbox.try_send(msg).unwrap();
378        }
379        let critical_msg = Message {
380            sender: None,
381            payload: MessagePayload::Custom(vec![99]),
382            priority: Priority::Critical,
383        };
384        mailbox.try_send(critical_msg).unwrap();
385
386        assert_eq!(mailbox.len(), 6);
387        mailbox.clear();
388        assert!(mailbox.is_empty());
389        assert!(!mailbox.is_backpressured());
390    }
391}