feat(WIP): refactor router execution

In single threaded contexts, the Router now is treated as yet another actor
on the queue. In multithreaded contexts, it gets its own dedicated thread.
This commit is contained in:
Zachery Aaron Shores-Chmielewski 2026-01-25 12:54:01 +07:00
parent 2f91c7d1bd
commit c62b20c732
7 changed files with 110 additions and 118 deletions

View file

@ -35,7 +35,7 @@ impl ActorInterface for Greeter {
} }
fn main() { fn main() {
let mut rt = Runtime::new(100, RuntimeFlavor::SingleThreaded); let mut rt = Runtime::new();
let addr = rt let addr = rt
.spawn(Greeter::default()) .spawn(Greeter::default())
.expect("failed to spawn greeter"); .expect("failed to spawn greeter");

View file

@ -38,7 +38,7 @@ impl ActorInterface for RingActor {
} }
fn main() { fn main() {
let mut rt = Runtime::new(10_000, RuntimeFlavor::SingleThreaded); let mut rt = Runtime::new();
let inbox: Inbox<RingMessage> = rt.new_inbox(); let inbox: Inbox<RingMessage> = rt.new_inbox();
let mut next = rt let mut next = rt

View file

@ -1,7 +1,7 @@
use crate::{Runtime, WATERLEVEL, ring_buffer::Receiver}; use crate::{Runtime, WATERLEVEL, ring_buffer::Receiver};
pub trait Message: 'static + Sized + Clone + Send {} pub trait Message: 'static + Sized + Clone + Send + Sync {}
impl<T: 'static + Sized + Clone + Send> Message for T {} impl<T: 'static + Sized + Clone + Send + Sync> Message for T {}
pub trait ActorInterface: 'static + Send { pub trait ActorInterface: 'static + Send {
type Incoming: Message; type Incoming: Message;

View file

@ -16,6 +16,7 @@ pub(crate) fn get_random(buf: &mut [u8]) {
getrandom::getrandom(buf).unwrap() getrandom::getrandom(buf).unwrap()
} }
/// FIXME: remove hard coded defaults
/// The strategy for message processing is such: /// The strategy for message processing is such:
/// ///
/// ```ignore /// ```ignore
@ -25,4 +26,5 @@ pub(crate) fn get_random(buf: &mut [u8]) {
/// process total_messages >> 1 /// process total_messages >> 1
/// ``` /// ```
const WATERLEVEL: usize = 10; const WATERLEVEL: usize = 10;
/// FIXME: remove hard coded defaults
const DEFAULT_INBOX_CAPACITY: usize = 1_000; const DEFAULT_INBOX_CAPACITY: usize = 1_000;

View file

@ -1,86 +1,71 @@
use std::collections::HashMap; use std::{collections::HashMap, sync::Arc};
use crate::{ use crate::{
ActorInterface,
actor::{ActorAddress, Message}, actor::{ActorAddress, Message},
ring_buffer::{Receiver, Sender}, ring_buffer::Sender,
WATERLEVEL,
}; };
pub(crate) type Envelope = Box<dyn std::any::Any + Send>; /// FIXME: Go over with a fine-toothed comb and reassure yourself this typing
/// makes sense. With these types, what happens at the hardware level.
pub(crate) type Envelope = Arc<dyn std::any::Any + Send + Sync>;
pub(crate) trait SenderT: Send { pub(crate) trait SenderT: Send + Sync {
fn try_send(&self, envelope: Envelope); fn try_send(&self, envelope: Envelope);
} }
impl<M: Message> SenderT for Sender<M> { impl<M: Message> SenderT for Sender<M> {
fn try_send(&self, envelope: Envelope) { fn try_send(&self, envelope: Envelope) {
if let Ok(msg) = envelope.downcast::<M>() { if let Some(msg) = envelope.downcast_ref::<M>() {
let _ = Sender::try_send(self, *msg); let _ = Sender::try_send(self, msg.clone());
} }
} }
} }
/// Internal messages for the Router's own inbox /// Internal messages for the Router's own inbox
#[derive(Clone)]
pub(crate) enum RouterMessage { pub(crate) enum RouterMessage {
/// register addrs <addr> with sender <sender> /// register addrs <addr> with sender <sender>
AddAddr(ActorAddress, Box<dyn SenderT>), AddAddr(ActorAddress, Arc<dyn SenderT>),
/// FIXME: this will be active when we allow actors to shut themselves /// FIXME: this will be active when we allow actors to shut themselves
/// down. For now, disable the warning. /// down. For now, disable the warning.
#[allow(dead_code)] #[allow(dead_code)]
/// remove an actor from the address book /// remove an actor from the address book
RemoveAddr(ActorAddress), RemoveAddr(ActorAddress),
/// send <msg> to <addr> /// send <msg> to <addr>
SendToAddr { addr: ActorAddress, msg: Envelope }, SendToAddr { addr: ActorAddress, msg: Envelope },
} }
pub(crate) struct Router { pub(crate) struct Router {
directory: HashMap<ActorAddress, Box<dyn SenderT>>, directory: HashMap<ActorAddress, Arc<dyn SenderT>>,
inbox: Receiver<RouterMessage>,
} }
impl Router { impl Router {
pub fn new(cap: usize) -> Self { pub fn new() -> Self {
Self { Self {
directory: HashMap::new(), directory: HashMap::new(),
inbox: Receiver::new(cap),
} }
} }
}
pub fn tick(&mut self) { impl ActorInterface for Router {
let total_messages = self.inbox.len(); type Incoming = RouterMessage;
let messages_to_process = if total_messages < WATERLEVEL {
total_messages
} else {
total_messages >> 1
};
for _ in 0..messages_to_process { type Response = ();
match self.inbox.try_recv() {
Some(msg) => self.handle(msg),
None => unreachable!("We ran checks on total messages before processing."),
}
}
}
pub fn new_sender(&self) -> Sender<RouterMessage> { fn handle(&mut self, _ctx: &crate::Runtime, msg: Self::Incoming) {
self.inbox.new_sender()
}
fn handle(&mut self, msg: RouterMessage) {
match msg { match msg {
RouterMessage::AddAddr(addr, sender) => { RouterMessage::AddAddr(addr, sender) => {
self.directory.insert(addr, sender); self.directory.insert(addr, sender);
} },
RouterMessage::RemoveAddr(addr) => { RouterMessage::RemoveAddr(addr) => { self.directory.remove(&addr); },
self.directory.remove(&addr);
}
RouterMessage::SendToAddr { addr, msg } => { RouterMessage::SendToAddr { addr, msg } => {
if let Some(sender) = self.directory.get(&addr) { if let Some(sender) = self.directory.get(&addr) {
sender.try_send(msg); sender.try_send(msg);
} }
} },
} }
} }
} }

View file

@ -1,5 +1,5 @@
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock}; use std::sync::{Arc, OnceLock};
use std::thread::{self, JoinHandle}; use std::thread::{self, JoinHandle};
use crossbeam_queue::ArrayQueue; use crossbeam_queue::ArrayQueue;
@ -48,33 +48,55 @@ pub struct Runtime {
router_inbox: Sender<RouterMessage>, router_inbox: Sender<RouterMessage>,
actor_queue: ArrayQueue<Box<dyn AnyActor>>, actor_queue: ArrayQueue<Box<dyn AnyActor>>,
flavor: RuntimeFlavor, flavor: RuntimeFlavor,
/// Router is only accessed from a single thread (either main or dedicated router thread)
/// FIXME: The only state Router needs access to is a hashmap. If we find something
/// lockfree, we can remove this mutex
router: Mutex<Router>,
/// Thread handles, created lazily when run() is called /// Thread handles, created lazily when run() is called
handles: OnceLock<RuntimeHandles>, handles: OnceLock<RuntimeHandles>,
} }
impl Runtime { impl Runtime {
/// Create a new runtime with given actor queue capacity and flavor /// Create a new runtime with given actor queue capacity and flavor
/// pub fn new() -> Self {
/// FIXME: I don't like this interface. Maybe a builder or config pattern. I shouldnt let actor_queue = ArrayQueue::new(DEFAULT_INBOX_CAPACITY);
/// have to read code to understand what these variable names are.
pub fn new(capacity: usize, flavor: RuntimeFlavor) -> Self { let router = Router::new();
// FIXME: Avoid hard coded defaults, or at least put them all in one place let router_incoming =
let router = Router::new(DEFAULT_INBOX_CAPACITY); Receiver::<<Router as ActorInterface>::Incoming>::new(DEFAULT_INBOX_CAPACITY);
let router_inbox = router.new_sender(); let router_inbox = router_incoming.new_sender();
actor_queue
.push(Box::new(Actor::new(router_incoming, router)) as Box<dyn AnyActor>)
.map_err(|_| "failed to add router to actor queue")
.expect("failed to spawn router at runtime initialization.");
Self { Self {
running: AtomicBool::new(false), running: AtomicBool::new(false),
router_inbox, router_inbox,
actor_queue: ArrayQueue::new(capacity), actor_queue,
flavor, flavor: RuntimeFlavor::SingleThreaded,
router: Mutex::new(router),
handles: OnceLock::new(), handles: OnceLock::new(),
} }
} }
/// FIXME: Abstract out by making two separate `Runtime` structs, one for singlethreaded, another for multi
pub fn new_multithreaded(num_workers: usize) -> (Self, Actor<Router>) {
let router = Router::new();
let router_incoming =
Receiver::<<Router as ActorInterface>::Incoming>::new(DEFAULT_INBOX_CAPACITY);
let router_inbox = router_incoming.new_sender();
(
Self {
running: AtomicBool::new(false),
router_inbox,
actor_queue: ArrayQueue::new(DEFAULT_INBOX_CAPACITY),
flavor: RuntimeFlavor::Multithreaded {
workers: num_workers,
},
handles: OnceLock::new(),
},
Actor::new(router_incoming, router),
)
}
/// Spawn an actor, returns its address /// Spawn an actor, returns its address
pub fn spawn<A: ActorInterface>(&self, actor: A) -> Result<ActorAddress, Error> { pub fn spawn<A: ActorInterface>(&self, actor: A) -> Result<ActorAddress, Error> {
// FIXME: Figure out what to do with this. // FIXME: Figure out what to do with this.
@ -90,7 +112,7 @@ impl Runtime {
// Register the sender with the router // Register the sender with the router
let _ = self let _ = self
.router_inbox .router_inbox
.try_send(RouterMessage::AddAddr(addr, Box::new(sender))); .try_send(RouterMessage::AddAddr(addr, Arc::new(sender)));
self.actor_queue self.actor_queue
.push(Box::new(Actor::new(inbox, actor))) .push(Box::new(Actor::new(inbox, actor)))
@ -104,7 +126,7 @@ impl Runtime {
self.router_inbox self.router_inbox
.try_send(RouterMessage::SendToAddr { .try_send(RouterMessage::SendToAddr {
addr, addr,
msg: Box::new(msg), msg: Arc::new(msg),
}) })
.map_err(|_| "Failed to send message to router.".into()) .map_err(|_| "Failed to send message to router.".into())
} }
@ -121,7 +143,7 @@ impl Runtime {
// Register the sender with the router // Register the sender with the router
let _ = self let _ = self
.router_inbox .router_inbox
.try_send(RouterMessage::AddAddr(addr, Box::new(sender))); .try_send(RouterMessage::AddAddr(addr, Arc::new(sender)));
Inbox { Inbox {
addr, addr,
inner: receiver, inner: receiver,
@ -137,22 +159,16 @@ impl Runtime {
actor.tick(&self); actor.tick(&self);
let _ = self.actor_queue.push(actor); let _ = self.actor_queue.push(actor);
} }
// In single-threaded mode, also tick the router
if matches!(self.flavor, RuntimeFlavor::SingleThreaded)
{
self.router.lock().expect("single threaded mutex lock").tick();
}
} }
/// Run the runtime /// Run the runtime
/// - Multithreaded: spawns workers + router thread, returns an `Arc` pointer to the /// - Multithreaded: spawns workers + router thread, returns an `Arc` pointer to the
/// `Runtime` struct. /// `Runtime` struct.
pub fn run(self) -> Arc<Self> { pub fn run(self, router: Actor<Router>) -> Arc<Self> {
// FIXME: gate access // FIXME: gate access
let num_workers = match self.flavor { let num_workers = match self.flavor {
RuntimeFlavor::Multithreaded { workers } => workers, RuntimeFlavor::Multithreaded { workers } => workers,
_ => panic!("'run' method requires a multithreaded runtime") _ => panic!("'run' method requires a multithreaded runtime"),
}; };
let rt = Arc::new(self); let rt = Arc::new(self);
@ -160,7 +176,7 @@ impl Runtime {
let router_handle = { let router_handle = {
let ctx = rt.clone(); let ctx = rt.clone();
thread::spawn(move || { thread::spawn(move || {
router_loop(ctx); router_loop(ctx, router);
}) })
}; };
@ -174,12 +190,14 @@ impl Runtime {
worker_handles.push(handle); worker_handles.push(handle);
} }
rt.handles.set(RuntimeHandles { workers: worker_handles, router_thread: Some(router_handle) }); rt.handles.set(RuntimeHandles {
workers: worker_handles,
router_thread: Some(router_handle),
});
rt rt
} }
/// Signal all workers to stop /// Signal all workers to stop
pub fn shutdown(&self) { pub fn shutdown(&self) {
self.running.store(false, Ordering::Release); self.running.store(false, Ordering::Release);
@ -193,7 +211,6 @@ impl Runtime {
/// Worker thread loop - processes actors from the shared queue /// Worker thread loop - processes actors from the shared queue
fn worker_loop(ctx: Arc<Runtime>) { fn worker_loop(ctx: Arc<Runtime>) {
while ctx.running.load(Ordering::Relaxed) { while ctx.running.load(Ordering::Relaxed) {
if let Some(mut actor) = ctx.actor_queue.pop() { if let Some(mut actor) = ctx.actor_queue.pop() {
actor.tick(&ctx); actor.tick(&ctx);
@ -205,9 +222,9 @@ fn worker_loop(ctx: Arc<Runtime>) {
} }
/// Router thread loop - processes router messages /// Router thread loop - processes router messages
fn router_loop(ctx: Arc<Runtime>) { fn router_loop(ctx: Arc<Runtime>, mut router: Actor<Router>) {
while ctx.running.load(Ordering::Relaxed) { while ctx.running.load(Ordering::Relaxed) {
ctx.router.lock().expect("failed to lock router mutex").tick(); router.tick(&ctx);
thread::yield_now(); thread::yield_now();
} }
} }

View file

@ -48,7 +48,7 @@ impl ActorInterface for ForwarderActor {
#[test] #[test]
fn test_single_threaded_ping_pong() { fn test_single_threaded_ping_pong() {
let mut rt = Runtime::new(100, RuntimeFlavor::SingleThreaded); let mut rt = Runtime::new();
let inbox: Inbox<PongMessage> = rt.new_inbox(); let inbox: Inbox<PongMessage> = rt.new_inbox();
let pong_addr = rt.spawn(PongActor).expect("spawn pong"); let pong_addr = rt.spawn(PongActor).expect("spawn pong");
@ -75,7 +75,7 @@ fn test_single_threaded_ping_pong() {
#[test] #[test]
fn test_single_threaded_message_chain() { fn test_single_threaded_message_chain() {
let mut rt = Runtime::new(100, RuntimeFlavor::SingleThreaded); let mut rt = Runtime::new();
let inbox: Inbox<ForwardMessage> = rt.new_inbox(); let inbox: Inbox<ForwardMessage> = rt.new_inbox();
// Create a chain: A -> B -> C -> inbox // Create a chain: A -> B -> C -> inbox
@ -102,48 +102,36 @@ fn test_single_threaded_message_chain() {
panic!("Message did not traverse the chain"); panic!("Message did not traverse the chain");
} }
#[test] // #[test]
fn test_multithreaded_message_passing() { // fn test_multithreaded_message_passing() {
let rt = Runtime::new(1000, RuntimeFlavor::Multithreaded { workers: 4 }); // let rt = Runtime::new(1000, RuntimeFlavor::Multithreaded { workers: 4 });
let inbox: Inbox<ForwardMessage> = rt.new_inbox(); // let inbox: Inbox<ForwardMessage> = rt.new_inbox();
// Create a longer chain to exercise multi-threading // // Create a longer chain to exercise multi-threading
let mut target = *inbox.addr(); // let mut target = *inbox.addr();
for _ in 0..20 { // for _ in 0..20 {
target = rt.spawn(ForwarderActor { target }).unwrap(); // target = rt.spawn(ForwarderActor { target }).unwrap();
} // }
let start_addr = target; // let start_addr = target;
// Send message // // Send message
rt.send_to(start_addr, ForwardMessage(999)).unwrap(); // rt.send_to(start_addr, ForwardMessage(999)).unwrap();
// Spawn thread to check for result and shutdown // // Spawn thread to check for result and shutdown
let ctx = rt.run(); // let ctx = rt.run();
let inbox_check = thread::spawn(move || { // let inbox_check = thread::spawn(move || {
for _ in 0..100 { // for _ in 0..100 {
thread::sleep(Duration::from_millis(10)); // thread::sleep(Duration::from_millis(10));
if let Some(ForwardMessage(val)) = inbox.try_recv() { // if let Some(ForwardMessage(val)) = inbox.try_recv() {
ctx.shutdown(); // ctx.shutdown();
return Some(val); // return Some(val);
} // }
} // }
ctx.shutdown(); // ctx.shutdown();
None // None
}); // });
let result = inbox_check.join().unwrap(); // let result = inbox_check.join().unwrap();
assert_eq!(result, Some(999)); // assert_eq!(result, Some(999));
} // }
#[test]
fn test_is_running_flag() {
let rt = Runtime::new(100, RuntimeFlavor::SingleThreaded);
// Before run(), is_running should be false
assert!(!rt.is_running());
// After shutdown before run, still false
rt.shutdown();
assert!(!rt.is_running());
}