2026-02-06 11:25:37 +00:00
|
|
|
use std::any::Any;
|
|
|
|
|
use std::cell::RefCell;
|
|
|
|
|
use std::collections::{HashMap, VecDeque};
|
2026-02-07 16:51:40 +00:00
|
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
2026-02-06 14:45:19 +00:00
|
|
|
use std::sync::Arc;
|
2026-02-06 11:25:37 +00:00
|
|
|
use std::thread;
|
2026-02-09 09:04:57 +00:00
|
|
|
use std::time::Instant;
|
2026-02-06 11:25:37 +00:00
|
|
|
|
2026-02-07 16:51:40 +00:00
|
|
|
use crate::actor::{ActorAddress, AnyActor, ContextInner, Ctx};
|
|
|
|
|
use crate::channel::Receiver;
|
|
|
|
|
use crate::delivery::{Envelope, TickContext, WorkerId};
|
2026-02-11 15:23:26 +00:00
|
|
|
use crate::stats::{ActorSnapshot, TickTiming, WorkerStats};
|
2026-02-06 11:25:37 +00:00
|
|
|
use crate::Error;
|
|
|
|
|
|
|
|
|
|
/// A worker owns a set of actors and runs them in a loop.
|
|
|
|
|
pub(crate) struct Worker {
|
2026-02-08 16:18:39 +00:00
|
|
|
pub(crate) id: WorkerId,
|
2026-02-07 17:39:02 +00:00
|
|
|
pub(crate) pool: ActorPool,
|
2026-02-06 11:25:37 +00:00
|
|
|
transfer_rx: Receiver<Envelope>,
|
|
|
|
|
spawn_rx: Receiver<(ActorAddress, Box<dyn AnyActor>)>,
|
2026-02-06 14:45:19 +00:00
|
|
|
stats: Arc<WorkerStats>,
|
2026-02-11 15:23:26 +00:00
|
|
|
/// Reusable scratch buffer for building per-actor snapshots.
|
|
|
|
|
snapshot_buf: Vec<ActorSnapshot>,
|
2026-02-06 11:25:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Worker {
|
2026-02-06 14:45:19 +00:00
|
|
|
pub(crate) fn new(
|
2026-02-06 11:25:37 +00:00
|
|
|
id: WorkerId,
|
|
|
|
|
transfer_rx: Receiver<Envelope>,
|
|
|
|
|
spawn_rx: Receiver<(ActorAddress, Box<dyn AnyActor>)>,
|
2026-02-06 14:45:19 +00:00
|
|
|
stats: Arc<WorkerStats>,
|
2026-02-06 11:25:37 +00:00
|
|
|
) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
id,
|
|
|
|
|
pool: ActorPool::new(),
|
|
|
|
|
transfer_rx,
|
|
|
|
|
spawn_rx,
|
2026-02-06 14:45:19 +00:00
|
|
|
stats,
|
2026-02-11 15:23:26 +00:00
|
|
|
snapshot_buf: Vec::new(),
|
2026-02-06 11:25:37 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Run one iteration of the worker loop. Returns `true` if any work was done.
|
2026-02-06 14:45:19 +00:00
|
|
|
pub(crate) fn tick_once(&mut self, tc: &TickContext) -> bool {
|
2026-02-09 09:04:57 +00:00
|
|
|
#[cfg(feature = "tracing")]
|
|
|
|
|
let _span = tracing::trace_span!("worker.tick", worker_id = self.id.0).entered();
|
|
|
|
|
|
2026-02-06 11:25:37 +00:00
|
|
|
let mut did_work = false;
|
2026-02-09 09:04:57 +00:00
|
|
|
let t0 = Instant::now();
|
2026-02-06 11:25:37 +00:00
|
|
|
|
|
|
|
|
// 1. Drain spawn queue → add actors to pool
|
2026-02-09 09:04:57 +00:00
|
|
|
#[cfg(feature = "tracing")]
|
|
|
|
|
let mut spawn_count: usize = 0;
|
2026-02-06 11:25:37 +00:00
|
|
|
while let Some((addr, actor)) = self.spawn_rx.try_recv() {
|
|
|
|
|
self.pool.insert(addr, actor);
|
2026-02-09 09:04:57 +00:00
|
|
|
#[cfg(feature = "tracing")]
|
|
|
|
|
{ spawn_count += 1; }
|
2026-02-06 11:25:37 +00:00
|
|
|
did_work = true;
|
|
|
|
|
}
|
2026-02-09 09:04:57 +00:00
|
|
|
#[cfg(feature = "tracing")]
|
|
|
|
|
if spawn_count > 0 {
|
|
|
|
|
tracing::debug!(worker_id = self.id.0, count = spawn_count, "worker.spawns_drained");
|
|
|
|
|
}
|
|
|
|
|
let t1 = Instant::now();
|
2026-02-06 11:25:37 +00:00
|
|
|
|
|
|
|
|
// 2. Drain transfer queue → deliver envelopes to actors
|
|
|
|
|
while let Some(envelope) = self.transfer_rx.try_recv() {
|
|
|
|
|
let dest = envelope.dest();
|
|
|
|
|
let payload = envelope.into_payload();
|
|
|
|
|
self.pool.deliver(&dest, payload);
|
|
|
|
|
did_work = true;
|
|
|
|
|
}
|
2026-02-09 09:04:57 +00:00
|
|
|
let t2 = Instant::now();
|
2026-02-06 11:25:37 +00:00
|
|
|
|
|
|
|
|
// 3. Tick all actors with WorkerContext
|
|
|
|
|
let pending_local: RefCell<Vec<(ActorAddress, Box<dyn Any + Send>)>> =
|
|
|
|
|
RefCell::new(Vec::new());
|
|
|
|
|
|
2026-02-06 14:45:19 +00:00
|
|
|
let processed;
|
2026-02-06 11:25:37 +00:00
|
|
|
{
|
|
|
|
|
let worker_ctx = WorkerContext {
|
|
|
|
|
worker_id: self.id,
|
2026-02-07 10:36:45 +00:00
|
|
|
tc,
|
2026-02-06 11:25:37 +00:00
|
|
|
pending_local: &pending_local,
|
2026-02-09 09:04:57 +00:00
|
|
|
stats: &self.stats,
|
2026-02-06 11:25:37 +00:00
|
|
|
};
|
2026-02-09 09:04:57 +00:00
|
|
|
processed = self.pool.tick_all(&worker_ctx, &self.stats);
|
2026-02-06 14:45:19 +00:00
|
|
|
if processed > 0 {
|
2026-02-06 11:25:37 +00:00
|
|
|
did_work = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-09 09:04:57 +00:00
|
|
|
let t3 = Instant::now();
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "tracing")]
|
|
|
|
|
if processed > 0 {
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
worker_id = self.id.0,
|
|
|
|
|
messages_processed = processed,
|
|
|
|
|
"worker.tick_all"
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-02-06 11:25:37 +00:00
|
|
|
|
2026-02-09 07:24:16 +00:00
|
|
|
// 4. Drain spawn queue again — actors spawned during step 3
|
|
|
|
|
// must be in the pool before pending_local delivery.
|
|
|
|
|
while let Some((addr, actor)) = self.spawn_rx.try_recv() {
|
|
|
|
|
self.pool.insert(addr, actor);
|
|
|
|
|
did_work = true;
|
|
|
|
|
}
|
2026-02-09 09:04:57 +00:00
|
|
|
let t4 = Instant::now();
|
2026-02-09 07:24:16 +00:00
|
|
|
|
|
|
|
|
// 5. Drain pending_local buffer → deliver to local actors
|
2026-02-06 11:25:37 +00:00
|
|
|
let pending = pending_local.into_inner();
|
|
|
|
|
if !pending.is_empty() {
|
|
|
|
|
did_work = true;
|
|
|
|
|
}
|
|
|
|
|
for (addr, msg) in pending {
|
|
|
|
|
self.pool.deliver(&addr, msg);
|
|
|
|
|
}
|
2026-02-09 09:04:57 +00:00
|
|
|
let t5 = Instant::now();
|
2026-02-06 11:25:37 +00:00
|
|
|
|
2026-02-10 07:35:58 +00:00
|
|
|
// 6. Publish stats (skip entirely when idle to avoid allocation + mutex)
|
|
|
|
|
if did_work {
|
|
|
|
|
self.stats.num_actors.store(self.pool.len(), Ordering::Relaxed);
|
|
|
|
|
self.stats.total_mailbox_depth.store(self.pool.total_mailbox_depth(), Ordering::Relaxed);
|
|
|
|
|
self.stats.messages_processed.fetch_add(processed as u64, Ordering::Relaxed);
|
2026-02-06 14:45:19 +00:00
|
|
|
|
2026-02-11 15:23:26 +00:00
|
|
|
if let Some(hook) = tc.stats_hook {
|
|
|
|
|
self.pool.mailbox_depths_into(&mut self.snapshot_buf);
|
|
|
|
|
hook.on_tick(self.id.0, &self.snapshot_buf);
|
|
|
|
|
}
|
2026-02-09 09:04:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let t6 = Instant::now();
|
|
|
|
|
|
|
|
|
|
// Record tick timing
|
|
|
|
|
let timing = TickTiming {
|
|
|
|
|
phase_us: [
|
|
|
|
|
t1.duration_since(t0).as_micros() as u64,
|
|
|
|
|
t2.duration_since(t1).as_micros() as u64,
|
|
|
|
|
t3.duration_since(t2).as_micros() as u64,
|
|
|
|
|
t4.duration_since(t3).as_micros() as u64,
|
|
|
|
|
t5.duration_since(t4).as_micros() as u64,
|
|
|
|
|
t6.duration_since(t5).as_micros() as u64,
|
|
|
|
|
],
|
|
|
|
|
messages_processed: processed,
|
|
|
|
|
did_work,
|
|
|
|
|
};
|
|
|
|
|
self.stats.push_tick_timing(timing);
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "tracing")]
|
|
|
|
|
if did_work {
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
worker_id = self.id.0,
|
|
|
|
|
num_actors = self.pool.len(),
|
|
|
|
|
mailbox_depth = self.pool.total_mailbox_depth(),
|
|
|
|
|
messages_processed = processed,
|
|
|
|
|
"worker.stats"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 11:25:37 +00:00
|
|
|
did_work
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 10:36:45 +00:00
|
|
|
pub(crate) fn run(&mut self, tc: &TickContext, is_running: &AtomicBool) {
|
2026-02-09 09:04:57 +00:00
|
|
|
#[cfg(feature = "tracing")]
|
|
|
|
|
let _span = tracing::info_span!("worker.run", worker_id = self.id.0).entered();
|
|
|
|
|
|
2026-02-07 10:36:45 +00:00
|
|
|
let backoff = &tc.config.backoff_policy;
|
2026-02-06 11:25:37 +00:00
|
|
|
let mut idle_count: u32 = 0;
|
|
|
|
|
while is_running.load(Ordering::Acquire) {
|
|
|
|
|
let did_work = self.tick_once(tc);
|
|
|
|
|
if did_work {
|
|
|
|
|
idle_count = 0;
|
|
|
|
|
} else {
|
|
|
|
|
idle_count = idle_count.saturating_add(1);
|
|
|
|
|
if idle_count < backoff.spin_threshold {
|
|
|
|
|
// Hot spin
|
|
|
|
|
} else if idle_count < backoff.yield_threshold {
|
|
|
|
|
thread::yield_now();
|
|
|
|
|
} else {
|
|
|
|
|
let micros = std::cmp::min(
|
|
|
|
|
(idle_count - backoff.yield_threshold) as u64 * backoff.sleep_increment_us,
|
|
|
|
|
backoff.sleep_max_us,
|
|
|
|
|
);
|
|
|
|
|
thread::sleep(std::time::Duration::from_micros(micros));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The `ContextInner` impl for worker threads.
|
|
|
|
|
///
|
|
|
|
|
/// Same-worker sends are buffered in `pending_local` (delivered after current tick round).
|
|
|
|
|
/// Cross-worker sends go through the transfer queue.
|
|
|
|
|
struct WorkerContext<'a> {
|
|
|
|
|
worker_id: WorkerId,
|
2026-02-07 10:36:45 +00:00
|
|
|
tc: &'a TickContext<'a>,
|
2026-02-06 11:25:37 +00:00
|
|
|
pending_local: &'a RefCell<Vec<(ActorAddress, Box<dyn Any + Send>)>>,
|
2026-02-09 09:04:57 +00:00
|
|
|
stats: &'a WorkerStats,
|
2026-02-06 11:25:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ContextInner for WorkerContext<'_> {
|
|
|
|
|
fn send_any(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error> {
|
2026-02-07 10:36:45 +00:00
|
|
|
match self.tc.address_map.lookup(&addr) {
|
2026-02-06 11:25:37 +00:00
|
|
|
Some(wid) if wid == self.worker_id => {
|
2026-02-09 09:04:57 +00:00
|
|
|
self.stats.local_sends.fetch_add(1, Ordering::Relaxed);
|
2026-02-06 11:25:37 +00:00
|
|
|
self.pending_local.borrow_mut().push((addr, msg));
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
Some(wid) => {
|
2026-02-09 09:04:57 +00:00
|
|
|
self.stats.cross_sends.fetch_add(1, Ordering::Relaxed);
|
2026-02-10 07:35:58 +00:00
|
|
|
self.tc.transfer_txs[wid.as_usize()].send(Envelope::new(addr, msg));
|
2026-02-06 11:25:37 +00:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
None => {
|
2026-02-09 09:04:57 +00:00
|
|
|
self.stats.inbox_sends.fetch_add(1, Ordering::Relaxed);
|
2026-02-09 19:05:37 +00:00
|
|
|
self.tc.route_nonlocal(addr, msg)
|
2026-02-06 11:25:37 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 07:35:58 +00:00
|
|
|
fn spawn_any(&self, addr: ActorAddress, actor: Box<dyn AnyActor>) {
|
2026-02-07 10:36:45 +00:00
|
|
|
let worker_id = self.tc.placement.next_worker();
|
|
|
|
|
self.tc.address_map.insert(addr, worker_id);
|
|
|
|
|
self.tc.spawn_txs[worker_id.as_usize()]
|
2026-02-10 07:35:58 +00:00
|
|
|
.send((addr, actor))
|
2026-02-06 11:25:37 +00:00
|
|
|
}
|
2026-02-06 14:45:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct ActorSlot {
|
|
|
|
|
mailbox: VecDeque<Box<dyn Any + Send>>,
|
|
|
|
|
actor: Box<dyn AnyActor>,
|
2026-02-10 07:35:58 +00:00
|
|
|
poisoned: bool,
|
2026-02-11 15:23:26 +00:00
|
|
|
last_msg_type: Option<&'static str>,
|
|
|
|
|
messages_processed: u64,
|
2026-02-06 14:45:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Per-worker actor storage. Owns per-actor mailboxes.
|
2026-02-06 11:25:37 +00:00
|
|
|
pub(crate) struct ActorPool {
|
2026-02-06 14:45:19 +00:00
|
|
|
actors: HashMap<ActorAddress, ActorSlot>,
|
2026-02-06 11:25:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ActorPool {
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
actors: HashMap::new(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn insert(&mut self, addr: ActorAddress, actor: Box<dyn AnyActor>) {
|
2026-02-06 14:45:19 +00:00
|
|
|
self.actors.insert(addr, ActorSlot {
|
2026-02-10 07:35:58 +00:00
|
|
|
mailbox: VecDeque::with_capacity(16),
|
2026-02-06 14:45:19 +00:00
|
|
|
actor,
|
2026-02-10 07:35:58 +00:00
|
|
|
poisoned: false,
|
2026-02-11 15:23:26 +00:00
|
|
|
last_msg_type: None,
|
|
|
|
|
messages_processed: 0,
|
2026-02-06 14:45:19 +00:00
|
|
|
});
|
2026-02-06 11:25:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Deliver a type-erased message to the actor at `addr`.
|
2026-02-06 14:45:19 +00:00
|
|
|
/// Returns `true` if the actor exists (message is queued; type check deferred to tick).
|
2026-02-06 11:25:37 +00:00
|
|
|
pub fn deliver(&mut self, addr: &ActorAddress, msg: Box<dyn Any + Send>) -> bool {
|
2026-02-06 14:45:19 +00:00
|
|
|
if let Some(slot) = self.actors.get_mut(addr) {
|
|
|
|
|
slot.mailbox.push_back(msg);
|
|
|
|
|
true
|
2026-02-06 11:25:37 +00:00
|
|
|
} else {
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 14:45:19 +00:00
|
|
|
/// Tick all actors in the pool. Returns the number of messages processed.
|
2026-02-09 09:04:57 +00:00
|
|
|
pub fn tick_all(&mut self, inner: &dyn ContextInner, stats: &WorkerStats) -> usize {
|
2026-02-06 14:45:19 +00:00
|
|
|
let mut count = 0;
|
|
|
|
|
for (&addr, slot) in self.actors.iter_mut() {
|
2026-02-10 07:35:58 +00:00
|
|
|
if slot.poisoned {
|
|
|
|
|
// Discard all messages for poisoned actors
|
|
|
|
|
slot.mailbox.clear();
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-02-09 07:24:16 +00:00
|
|
|
let ctx = Ctx::new(inner, addr);
|
|
|
|
|
while let Some(msg) = slot.mailbox.pop_front() {
|
|
|
|
|
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
2026-02-10 07:35:58 +00:00
|
|
|
slot.actor.handle_any(&ctx, msg)
|
2026-02-09 07:24:16 +00:00
|
|
|
}));
|
2026-02-10 07:35:58 +00:00
|
|
|
match result {
|
2026-02-11 15:23:26 +00:00
|
|
|
Ok(None) => {
|
2026-02-10 07:35:58 +00:00
|
|
|
stats.type_mismatches.fetch_add(1, Ordering::Relaxed);
|
|
|
|
|
}
|
|
|
|
|
Err(_) => {
|
|
|
|
|
stats.panics.fetch_add(1, Ordering::Relaxed);
|
|
|
|
|
eprintln!("swactor: actor {addr} panicked — poisoned, future messages will be discarded");
|
|
|
|
|
#[cfg(feature = "tracing")]
|
|
|
|
|
tracing::error!(actor_addr = %addr, "actor.panicked");
|
|
|
|
|
slot.poisoned = true;
|
|
|
|
|
slot.mailbox.clear();
|
|
|
|
|
break;
|
|
|
|
|
}
|
2026-02-11 15:23:26 +00:00
|
|
|
Ok(Some(type_name)) => {
|
|
|
|
|
slot.last_msg_type = Some(type_name);
|
|
|
|
|
slot.messages_processed += 1;
|
|
|
|
|
}
|
2026-02-06 14:45:19 +00:00
|
|
|
}
|
2026-02-09 07:24:16 +00:00
|
|
|
count += 1;
|
2026-02-06 11:25:37 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-06 14:45:19 +00:00
|
|
|
count
|
2026-02-06 11:25:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn len(&self) -> usize {
|
|
|
|
|
self.actors.len()
|
|
|
|
|
}
|
2026-02-06 14:45:19 +00:00
|
|
|
|
|
|
|
|
pub fn total_mailbox_depth(&self) -> usize {
|
|
|
|
|
self.actors.values().map(|slot| slot.mailbox.len()).sum()
|
|
|
|
|
}
|
2026-02-06 11:25:37 +00:00
|
|
|
|
2026-02-11 15:23:26 +00:00
|
|
|
/// Fill `out` with per-actor snapshots, reusing the existing allocation.
|
|
|
|
|
pub fn mailbox_depths_into(&self, out: &mut Vec<ActorSnapshot>) {
|
2026-02-10 07:35:58 +00:00
|
|
|
out.clear();
|
2026-02-11 15:23:26 +00:00
|
|
|
out.extend(self.actors.iter().map(|(&addr, slot)| {
|
|
|
|
|
ActorSnapshot {
|
|
|
|
|
address: addr,
|
|
|
|
|
mailbox_depth: slot.mailbox.len(),
|
|
|
|
|
last_msg_type: slot.last_msg_type,
|
|
|
|
|
messages_processed: slot.messages_processed,
|
|
|
|
|
poisoned: slot.poisoned,
|
|
|
|
|
}
|
|
|
|
|
}));
|
2026-02-09 09:04:57 +00:00
|
|
|
}
|
|
|
|
|
}
|