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:
parent
2f91c7d1bd
commit
c62b20c732
7 changed files with 110 additions and 118 deletions
|
|
@ -35,7 +35,7 @@ impl ActorInterface for Greeter {
|
|||
}
|
||||
|
||||
fn main() {
|
||||
let mut rt = Runtime::new(100, RuntimeFlavor::SingleThreaded);
|
||||
let mut rt = Runtime::new();
|
||||
let addr = rt
|
||||
.spawn(Greeter::default())
|
||||
.expect("failed to spawn greeter");
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ impl ActorInterface for RingActor {
|
|||
}
|
||||
|
||||
fn main() {
|
||||
let mut rt = Runtime::new(10_000, RuntimeFlavor::SingleThreaded);
|
||||
let mut rt = Runtime::new();
|
||||
let inbox: Inbox<RingMessage> = rt.new_inbox();
|
||||
|
||||
let mut next = rt
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{Runtime, WATERLEVEL, ring_buffer::Receiver};
|
||||
|
||||
pub trait Message: 'static + Sized + Clone + Send {}
|
||||
impl<T: 'static + Sized + Clone + Send> Message for T {}
|
||||
pub trait Message: 'static + Sized + Clone + Send + Sync {}
|
||||
impl<T: 'static + Sized + Clone + Send + Sync> Message for T {}
|
||||
|
||||
pub trait ActorInterface: 'static + Send {
|
||||
type Incoming: Message;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ pub(crate) fn get_random(buf: &mut [u8]) {
|
|||
getrandom::getrandom(buf).unwrap()
|
||||
}
|
||||
|
||||
/// FIXME: remove hard coded defaults
|
||||
/// The strategy for message processing is such:
|
||||
///
|
||||
/// ```ignore
|
||||
|
|
@ -25,4 +26,5 @@ pub(crate) fn get_random(buf: &mut [u8]) {
|
|||
/// process total_messages >> 1
|
||||
/// ```
|
||||
const WATERLEVEL: usize = 10;
|
||||
/// FIXME: remove hard coded defaults
|
||||
const DEFAULT_INBOX_CAPACITY: usize = 1_000;
|
||||
|
|
|
|||
|
|
@ -1,29 +1,32 @@
|
|||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
ActorInterface,
|
||||
actor::{ActorAddress, Message},
|
||||
ring_buffer::{Receiver, Sender},
|
||||
WATERLEVEL,
|
||||
ring_buffer::Sender,
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
impl<M: Message> SenderT for Sender<M> {
|
||||
fn try_send(&self, envelope: Envelope) {
|
||||
if let Ok(msg) = envelope.downcast::<M>() {
|
||||
let _ = Sender::try_send(self, *msg);
|
||||
if let Some(msg) = envelope.downcast_ref::<M>() {
|
||||
let _ = Sender::try_send(self, msg.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal messages for the Router's own inbox
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum RouterMessage {
|
||||
/// 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
|
||||
/// down. For now, disable the warning.
|
||||
|
|
@ -36,51 +39,33 @@ pub(crate) enum RouterMessage {
|
|||
}
|
||||
|
||||
pub(crate) struct Router {
|
||||
directory: HashMap<ActorAddress, Box<dyn SenderT>>,
|
||||
inbox: Receiver<RouterMessage>,
|
||||
directory: HashMap<ActorAddress, Arc<dyn SenderT>>,
|
||||
}
|
||||
|
||||
impl Router {
|
||||
pub fn new(cap: usize) -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
directory: HashMap::new(),
|
||||
inbox: Receiver::new(cap),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick(&mut self) {
|
||||
let total_messages = self.inbox.len();
|
||||
let messages_to_process = if total_messages < WATERLEVEL {
|
||||
total_messages
|
||||
} else {
|
||||
total_messages >> 1
|
||||
};
|
||||
|
||||
for _ in 0..messages_to_process {
|
||||
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> {
|
||||
self.inbox.new_sender()
|
||||
}
|
||||
impl ActorInterface for Router {
|
||||
type Incoming = RouterMessage;
|
||||
|
||||
fn handle(&mut self, msg: RouterMessage) {
|
||||
type Response = ();
|
||||
|
||||
fn handle(&mut self, _ctx: &crate::Runtime, msg: Self::Incoming) {
|
||||
match msg {
|
||||
RouterMessage::AddAddr(addr, sender) => {
|
||||
self.directory.insert(addr, sender);
|
||||
}
|
||||
RouterMessage::RemoveAddr(addr) => {
|
||||
self.directory.remove(&addr);
|
||||
}
|
||||
},
|
||||
RouterMessage::RemoveAddr(addr) => { self.directory.remove(&addr); },
|
||||
RouterMessage::SendToAddr { addr, msg } => {
|
||||
if let Some(sender) = self.directory.get(&addr) {
|
||||
sender.try_send(msg);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::thread::{self, JoinHandle};
|
||||
|
||||
use crossbeam_queue::ArrayQueue;
|
||||
|
|
@ -48,33 +48,55 @@ pub struct Runtime {
|
|||
router_inbox: Sender<RouterMessage>,
|
||||
actor_queue: ArrayQueue<Box<dyn AnyActor>>,
|
||||
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
|
||||
handles: OnceLock<RuntimeHandles>,
|
||||
}
|
||||
|
||||
impl Runtime {
|
||||
/// Create a new runtime with given actor queue capacity and flavor
|
||||
///
|
||||
/// FIXME: I don't like this interface. Maybe a builder or config pattern. I shouldnt
|
||||
/// have to read code to understand what these variable names are.
|
||||
pub fn new(capacity: usize, flavor: RuntimeFlavor) -> Self {
|
||||
// FIXME: Avoid hard coded defaults, or at least put them all in one place
|
||||
let router = Router::new(DEFAULT_INBOX_CAPACITY);
|
||||
let router_inbox = router.new_sender();
|
||||
pub fn new() -> Self {
|
||||
let actor_queue = ArrayQueue::new(DEFAULT_INBOX_CAPACITY);
|
||||
|
||||
let router = Router::new();
|
||||
let router_incoming =
|
||||
Receiver::<<Router as ActorInterface>::Incoming>::new(DEFAULT_INBOX_CAPACITY);
|
||||
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 {
|
||||
running: AtomicBool::new(false),
|
||||
router_inbox,
|
||||
actor_queue: ArrayQueue::new(capacity),
|
||||
flavor,
|
||||
router: Mutex::new(router),
|
||||
actor_queue,
|
||||
flavor: RuntimeFlavor::SingleThreaded,
|
||||
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
|
||||
pub fn spawn<A: ActorInterface>(&self, actor: A) -> Result<ActorAddress, Error> {
|
||||
// FIXME: Figure out what to do with this.
|
||||
|
|
@ -90,7 +112,7 @@ impl Runtime {
|
|||
// Register the sender with the router
|
||||
let _ = self
|
||||
.router_inbox
|
||||
.try_send(RouterMessage::AddAddr(addr, Box::new(sender)));
|
||||
.try_send(RouterMessage::AddAddr(addr, Arc::new(sender)));
|
||||
|
||||
self.actor_queue
|
||||
.push(Box::new(Actor::new(inbox, actor)))
|
||||
|
|
@ -104,7 +126,7 @@ impl Runtime {
|
|||
self.router_inbox
|
||||
.try_send(RouterMessage::SendToAddr {
|
||||
addr,
|
||||
msg: Box::new(msg),
|
||||
msg: Arc::new(msg),
|
||||
})
|
||||
.map_err(|_| "Failed to send message to router.".into())
|
||||
}
|
||||
|
|
@ -121,7 +143,7 @@ impl Runtime {
|
|||
// Register the sender with the router
|
||||
let _ = self
|
||||
.router_inbox
|
||||
.try_send(RouterMessage::AddAddr(addr, Box::new(sender)));
|
||||
.try_send(RouterMessage::AddAddr(addr, Arc::new(sender)));
|
||||
Inbox {
|
||||
addr,
|
||||
inner: receiver,
|
||||
|
|
@ -137,22 +159,16 @@ impl Runtime {
|
|||
actor.tick(&self);
|
||||
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
|
||||
/// - Multithreaded: spawns workers + router thread, returns an `Arc` pointer to the
|
||||
/// `Runtime` struct.
|
||||
pub fn run(self) -> Arc<Self> {
|
||||
pub fn run(self, router: Actor<Router>) -> Arc<Self> {
|
||||
// FIXME: gate access
|
||||
let num_workers = match self.flavor {
|
||||
RuntimeFlavor::Multithreaded { workers } => workers,
|
||||
_ => panic!("'run' method requires a multithreaded runtime")
|
||||
_ => panic!("'run' method requires a multithreaded runtime"),
|
||||
};
|
||||
let rt = Arc::new(self);
|
||||
|
||||
|
|
@ -160,7 +176,7 @@ impl Runtime {
|
|||
let router_handle = {
|
||||
let ctx = rt.clone();
|
||||
thread::spawn(move || {
|
||||
router_loop(ctx);
|
||||
router_loop(ctx, router);
|
||||
})
|
||||
};
|
||||
|
||||
|
|
@ -174,12 +190,14 @@ impl Runtime {
|
|||
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
|
||||
}
|
||||
|
||||
|
||||
/// Signal all workers to stop
|
||||
pub fn shutdown(&self) {
|
||||
self.running.store(false, Ordering::Release);
|
||||
|
|
@ -193,7 +211,6 @@ impl Runtime {
|
|||
|
||||
/// Worker thread loop - processes actors from the shared queue
|
||||
fn worker_loop(ctx: Arc<Runtime>) {
|
||||
|
||||
while ctx.running.load(Ordering::Relaxed) {
|
||||
if let Some(mut actor) = ctx.actor_queue.pop() {
|
||||
actor.tick(&ctx);
|
||||
|
|
@ -205,9 +222,9 @@ fn worker_loop(ctx: Arc<Runtime>) {
|
|||
}
|
||||
|
||||
/// 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) {
|
||||
ctx.router.lock().expect("failed to lock router mutex").tick();
|
||||
router.tick(&ctx);
|
||||
thread::yield_now();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ impl ActorInterface for ForwarderActor {
|
|||
|
||||
#[test]
|
||||
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 pong_addr = rt.spawn(PongActor).expect("spawn pong");
|
||||
|
|
@ -75,7 +75,7 @@ fn test_single_threaded_ping_pong() {
|
|||
|
||||
#[test]
|
||||
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();
|
||||
|
||||
// Create a chain: A -> B -> C -> inbox
|
||||
|
|
@ -102,48 +102,36 @@ fn test_single_threaded_message_chain() {
|
|||
panic!("Message did not traverse the chain");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multithreaded_message_passing() {
|
||||
let rt = Runtime::new(1000, RuntimeFlavor::Multithreaded { workers: 4 });
|
||||
let inbox: Inbox<ForwardMessage> = rt.new_inbox();
|
||||
// #[test]
|
||||
// fn test_multithreaded_message_passing() {
|
||||
// let rt = Runtime::new(1000, RuntimeFlavor::Multithreaded { workers: 4 });
|
||||
// let inbox: Inbox<ForwardMessage> = rt.new_inbox();
|
||||
|
||||
// Create a longer chain to exercise multi-threading
|
||||
let mut target = *inbox.addr();
|
||||
for _ in 0..20 {
|
||||
target = rt.spawn(ForwarderActor { target }).unwrap();
|
||||
}
|
||||
// // Create a longer chain to exercise multi-threading
|
||||
// let mut target = *inbox.addr();
|
||||
// for _ in 0..20 {
|
||||
// target = rt.spawn(ForwarderActor { target }).unwrap();
|
||||
// }
|
||||
|
||||
let start_addr = target;
|
||||
// let start_addr = target;
|
||||
|
||||
// Send message
|
||||
rt.send_to(start_addr, ForwardMessage(999)).unwrap();
|
||||
// // Send message
|
||||
// rt.send_to(start_addr, ForwardMessage(999)).unwrap();
|
||||
|
||||
// Spawn thread to check for result and shutdown
|
||||
let ctx = rt.run();
|
||||
let inbox_check = thread::spawn(move || {
|
||||
for _ in 0..100 {
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
if let Some(ForwardMessage(val)) = inbox.try_recv() {
|
||||
ctx.shutdown();
|
||||
return Some(val);
|
||||
}
|
||||
}
|
||||
ctx.shutdown();
|
||||
None
|
||||
});
|
||||
// // Spawn thread to check for result and shutdown
|
||||
// let ctx = rt.run();
|
||||
// let inbox_check = thread::spawn(move || {
|
||||
// for _ in 0..100 {
|
||||
// thread::sleep(Duration::from_millis(10));
|
||||
// if let Some(ForwardMessage(val)) = inbox.try_recv() {
|
||||
// ctx.shutdown();
|
||||
// return Some(val);
|
||||
// }
|
||||
// }
|
||||
// ctx.shutdown();
|
||||
// None
|
||||
// });
|
||||
|
||||
let result = inbox_check.join().unwrap();
|
||||
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());
|
||||
}
|
||||
// let result = inbox_check.join().unwrap();
|
||||
// assert_eq!(result, Some(999));
|
||||
// }
|
||||
|
|
|
|||
Loading…
Reference in a new issue