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    /// The `Notified` future is created and enabled *before* the emptiness
198    /// check, so a `notify_waiters` racing with this call cannot be missed
199    /// (lost-wakeup).
200    pub async fn recv(&self) -> Message {
201        loop {
202            let notified = self.notify.notified();
203            tokio::pin!(notified);
204            notified.as_mut().enable();
205            if let Some(msg) = self.try_recv() {
206                return msg;
207            }
208            notified.await;
209        }
210    }
211
212    /// Get the current queue size.
213    pub fn len(&self) -> usize {
214        self.size.load(Ordering::Relaxed)
215    }
216
217    /// Check if the mailbox is empty.
218    pub fn is_empty(&self) -> bool {
219        self.len() == 0
220    }
221
222    /// Check if the mailbox is in backpressure state.
223    pub fn is_backpressured(&self) -> bool {
224        self.backpressure.load(Ordering::Relaxed)
225    }
226
227    /// Get the mailbox capacity.
228    pub fn capacity(&self) -> usize {
229        self.config.capacity
230    }
231
232    /// Get remaining capacity.
233    pub fn remaining_capacity(&self) -> usize {
234        self.config.capacity.saturating_sub(self.len())
235    }
236
237    /// Clear all messages from the mailbox.
238    pub fn clear(&self) {
239        let mut total = 0;
240
241        while self.critical_queue.pop().is_some() {
242            total += 1;
243        }
244        while self.queue.pop().is_some() {
245            total += 1;
246        }
247
248        if total > 0 {
249            self.size.fetch_sub(total, Ordering::Relaxed);
250            self.semaphore.add_permits(total);
251        }
252        self.backpressure.store(false, Ordering::Relaxed);
253    }
254
255    fn check_backpressure(&self) {
256        let size = self.size.load(Ordering::Relaxed);
257        if size < (self.config.backpressure_count() / 2) {
258            self.backpressure.store(false, Ordering::Relaxed);
259        }
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266    use crate::MessagePayload;
267
268    #[test]
269    fn test_mailbox_basic() {
270        let mailbox = Mailbox::new(ActorId::new(), MailboxConfig::default());
271
272        let msg = Message {
273            sender: None,
274            payload: MessagePayload::Start,
275            priority: Priority::Normal,
276        };
277
278        mailbox.try_send(msg).unwrap();
279        assert_eq!(mailbox.len(), 1);
280
281        let received = mailbox.try_recv();
282        assert!(received.is_some());
283        assert!(mailbox.is_empty());
284    }
285
286    #[test]
287    fn test_mailbox_backpressure() {
288        let config = MailboxConfig {
289            capacity: 10,
290            backpressure_threshold: 0.8,
291            ..Default::default()
292        };
293        let mailbox = Mailbox::new(ActorId::new(), config);
294
295        for i in 0..8 {
296            let msg = Message {
297                sender: None,
298                payload: MessagePayload::Custom(vec![i as u8]),
299                priority: Priority::Normal,
300            };
301            mailbox.try_send(msg).unwrap();
302        }
303
304        assert!(mailbox.is_backpressured());
305    }
306
307    #[test]
308    fn test_mailbox_priority() {
309        let config = MailboxConfig {
310            priority_queue: true,
311            ..Default::default()
312        };
313        let mailbox = Mailbox::new(ActorId::new(), config);
314
315        let normal_msg = Message {
316            sender: None,
317            payload: MessagePayload::Custom(vec![1]),
318            priority: Priority::Normal,
319        };
320        let critical_msg = Message {
321            sender: None,
322            payload: MessagePayload::Custom(vec![2]),
323            priority: Priority::Critical,
324        };
325
326        mailbox.try_send(normal_msg).unwrap();
327        mailbox.try_send(critical_msg).unwrap();
328
329        let first = mailbox.try_recv().unwrap();
330        assert_eq!(first.priority, Priority::Critical);
331    }
332
333    #[tokio::test]
334    async fn test_mailbox_async() {
335        let mailbox = Mailbox::new(ActorId::new(), MailboxConfig::default());
336
337        let msg = Message {
338            sender: None,
339            payload: MessagePayload::Start,
340            priority: Priority::Normal,
341        };
342
343        mailbox.send(msg).await.unwrap();
344        let received = mailbox.recv().await;
345        assert!(matches!(received.payload, MessagePayload::Start));
346    }
347
348    #[tokio::test]
349    async fn test_recv_waits_then_wakes() {
350        let mailbox = Arc::new(Mailbox::new(ActorId::new(), MailboxConfig::default()));
351        let mailbox2 = mailbox.clone();
352
353        let handle = tokio::spawn(async move { mailbox2.recv().await });
354
355        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
356
357        let msg = Message {
358            sender: None,
359            payload: MessagePayload::Custom(vec![42]),
360            priority: Priority::Normal,
361        };
362        mailbox.try_send(msg).unwrap();
363
364        let received = handle.await.expect("recv task should not panic");
365        assert!(matches!(received.payload, MessagePayload::Custom(_)));
366    }
367
368    #[test]
369    fn test_mailbox_clear() {
370        let config = MailboxConfig {
371            capacity: 10,
372            priority_queue: true,
373            ..Default::default()
374        };
375        let mailbox = Mailbox::new(ActorId::new(), config);
376
377        for i in 0..5 {
378            let msg = Message {
379                sender: None,
380                payload: MessagePayload::Custom(vec![i]),
381                priority: Priority::Normal,
382            };
383            mailbox.try_send(msg).unwrap();
384        }
385        let critical_msg = Message {
386            sender: None,
387            payload: MessagePayload::Custom(vec![99]),
388            priority: Priority::Critical,
389        };
390        mailbox.try_send(critical_msg).unwrap();
391
392        assert_eq!(mailbox.len(), 6);
393        mailbox.clear();
394        assert!(mailbox.is_empty());
395        assert!(!mailbox.is_backpressured());
396    }
397}