use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::thread::{self, JoinHandle}; use crossbeam_queue::ArrayQueue; use crate::{ Error, actor::{Actor, ActorAddress, ActorInterface, AnyActor, Message}, ring_buffer::{Receiver, Sender}, router::{Router, RouterMessage}, }; /// Generic message inbox for receiving messages outside of the runtime. pub struct Inbox { addr: ActorAddress, inner: Receiver, } impl Inbox { pub fn addr(&self) -> &ActorAddress { &self.addr } pub fn try_recv(&self) -> Option { self.inner.try_recv() } } /// The tunable settings for the runtime. pub struct RuntimeConfig { pub max_actors: usize, pub router_max_messages: usize, pub actor_max_messages: usize, pub num_threads: usize, } /// 8kB for the `Box<..>` before counting the rest of the memory const DEFAULT_MAX_ACTORS: usize = 1_000; /// 160kB for the `Arc<..>` before counting the rest of the memory const DEFAULT_ROUTER_MAX_MESSAGES: usize = 10_000; /// 16kB PER ACTOR to alloc space for storing the `Arc<..>` pointers /// With default setting of [DEFAULT_MAX_ACTORS] this is: /// 1_000 * 16kB = 16MB const DEFAULT_ACTOR_MAX_MESSAGES: usize = 1_000; impl Default for RuntimeConfig { fn default() -> Self { Self { max_actors: DEFAULT_MAX_ACTORS, router_max_messages: DEFAULT_ROUTER_MAX_MESSAGES, actor_max_messages: DEFAULT_ACTOR_MAX_MESSAGES, num_threads: 1, } } } /// The `Runtime` struct is the primary gateway for interacting with the framework. pub struct Runtime { config: RuntimeConfig, actor_queue: ArrayQueue>, router_interface: Sender, router: Option>, // `None` if single-threaded // for multithreaded contexts is_running: AtomicBool, } /// Handle for dealing with a runtime that has started via the `Runtime::run()` method. pub struct RuntimeHandle { pub runtime: Arc, threads: Vec>, } impl RuntimeHandle { pub fn join(self) { for handle in self.threads { let _ = handle.join(); } } /// Simple helper, calls the inner `Runtime::shutdown()` method pub fn shutdown(&self) { self.runtime.shutdown(); } } impl Runtime { /// Builds a new `Runtime` struct, but does not yet run anything. If multithreaded, call /// `run()`, if single threaded, needs to be driven by calls to the `tick()` method. pub fn new(config: RuntimeConfig) -> Self { let actor_queue = ArrayQueue::new(config.max_actors); // router is a unique actor in that the runtime needs access to it's `Sender` handle let router_inner = Router::new(); let router_inbox: Receiver = Receiver::<::Incoming>::new(config.router_max_messages); let router_sender = router_inbox.new_sender(); let router = Actor::new(router_inbox, router_inner); // Single-threaded: router goes in queue. Multi-threaded: stays in Option let router_option = if config.num_threads < 2 { actor_queue .push(Box::new(router) as Box) .map_err(|_| "failed to add router to actor queue") .expect("failed to spawn router at runtime initialization."); None } else { Some(router) }; Self { config, actor_queue, router_interface: router_sender, is_running: AtomicBool::new(false), router: router_option, } } /// Spawn an actor, returns its address pub fn spawn(&self, actor: A) -> Result { // assign a stochastic let addr = ActorAddress::new_random(); let inbox = Receiver::::new(self.config.actor_max_messages); let sender = inbox.new_sender(); // Register the sender with the router self.router_interface .try_send(RouterMessage::AddAddr(addr, Arc::new(sender))) .map_err(|_| { Error::from("Runtime error: failed to add actor to router. Router inbox full") })?; self.actor_queue .push(Box::new(Actor::new(inbox, actor))) .map_err(|_| Error::from("Runtime error: Failed to spawn actor. Queue full."))?; Ok(addr) } /// Send a message to an actor address pub fn send_to(&self, addr: ActorAddress, msg: M) -> Result<(), Error> { self.router_interface .try_send(RouterMessage::SendToAddr { addr, msg: Arc::new(msg), }) .map_err(|_| Error::from("Failed to send message to router.")) } /// Create an external inbox for receiving messages in the outer process containing the runtime pub fn new_inbox(&self) -> Result, Error> { let addr = ActorAddress::new_random(); let receiver = Receiver::::new(self.config.actor_max_messages); let sender = receiver.new_sender(); // Register the sender with the router self.router_interface .try_send(RouterMessage::AddAddr(addr, Arc::new(sender))) .map_err(|_| { Error::from( "Runtime error: failed to add a new inbox channel. Router inbox is full.", ) })?; Ok(Inbox { addr, inner: receiver, }) } /// Spawn worker threads and start processing, returning a set of handles and /// a Runtime object to interface with. /// /// ### WARN: /// ##### This function panics if the configuration is set as single threaded /// `config.num_threads == 1` pub fn run(mut self) -> Result { if self.config.num_threads < 2 { return Err(Error::from( "Runtime error: cannot call `Runtime::run()` from a single-threaded context.", )); } self.is_running.store(true, Ordering::Release); // Take router out before wrapping in Arc - it will be owned by router thread let mut router = self .router .take() .expect("Router must be present for multi-threaded runtime"); let rt = Arc::new(self); let mut handles: Vec> = vec![]; // Router thread owns the router directly - no synchronization needed let router_handle = { let ctx = rt.clone(); thread::spawn(move || { while ctx.is_running.load(Ordering::Acquire) { router.tick(&ctx); thread::yield_now(); } }) }; handles.push(router_handle); // Spawn worker threads let num_workers = rt.config.num_threads - 1; for _ in 0..num_workers { let ctx = rt.clone(); let handle = thread::spawn(move || { while ctx.is_running.load(Ordering::Acquire) { if let Some(mut actor) = ctx.actor_queue.pop() { actor.tick(&ctx); if let Err(_) = ctx.actor_queue.push(actor) { panic!( "Runtime panic: attempted to return an actor to the queue, but queue was full." ) } } else { thread::yield_now(); } } }); handles.push(handle); } Ok(RuntimeHandle { runtime: rt, threads: handles, }) } /// Pop the actor off the top of the queue and process it's messages, returning it to the back of /// the queue upon completion. pub fn tick(&self) { if let Some(mut actor) = self.actor_queue.pop() { actor.tick(&self); let _ = self.actor_queue.push(actor); } } /// Signal all workers to stop pub fn shutdown(&self) { self.is_running.store(false, Ordering::Release); } }