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