Skip to content

Commit 85480cd

Browse files
committed
feat(actor): implement selective receive with pattern matching
Add Erlang-style selective receive support to actors, matching the semantics of Erlang's receive ... end with pattern matching. Changes: - Add pending queue (Mutex<VecDeque<Envelope>>) to Mailbox for non-matching messages - Implement recv_matching() with timeout support for waiting on specific messages - Implement try_recv_matching() for non-blocking selective receive - Add ActorContext methods: receive(), receive_timeout(), try_receive() - Messages check pending queue first, then channel - Non-matching messages saved to pending queue for later processing - Signals always go to pending queue (processed by handle_signal) - Add comprehensive test suite with 6 tests covering all scenarios - Fix Send/Sync issues by wrapping pending queue in Mutex Implementation notes: - Predicate functions: FnMut(&Message) -> Option<T> - Timeout using tokio::time::timeout_at - Allows actors to wait for specific messages while leaving others queued - Maintains message ordering through VecDeque Examples: - RPC with correlation IDs - Selective message processing - Timeout-based patterns - Pending queue verification
1 parent beb0ede commit 85480cd

4 files changed

Lines changed: 690 additions & 4 deletions

File tree

joerl/src/actor.rs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,151 @@ impl ActorContext {
298298
}
299299
Err(crate::ActorError::SendFailed(to))
300300
}
301+
302+
/// Selectively receive a message matching the predicate.
303+
///
304+
/// This is similar to Erlang's `receive` with pattern matching.
305+
/// Messages that don't match remain in the mailbox and will be
306+
/// checked again on subsequent receive calls or regular message processing.
307+
///
308+
/// This allows actors to wait for specific messages while leaving
309+
/// other messages in the mailbox for later processing.
310+
///
311+
/// In Erlang: `receive Pattern -> Body end`
312+
///
313+
/// # Arguments
314+
///
315+
/// * `predicate` - Function that returns `Some(T)` if message matches
316+
///
317+
/// # Examples
318+
///
319+
/// ```rust
320+
/// use joerl::{Actor, ActorContext, Message};
321+
/// use async_trait::async_trait;
322+
///
323+
/// #[derive(Clone)]
324+
/// struct Response {
325+
/// id: u64,
326+
/// data: String,
327+
/// }
328+
///
329+
/// struct RpcActor {
330+
/// next_id: u64,
331+
/// }
332+
///
333+
/// #[async_trait]
334+
/// impl Actor for RpcActor {
335+
/// async fn handle_message(&mut self, msg: Message, ctx: &mut ActorContext) {
336+
/// // Make a request with an ID
337+
/// let req_id = self.next_id;
338+
/// self.next_id += 1;
339+
///
340+
/// // ... send request somewhere ...
341+
///
342+
/// // Wait for specific response
343+
/// let response = ctx.receive(|msg| {
344+
/// msg.downcast_ref::<Response>()
345+
/// .filter(|r| r.id == req_id)
346+
/// .cloned()
347+
/// }).await;
348+
///
349+
/// // Meanwhile, other messages stay in mailbox
350+
/// }
351+
/// }
352+
/// ```
353+
pub async fn receive<F, T>(&mut self, predicate: F) -> Option<T>
354+
where
355+
F: FnMut(&Message) -> Option<T>,
356+
{
357+
self.mailbox.recv_matching(predicate, None).await
358+
}
359+
360+
/// Selectively receive a message with timeout.
361+
///
362+
/// Like `receive()`, but returns `None` if no matching message
363+
/// arrives within the timeout duration.
364+
///
365+
/// In Erlang: `receive Pattern -> Body after Timeout -> TimeoutBody end`
366+
///
367+
/// # Arguments
368+
///
369+
/// * `predicate` - Function that returns `Some(T)` if message matches
370+
/// * `timeout` - Maximum time to wait
371+
///
372+
/// # Examples
373+
///
374+
/// ```rust
375+
/// use joerl::{Actor, ActorContext, Message};
376+
/// use async_trait::async_trait;
377+
/// use std::time::Duration;
378+
///
379+
/// #[derive(Clone)]
380+
/// struct Ack {
381+
/// id: u64,
382+
/// }
383+
///
384+
/// struct Worker;
385+
///
386+
/// #[async_trait]
387+
/// impl Actor for Worker {
388+
/// async fn handle_message(&mut self, _msg: Message, ctx: &mut ActorContext) {
389+
/// // Wait for ack, but timeout after 5 seconds
390+
/// let ack = ctx.receive_timeout(
391+
/// |msg| msg.downcast_ref::<Ack>().cloned(),
392+
/// Duration::from_secs(5)
393+
/// ).await;
394+
///
395+
/// match ack {
396+
/// Some(ack) => println!("Got ack: {}", ack.id),
397+
/// None => println!("Timeout waiting for ack"),
398+
/// }
399+
/// }
400+
/// }
401+
/// ```
402+
pub async fn receive_timeout<F, T>(
403+
&mut self,
404+
predicate: F,
405+
timeout: std::time::Duration,
406+
) -> Option<T>
407+
where
408+
F: FnMut(&Message) -> Option<T>,
409+
{
410+
self.mailbox.recv_matching(predicate, Some(timeout)).await
411+
}
412+
413+
/// Try to receive a matching message without blocking.
414+
///
415+
/// Returns immediately with `Some(T)` if a matching message is found,
416+
/// or `None` if no match is available.
417+
///
418+
/// # Examples
419+
///
420+
/// ```rust
421+
/// use joerl::{Actor, ActorContext, Message};
422+
/// use async_trait::async_trait;
423+
///
424+
/// struct Worker;
425+
///
426+
/// #[async_trait]
427+
/// impl Actor for Worker {
428+
/// async fn handle_message(&mut self, _msg: Message, ctx: &mut ActorContext) {
429+
/// // Check if there's a ready message without waiting
430+
/// if let Some(ready) = ctx.try_receive(|msg| {
431+
/// msg.downcast_ref::<String>()
432+
/// .filter(|s| s == "ready")
433+
/// .cloned()
434+
/// }) {
435+
/// println!("Got ready signal: {}", ready);
436+
/// }
437+
/// }
438+
/// }
439+
/// ```
440+
pub fn try_receive<F, T>(&mut self, predicate: F) -> Option<T>
441+
where
442+
F: FnMut(&Message) -> Option<T>,
443+
{
444+
self.mailbox.try_recv_matching(predicate)
445+
}
301446
}
302447

303448
#[cfg(test)]

joerl/src/mailbox.rs

Lines changed: 135 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,26 @@
55
66
use crate::message::Envelope;
77
use crate::telemetry::MessageMetrics;
8+
use std::collections::VecDeque;
9+
use std::sync::Mutex;
10+
use std::time::Duration;
811
use tokio::sync::mpsc;
9-
10-
/// Default mailbox capacity.
1112
pub const DEFAULT_MAILBOX_CAPACITY: usize = 100;
1213

1314
/// Actor mailbox for receiving messages.
1415
///
1516
/// Mailboxes are bounded channels that provide backpressure when full.
1617
/// This prevents fast senders from overwhelming slow receivers.
18+
///
19+
/// The mailbox now supports selective receive (similar to Erlang's `receive`)
20+
/// by maintaining a pending queue for messages that don't match the current
21+
/// receive pattern.
1722
pub struct Mailbox {
1823
rx: mpsc::Receiver<Envelope>,
24+
/// Messages that didn't match a selective receive predicate.
25+
/// These are checked first on subsequent receives.
26+
/// Wrapped in Mutex to maintain Sync for ActorContext.
27+
pending: Mutex<VecDeque<Envelope>>,
1928
}
2029

2130
impl Mailbox {
@@ -28,7 +37,10 @@ impl Mailbox {
2837
/// Creates a new mailbox with actor type for telemetry.
2938
pub(crate) fn new_with_type(capacity: usize, actor_type: String) -> (Self, MailboxSender) {
3039
let (tx, rx) = mpsc::channel(capacity);
31-
let mailbox = Mailbox { rx };
40+
let mailbox = Mailbox {
41+
rx,
42+
pending: Mutex::new(VecDeque::new()),
43+
};
3244
let sender = MailboxSender {
3345
tx,
3446
actor_type,
@@ -40,7 +52,13 @@ impl Mailbox {
4052
/// Receives the next message from the mailbox.
4153
///
4254
/// Returns `None` if all senders have been dropped.
55+
///
56+
/// This now checks the pending queue first before receiving from the channel.
4357
pub(crate) async fn recv(&mut self) -> Option<Envelope> {
58+
// Check pending queue first
59+
if let Some(envelope) = self.pending.lock().unwrap().pop_front() {
60+
return Some(envelope);
61+
}
4462
self.rx.recv().await
4563
}
4664

@@ -56,6 +74,120 @@ impl Mailbox {
5674
pub fn close(&mut self) {
5775
self.rx.close();
5876
}
77+
78+
/// Selectively receive a message matching the predicate.
79+
///
80+
/// This is similar to Erlang's `receive` with pattern matching.
81+
/// Messages that don't match are saved in the pending queue and
82+
/// will be checked again on subsequent receives.
83+
///
84+
/// # Arguments
85+
///
86+
/// * `predicate` - Function that returns `Some(T)` if the message matches
87+
/// * `timeout` - Optional timeout duration
88+
///
89+
/// # Returns
90+
///
91+
/// * `Some(T)` - A matching message was found
92+
/// * `None` - Timeout expired or all senders dropped
93+
pub(crate) async fn recv_matching<F, T>(
94+
&mut self,
95+
mut predicate: F,
96+
timeout: Option<Duration>,
97+
) -> Option<T>
98+
where
99+
F: FnMut(&crate::message::Message) -> Option<T>,
100+
{
101+
use crate::message::EnvelopeContent;
102+
103+
// First check pending messages
104+
{
105+
let mut pending = self.pending.lock().unwrap();
106+
for i in 0..pending.len() {
107+
if let Some(envelope) = pending.get(i)
108+
&& let EnvelopeContent::Message(msg) = &envelope.content
109+
&& let Some(result) = predicate(msg)
110+
{
111+
// Found a match - remove it and return
112+
pending.remove(i);
113+
return Some(result);
114+
}
115+
}
116+
}
117+
118+
// Then check incoming messages
119+
let deadline = timeout.map(|d| tokio::time::Instant::now() + d);
120+
121+
loop {
122+
let envelope = if let Some(deadline) = deadline {
123+
match tokio::time::timeout_at(deadline, self.rx.recv()).await {
124+
Ok(Some(env)) => env,
125+
Ok(None) => return None, // Channel closed
126+
Err(_) => return None, // Timeout
127+
}
128+
} else {
129+
self.rx.recv().await?
130+
};
131+
132+
match envelope.content {
133+
EnvelopeContent::Message(ref msg) => {
134+
if let Some(result) = predicate(msg) {
135+
// Found a match
136+
return Some(result);
137+
} else {
138+
// Doesn't match - save for later
139+
self.pending.lock().unwrap().push_back(envelope);
140+
}
141+
}
142+
EnvelopeContent::Signal(_) => {
143+
// Signals always go to pending - they should be processed by handle_signal
144+
self.pending.lock().unwrap().push_back(envelope);
145+
}
146+
}
147+
}
148+
}
149+
150+
/// Try to receive a matching message without blocking.
151+
///
152+
/// Only checks the pending queue and tries one receive from the channel.
153+
pub(crate) fn try_recv_matching<F, T>(&mut self, mut predicate: F) -> Option<T>
154+
where
155+
F: FnMut(&crate::message::Message) -> Option<T>,
156+
{
157+
use crate::message::EnvelopeContent;
158+
159+
// Check pending messages first
160+
{
161+
let mut pending = self.pending.lock().unwrap();
162+
for i in 0..pending.len() {
163+
if let Some(envelope) = pending.get(i)
164+
&& let EnvelopeContent::Message(msg) = &envelope.content
165+
&& let Some(result) = predicate(msg)
166+
{
167+
pending.remove(i);
168+
return Some(result);
169+
}
170+
}
171+
}
172+
173+
// Try one receive without blocking
174+
if let Ok(envelope) = self.rx.try_recv() {
175+
match envelope.content {
176+
EnvelopeContent::Message(ref msg) => {
177+
if let Some(result) = predicate(msg) {
178+
return Some(result);
179+
} else {
180+
self.pending.lock().unwrap().push_back(envelope);
181+
}
182+
}
183+
EnvelopeContent::Signal(_) => {
184+
self.pending.lock().unwrap().push_back(envelope);
185+
}
186+
}
187+
}
188+
189+
None
190+
}
59191
}
60192

61193
/// Handle for sending messages to an actor's mailbox.

0 commit comments

Comments
 (0)