2026-06-23 15:42:28 +00:00
|
|
|
use crate::Instant;
|
2026-07-08 15:23:03 +00:00
|
|
|
use std::any::{Any, TypeId};
|
2026-02-06 11:25:37 +00:00
|
|
|
use std::cell::RefCell;
|
2026-07-08 15:23:03 +00:00
|
|
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
2026-02-13 15:00:53 +00:00
|
|
|
use std::sync::{Arc, OnceLock};
|
2026-06-23 15:42:28 +00:00
|
|
|
use std::thread::Thread;
|
2026-02-13 13:27:34 +00:00
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
use std::thread::{self, JoinHandle};
|
2026-01-25 13:38:34 +00:00
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
use crate::actor::{
|
2026-07-08 15:23:03 +00:00
|
|
|
Actor, ActorAddress, ActorInterface, ActorTypeMetadata, AnyActor, Environment, ExitValue,
|
|
|
|
|
Message, ResumeSignal, SpawnRequest, StopSignal, StopWithSignal, SystemInfo,
|
|
|
|
|
};
|
|
|
|
|
use crate::admin::{
|
|
|
|
|
ActorStateSnapshot, Admin, AdminCommand, AdminError, AdminResult, GetActorStateResponse,
|
|
|
|
|
InspectActorResponse, ListActorsAccumulator, ListActorsResponse, OperationResult, RuntimeAdmin,
|
2026-06-23 15:42:28 +00:00
|
|
|
};
|
2026-02-06 11:25:37 +00:00
|
|
|
use crate::channel::{Receiver, Sender};
|
|
|
|
|
// Re-export config types so existing code using `runtime::RuntimeConfig` still works
|
2026-03-28 05:08:58 +00:00
|
|
|
pub use crate::config::RuntimeConfig;
|
2026-02-07 16:51:40 +00:00
|
|
|
use crate::delivery::{AddressMap, Envelope, InboxRegistry, Placement, TickContext, WorkerId};
|
2026-02-13 07:11:24 +00:00
|
|
|
use crate::extension::RuntimeExtension;
|
2026-02-11 15:23:26 +00:00
|
|
|
use crate::stats::{StatsHook, WorkerStats};
|
2026-02-07 16:51:40 +00:00
|
|
|
// Re-export stats types so existing code using `runtime::*` still works
|
2026-06-23 15:42:28 +00:00
|
|
|
use crate::Error;
|
2026-02-07 16:51:40 +00:00
|
|
|
pub use crate::stats::{RuntimeStats, WorkerInfo};
|
2026-02-13 15:00:53 +00:00
|
|
|
use crate::worker::Worker;
|
2026-02-06 11:25:37 +00:00
|
|
|
|
2026-01-25 13:38:34 +00:00
|
|
|
/// Generic message inbox for receiving messages outside of the runtime.
|
|
|
|
|
pub struct Inbox<M: Message> {
|
|
|
|
|
addr: ActorAddress,
|
|
|
|
|
inner: Receiver<M>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<M: Message> Inbox<M> {
|
|
|
|
|
pub fn addr(&self) -> &ActorAddress {
|
|
|
|
|
&self.addr
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn try_recv(&self) -> Option<M> {
|
|
|
|
|
self.inner.try_recv()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-13 07:11:24 +00:00
|
|
|
/// Pending ask response — wraps an inbox with convenience recv methods.
|
|
|
|
|
///
|
|
|
|
|
/// Created by [`Runtime::ask`]. Provides `try_recv()` for polling and
|
|
|
|
|
/// `recv_ticking()` for automatic tick-until-response.
|
|
|
|
|
pub struct Ask<R: Message> {
|
|
|
|
|
inbox: Inbox<R>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<R: Message> Ask<R> {
|
|
|
|
|
/// Try to receive the response without ticking.
|
|
|
|
|
pub fn try_recv(&self) -> Option<R> {
|
|
|
|
|
self.inbox.try_recv()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Tick the runtime until a response arrives or `max_ticks` is exhausted.
|
|
|
|
|
///
|
|
|
|
|
/// Only valid for single-threaded runtimes (panics if `num_threads >= 2`).
|
|
|
|
|
pub fn recv_ticking(&self, rt: &Runtime, max_ticks: usize) -> Result<R, Error> {
|
|
|
|
|
for _ in 0..max_ticks {
|
|
|
|
|
rt.tick();
|
|
|
|
|
if let Some(resp) = self.inbox.try_recv() {
|
|
|
|
|
return Ok(resp);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(Error::from("ask timeout: no response within max_ticks"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get the reply address (for manual message construction).
|
|
|
|
|
pub fn reply_addr(&self) -> &ActorAddress {
|
|
|
|
|
self.inbox.addr()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 13:38:34 +00:00
|
|
|
/// Handle for dealing with a runtime that has started via the `Runtime::run()` method.
|
2026-02-13 13:27:34 +00:00
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
2026-01-25 13:38:34 +00:00
|
|
|
pub struct RuntimeHandle {
|
|
|
|
|
pub runtime: Arc<Runtime>,
|
|
|
|
|
threads: Vec<JoinHandle<()>>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-13 13:27:34 +00:00
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
2026-01-25 13:38:34 +00:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 16:51:40 +00:00
|
|
|
// Re-export Ctx for backwards compatibility
|
|
|
|
|
use crate::actor::ContextInner;
|
2026-06-23 15:42:28 +00:00
|
|
|
pub use crate::actor::Ctx;
|
2026-02-06 11:25:37 +00:00
|
|
|
|
|
|
|
|
// ─── Runtime ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// The `Runtime` struct is the primary gateway for interacting with the framework.
|
|
|
|
|
pub struct Runtime {
|
|
|
|
|
config: RuntimeConfig,
|
|
|
|
|
address_map: Arc<AddressMap>,
|
|
|
|
|
inbox_registry: Arc<InboxRegistry>,
|
2026-02-13 07:11:24 +00:00
|
|
|
extension: Option<Arc<dyn RuntimeExtension>>,
|
2026-02-06 11:25:37 +00:00
|
|
|
transfer_txs: Vec<Sender<Envelope>>,
|
2026-02-20 17:34:43 +00:00
|
|
|
spawn_txs: Vec<Sender<SpawnRequest>>,
|
2026-07-08 15:23:03 +00:00
|
|
|
admin_txs: Vec<Sender<AdminCommand>>,
|
2026-02-06 11:25:37 +00:00
|
|
|
placement: Placement,
|
|
|
|
|
is_running: AtomicBool,
|
2026-02-06 14:45:19 +00:00
|
|
|
worker_stats: Vec<Arc<WorkerStats>>,
|
2026-02-11 15:23:26 +00:00
|
|
|
stats_hook: Option<Arc<dyn StatsHook>>,
|
2026-06-06 17:53:25 +00:00
|
|
|
process_output_observer: OnceLock<Arc<dyn crate::process_observer::ProcessOutputObserver>>,
|
2026-02-09 07:24:16 +00:00
|
|
|
/// Workers available for tick(). run() drains this and moves workers to threads.
|
|
|
|
|
tick_workers: RefCell<Vec<Worker>>,
|
2026-02-13 07:11:24 +00:00
|
|
|
/// Thread handles for waking parked workers. Set by workers on startup via OnceLock.
|
2026-02-23 04:44:46 +00:00
|
|
|
worker_threads: Arc<Vec<OnceLock<Thread>>>,
|
2026-02-11 15:23:26 +00:00
|
|
|
created_at: Instant,
|
2026-02-09 19:05:37 +00:00
|
|
|
#[cfg(feature = "transport")]
|
2026-06-09 09:29:07 +00:00
|
|
|
remote_sink: Option<Arc<dyn RemoteSink>>,
|
2026-02-06 11:25:37 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-09 07:24:16 +00:00
|
|
|
// Safety: RefCell<Vec<Worker>> is only accessed from the owning thread via tick().
|
|
|
|
|
// After run() the RefCell is empty and not accessed by worker threads.
|
2026-02-06 11:25:37 +00:00
|
|
|
unsafe impl Sync for Runtime {}
|
|
|
|
|
|
2026-06-09 09:29:07 +00:00
|
|
|
/// Core's only hook for delivering a message to a **non-local** address.
|
|
|
|
|
///
|
|
|
|
|
/// Implemented outside core (e.g. `swactor-transport`'s `CodecRemoteSink`),
|
|
|
|
|
/// which owns all codec/transport concerns. Core stays codec-free: it hands the
|
|
|
|
|
/// sink a type-erased message and an address, and nothing more. `Send + Sync`
|
|
|
|
|
/// because the sink is stored in an `Arc` and shared across worker threads.
|
|
|
|
|
#[cfg(feature = "transport")]
|
|
|
|
|
pub trait RemoteSink: Send + Sync {
|
|
|
|
|
fn send(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error>;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 15:23:26 +00:00
|
|
|
/// Globally unique identity of a swactor runtime instance.
|
|
|
|
|
/// Pure identity — no networking info. A runtime can exist on any device,
|
|
|
|
|
/// any protocol, or no network at all.
|
|
|
|
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
|
|
|
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
|
|
|
|
pub struct RuntimeAddress(pub [u8; 32]);
|
|
|
|
|
|
|
|
|
|
impl std::fmt::Display for RuntimeAddress {
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
for b in &self.0[..8] {
|
|
|
|
|
write!(f, "{:02x}", b)?;
|
|
|
|
|
}
|
|
|
|
|
write!(f, "\u{2026}")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl RuntimeAddress {
|
|
|
|
|
pub fn new_random() -> Self {
|
|
|
|
|
let mut bytes = [0u8; 32];
|
|
|
|
|
crate::get_random(&mut bytes);
|
|
|
|
|
Self(bytes)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 04:44:46 +00:00
|
|
|
/// A cloneable, `Send + Sync` handle for injecting messages into actor mailboxes
|
|
|
|
|
/// from any thread — including non-actor I/O threads.
|
|
|
|
|
///
|
|
|
|
|
/// Created via [`Runtime::create_sender`]. The primary use case is bridging
|
|
|
|
|
/// background I/O (e.g., pipe readers, network listeners) with the tick-based
|
|
|
|
|
/// actor system.
|
|
|
|
|
pub struct ExternalSender {
|
|
|
|
|
address_map: Arc<AddressMap>,
|
|
|
|
|
transfer_txs: Vec<Sender<Envelope>>,
|
|
|
|
|
worker_threads: Arc<Vec<OnceLock<Thread>>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Clone for ExternalSender {
|
|
|
|
|
fn clone(&self) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
address_map: self.address_map.clone(),
|
|
|
|
|
transfer_txs: self.transfer_txs.clone(),
|
|
|
|
|
worker_threads: self.worker_threads.clone(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Safety: All fields are Send+Sync (Arc<AddressMap> uses RwLock,
|
|
|
|
|
// Sender<Envelope> wraps Arc<HybridChannel>, Thread is Send+Sync).
|
|
|
|
|
unsafe impl Send for ExternalSender {}
|
|
|
|
|
unsafe impl Sync for ExternalSender {}
|
|
|
|
|
|
|
|
|
|
impl ExternalSender {
|
|
|
|
|
/// Send a typed message to an actor address, waking the owning worker thread.
|
|
|
|
|
///
|
2026-03-28 05:08:58 +00:00
|
|
|
/// Returns `Ok(())` if the message was accepted for routing. This does **not**
|
|
|
|
|
/// guarantee delivery — the recipient may stop before processing it. If
|
|
|
|
|
/// delivery confirmation is needed, implement an application-level ACK.
|
|
|
|
|
///
|
2026-02-23 04:44:46 +00:00
|
|
|
/// Returns `Err` if the address is not found in the runtime's address map.
|
|
|
|
|
pub fn send_to<M: Message>(&self, addr: ActorAddress, msg: M) -> Result<(), Error> {
|
|
|
|
|
match self.address_map.lookup(&addr) {
|
|
|
|
|
Some(wid) => {
|
2026-06-23 15:42:28 +00:00
|
|
|
self.transfer_txs[wid.as_usize()].send(Envelope::new(addr, Box::new(msg)));
|
2026-02-23 04:44:46 +00:00
|
|
|
notify_worker(&self.worker_threads, wid.as_usize());
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
None => Err(Error::from("Address not found")),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 13:38:34 +00:00
|
|
|
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 {
|
2026-02-06 11:25:37 +00:00
|
|
|
let num_workers = if config.num_threads < 2 {
|
|
|
|
|
1
|
2026-01-25 13:38:34 +00:00
|
|
|
} else {
|
2026-02-06 11:25:37 +00:00
|
|
|
config.num_threads
|
2026-01-25 13:38:34 +00:00
|
|
|
};
|
|
|
|
|
|
2026-02-06 11:25:37 +00:00
|
|
|
let address_map = Arc::new(AddressMap::with_capacity(config.max_actors));
|
|
|
|
|
let inbox_registry = Arc::new(InboxRegistry::new());
|
|
|
|
|
|
|
|
|
|
let mut transfer_txs = Vec::with_capacity(num_workers);
|
|
|
|
|
let mut spawn_txs = Vec::with_capacity(num_workers);
|
2026-07-08 15:23:03 +00:00
|
|
|
let mut admin_txs = Vec::with_capacity(num_workers);
|
2026-02-06 14:45:19 +00:00
|
|
|
let mut worker_stats = Vec::with_capacity(num_workers);
|
2026-02-06 11:25:37 +00:00
|
|
|
let mut workers = Vec::with_capacity(num_workers);
|
|
|
|
|
|
|
|
|
|
for i in 0..num_workers {
|
2026-02-10 07:35:58 +00:00
|
|
|
let transfer_rx = Receiver::<Envelope>::new(config.channel_buffer_size);
|
2026-02-06 11:25:37 +00:00
|
|
|
let transfer_tx = transfer_rx.new_sender();
|
|
|
|
|
transfer_txs.push(transfer_tx);
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
let spawn_rx = Receiver::<SpawnRequest>::new(config.max_actors);
|
2026-02-06 11:25:37 +00:00
|
|
|
let spawn_tx = spawn_rx.new_sender();
|
|
|
|
|
spawn_txs.push(spawn_tx);
|
|
|
|
|
|
2026-07-08 15:23:03 +00:00
|
|
|
let admin_rx = Receiver::<AdminCommand>::new(config.channel_buffer_size);
|
|
|
|
|
let admin_tx = admin_rx.new_sender();
|
|
|
|
|
admin_txs.push(admin_tx);
|
|
|
|
|
|
2026-02-06 14:45:19 +00:00
|
|
|
let stats = Arc::new(WorkerStats::new());
|
|
|
|
|
worker_stats.push(stats.clone());
|
2026-07-08 15:23:03 +00:00
|
|
|
workers.push(Worker::new(
|
|
|
|
|
WorkerId(i),
|
|
|
|
|
transfer_rx,
|
|
|
|
|
spawn_rx,
|
|
|
|
|
admin_rx,
|
|
|
|
|
stats,
|
|
|
|
|
));
|
2026-02-06 11:25:37 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-13 07:11:24 +00:00
|
|
|
let placement = Placement::new(num_workers, worker_stats.clone());
|
|
|
|
|
|
2026-02-23 04:44:46 +00:00
|
|
|
let worker_threads: Arc<Vec<OnceLock<Thread>>> =
|
|
|
|
|
Arc::new((0..num_workers).map(|_| OnceLock::new()).collect());
|
2026-02-13 07:11:24 +00:00
|
|
|
|
2026-02-09 09:04:57 +00:00
|
|
|
let rt = Self {
|
2026-02-09 07:24:16 +00:00
|
|
|
config,
|
|
|
|
|
address_map,
|
|
|
|
|
inbox_registry,
|
2026-02-13 07:11:24 +00:00
|
|
|
extension: None,
|
2026-02-09 07:24:16 +00:00
|
|
|
transfer_txs,
|
|
|
|
|
spawn_txs,
|
2026-07-08 15:23:03 +00:00
|
|
|
admin_txs,
|
2026-02-09 07:24:16 +00:00
|
|
|
placement,
|
|
|
|
|
is_running: AtomicBool::new(false),
|
|
|
|
|
worker_stats,
|
2026-02-11 15:23:26 +00:00
|
|
|
stats_hook: None,
|
2026-06-06 17:53:25 +00:00
|
|
|
process_output_observer: OnceLock::new(),
|
2026-02-09 07:24:16 +00:00
|
|
|
tick_workers: RefCell::new(workers),
|
2026-02-13 07:11:24 +00:00
|
|
|
worker_threads,
|
2026-02-11 15:23:26 +00:00
|
|
|
created_at: Instant::now(),
|
2026-02-09 19:05:37 +00:00
|
|
|
#[cfg(feature = "transport")]
|
2026-06-09 09:29:07 +00:00
|
|
|
remote_sink: None,
|
2026-02-09 09:04:57 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "tracing")]
|
|
|
|
|
tracing::info!(
|
|
|
|
|
num_workers,
|
|
|
|
|
max_actors = rt.config.max_actors,
|
|
|
|
|
"runtime.created"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
rt
|
2026-01-25 13:38:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Spawn an actor, returns its address
|
|
|
|
|
pub fn spawn<A: ActorInterface>(&self, actor: A) -> Result<ActorAddress, Error> {
|
|
|
|
|
let addr = ActorAddress::new_random();
|
2026-02-06 11:25:37 +00:00
|
|
|
let worker_id = self.placement.next_worker();
|
|
|
|
|
self.address_map.insert(addr, worker_id);
|
2026-02-06 14:45:19 +00:00
|
|
|
let boxed: Box<dyn AnyActor> = Box::new(Actor::new(actor));
|
2026-06-23 15:42:28 +00:00
|
|
|
self.spawn_txs[worker_id.as_usize()].send(SpawnRequest {
|
|
|
|
|
addr,
|
|
|
|
|
actor: boxed,
|
|
|
|
|
parent: None,
|
|
|
|
|
env: Environment::new(),
|
|
|
|
|
});
|
2026-02-20 17:34:43 +00:00
|
|
|
|
|
|
|
|
#[cfg(feature = "tracing")]
|
|
|
|
|
tracing::info!(
|
|
|
|
|
actor_addr = %addr,
|
|
|
|
|
worker_id = worker_id.as_usize(),
|
|
|
|
|
"actor.spawned"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
Ok(addr)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Spawn an actor with a pre-built environment, returns its address.
|
2026-06-23 15:42:28 +00:00
|
|
|
pub fn spawn_with_env<A: ActorInterface>(
|
|
|
|
|
&self,
|
|
|
|
|
actor: A,
|
|
|
|
|
env: Environment,
|
|
|
|
|
) -> Result<ActorAddress, Error> {
|
2026-02-20 17:34:43 +00:00
|
|
|
let addr = ActorAddress::new_random();
|
|
|
|
|
let worker_id = self.placement.next_worker();
|
|
|
|
|
self.address_map.insert(addr, worker_id);
|
|
|
|
|
let boxed: Box<dyn AnyActor> = Box::new(Actor::new(actor));
|
2026-06-23 15:42:28 +00:00
|
|
|
self.spawn_txs[worker_id.as_usize()].send(SpawnRequest {
|
|
|
|
|
addr,
|
|
|
|
|
actor: boxed,
|
|
|
|
|
parent: None,
|
|
|
|
|
env,
|
|
|
|
|
});
|
2026-02-09 09:04:57 +00:00
|
|
|
|
|
|
|
|
#[cfg(feature = "tracing")]
|
|
|
|
|
tracing::info!(
|
|
|
|
|
actor_addr = %addr,
|
|
|
|
|
worker_id = worker_id.as_usize(),
|
|
|
|
|
"actor.spawned"
|
|
|
|
|
);
|
|
|
|
|
|
2026-01-25 13:38:34 +00:00
|
|
|
Ok(addr)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-13 07:11:24 +00:00
|
|
|
/// Install a runtime extension. Extensions provide higher-level features
|
|
|
|
|
/// (naming, monitoring, groups) via lifecycle hooks.
|
|
|
|
|
///
|
|
|
|
|
/// Must be called before `run()` or `tick()`.
|
|
|
|
|
pub fn with_extension(mut self, ext: Arc<dyn RuntimeExtension>) -> Self {
|
2026-02-13 15:00:53 +00:00
|
|
|
// Create per-worker extensions (e.g., timer wheels)
|
|
|
|
|
for worker in self.tick_workers.get_mut().iter_mut() {
|
|
|
|
|
if let Some(wext) = ext.create_worker_extension() {
|
|
|
|
|
worker.worker_ext = Some(wext);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-13 07:11:24 +00:00
|
|
|
self.extension = Some(ext);
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Access the installed runtime extension (if any).
|
|
|
|
|
pub fn extension(&self) -> Option<&dyn RuntimeExtension> {
|
|
|
|
|
self.extension.as_deref()
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 15:23:03 +00:00
|
|
|
pub fn admin(&self) -> RuntimeAdmin<'_> {
|
|
|
|
|
RuntimeAdmin { runtime: self }
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-13 07:11:24 +00:00
|
|
|
/// Send a request and get a handle for the response.
|
|
|
|
|
///
|
|
|
|
|
/// Creates a temporary inbox, calls `msg_builder` with the inbox's address
|
|
|
|
|
/// (so you can embed it as `reply_to`), sends the message, and returns an
|
|
|
|
|
/// [`Ask`] handle for receiving the response.
|
|
|
|
|
pub fn ask<Req: Message, Resp: Message>(
|
|
|
|
|
&self,
|
|
|
|
|
addr: ActorAddress,
|
|
|
|
|
msg_builder: impl FnOnce(ActorAddress) -> Req,
|
|
|
|
|
) -> Result<Ask<Resp>, Error> {
|
|
|
|
|
let inbox = self.new_inbox::<Resp>()?;
|
|
|
|
|
let msg = msg_builder(*inbox.addr());
|
|
|
|
|
self.send_to(addr, msg)?;
|
|
|
|
|
Ok(Ask { inbox })
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 05:08:58 +00:00
|
|
|
/// Send a message to an actor address.
|
|
|
|
|
///
|
|
|
|
|
/// Returns `Ok(())` if the message was accepted for routing. This does **not**
|
|
|
|
|
/// guarantee delivery — the recipient may stop before processing it. If
|
|
|
|
|
/// delivery confirmation is needed, implement an application-level ACK.
|
|
|
|
|
///
|
|
|
|
|
/// Returns `Err` if the address is unknown to the runtime.
|
2026-01-25 13:38:34 +00:00
|
|
|
pub fn send_to<M: Message>(&self, addr: ActorAddress, msg: M) -> Result<(), Error> {
|
2026-02-09 19:05:37 +00:00
|
|
|
let result = self.send_any(addr, Box::new(msg));
|
2026-02-09 09:04:57 +00:00
|
|
|
|
|
|
|
|
#[cfg(feature = "tracing")]
|
|
|
|
|
tracing::trace!(dest = %addr, "message.sent");
|
|
|
|
|
|
2026-02-09 19:05:37 +00:00
|
|
|
result
|
2026-01-25 13:38:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create an external inbox for receiving messages in the outer process containing the runtime
|
|
|
|
|
pub fn new_inbox<M: Message>(&self) -> Result<Inbox<M>, Error> {
|
|
|
|
|
let addr = ActorAddress::new_random();
|
2026-02-10 07:35:58 +00:00
|
|
|
let receiver = Receiver::<M>::new(self.config.channel_buffer_size);
|
2026-01-25 13:38:34 +00:00
|
|
|
let sender = receiver.new_sender();
|
2026-02-06 11:25:37 +00:00
|
|
|
self.inbox_registry.register(addr, Arc::new(sender));
|
2026-01-25 13:38:34 +00:00
|
|
|
Ok(Inbox {
|
|
|
|
|
addr,
|
|
|
|
|
inner: receiver,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 04:44:46 +00:00
|
|
|
/// Create an [`ExternalSender`] handle for injecting messages from any thread.
|
|
|
|
|
///
|
|
|
|
|
/// The returned handle is `Clone + Send + Sync` and can be moved into
|
|
|
|
|
/// background I/O threads to bridge external events into the actor system.
|
|
|
|
|
pub fn create_sender(&self) -> ExternalSender {
|
|
|
|
|
ExternalSender {
|
|
|
|
|
address_map: self.address_map.clone(),
|
2026-02-24 09:12:28 +00:00
|
|
|
transfer_txs: self.transfer_txs.iter().cloned().collect(),
|
2026-02-23 04:44:46 +00:00
|
|
|
worker_threads: self.worker_threads.clone(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 19:05:37 +00:00
|
|
|
fn make_tick_context(&self) -> TickContext<'_> {
|
|
|
|
|
TickContext {
|
|
|
|
|
address_map: &self.address_map,
|
|
|
|
|
transfer_txs: &self.transfer_txs,
|
|
|
|
|
spawn_txs: &self.spawn_txs,
|
|
|
|
|
placement: &self.placement,
|
|
|
|
|
inbox_registry: &self.inbox_registry,
|
|
|
|
|
config: &self.config,
|
2026-02-13 07:11:24 +00:00
|
|
|
extension: self.extension.as_deref(),
|
2026-06-06 17:53:25 +00:00
|
|
|
process_output_observer: self.process_output_observer.get(),
|
2026-02-11 15:23:26 +00:00
|
|
|
stats_hook: self.stats_hook.as_deref(),
|
2026-02-13 07:11:24 +00:00
|
|
|
worker_threads: &self.worker_threads,
|
2026-02-20 17:34:43 +00:00
|
|
|
worker_stats: &self.worker_stats,
|
|
|
|
|
created_at: self.created_at,
|
2026-02-09 19:05:37 +00:00
|
|
|
#[cfg(feature = "transport")]
|
2026-06-09 09:29:07 +00:00
|
|
|
remote_sink: self.remote_sink.as_deref(),
|
2026-02-09 19:05:37 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 11:25:37 +00:00
|
|
|
/// Drive one tick of the single-threaded worker.
|
2026-02-09 07:24:16 +00:00
|
|
|
///
|
|
|
|
|
/// Panics if called on a multi-threaded runtime — use `run()` instead.
|
2026-02-06 11:25:37 +00:00
|
|
|
pub fn tick(&self) {
|
2026-02-09 07:24:16 +00:00
|
|
|
assert!(
|
|
|
|
|
self.config.num_threads < 2,
|
|
|
|
|
"tick() is only valid for single-threaded runtimes; use run() for multi-threaded"
|
|
|
|
|
);
|
2026-02-09 19:05:37 +00:00
|
|
|
let tc = self.make_tick_context();
|
2026-02-09 07:24:16 +00:00
|
|
|
for worker in self.tick_workers.borrow_mut().iter_mut() {
|
|
|
|
|
worker.tick_once(&tc);
|
2026-02-06 11:25:37 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 12:47:51 +00:00
|
|
|
/// Spawn worker threads and start processing, returning a handle
|
|
|
|
|
/// to interact with the runtime and join the threads later.
|
2026-01-25 13:38:34 +00:00
|
|
|
///
|
2026-02-06 12:47:51 +00:00
|
|
|
/// Works in both single-threaded and multi-threaded configurations.
|
|
|
|
|
/// In single-threaded mode, one background thread is spawned.
|
2026-02-13 13:27:34 +00:00
|
|
|
///
|
|
|
|
|
/// Not available on wasm32 — use the browser crate's Web Worker-based run instead.
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
2026-02-09 07:24:16 +00:00
|
|
|
pub fn run(self) -> Result<RuntimeHandle, Error> {
|
2026-01-25 13:38:34 +00:00
|
|
|
self.is_running.store(true, Ordering::Release);
|
|
|
|
|
|
2026-02-09 09:04:57 +00:00
|
|
|
#[cfg(feature = "tracing")]
|
2026-06-23 15:42:28 +00:00
|
|
|
tracing::info!(
|
|
|
|
|
num_workers = self.config.num_threads.max(1),
|
|
|
|
|
"runtime.started"
|
|
|
|
|
);
|
2026-02-09 09:04:57 +00:00
|
|
|
|
2026-02-09 07:24:16 +00:00
|
|
|
let workers: Vec<Worker> = self.tick_workers.replace(Vec::new());
|
2026-01-25 13:38:34 +00:00
|
|
|
|
|
|
|
|
let rt = Arc::new(self);
|
2026-02-06 11:25:37 +00:00
|
|
|
let mut handles: Vec<JoinHandle<()>> = Vec::with_capacity(workers.len());
|
2026-01-25 13:38:34 +00:00
|
|
|
|
2026-02-06 11:25:37 +00:00
|
|
|
for mut worker in workers {
|
|
|
|
|
let rt_clone = rt.clone();
|
2026-02-13 07:11:24 +00:00
|
|
|
let worker_id = worker.id.0;
|
|
|
|
|
let name = format!("swactor-worker-{}", worker_id);
|
2026-02-08 16:18:39 +00:00
|
|
|
let handle = thread::Builder::new()
|
|
|
|
|
.name(name)
|
|
|
|
|
.spawn(move || {
|
2026-02-13 07:11:24 +00:00
|
|
|
// Register this thread so send_to/spawn can unpark us
|
|
|
|
|
let _ = rt_clone.worker_threads[worker_id].set(thread::current());
|
2026-02-09 19:05:37 +00:00
|
|
|
let tc = rt_clone.make_tick_context();
|
2026-02-08 16:18:39 +00:00
|
|
|
worker.run(&tc, &rt_clone.is_running);
|
|
|
|
|
})
|
|
|
|
|
.expect("failed to spawn worker thread");
|
2026-01-25 13:38:34 +00:00
|
|
|
handles.push(handle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(RuntimeHandle {
|
|
|
|
|
runtime: rt,
|
|
|
|
|
threads: handles,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 14:45:19 +00:00
|
|
|
/// Returns a snapshot of runtime stats: actor placements and per-worker info.
|
|
|
|
|
pub fn stats(&self) -> RuntimeStats {
|
2026-06-23 15:42:28 +00:00
|
|
|
let num_workers = if self.config.num_threads < 2 {
|
|
|
|
|
1
|
|
|
|
|
} else {
|
|
|
|
|
self.config.num_threads
|
|
|
|
|
};
|
2026-02-09 19:05:37 +00:00
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
let workers = self
|
|
|
|
|
.worker_stats
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
2026-02-09 19:05:37 +00:00
|
|
|
.map(|(i, ws)| ws.snapshot(i))
|
2026-02-06 14:45:19 +00:00
|
|
|
.collect();
|
2026-02-09 19:05:37 +00:00
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
let actors = self
|
|
|
|
|
.address_map
|
|
|
|
|
.snapshot()
|
|
|
|
|
.into_iter()
|
2026-02-06 14:45:19 +00:00
|
|
|
.map(|(addr, wid)| (addr, wid.as_usize()))
|
|
|
|
|
.collect();
|
2026-02-09 09:04:57 +00:00
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
let tick_timings = self
|
|
|
|
|
.worker_stats
|
|
|
|
|
.iter()
|
2026-02-09 09:04:57 +00:00
|
|
|
.map(|ws| ws.drain_tick_timings())
|
|
|
|
|
.collect();
|
|
|
|
|
|
2026-02-11 15:23:26 +00:00
|
|
|
let uptime_ms = self.created_at.elapsed().as_millis() as u64;
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
RuntimeStats {
|
|
|
|
|
num_workers,
|
|
|
|
|
uptime_ms,
|
|
|
|
|
actors,
|
|
|
|
|
workers,
|
|
|
|
|
actor_details: Vec::new(),
|
|
|
|
|
tick_timings,
|
|
|
|
|
}
|
2026-02-06 13:35:46 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-13 07:11:24 +00:00
|
|
|
/// Request an actor to stop gracefully.
|
|
|
|
|
///
|
|
|
|
|
/// The actor's `on_stop()` hook is called before removal. Pending messages
|
|
|
|
|
/// in the mailbox are discarded. The stop takes effect on the next tick.
|
|
|
|
|
///
|
|
|
|
|
/// Returns `Err` if the actor address is not found in the runtime.
|
|
|
|
|
pub fn stop_actor(&self, addr: ActorAddress) -> Result<(), Error> {
|
|
|
|
|
match self.address_map.lookup(&addr) {
|
|
|
|
|
Some(wid) => {
|
2026-06-23 15:42:28 +00:00
|
|
|
self.transfer_txs[wid.as_usize()].send(Envelope::new(addr, Box::new(StopSignal)));
|
2026-02-13 07:11:24 +00:00
|
|
|
notify_worker(&self.worker_threads, wid.as_usize());
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
None => Err(Error::from("Actor not found")),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Signal all workers to stop and wake any that are parked.
|
2026-01-25 13:38:34 +00:00
|
|
|
pub fn shutdown(&self) {
|
2026-02-09 09:04:57 +00:00
|
|
|
#[cfg(feature = "tracing")]
|
|
|
|
|
tracing::info!("runtime.shutdown");
|
|
|
|
|
|
2026-01-25 13:38:34 +00:00
|
|
|
self.is_running.store(false, Ordering::Release);
|
2026-02-13 07:11:24 +00:00
|
|
|
// Wake all parked workers so they see the shutdown flag immediately
|
2026-02-23 04:44:46 +00:00
|
|
|
for thread in self.worker_threads.iter() {
|
2026-02-13 07:11:24 +00:00
|
|
|
if let Some(t) = thread.get() {
|
|
|
|
|
t.unpark();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-25 13:38:34 +00:00
|
|
|
}
|
2026-02-09 19:05:37 +00:00
|
|
|
|
2026-02-11 15:23:26 +00:00
|
|
|
/// Set a stats hook to receive per-actor snapshots from workers.
|
|
|
|
|
///
|
|
|
|
|
/// Must be called before [`run()`](Self::run) or [`tick()`](Self::tick).
|
|
|
|
|
pub fn set_stats_hook(&mut self, hook: Arc<dyn StatsHook>) {
|
|
|
|
|
self.stats_hook = Some(hook);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-06 17:53:25 +00:00
|
|
|
/// Install the per-node process-output observer. Every process spawned
|
|
|
|
|
/// through the process facility on this runtime hands its stdout/stderr to
|
|
|
|
|
/// `obs`, labeled by command basename. Takes `&self` (the slot is a
|
|
|
|
|
/// `OnceLock`) so it can be installed on an already-shared `Arc<Runtime>`,
|
|
|
|
|
/// before the first managed process is spawned. Subsequent calls are no-ops.
|
|
|
|
|
pub fn set_process_output_observer(
|
|
|
|
|
&self,
|
|
|
|
|
obs: Arc<dyn crate::process_observer::ProcessOutputObserver>,
|
|
|
|
|
) {
|
|
|
|
|
let _ = self.process_output_observer.set(obs);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 09:29:07 +00:00
|
|
|
/// Set the sink for non-local (remote) message delivery.
|
|
|
|
|
///
|
|
|
|
|
/// The sink owns all codec/transport concerns; core only knows how to hand
|
|
|
|
|
/// it a type-erased message destined for a non-local address.
|
2026-02-09 19:05:37 +00:00
|
|
|
#[cfg(feature = "transport")]
|
2026-06-09 09:29:07 +00:00
|
|
|
pub fn set_remote_sink(&mut self, sink: Arc<dyn RemoteSink>) {
|
|
|
|
|
self.remote_sink = Some(sink);
|
2026-02-09 19:05:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Deliver a raw deserialized message into the runtime.
|
|
|
|
|
///
|
2026-06-09 09:29:07 +00:00
|
|
|
/// Whoever owns the socket decodes the wire bytes outside core and calls
|
|
|
|
|
/// this to inject the resulting message for a local actor or inbox.
|
2026-02-09 19:05:37 +00:00
|
|
|
#[cfg(feature = "transport")]
|
2026-06-23 15:42:28 +00:00
|
|
|
pub fn deliver_raw(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error> {
|
2026-02-09 19:05:37 +00:00
|
|
|
match self.address_map.lookup(&addr) {
|
2026-02-10 07:35:58 +00:00
|
|
|
Some(wid) => {
|
2026-06-23 15:42:28 +00:00
|
|
|
self.transfer_txs[wid.as_usize()].send(Envelope::new(addr, msg));
|
2026-02-10 07:35:58 +00:00
|
|
|
Ok(())
|
|
|
|
|
}
|
2026-02-09 19:05:37 +00:00
|
|
|
None => self.inbox_registry.try_deliver(addr, msg),
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-25 13:38:34 +00:00
|
|
|
}
|
2026-02-06 11:25:37 +00:00
|
|
|
|
2026-07-08 15:23:03 +00:00
|
|
|
impl RuntimeAdmin<'_> {
|
|
|
|
|
fn new_admin<T: Message>(&self) -> Result<(Admin<T>, ActorAddress), Error> {
|
|
|
|
|
let inbox = self.runtime.new_inbox::<AdminResult<T>>()?;
|
|
|
|
|
let reply_to = *inbox.addr();
|
|
|
|
|
Ok((Admin::new(inbox), reply_to))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn ready<T: Message>(&self, result: AdminResult<T>) -> Result<Admin<T>, Error> {
|
|
|
|
|
let (admin, reply_to) = self.new_admin::<T>()?;
|
|
|
|
|
let _ = self
|
|
|
|
|
.runtime
|
|
|
|
|
.inbox_registry
|
|
|
|
|
.try_deliver(reply_to, Box::new(result));
|
|
|
|
|
Ok(admin)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn list_actors(&self) -> Result<Admin<ListActorsResponse>, Error> {
|
|
|
|
|
let (admin, reply_to) = self.new_admin::<ListActorsResponse>()?;
|
|
|
|
|
let acc = Arc::new(ListActorsAccumulator {
|
|
|
|
|
remaining: AtomicUsize::new(self.runtime.admin_txs.len()),
|
|
|
|
|
summaries: parking_lot::Mutex::new(Vec::new()),
|
|
|
|
|
reply_to,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
for (idx, tx) in self.runtime.admin_txs.iter().enumerate() {
|
|
|
|
|
tx.send(AdminCommand::ListActors { acc: acc.clone() });
|
|
|
|
|
notify_worker(&self.runtime.worker_threads, idx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(admin)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn inspect_actor(&self, actor: ActorAddress) -> Result<Admin<InspectActorResponse>, Error> {
|
|
|
|
|
let Some(wid) = self.runtime.address_map.lookup(&actor) else {
|
|
|
|
|
return self.ready::<InspectActorResponse>(Err(AdminError::ActorNotFound { actor }));
|
|
|
|
|
};
|
|
|
|
|
let (admin, reply_to) = self.new_admin::<InspectActorResponse>()?;
|
|
|
|
|
let worker_idx = wid.as_usize();
|
|
|
|
|
self.runtime.admin_txs[worker_idx].send(AdminCommand::InspectActor { actor, reply_to });
|
|
|
|
|
notify_worker(&self.runtime.worker_threads, worker_idx);
|
|
|
|
|
Ok(admin)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn get_actor_state<A>(
|
|
|
|
|
&self,
|
|
|
|
|
actor: ActorAddress,
|
|
|
|
|
) -> Result<Admin<GetActorStateResponse<A>>, Error>
|
|
|
|
|
where
|
|
|
|
|
A: ActorInterface + Clone + Sync,
|
|
|
|
|
{
|
|
|
|
|
let Some(wid) = self.runtime.address_map.lookup(&actor) else {
|
|
|
|
|
return self
|
|
|
|
|
.ready::<GetActorStateResponse<A>>(Err(AdminError::ActorNotFound { actor }));
|
|
|
|
|
};
|
|
|
|
|
let (admin, reply_to) = self.new_admin::<GetActorStateResponse<A>>()?;
|
|
|
|
|
|
|
|
|
|
let get = Box::new(
|
|
|
|
|
|actor: ActorAddress,
|
|
|
|
|
erased: &dyn AnyActor,
|
|
|
|
|
metadata: ActorTypeMetadata|
|
|
|
|
|
-> Box<dyn Any + Send> {
|
|
|
|
|
let expected_actor_type = std::any::type_name::<A>();
|
|
|
|
|
let expected_message_type = std::any::type_name::<A::Incoming>();
|
|
|
|
|
if metadata.actor_type_id != TypeId::of::<A>()
|
|
|
|
|
|| metadata.message_type_id != TypeId::of::<A::Incoming>()
|
|
|
|
|
{
|
|
|
|
|
return Box::new(Err::<GetActorStateResponse<A>, AdminError>(
|
|
|
|
|
AdminError::TypeMismatch {
|
|
|
|
|
expected_actor_type,
|
|
|
|
|
expected_message_type,
|
|
|
|
|
actual_actor_type: metadata.actor_type_name,
|
|
|
|
|
actual_message_type: metadata.message_type_name,
|
|
|
|
|
},
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let Some(typed) = erased.as_any().downcast_ref::<Actor<A>>() else {
|
|
|
|
|
return Box::new(Err::<GetActorStateResponse<A>, AdminError>(
|
|
|
|
|
AdminError::TypeMismatch {
|
|
|
|
|
expected_actor_type,
|
|
|
|
|
expected_message_type,
|
|
|
|
|
actual_actor_type: metadata.actor_type_name,
|
|
|
|
|
actual_message_type: metadata.message_type_name,
|
|
|
|
|
},
|
|
|
|
|
));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Box::new(Ok::<GetActorStateResponse<A>, AdminError>(
|
|
|
|
|
GetActorStateResponse {
|
|
|
|
|
state: ActorStateSnapshot {
|
|
|
|
|
actor,
|
|
|
|
|
actor_type: metadata.actor_type_name,
|
|
|
|
|
message_type: metadata.message_type_name,
|
|
|
|
|
actor_instance: typed.inner().clone(),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
))
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
let not_found = Box::new(|actor| {
|
|
|
|
|
Box::new(Err::<GetActorStateResponse<A>, AdminError>(
|
|
|
|
|
AdminError::ActorNotFound { actor },
|
|
|
|
|
)) as Box<dyn Any + Send>
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let worker_idx = wid.as_usize();
|
|
|
|
|
self.runtime.admin_txs[worker_idx].send(AdminCommand::GetActorState {
|
|
|
|
|
actor,
|
|
|
|
|
reply_to,
|
|
|
|
|
get,
|
|
|
|
|
not_found,
|
|
|
|
|
});
|
|
|
|
|
notify_worker(&self.runtime.worker_threads, worker_idx);
|
|
|
|
|
Ok(admin)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn replace_actor_state<A>(
|
|
|
|
|
&self,
|
|
|
|
|
actor: ActorAddress,
|
|
|
|
|
state: ActorStateSnapshot<A>,
|
|
|
|
|
) -> Result<Admin<OperationResult>, Error>
|
|
|
|
|
where
|
|
|
|
|
A: ActorInterface,
|
|
|
|
|
{
|
|
|
|
|
if state.actor != actor {
|
|
|
|
|
return self.ready::<OperationResult>(Err(AdminError::AddressMismatch {
|
|
|
|
|
requested: actor,
|
|
|
|
|
snapshot: state.actor,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let Some(wid) = self.runtime.address_map.lookup(&actor) else {
|
|
|
|
|
return self.ready::<OperationResult>(Err(AdminError::ActorNotFound { actor }));
|
|
|
|
|
};
|
|
|
|
|
let (admin, reply_to) = self.new_admin::<OperationResult>()?;
|
|
|
|
|
|
|
|
|
|
let actor_instance = state.actor_instance;
|
|
|
|
|
let replace = Box::new(
|
|
|
|
|
move |erased: &mut dyn AnyActor,
|
|
|
|
|
metadata: ActorTypeMetadata|
|
|
|
|
|
-> AdminResult<OperationResult> {
|
|
|
|
|
let expected_actor_type = std::any::type_name::<A>();
|
|
|
|
|
let expected_message_type = std::any::type_name::<A::Incoming>();
|
|
|
|
|
if metadata.actor_type_id != TypeId::of::<A>()
|
|
|
|
|
|| metadata.message_type_id != TypeId::of::<A::Incoming>()
|
|
|
|
|
{
|
|
|
|
|
return Err(AdminError::TypeMismatch {
|
|
|
|
|
expected_actor_type,
|
|
|
|
|
expected_message_type,
|
|
|
|
|
actual_actor_type: metadata.actor_type_name,
|
|
|
|
|
actual_message_type: metadata.message_type_name,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let Some(typed) = erased.as_any_mut().downcast_mut::<Actor<A>>() else {
|
|
|
|
|
return Err(AdminError::TypeMismatch {
|
|
|
|
|
expected_actor_type,
|
|
|
|
|
expected_message_type,
|
|
|
|
|
actual_actor_type: metadata.actor_type_name,
|
|
|
|
|
actual_message_type: metadata.message_type_name,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
typed.replace_inner(actor_instance);
|
|
|
|
|
Ok(OperationResult { applied: true })
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let worker_idx = wid.as_usize();
|
|
|
|
|
self.runtime.admin_txs[worker_idx].send(AdminCommand::ReplaceActorState {
|
|
|
|
|
actor,
|
|
|
|
|
reply_to,
|
|
|
|
|
replace,
|
|
|
|
|
});
|
|
|
|
|
notify_worker(&self.runtime.worker_threads, worker_idx);
|
|
|
|
|
Ok(admin)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn stop_actor(&self, actor: ActorAddress) -> Result<Admin<OperationResult>, Error> {
|
|
|
|
|
let Some(wid) = self.runtime.address_map.lookup(&actor) else {
|
|
|
|
|
return self.ready::<OperationResult>(Err(AdminError::ActorNotFound { actor }));
|
|
|
|
|
};
|
|
|
|
|
let (admin, reply_to) = self.new_admin::<OperationResult>()?;
|
|
|
|
|
let worker_idx = wid.as_usize();
|
|
|
|
|
self.runtime.admin_txs[worker_idx].send(AdminCommand::StopActor { actor, reply_to });
|
|
|
|
|
notify_worker(&self.runtime.worker_threads, worker_idx);
|
|
|
|
|
Ok(admin)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn suspend_actor(&self, actor: ActorAddress) -> Result<Admin<OperationResult>, Error> {
|
|
|
|
|
let Some(wid) = self.runtime.address_map.lookup(&actor) else {
|
|
|
|
|
return self.ready::<OperationResult>(Err(AdminError::ActorNotFound { actor }));
|
|
|
|
|
};
|
|
|
|
|
let (admin, reply_to) = self.new_admin::<OperationResult>()?;
|
|
|
|
|
let worker_idx = wid.as_usize();
|
|
|
|
|
self.runtime.admin_txs[worker_idx].send(AdminCommand::SuspendActor { actor, reply_to });
|
|
|
|
|
notify_worker(&self.runtime.worker_threads, worker_idx);
|
|
|
|
|
Ok(admin)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn resume_actor(&self, actor: ActorAddress) -> Result<Admin<OperationResult>, Error> {
|
|
|
|
|
let Some(wid) = self.runtime.address_map.lookup(&actor) else {
|
|
|
|
|
return self.ready::<OperationResult>(Err(AdminError::ActorNotFound { actor }));
|
|
|
|
|
};
|
|
|
|
|
let (admin, reply_to) = self.new_admin::<OperationResult>()?;
|
|
|
|
|
let worker_idx = wid.as_usize();
|
|
|
|
|
self.runtime.admin_txs[worker_idx].send(AdminCommand::ResumeActor { actor, reply_to });
|
|
|
|
|
notify_worker(&self.runtime.worker_threads, worker_idx);
|
|
|
|
|
Ok(admin)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-13 07:11:24 +00:00
|
|
|
/// Wake a parked worker thread so it can process new work.
|
|
|
|
|
/// No-op if the thread handle hasn't been registered yet (single-threaded tick mode).
|
|
|
|
|
#[inline]
|
|
|
|
|
pub(crate) fn notify_worker(threads: &[OnceLock<Thread>], wid: usize) {
|
|
|
|
|
if let Some(t) = threads.get(wid).and_then(|o| o.get()) {
|
|
|
|
|
t.unpark();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(private_interfaces)]
|
2026-02-06 11:25:37 +00:00
|
|
|
impl ContextInner for Runtime {
|
|
|
|
|
fn send_any(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error> {
|
|
|
|
|
match self.address_map.lookup(&addr) {
|
2026-02-10 07:35:58 +00:00
|
|
|
Some(wid) => {
|
2026-06-23 15:42:28 +00:00
|
|
|
self.transfer_txs[wid.as_usize()].send(Envelope::new(addr, msg));
|
2026-02-13 07:11:24 +00:00
|
|
|
notify_worker(&self.worker_threads, wid.as_usize());
|
2026-02-10 07:35:58 +00:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
None => self.make_tick_context().route_nonlocal(addr, msg),
|
2026-02-06 11:25:37 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 17:34:43 +00:00
|
|
|
fn spawn_any(&self, request: SpawnRequest) {
|
2026-02-06 11:25:37 +00:00
|
|
|
let worker_id = self.placement.next_worker();
|
2026-02-20 17:34:43 +00:00
|
|
|
self.address_map.insert(request.addr, worker_id);
|
2026-06-23 15:42:28 +00:00
|
|
|
self.spawn_txs[worker_id.as_usize()].send(request);
|
2026-02-13 07:11:24 +00:00
|
|
|
notify_worker(&self.worker_threads, worker_id.as_usize());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn request_stop(&self, addr: ActorAddress) {
|
|
|
|
|
// From spawn context (outside worker), send StopSignal through transfer queue
|
|
|
|
|
if let Some(wid) = self.address_map.lookup(&addr) {
|
2026-06-23 15:42:28 +00:00
|
|
|
self.transfer_txs[wid.as_usize()].send(Envelope::new(addr, Box::new(StopSignal)));
|
2026-02-13 07:11:24 +00:00
|
|
|
notify_worker(&self.worker_threads, wid.as_usize());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 17:34:43 +00:00
|
|
|
fn request_stop_with(&self, addr: ActorAddress, value: ExitValue) {
|
|
|
|
|
if let Some(wid) = self.address_map.lookup(&addr) {
|
|
|
|
|
self.transfer_txs[wid.as_usize()]
|
|
|
|
|
.send(Envelope::new(addr, Box::new(StopWithSignal(value))));
|
|
|
|
|
notify_worker(&self.worker_threads, wid.as_usize());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn request_suspend(&self, addr: ActorAddress) {
|
|
|
|
|
// Outside worker context — not supported (suspend is per-actor, from handler)
|
|
|
|
|
eprintln!("swactor: request_suspend called outside worker context for {addr} — ignored");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn request_resume(&self, addr: ActorAddress) {
|
|
|
|
|
if let Some(wid) = self.address_map.lookup(&addr) {
|
2026-06-23 15:42:28 +00:00
|
|
|
self.transfer_txs[wid.as_usize()].send(Envelope::new(addr, Box::new(ResumeSignal)));
|
2026-02-20 17:34:43 +00:00
|
|
|
notify_worker(&self.worker_threads, wid.as_usize());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-13 15:00:53 +00:00
|
|
|
fn post_worker_request(&self, _request: Box<dyn Any + Send>) {
|
|
|
|
|
// Worker requests (e.g., timers) are per-worker; posting from outside
|
2026-02-13 07:11:24 +00:00
|
|
|
// a worker context (e.g., rt.spawn() callback) is not supported.
|
2026-02-13 15:00:53 +00:00
|
|
|
eprintln!("swactor: post_worker_request called outside worker context — ignored");
|
2026-02-13 07:11:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn extension(&self) -> Option<&dyn RuntimeExtension> {
|
|
|
|
|
self.extension.as_deref()
|
2026-02-06 11:25:37 +00:00
|
|
|
}
|
2026-02-20 17:34:43 +00:00
|
|
|
|
2026-06-06 17:53:25 +00:00
|
|
|
fn process_output_observer(
|
|
|
|
|
&self,
|
|
|
|
|
) -> Option<Arc<dyn crate::process_observer::ProcessOutputObserver>> {
|
|
|
|
|
self.process_output_observer.get().cloned()
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 17:34:43 +00:00
|
|
|
fn system_info(&self) -> SystemInfo {
|
|
|
|
|
let num_workers = self.config.num_threads.max(1);
|
2026-06-23 15:42:28 +00:00
|
|
|
let total_actors: usize = self
|
|
|
|
|
.worker_stats
|
|
|
|
|
.iter()
|
2026-02-20 17:34:43 +00:00
|
|
|
.map(|ws| ws.num_actors.load(Ordering::Relaxed))
|
|
|
|
|
.sum();
|
|
|
|
|
SystemInfo {
|
|
|
|
|
worker_id: 0,
|
|
|
|
|
num_workers,
|
|
|
|
|
total_actors,
|
|
|
|
|
uptime_ms: self.created_at.elapsed().as_millis() as u64,
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-06 11:25:37 +00:00
|
|
|
}
|