fix: consolidate modules

Better consoliation of modules into more logical components.
This commit is contained in:
Zachery Aaron Shores-Chmielewski 2026-02-06 14:06:37 +07:00
parent 06566b5e0f
commit c80060f453
18 changed files with 206 additions and 250 deletions

View file

@ -8,9 +8,8 @@
use crate::harness::{black_box, Bench, BenchSuite}; use crate::harness::{black_box, Bench, BenchSuite};
use std::thread; use std::thread;
use swactor::{ use swactor::{
Ctx,
actor::ActorInterface, actor::ActorInterface,
runtime::{Runtime, RuntimeConfig}, runtime::{Ctx, Runtime, RuntimeConfig},
}; };
// ============================================================================ // ============================================================================

View file

@ -8,9 +8,8 @@
use crate::harness::{black_box, Bench, BenchSuite}; use crate::harness::{black_box, Bench, BenchSuite};
use swactor::{ use swactor::{
Ctx,
actor::{ActorAddress, ActorInterface}, actor::{ActorAddress, ActorInterface},
runtime::{Runtime, RuntimeConfig}, runtime::{Ctx, Runtime, RuntimeConfig},
}; };
// ============================================================================ // ============================================================================

View file

@ -1,7 +1,6 @@
use swactor::{ use swactor::{
Ctx,
actor::{ActorAddress, ActorInterface}, actor::{ActorAddress, ActorInterface},
runtime::{Runtime, RuntimeConfig}, runtime::{Ctx, Runtime, RuntimeConfig},
}; };
#[derive(Debug, Default)] #[derive(Debug, Default)]

View file

@ -1,7 +1,6 @@
use swactor::{ use swactor::{
Ctx,
actor::{ActorAddress, ActorInterface}, actor::{ActorAddress, ActorInterface},
runtime::{Inbox, Runtime, RuntimeConfig}, runtime::{Ctx, Inbox, Runtime, RuntimeConfig},
}; };
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]

View file

@ -1,7 +1,6 @@
use std::any::Any; use std::any::Any;
use crate::{get_random, worker::mailbox::Mailbox}; use crate::{get_random, runtime::{ContextInner, Ctx}, worker::Mailbox};
use crate::context::{ContextInner, Ctx};
/// The primary trait defining data that can be passed to and from actor processes /// The primary trait defining data that can be passed to and from actor processes
pub trait Message: 'static + Sized + Clone + Send + Sync {} pub trait Message: 'static + Sized + Clone + Send + Sync {}

View file

@ -1,5 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::RwLock; use std::sync::RwLock;
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::actor::ActorAddress; use crate::actor::ActorAddress;
@ -50,6 +51,26 @@ impl AddressMap {
} }
} }
/// Round-robin actor placement strategy.
pub(crate) struct Placement {
next: AtomicUsize,
num_workers: usize,
}
impl Placement {
pub fn new(num_workers: usize) -> Self {
Self {
next: AtomicUsize::new(0),
num_workers,
}
}
pub fn next_worker(&self) -> WorkerId {
let id = self.next.fetch_add(1, Ordering::Relaxed) % self.num_workers;
WorkerId(id)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -87,4 +108,13 @@ mod tests {
map.insert(addr1, WorkerId(0)); map.insert(addr1, WorkerId(0));
assert_eq!(map.len(), 1); assert_eq!(map.len(), 1);
} }
#[test]
fn round_robin() {
let p = Placement::new(3);
assert_eq!(p.next_worker(), WorkerId(0));
assert_eq!(p.next_worker(), WorkerId(1));
assert_eq!(p.next_worker(), WorkerId(2));
assert_eq!(p.next_worker(), WorkerId(0));
}
} }

View file

@ -1,13 +1,11 @@
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use crossbeam_queue::{ArrayQueue, SegQueue}; use crossbeam_queue::{ArrayQueue, SegQueue};
pub struct HybridChannel<T> { pub struct HybridChannel<T> {
ring: ArrayQueue<T>, ring: ArrayQueue<T>,
overflow: SegQueue<T>, overflow: SegQueue<T>,
overflow_len: AtomicUsize,
} }
impl<T> HybridChannel<T> { impl<T> HybridChannel<T> {
@ -15,7 +13,6 @@ impl<T> HybridChannel<T> {
Self { Self {
ring: ArrayQueue::new(capacity), ring: ArrayQueue::new(capacity),
overflow: SegQueue::new(), overflow: SegQueue::new(),
overflow_len: AtomicUsize::new(0),
} }
} }
@ -24,7 +21,6 @@ impl<T> HybridChannel<T> {
Ok(()) => Ok(()), Ok(()) => Ok(()),
Err(v) => { Err(v) => {
self.overflow.push(v); self.overflow.push(v);
self.overflow_len.fetch_add(1, Ordering::Relaxed);
Ok(()) Ok(())
} }
} }
@ -37,16 +33,12 @@ impl<T> HybridChannel<T> {
match self.overflow.pop() { match self.overflow.pop() {
Some(value) => { Some(value) => {
self.overflow_len.fetch_sub(1, Ordering::Relaxed);
Some(value) Some(value)
} }
None => None, None => None,
} }
} }
pub fn len(&self) -> usize {
self.ring.len() + self.overflow_len.load(Ordering::Relaxed)
}
} }
pub(crate) struct Receiver<T> { pub(crate) struct Receiver<T> {
@ -59,11 +51,6 @@ impl<T> Receiver<T> {
Self { queue } Self { queue }
} }
pub fn len(&self) -> usize {
self.queue.len()
}
pub fn try_recv(&self) -> Option<T> { pub fn try_recv(&self) -> Option<T> {
return self.queue.pop(); return self.queue.pop();
} }

View file

@ -1,47 +0,0 @@
use std::any::Any;
use crate::actor::{Actor, ActorAddress, ActorInterface, AnyActor, Message};
use crate::worker::mailbox::Mailbox;
use crate::Error;
/// Object-safe inner trait for sending type-erased messages.
pub(crate) trait ContextInner {
fn send_any(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error>;
fn spawn_any(&self, addr: ActorAddress, actor: Box<dyn AnyActor>) -> Result<(), Error>;
fn mailbox_waterlevel(&self) -> usize;
}
/// Actor syscall interface — passed to `ActorInterface::handle()`.
///
/// Wraps a `&dyn ContextInner` to solve the object-safety problem while
/// providing a typed public API.
pub struct Ctx<'a> {
inner: &'a dyn ContextInner,
self_addr: ActorAddress,
}
impl<'a> Ctx<'a> {
pub(crate) fn new(inner: &'a dyn ContextInner, self_addr: ActorAddress) -> Self {
Self { inner, self_addr }
}
/// Returns the address of the actor currently being ticked.
pub fn self_addr(&self) -> ActorAddress {
self.self_addr
}
/// Send a typed message to an actor address.
pub fn send<M: Message>(&self, addr: ActorAddress, msg: M) -> Result<(), Error> {
self.inner.send_any(addr, Box::new(msg))
}
/// Spawn a new actor, returning its address.
pub fn spawn<A: ActorInterface>(&self, actor: A) -> Result<ActorAddress, Error> {
let addr = ActorAddress::new_random();
let waterlevel = self.inner.mailbox_waterlevel();
let actor = Actor::new(addr, Mailbox::new(waterlevel), actor);
let boxed: Box<dyn AnyActor> = Box::new(actor);
self.inner.spawn_any(addr, boxed)?;
Ok(addr)
}
}

View file

@ -2,31 +2,6 @@ use std::any::Any;
use crate::actor::ActorAddress; use crate::actor::ActorAddress;
/// A type-erased message envelope for cross-worker delivery.
///
/// Uses `Box` (no atomic refcount) and move semantics (no clone).
pub(crate) struct Envelope {
dest: ActorAddress,
payload: Box<dyn Any + Send>,
}
impl Envelope {
pub fn new(dest: ActorAddress, payload: Box<dyn Any + Send>) -> Self {
Self { dest, payload }
}
pub fn dest(&self) -> ActorAddress {
self.dest
}
pub fn downcast<M: 'static>(self) -> Option<M> {
self.payload.downcast::<M>().ok().map(|b| *b)
}
pub fn into_payload(self) -> Box<dyn Any + Send> {
self.payload
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {

View file

@ -5,12 +5,8 @@ pub(crate) mod channel;
pub(crate) mod error; pub(crate) mod error;
pub use error::Error; pub use error::Error;
pub mod context;
pub use context::Ctx;
pub(crate) mod envelope;
pub(crate) mod address_map; pub(crate) mod address_map;
pub(crate) mod placement;
pub mod config; pub mod config;
pub mod runtime; pub mod runtime;

View file

@ -1,37 +0,0 @@
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::address_map::WorkerId;
/// Round-robin actor placement strategy.
pub(crate) struct Placement {
next: AtomicUsize,
num_workers: usize,
}
impl Placement {
pub fn new(num_workers: usize) -> Self {
Self {
next: AtomicUsize::new(0),
num_workers,
}
}
pub fn next_worker(&self) -> WorkerId {
let id = self.next.fetch_add(1, Ordering::Relaxed) % self.num_workers;
WorkerId(id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_robin() {
let p = Placement::new(3);
assert_eq!(p.next_worker(), WorkerId(0));
assert_eq!(p.next_worker(), WorkerId(1));
assert_eq!(p.next_worker(), WorkerId(2));
assert_eq!(p.next_worker(), WorkerId(0));
}
}

View file

@ -6,14 +6,11 @@ use std::sync::{Arc, RwLock};
use std::thread::{self, JoinHandle}; use std::thread::{self, JoinHandle};
use crate::actor::{Actor, ActorAddress, ActorInterface, AnyActor, Message}; use crate::actor::{Actor, ActorAddress, ActorInterface, AnyActor, Message};
use crate::address_map::{AddressMap, WorkerId}; use crate::address_map::{AddressMap, Placement, WorkerId};
use crate::channel::{Receiver, Sender}; use crate::channel::{Receiver, Sender};
// Re-export config types so existing code using `runtime::RuntimeConfig` still works // Re-export config types so existing code using `runtime::RuntimeConfig` still works
pub use crate::config::{BackoffPolicy, RuntimeConfig}; pub use crate::config::{BackoffPolicy, RuntimeConfig};
use crate::context::ContextInner; use crate::worker::Mailbox;
use crate::envelope::Envelope;
use crate::placement::Placement;
use crate::worker::mailbox::Mailbox;
use crate::worker::{TickContext, Worker}; use crate::worker::{TickContext, Worker};
use crate::Error; use crate::Error;
@ -32,6 +29,34 @@ impl<M: Message> SenderT for Sender<M> {
} }
} }
/// A type-erased message envelope for cross-worker delivery.
///
/// Uses `Box` (no atomic refcount) and move semantics (no clone).
pub(crate) struct Envelope {
dest: ActorAddress,
payload: Box<dyn Any + Send>,
}
impl Envelope {
pub fn new(dest: ActorAddress, payload: Box<dyn Any + Send>) -> Self {
Self { dest, payload }
}
pub fn dest(&self) -> ActorAddress {
self.dest
}
pub fn downcast<M: 'static>(self) -> Option<M> {
self.payload.downcast::<M>().ok().map(|b| *b)
}
pub fn into_payload(self) -> Box<dyn Any + Send> {
self.payload
}
}
// ─── InboxRegistry ─────────────────────────────────────────────────────────── // ─── InboxRegistry ───────────────────────────────────────────────────────────
/// Registry of external inboxes — replaces the Router's role for non-actor receivers. /// Registry of external inboxes — replaces the Router's role for non-actor receivers.
@ -83,6 +108,50 @@ impl<M: Message> Inbox<M> {
} }
} }
/// Object-safe inner trait for sending type-erased messages.
pub(crate) trait ContextInner {
fn send_any(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error>;
fn spawn_any(&self, addr: ActorAddress, actor: Box<dyn AnyActor>) -> Result<(), Error>;
fn mailbox_waterlevel(&self) -> usize;
}
/// Actor syscall interface — passed to `ActorInterface::handle()`.
///
/// Wraps a `&dyn ContextInner` to solve the object-safety problem while
/// providing a typed public API.
pub struct Ctx<'a> {
inner: &'a dyn ContextInner,
self_addr: ActorAddress,
}
impl<'a> Ctx<'a> {
pub(crate) fn new(inner: &'a dyn ContextInner, self_addr: ActorAddress) -> Self {
Self { inner, self_addr }
}
/// Returns the address of the actor currently being ticked.
pub fn self_addr(&self) -> ActorAddress {
self.self_addr
}
/// Send a typed message to an actor address.
pub fn send<M: Message>(&self, addr: ActorAddress, msg: M) -> Result<(), Error> {
self.inner.send_any(addr, Box::new(msg))
}
/// Spawn a new actor, returning its address.
pub fn spawn<A: ActorInterface>(&self, actor: A) -> Result<ActorAddress, Error> {
let addr = ActorAddress::new_random();
let waterlevel = self.inner.mailbox_waterlevel();
let actor = Actor::new(addr, Mailbox::new(waterlevel), actor);
let boxed: Box<dyn AnyActor> = Box::new(actor);
self.inner.spawn_any(addr, boxed)?;
Ok(addr)
}
}
// ─── Runtime ───────────────────────────────────────────────────────────────── // ─── Runtime ─────────────────────────────────────────────────────────────────
/// The `Runtime` struct is the primary gateway for interacting with the framework. /// The `Runtime` struct is the primary gateway for interacting with the framework.

View file

@ -1,19 +1,13 @@
pub mod mailbox;
pub(crate) mod pool;
use std::any::Any; use std::any::Any;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::{HashMap, VecDeque};
use crate::actor::{ActorAddress, AnyActor}; use crate::actor::{ActorAddress, AnyActor, Message};
use crate::address_map::{AddressMap, WorkerId}; use crate::address_map::{AddressMap, Placement, WorkerId};
use crate::channel::{Receiver, Sender}; use crate::channel::{Receiver, Sender};
use crate::config::RuntimeConfig; use crate::config::RuntimeConfig;
use crate::context::ContextInner; use crate::runtime::{ContextInner, Envelope, InboxRegistry};
use crate::envelope::Envelope;
use crate::placement::Placement;
use crate::runtime::InboxRegistry;
use crate::Error; use crate::Error;
use pool::ActorPool;
/// Shared state passed to tick_once — single thin pointer avoids register spill. /// Shared state passed to tick_once — single thin pointer avoids register spill.
pub(crate) struct TickContext<'a> { pub(crate) struct TickContext<'a> {
@ -146,3 +140,93 @@ impl ContextInner for WorkerContext<'_> {
self.config.mailbox_waterlevel self.config.mailbox_waterlevel
} }
} }
/// Per-worker actor storage.
pub(crate) struct ActorPool {
actors: HashMap<ActorAddress, Box<dyn AnyActor>>,
}
impl ActorPool {
pub fn new() -> Self {
Self {
actors: HashMap::new(),
}
}
pub fn insert(&mut self, addr: ActorAddress, actor: Box<dyn AnyActor>) {
self.actors.insert(addr, actor);
}
pub fn remove(&mut self, addr: &ActorAddress) -> Option<Box<dyn AnyActor>> {
self.actors.remove(addr)
}
/// Deliver a type-erased message to the actor at `addr`.
/// Returns `true` if the actor was found and the message type matched.
pub fn deliver(&mut self, addr: &ActorAddress, msg: Box<dyn Any + Send>) -> bool {
if let Some(actor) = self.actors.get_mut(addr) {
actor.deliver(msg)
} else {
false
}
}
/// Tick all actors in the pool. Returns `true` if any actor processed messages.
pub fn tick_all(&mut self, inner: &dyn ContextInner) -> bool {
let mut did_work = false;
for actor in self.actors.values_mut() {
if actor.tick(inner) {
did_work = true;
}
}
did_work
}
pub fn len(&self) -> usize {
self.actors.len()
}
}
pub struct Mailbox<M: Message> {
queue: VecDeque<M>,
waterlevel: usize,
}
impl<M: Message> Mailbox<M> {
pub fn new(waterlevel: usize) -> Self {
Self {
queue: VecDeque::new(),
waterlevel,
}
}
pub fn push(&mut self, msg: M) {
self.queue.push_back(msg);
}
pub fn pop(&mut self) -> Option<M> {
self.queue.pop_front()
}
pub fn len(&self) -> usize {
self.queue.len()
}
pub fn is_empty(&self) -> bool {
self.queue.is_empty()
}
/// How many messages to process this tick:
/// - `len < waterlevel` → process all (`len`)
/// - `len >= waterlevel` → process half (`len >> 1`)
pub fn drain_count(&self) -> usize {
let len = self.queue.len();
if len < self.waterlevel {
len
} else {
len >> 1
}
}
}

View file

@ -1,45 +0,0 @@
use std::collections::VecDeque;
use crate::actor::Message;
pub struct Mailbox<M: Message> {
queue: VecDeque<M>,
waterlevel: usize,
}
impl<M: Message> Mailbox<M> {
pub fn new(waterlevel: usize) -> Self {
Self {
queue: VecDeque::new(),
waterlevel,
}
}
pub fn push(&mut self, msg: M) {
self.queue.push_back(msg);
}
pub fn pop(&mut self) -> Option<M> {
self.queue.pop_front()
}
pub fn len(&self) -> usize {
self.queue.len()
}
pub fn is_empty(&self) -> bool {
self.queue.is_empty()
}
/// How many messages to process this tick:
/// - `len < waterlevel` → process all (`len`)
/// - `len >= waterlevel` → process half (`len >> 1`)
pub fn drain_count(&self) -> usize {
let len = self.queue.len();
if len < self.waterlevel {
len
} else {
len >> 1
}
}
}

View file

@ -1,51 +0,0 @@
use std::any::Any;
use std::collections::HashMap;
use crate::actor::{ActorAddress, AnyActor};
use crate::context::ContextInner;
/// Per-worker actor storage.
pub(crate) struct ActorPool {
actors: HashMap<ActorAddress, Box<dyn AnyActor>>,
}
impl ActorPool {
pub fn new() -> Self {
Self {
actors: HashMap::new(),
}
}
pub fn insert(&mut self, addr: ActorAddress, actor: Box<dyn AnyActor>) {
self.actors.insert(addr, actor);
}
pub fn remove(&mut self, addr: &ActorAddress) -> Option<Box<dyn AnyActor>> {
self.actors.remove(addr)
}
/// Deliver a type-erased message to the actor at `addr`.
/// Returns `true` if the actor was found and the message type matched.
pub fn deliver(&mut self, addr: &ActorAddress, msg: Box<dyn Any + Send>) -> bool {
if let Some(actor) = self.actors.get_mut(addr) {
actor.deliver(msg)
} else {
false
}
}
/// Tick all actors in the pool. Returns `true` if any actor processed messages.
pub fn tick_all(&mut self, inner: &dyn ContextInner) -> bool {
let mut did_work = false;
for actor in self.actors.values_mut() {
if actor.tick(inner) {
did_work = true;
}
}
did_work
}
pub fn len(&self) -> usize {
self.actors.len()
}
}

View file

@ -1,4 +1,4 @@
use swactor::worker::mailbox::Mailbox; use swactor::worker::Mailbox;
// ── Basic operations ── // ── Basic operations ──

View file

@ -1,4 +1,4 @@
use swactor::{Ctx, actor::{ActorAddress, ActorInterface}, runtime::{Inbox, Runtime, RuntimeConfig}}; use swactor::{actor::{ActorAddress, ActorInterface}, runtime::{Ctx, Inbox, Runtime, RuntimeConfig}};
#[derive(Clone)] #[derive(Clone)]
struct PingMessage { struct PingMessage {

View file

@ -165,7 +165,7 @@ impl Stress {
} }
// Test actors used across stress tests // Test actors used across stress tests
use swactor::{Ctx, actor::ActorInterface}; use swactor::{actor::ActorInterface, runtime::Ctx};
/// An actor that just absorbs messages /// An actor that just absorbs messages
pub struct BlackHole; pub struct BlackHole;