feat: no_std core runtime

This commit is contained in:
Zachery Aaron Shores-Chmielewski 2026-03-03 00:48:03 +07:00
parent 90991fac48
commit 61701bd7f2
17 changed files with 1032 additions and 174 deletions

5
Cargo.lock generated
View file

@ -5324,10 +5324,15 @@ dependencies = [
"proptest", "proptest",
"proptest-state-machine", "proptest-state-machine",
"serde", "serde",
"swactor-core",
"tracing", "tracing",
"web-time 0.2.4", "web-time 0.2.4",
] ]
[[package]]
name = "swactor-core"
version = "0.1.0"
[[package]] [[package]]
name = "swactor-datastore" name = "swactor-datastore"
version = "0.1.0" version = "0.1.0"

View file

@ -1,6 +1,7 @@
[workspace] [workspace]
members = [ members = [
".", ".",
"crates/core",
"crates/bindings/python", "crates/bindings/python",
"crates/bindings/wasm-runtime", "crates/bindings/wasm-runtime",
"crates/simulation", "crates/simulation",
@ -41,6 +42,7 @@ transport = [
wasm = ["no_random", "dep:web-time"] # browser/wasm32 target support wasm = ["no_random", "dep:web-time"] # browser/wasm32 target support
[dependencies] [dependencies]
swactor-core = { path = "crates/core" }
getrandom = { version = "0.2", optional = true } getrandom = { version = "0.2", optional = true }
serde = { version = "1", features = ["derive"], optional = true } serde = { version = "1", features = ["derive"], optional = true }
tracing = { version = "0.1", optional = true } tracing = { version = "0.1", optional = true }

4
crates/core/Cargo.toml Normal file
View file

@ -0,0 +1,4 @@
[package]
name = "swactor-core"
version = "0.1.0"
edition = "2024"

273
crates/core/src/lib.rs Normal file
View file

@ -0,0 +1,273 @@
//! Pure, synchronous, deterministic core for the swactor actor framework.
//!
//! This crate contains the decision logic that maps 1:1 to a TLA+ specification.
//! Every public function is a pure function: `fn(inputs) -> output` with no side
//! effects, no IO, no allocation, and no panics.
//!
//! The runtime crate (`swactor`) calls these functions to make decisions; this crate
//! never calls back into the runtime. If you can't trivially transliterate a
//! function's signature into TLA+, it doesn't belong here.
#![no_std]
#![deny(unsafe_code)]
// ─── Actor Lifecycle Phase ───────────────────────────────────────────────────
/// The lifecycle phase of an actor slot.
///
/// Replaces the four boolean fields (`started`, `poisoned`, `stopping`, `suspended`)
/// with a single state machine. Every phase transition goes through
/// [`lifecycle_transition`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ActorPhase {
/// Actor has been inserted into the pool but `on_start` has not been called yet.
Unstarted,
/// Actor is running normally — processing messages from its mailbox.
Running,
/// Actor is suspended — messages continue to queue but are not processed.
Suspended,
/// Actor has been requested to stop gracefully. `on_stop` will be called during cleanup.
Stopping,
/// Actor panicked during `on_start` or message handling. No further processing occurs.
Poisoned,
}
// ─── Stop / Exit Reasons ─────────────────────────────────────────────────────
/// Reason an actor was removed from the runtime.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StopReason {
/// Graceful stop (via `ctx.stop_self()` or `Runtime::stop_actor()`).
Normal,
/// Actor panicked and could not be restarted.
Panicked,
/// Actor stopped with a typed exit value (via `Ctx::stop_with`).
Completed,
}
/// Why an actor exited — delivered to watchers.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExitReason {
/// Actor was explicitly stopped or removed from the pool.
Stopped,
/// Actor panicked during message handling.
Panicked,
/// The node hosting the actor left the cluster (SWIM Dead).
NodeDown,
/// Actor stopped with a typed exit value (via `Ctx::stop_with`).
Completed,
}
// ─── Mailbox ─────────────────────────────────────────────────────────────────
/// What to do when a bounded mailbox is full.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MailboxOverflow {
/// Drop the incoming message (newest). The message is silently discarded.
DropNewest,
/// Drop the oldest message in the queue to make room for the new one.
DropOldest,
}
/// Result of the mailbox capacity check.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MailboxDecision {
/// The mailbox has room (or is unbounded). Accept the message.
Accept,
/// The mailbox is full and the policy is DropNewest — reject the incoming message.
RejectNewest,
/// The mailbox is full and the policy is DropOldest — evict the front of the queue.
EvictOldest,
}
// ─── Lifecycle Events ────────────────────────────────────────────────────────
/// Events that drive the actor lifecycle state machine.
///
/// Each variant corresponds to a runtime action that may cause a phase transition.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LifecycleEvent {
/// `on_start` completed successfully.
Started,
/// `on_start` panicked.
StartPanicked,
/// A message handler panicked.
MessagePanicked,
/// A graceful stop was requested (via `ctx.stop_self()` or `StopSignal`).
StopRequested,
/// A stop-with-value was requested (via `ctx.stop_with()`).
StopWithRequested,
/// The actor was suspended (via `ctx.suspend_self()`).
SuspendRequested,
/// A suspended actor was resumed (via `ResumeSignal`).
Resumed,
}
// ─── Deliver Decision ────────────────────────────────────────────────────────
/// What should happen when a message arrives at an actor in a particular phase.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeliverDecision {
/// Enqueue the message in the actor's mailbox (subject to overflow policy).
Enqueue,
/// The actor is dead/stopping — discard the message silently.
Discard,
/// The actor is suspended — enqueue but do not process until resumed.
Queue,
}
// ─── Pure Functions ─────────────────────────────────────────────────────────
// TLA+: LifecycleTransition
/// The actor lifecycle state machine.
///
/// Given the current phase and an event, returns the new phase. Every phase
/// transition in the runtime must go through this function.
///
/// Illegal transitions (e.g., `Resumed` on a non-suspended actor) return the
/// phase unchanged — the runtime is responsible for not issuing nonsensical
/// events, but the core never panics.
#[must_use]
pub fn lifecycle_transition(phase: ActorPhase, event: LifecycleEvent) -> ActorPhase {
match (phase, event) {
// Unstarted: waiting for on_start result
(ActorPhase::Unstarted, LifecycleEvent::Started) => ActorPhase::Running,
(ActorPhase::Unstarted, LifecycleEvent::StartPanicked) => ActorPhase::Poisoned,
(ActorPhase::Unstarted, LifecycleEvent::StopRequested) => ActorPhase::Stopping,
(ActorPhase::Unstarted, LifecycleEvent::StopWithRequested) => ActorPhase::Stopping,
(ActorPhase::Unstarted, LifecycleEvent::SuspendRequested) => ActorPhase::Suspended,
(ActorPhase::Unstarted, LifecycleEvent::MessagePanicked) => phase,
(ActorPhase::Unstarted, LifecycleEvent::Resumed) => phase,
// Running: normal operation
(ActorPhase::Running, LifecycleEvent::MessagePanicked) => ActorPhase::Poisoned,
(ActorPhase::Running, LifecycleEvent::StopRequested) => ActorPhase::Stopping,
(ActorPhase::Running, LifecycleEvent::StopWithRequested) => ActorPhase::Stopping,
(ActorPhase::Running, LifecycleEvent::SuspendRequested) => ActorPhase::Suspended,
(ActorPhase::Running, LifecycleEvent::Started) => phase,
(ActorPhase::Running, LifecycleEvent::StartPanicked) => phase,
(ActorPhase::Running, LifecycleEvent::Resumed) => phase,
// Suspended: waiting for resume or stop
(ActorPhase::Suspended, LifecycleEvent::Resumed) => ActorPhase::Running,
(ActorPhase::Suspended, LifecycleEvent::StopRequested) => ActorPhase::Stopping,
(ActorPhase::Suspended, LifecycleEvent::StopWithRequested) => ActorPhase::Stopping,
(ActorPhase::Suspended, LifecycleEvent::Started) => phase,
(ActorPhase::Suspended, LifecycleEvent::StartPanicked) => phase,
(ActorPhase::Suspended, LifecycleEvent::MessagePanicked) => phase,
(ActorPhase::Suspended, LifecycleEvent::SuspendRequested) => phase,
// Terminal states: Stopping and Poisoned absorb all events
(ActorPhase::Stopping, _) => ActorPhase::Stopping,
(ActorPhase::Poisoned, _) => ActorPhase::Poisoned,
}
}
// TLA+: MailboxAccept
/// Decide what to do with an incoming message given mailbox state and policy.
///
/// `capacity` of 0 means unbounded (always accept). When the mailbox is full,
/// the overflow policy determines whether the newest or oldest message is dropped.
#[must_use]
pub fn mailbox_accept(
mailbox_len: usize,
capacity: usize,
policy: MailboxOverflow,
) -> MailboxDecision {
if capacity == 0 || mailbox_len < capacity {
MailboxDecision::Accept
} else {
match policy {
MailboxOverflow::DropNewest => MailboxDecision::RejectNewest,
MailboxOverflow::DropOldest => MailboxDecision::EvictOldest,
}
}
}
// TLA+: ShouldTickActor
/// Whether an actor in the given phase should be ticked (i.e., have messages processed).
///
/// Returns `false` for `Poisoned`, `Stopping`, and `Suspended` — these actors
/// either cannot process messages or must not.
#[must_use]
pub fn should_tick_actor(phase: ActorPhase) -> bool {
match phase {
ActorPhase::Unstarted | ActorPhase::Running => true,
ActorPhase::Suspended | ActorPhase::Stopping | ActorPhase::Poisoned => false,
}
}
// TLA+: BudgetExhausted
/// Whether the per-actor message budget has been exhausted.
///
/// `budget` of 0 means unlimited — never exhausted.
#[must_use]
pub fn budget_exhausted(processed: usize, budget: usize) -> bool {
budget > 0 && processed >= budget
}
// TLA+: CleanupStopReason
/// Determine the stop reason for a dead actor given its phase and whether
/// it has a typed exit value.
///
/// - `Poisoned` → `StopReason::Panicked`
/// - Has exit value → `StopReason::Completed`
/// - Otherwise → `StopReason::Normal`
#[must_use]
pub fn cleanup_stop_reason(phase: ActorPhase, has_exit_value: bool) -> StopReason {
if phase == ActorPhase::Poisoned {
StopReason::Panicked
} else if has_exit_value {
StopReason::Completed
} else {
StopReason::Normal
}
}
// ─── Invariant Predicates ───────────────────────────────────────────────────
// TLA+: PhaseIsTerminal
/// Returns `true` if the phase is terminal — the actor will not process any
/// further messages and is awaiting cleanup.
#[must_use]
pub fn phase_is_terminal(phase: ActorPhase) -> bool {
match phase {
ActorPhase::Stopping | ActorPhase::Poisoned => true,
ActorPhase::Unstarted | ActorPhase::Running | ActorPhase::Suspended => false,
}
}
// TLA+: ValidPhaseTransition
/// Returns `true` only for transitions that exist in the lifecycle state machine.
///
/// This is an invariant predicate — useful for property-based testing and Kani
/// harnesses to verify that no illegal transition is ever taken.
#[must_use]
pub fn valid_phase_transition(from: ActorPhase, to: ActorPhase) -> bool {
match (from, to) {
// Self-transitions are always valid (no-op events)
(a, b) if a == b => true,
// From Unstarted
(ActorPhase::Unstarted, ActorPhase::Running) => true,
(ActorPhase::Unstarted, ActorPhase::Poisoned) => true,
(ActorPhase::Unstarted, ActorPhase::Stopping) => true,
(ActorPhase::Unstarted, ActorPhase::Suspended) => true,
// From Running
(ActorPhase::Running, ActorPhase::Poisoned) => true,
(ActorPhase::Running, ActorPhase::Stopping) => true,
(ActorPhase::Running, ActorPhase::Suspended) => true,
// From Suspended
(ActorPhase::Suspended, ActorPhase::Running) => true,
(ActorPhase::Suspended, ActorPhase::Stopping) => true,
// Terminal states never transition out
(ActorPhase::Stopping, _) => false,
(ActorPhase::Poisoned, _) => false,
// Everything else is invalid
_ => false,
}
}

View file

@ -1,7 +1,9 @@
use std::any::Any; #[cfg(not(feature = "std"))]
use std::any::TypeId; use crate::compat::{Box, String, Vec};
use std::collections::{HashMap, HashSet};
use std::sync::Arc; use core::any::{Any, TypeId};
use crate::compat::{Arc, TypeIdMap, TypeIdSet};
use crate::delivery::AddrSet;
use crate::Error; use crate::Error;
@ -36,8 +38,8 @@ impl PartialEq for ExitValue {
impl Eq for ExitValue {} impl Eq for ExitValue {}
impl std::fmt::Debug for ExitValue { impl core::fmt::Debug for ExitValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("ExitValue(..)") f.write_str("ExitValue(..)")
} }
} }
@ -55,36 +57,60 @@ pub struct SystemInfo {
pub uptime_ms: u64, pub uptime_ms: u64,
} }
/// Why an actor exited. pub use swactor_core::ExitReason;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ExitReason {
/// Actor was explicitly stopped or removed from the pool.
Stopped,
/// Actor panicked during message handling.
Panicked,
/// The node hosting the actor left the cluster (SWIM Dead).
NodeDown,
/// Actor stopped with a typed exit value (via [`Ctx::stop_with`]).
Completed,
}
/// Delivered to watchers when a watched actor exits. /// Delivered to watchers when a watched actor exits.
/// ///
/// Implements `Message` (Clone + Send + Sync + 'static) so it can be /// Implements `Message` (Clone + Send + Sync + 'static) so it can be
/// delivered through normal mailbox channels. /// delivered through normal mailbox channels.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ActorExited { pub struct ActorExited {
/// The address of the actor that died. /// The address of the actor that died.
pub addr: ActorAddress, pub addr: ActorAddress,
/// Why it exited. /// Why it exited.
pub reason: ExitReason, pub reason: ExitReason,
/// Typed exit value if the actor called [`Ctx::stop_with`]. /// Typed exit value if the actor called [`Ctx::stop_with`].
#[cfg_attr(feature = "serde", serde(skip))]
pub exit_value: Option<ExitValue>, pub exit_value: Option<ExitValue>,
} }
#[cfg(feature = "serde")]
impl serde::Serialize for ActorExited {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut s = serializer.serialize_struct("ActorExited", 2)?;
s.serialize_field("addr", &self.addr)?;
let reason_str = match self.reason {
ExitReason::Stopped => "Stopped",
ExitReason::Panicked => "Panicked",
ExitReason::NodeDown => "NodeDown",
ExitReason::Completed => "Completed",
};
s.serialize_field("reason", reason_str)?;
s.end()
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for ActorExited {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(serde::Deserialize)]
struct Helper {
addr: ActorAddress,
reason: HelperExitReason,
}
#[derive(serde::Deserialize)]
enum HelperExitReason { Stopped, Panicked, NodeDown, Completed }
let h = Helper::deserialize(deserializer)?;
let reason = match h.reason {
HelperExitReason::Stopped => ExitReason::Stopped,
HelperExitReason::Panicked => ExitReason::Panicked,
HelperExitReason::NodeDown => ExitReason::NodeDown,
HelperExitReason::Completed => ExitReason::Completed,
};
Ok(ActorExited { addr: h.addr, reason, exit_value: None })
}
}
impl PartialEq for ActorExited { impl PartialEq for ActorExited {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.addr == other.addr && self.reason == other.reason self.addr == other.addr && self.reason == other.reason
@ -140,9 +166,9 @@ pub struct ActorAddress(pub [u8; 32]);
/// Custom Hash: only hash the first 8 bytes since all 32 are random. /// Custom Hash: only hash the first 8 bytes since all 32 are random.
/// SipHash on 8 bytes is ~3x faster than on 32 bytes, with identical /// SipHash on 8 bytes is ~3x faster than on 32 bytes, with identical
/// collision properties (2^64 possible values from cryptographic randomness). /// collision properties (2^64 possible values from cryptographic randomness).
impl std::hash::Hash for ActorAddress { impl core::hash::Hash for ActorAddress {
#[inline] #[inline]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
// SAFETY: ActorAddress is always 32 bytes, so [..8] is valid. // SAFETY: ActorAddress is always 32 bytes, so [..8] is valid.
state.write_u64(u64::from_ne_bytes( state.write_u64(u64::from_ne_bytes(
self.0[..8].try_into().unwrap(), self.0[..8].try_into().unwrap(),
@ -150,8 +176,20 @@ impl std::hash::Hash for ActorAddress {
} }
} }
impl std::fmt::Display for ActorAddress { impl PartialOrd for ActorAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for ActorAddress {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.0.cmp(&other.0)
}
}
impl core::fmt::Display for ActorAddress {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
for b in &self.0[..8] { for b in &self.0[..8] {
write!(f, "{:02x}", b)?; write!(f, "{:02x}", b)?;
} }
@ -177,14 +215,14 @@ impl ActorAddress {
/// can clone individual entries cheaply (Arc bump) for copy-on-write overrides. /// can clone individual entries cheaply (Arc bump) for copy-on-write overrides.
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct Environment { pub struct Environment {
inner: Arc<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>, inner: Arc<TypeIdMap<Arc<dyn Any + Send + Sync>>>,
} }
impl Environment { impl Environment {
/// Create an empty environment. /// Create an empty environment.
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
inner: Arc::new(HashMap::new()), inner: Arc::new(TypeIdMap::new()),
} }
} }
@ -223,14 +261,14 @@ impl Environment {
/// ///
/// Allows inserting/replacing typed values before freezing into an immutable `Environment`. /// Allows inserting/replacing typed values before freezing into an immutable `Environment`.
pub struct EnvironmentBuilder { pub struct EnvironmentBuilder {
map: HashMap<TypeId, Arc<dyn Any + Send + Sync>>, map: TypeIdMap<Arc<dyn Any + Send + Sync>>,
} }
impl EnvironmentBuilder { impl EnvironmentBuilder {
/// Create an empty builder. /// Create an empty builder.
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
map: HashMap::new(), map: TypeIdMap::new(),
} }
} }
@ -304,14 +342,14 @@ impl LogicalName {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ServiceBinding<S: 'static + Send + Sync> { pub struct ServiceBinding<S: 'static + Send + Sync> {
pub addr: ActorAddress, pub addr: ActorAddress,
_marker: std::marker::PhantomData<S>, _marker: core::marker::PhantomData<S>,
} }
impl<S: 'static + Send + Sync> ServiceBinding<S> { impl<S: 'static + Send + Sync> ServiceBinding<S> {
pub fn new(addr: ActorAddress) -> Self { pub fn new(addr: ActorAddress) -> Self {
Self { Self {
addr, addr,
_marker: std::marker::PhantomData, _marker: core::marker::PhantomData,
} }
} }
} }
@ -328,11 +366,11 @@ impl<S: 'static + Send + Sync> ServiceBinding<S> {
/// Built via fluent API: `CapabilitySet::new().with_send(addr).with_spawn()`. /// Built via fluent API: `CapabilitySet::new().with_send(addr).with_spawn()`.
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct CapabilitySet { pub struct CapabilitySet {
send_any: HashSet<ActorAddress, crate::delivery::AddrBuildHasher>, send_any: AddrSet,
send_typed: HashSet<(TypeId, ActorAddress)>, send_typed: crate::compat::TypedAddrSet,
can_spawn: bool, can_spawn: bool,
service_types: HashSet<TypeId>, service_types: TypeIdSet,
monitor_targets: HashSet<ActorAddress, crate::delivery::AddrBuildHasher>, monitor_targets: AddrSet,
} }
impl CapabilitySet { impl CapabilitySet {
@ -386,8 +424,8 @@ impl CapabilitySet {
} }
} }
impl std::fmt::Debug for CapabilitySet { impl core::fmt::Debug for CapabilitySet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("CapabilitySet") f.debug_struct("CapabilitySet")
.field("send_any", &self.send_any.len()) .field("send_any", &self.send_any.len())
.field("send_typed", &self.send_typed.len()) .field("send_typed", &self.send_typed.len())
@ -442,7 +480,7 @@ where
let msg = match msg.downcast::<A::Incoming>() { let msg = match msg.downcast::<A::Incoming>() {
Ok(typed) => { Ok(typed) => {
self.inner.handle(ctx, *typed); self.inner.handle(ctx, *typed);
return Some(std::any::type_name::<A::Incoming>()); return Some(core::any::type_name::<A::Incoming>());
} }
Err(msg) => msg, Err(msg) => msg,
}; };
@ -484,16 +522,7 @@ impl MonitorRef {
} }
} }
/// Reason an actor was removed from the runtime. pub use swactor_core::StopReason;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StopReason {
/// Graceful stop (via `ctx.stop_self()` or `Runtime::stop_actor()`).
Normal,
/// Actor panicked and could not be restarted.
Panicked,
/// Actor stopped with a typed exit value (via [`Ctx::stop_with`]).
Completed,
}
/// Death notification delivered as a normal message when a monitored actor dies. /// Death notification delivered as a normal message when a monitored actor dies.
/// ///

View file

@ -1,4 +1,4 @@
use std::sync::Arc; use crate::compat::Arc;
use crossbeam_queue::{ArrayQueue, SegQueue}; use crossbeam_queue::{ArrayQueue, SegQueue};

190
src/compat.rs Normal file
View file

@ -0,0 +1,190 @@
// Compatibility module: re-exports types from std or core/alloc depending on feature flags.
//
// Core types (Any, fmt, atomics, etc.) are imported directly from `core::` in each file.
// This module provides conditional re-exports for types that differ between std and no_std.
// --- alloc types (conditional on std feature) ---
#[cfg(feature = "std")]
pub use std::sync::Arc;
#[cfg(not(feature = "std"))]
pub use alloc::sync::Arc;
#[cfg(feature = "std")]
pub use std::collections::VecDeque;
#[cfg(not(feature = "std"))]
pub use alloc::collections::VecDeque;
// Vec, String, Box — in std prelude normally, but need explicit import in no_std.
// vec! and format! macros are available via `extern crate alloc` in lib.rs.
#[cfg(not(feature = "std"))]
pub use alloc::{boxed::Box, string::String, string::ToString, vec::Vec};
// --- sync primitives (std vs spin-based) ---
#[cfg(feature = "std")]
pub use std::sync::{RwLock, OnceLock};
#[cfg(not(feature = "std"))]
pub use crate::sync_impl::SpinRwLock as RwLock;
// --- collections (std vs alloc-based alternatives) ---
#[cfg(feature = "std")]
pub use std::collections::HashMap;
#[cfg(not(feature = "std"))]
pub use alloc::collections::BTreeMap as HashMap;
// --- TypeId-keyed map/set ---
//
// TypeId does not implement Ord, so BTreeMap cannot be used in no_std.
// In std mode: thin wrappers around HashMap/HashSet.
// In no_std mode: linear-scan Vec wrappers (<10 entries typical).
use core::any::TypeId as StdTypeId;
/// A map keyed by `TypeId`.
/// std: HashMap<TypeId, V>. no_std: linear-scan Vec.
#[derive(Clone)]
pub struct TypeIdMap<V> {
#[cfg(feature = "std")]
inner: std::collections::HashMap<StdTypeId, V>,
#[cfg(not(feature = "std"))]
inner: Vec<(StdTypeId, V)>,
}
impl<V> TypeIdMap<V> {
pub fn new() -> Self {
Self {
#[cfg(feature = "std")]
inner: std::collections::HashMap::new(),
#[cfg(not(feature = "std"))]
inner: Vec::new(),
}
}
#[cfg(feature = "std")]
pub fn get(&self, key: &StdTypeId) -> Option<&V> {
self.inner.get(key)
}
#[cfg(not(feature = "std"))]
pub fn get(&self, key: &StdTypeId) -> Option<&V> {
self.inner.iter().find(|(k, _)| k == key).map(|(_, v)| v)
}
#[cfg(feature = "std")]
pub fn contains_key(&self, key: &StdTypeId) -> bool {
self.inner.contains_key(key)
}
#[cfg(not(feature = "std"))]
pub fn contains_key(&self, key: &StdTypeId) -> bool {
self.inner.iter().any(|(k, _)| k == key)
}
#[cfg(feature = "std")]
pub fn insert(&mut self, key: StdTypeId, value: V) -> Option<V> {
self.inner.insert(key, value)
}
#[cfg(not(feature = "std"))]
pub fn insert(&mut self, key: StdTypeId, value: V) -> Option<V> {
for entry in &mut self.inner {
if entry.0 == key {
let old = core::mem::replace(&mut entry.1, value);
return Some(old);
}
}
self.inner.push((key, value));
None
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
impl<V> Default for TypeIdMap<V> {
fn default() -> Self {
Self::new()
}
}
/// A set of `TypeId` values.
/// std: HashSet<TypeId>. no_std: linear-scan Vec.
#[derive(Clone, Default)]
pub struct TypeIdSet {
#[cfg(feature = "std")]
inner: std::collections::HashSet<StdTypeId>,
#[cfg(not(feature = "std"))]
inner: Vec<StdTypeId>,
}
impl TypeIdSet {
#[allow(dead_code)]
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, id: StdTypeId) -> bool {
#[cfg(feature = "std")]
{ self.inner.insert(id) }
#[cfg(not(feature = "std"))]
{
if self.inner.contains(&id) {
false
} else {
self.inner.push(id);
true
}
}
}
pub fn contains(&self, id: &StdTypeId) -> bool {
self.inner.contains(id)
}
pub fn len(&self) -> usize {
self.inner.len()
}
}
/// A set of `(TypeId, ActorAddress)` pairs.
/// Used for typed send capabilities. std: HashSet. no_std: linear-scan Vec.
#[derive(Clone, Default)]
pub struct TypedAddrSet {
#[cfg(feature = "std")]
inner: std::collections::HashSet<(StdTypeId, crate::actor::ActorAddress)>,
#[cfg(not(feature = "std"))]
inner: Vec<(StdTypeId, crate::actor::ActorAddress)>,
}
impl TypedAddrSet {
#[allow(dead_code)]
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, pair: (StdTypeId, crate::actor::ActorAddress)) -> bool {
#[cfg(feature = "std")]
{ self.inner.insert(pair) }
#[cfg(not(feature = "std"))]
{
if self.inner.contains(&pair) {
false
} else {
self.inner.push(pair);
true
}
}
}
pub fn contains(&self, pair: &(StdTypeId, crate::actor::ActorAddress)) -> bool {
self.inner.contains(pair)
}
pub fn len(&self) -> usize {
self.inner.len()
}
}

View file

@ -1,11 +1,4 @@
/// What to do when a bounded mailbox is full. pub use swactor_core::MailboxOverflow;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MailboxOverflow {
/// Drop the incoming message (newest). The message is silently discarded.
DropNewest,
/// Drop the oldest message in the queue to make room for the new one.
DropOldest,
}
/// The tunable settings for the runtime. /// The tunable settings for the runtime.
pub struct RuntimeConfig { pub struct RuntimeConfig {

View file

@ -1,8 +1,16 @@
use std::any::Any; #[cfg(not(feature = "std"))]
use std::collections::{HashMap, HashSet}; use crate::compat::{Box, Vec};
use std::hash::{BuildHasher, Hasher};
use std::sync::atomic::{AtomicUsize, Ordering}; use core::any::Any;
use std::sync::{Arc, OnceLock, RwLock}; #[cfg(feature = "std")]
use core::hash::{BuildHasher, Hasher};
#[cfg(feature = "std")]
use std::collections::HashMap;
use core::sync::atomic::{AtomicUsize, Ordering};
use crate::compat::{Arc, RwLock};
#[cfg(feature = "std")]
use crate::compat::OnceLock;
#[cfg(feature = "std")]
use std::thread::Thread; use std::thread::Thread;
use crate::actor::{ActorAddress, Message, SpawnRequest}; use crate::actor::{ActorAddress, Message, SpawnRequest};
@ -21,8 +29,10 @@ use crate::Error;
/// ///
/// This is safe because the input is already random (uniform distribution), /// This is safe because the input is already random (uniform distribution),
/// so additional mixing would be redundant. /// so additional mixing would be redundant.
#[cfg(feature = "std")]
pub struct AddrHasher(u64); pub struct AddrHasher(u64);
#[cfg(feature = "std")]
impl Hasher for AddrHasher { impl Hasher for AddrHasher {
#[inline] #[inline]
fn finish(&self) -> u64 { fn finish(&self) -> u64 {
@ -41,9 +51,11 @@ impl Hasher for AddrHasher {
} }
/// BuildHasher for creating AddrHasher instances. /// BuildHasher for creating AddrHasher instances.
#[cfg(feature = "std")]
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub struct AddrBuildHasher; pub struct AddrBuildHasher;
#[cfg(feature = "std")]
impl BuildHasher for AddrBuildHasher { impl BuildHasher for AddrBuildHasher {
type Hasher = AddrHasher; type Hasher = AddrHasher;
@ -55,10 +67,18 @@ impl BuildHasher for AddrBuildHasher {
/// HashMap optimized for ActorAddress keys. /// HashMap optimized for ActorAddress keys.
/// Uses identity hashing since ActorAddress bytes are already random. /// Uses identity hashing since ActorAddress bytes are already random.
/// In no_std: BTreeMap using ActorAddress's Ord impl.
#[cfg(feature = "std")]
pub type AddrMap<V> = HashMap<ActorAddress, V, AddrBuildHasher>; pub type AddrMap<V> = HashMap<ActorAddress, V, AddrBuildHasher>;
#[cfg(not(feature = "std"))]
pub type AddrMap<V> = alloc::collections::BTreeMap<ActorAddress, V>;
/// HashSet optimized for ActorAddress keys. /// HashSet optimized for ActorAddress keys.
pub type AddrSet = HashSet<ActorAddress, AddrBuildHasher>; /// In no_std: BTreeSet using ActorAddress's Ord impl.
#[cfg(feature = "std")]
pub type AddrSet = std::collections::HashSet<ActorAddress, AddrBuildHasher>;
#[cfg(not(feature = "std"))]
pub type AddrSet = alloc::collections::BTreeSet<ActorAddress>;
// ─── Address Map Types ─────────────────────────────────────────────────────── // ─── Address Map Types ───────────────────────────────────────────────────────
@ -80,12 +100,20 @@ pub(crate) struct AddressMap {
} }
impl AddressMap { impl AddressMap {
#[cfg(feature = "std")]
pub fn with_capacity(cap: usize) -> Self { pub fn with_capacity(cap: usize) -> Self {
Self { Self {
inner: RwLock::new(HashMap::with_capacity_and_hasher(cap, AddrBuildHasher)), inner: RwLock::new(HashMap::with_capacity_and_hasher(cap, AddrBuildHasher)),
} }
} }
#[cfg(not(feature = "std"))]
pub fn with_capacity(_cap: usize) -> Self {
Self {
inner: RwLock::new(AddrMap::new()),
}
}
pub fn insert(&self, addr: ActorAddress, worker: WorkerId) { pub fn insert(&self, addr: ActorAddress, worker: WorkerId) {
self.inner.write().unwrap().insert(addr, worker); self.inner.write().unwrap().insert(addr, worker);
} }
@ -200,18 +228,26 @@ pub(crate) struct InboxRegistry {
} }
impl InboxRegistry { impl InboxRegistry {
#[cfg(feature = "std")]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
senders: RwLock::new(HashMap::with_hasher(AddrBuildHasher)), senders: RwLock::new(HashMap::with_hasher(AddrBuildHasher)),
} }
} }
#[cfg(not(feature = "std"))]
pub fn new() -> Self {
Self {
senders: RwLock::new(AddrMap::new()),
}
}
pub fn register(&self, addr: ActorAddress, sender: Arc<dyn SenderT>) { pub fn register(&self, addr: ActorAddress, sender: Arc<dyn SenderT>) {
self.senders.write().unwrap().insert(addr, sender); self.senders.write().unwrap().insert(addr, sender);
} }
/// Check if an address is registered without consuming a message. /// Check if an address is registered without consuming a message.
#[cfg(feature = "transport")] #[cfg(all(feature = "transport", feature = "std"))]
pub fn contains(&self, addr: &ActorAddress) -> bool { pub fn contains(&self, addr: &ActorAddress) -> bool {
self.senders.read().unwrap().contains_key(addr) self.senders.read().unwrap().contains_key(addr)
} }
@ -242,14 +278,16 @@ pub(crate) struct TickContext<'a> {
pub(crate) extension: Option<&'a dyn crate::extension::RuntimeExtension>, pub(crate) extension: Option<&'a dyn crate::extension::RuntimeExtension>,
pub(crate) stats_hook: Option<&'a dyn crate::stats::StatsHook>, pub(crate) stats_hook: Option<&'a dyn crate::stats::StatsHook>,
/// Thread handles for waking parked workers on cross-worker sends. /// Thread handles for waking parked workers on cross-worker sends.
#[cfg(feature = "std")]
pub(crate) worker_threads: &'a [OnceLock<Thread>], pub(crate) worker_threads: &'a [OnceLock<Thread>],
/// Per-worker stats for summing total_actors across workers. /// Per-worker stats for summing total_actors across workers.
pub(crate) worker_stats: &'a [Arc<WorkerStats>], pub(crate) worker_stats: &'a [Arc<WorkerStats>],
/// Runtime creation time for computing uptime_ms. /// Runtime creation time for computing uptime_ms.
#[cfg(feature = "std")]
pub(crate) created_at: crate::Instant, pub(crate) created_at: crate::Instant,
#[cfg(feature = "transport")] #[cfg(all(feature = "transport", feature = "std"))]
pub(crate) codec_registry: Option<&'a crate::transport::CodecRegistry>, pub(crate) codec_registry: Option<&'a crate::transport::CodecRegistry>,
#[cfg(feature = "transport")] #[cfg(all(feature = "transport", feature = "std"))]
pub(crate) transport_router: Option<&'a crate::transport::TransportRouter>, pub(crate) transport_router: Option<&'a crate::transport::TransportRouter>,
} }
@ -261,7 +299,7 @@ impl<'a> TickContext<'a> {
addr: ActorAddress, addr: ActorAddress,
msg: Box<dyn Any + Send>, msg: Box<dyn Any + Send>,
) -> Result<(), Error> { ) -> Result<(), Error> {
#[cfg(feature = "transport")] #[cfg(all(feature = "transport", feature = "std"))]
{ {
if self.inbox_registry.contains(&addr) { if self.inbox_registry.contains(&addr) {
return self.inbox_registry.try_deliver(addr, msg); return self.inbox_registry.try_deliver(addr, msg);

View file

@ -1,3 +1,6 @@
#[cfg(not(feature = "std"))]
use crate::compat::{Box, String, ToString};
/// Simple, ergonomic, local `Error` type. /// Simple, ergonomic, local `Error` type.
/// # Usage /// # Usage
/// ``` /// ```
@ -13,16 +16,29 @@
/// } /// }
/// ``` /// ```
#[derive(Debug)] #[derive(Debug)]
pub struct Error(Box<dyn std::error::Error + Send + Sync + 'static>); pub struct Error(Box<dyn core::error::Error + Send + Sync + 'static>);
/// Small wrapper so `String` can become `Box<dyn core::error::Error>` in both
/// std and no_std (where String doesn't auto-impl core::error::Error via blanket).
#[derive(Debug)]
struct StringError(String);
impl core::fmt::Display for StringError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(&self.0)
}
}
impl core::error::Error for StringError {}
impl<T: AsRef<str>> From<T> for Error { impl<T: AsRef<str>> From<T> for Error {
fn from(value: T) -> Self { fn from(value: T) -> Self {
Error(value.as_ref().to_string().into()) Error(Box::new(StringError(value.as_ref().to_string())))
} }
} }
impl std::fmt::Display for Error { impl core::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.0) write!(f, "{}", self.0)
} }
} }

View file

@ -1,4 +1,7 @@
use std::any::Any; #[cfg(not(feature = "std"))]
use crate::compat::{Box, Vec};
use core::any::Any;
use crate::actor::{ActorAddress, Environment, ExitValue, StopReason}; use crate::actor::{ActorAddress, Environment, ExitValue, StopReason};

View file

@ -1,3 +1,12 @@
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(not(feature = "std"))]
extern crate alloc;
pub(crate) mod compat;
#[cfg(not(feature = "std"))]
pub(crate) mod sync_impl;
pub mod actor; pub mod actor;
pub mod extension; pub mod extension;
pub mod worker; pub mod worker;
@ -10,7 +19,9 @@ pub(crate) mod error;
pub use error::Error; pub use error::Error;
// Re-export identity hashing types for ActorAddress-keyed collections. // Re-export identity hashing types for ActorAddress-keyed collections.
pub use delivery::{AddrBuildHasher, AddrMap, AddrSet}; #[cfg(feature = "std")]
pub use delivery::AddrBuildHasher;
pub use delivery::{AddrMap, AddrSet};
pub mod config; pub mod config;
pub(crate) mod delivery; pub(crate) mod delivery;
@ -18,7 +29,7 @@ pub mod stats;
pub mod runtime; pub mod runtime;
#[cfg(feature = "transport")] #[cfg(all(feature = "transport", feature = "std"))]
pub mod transport; pub mod transport;
#[cfg(feature = "std")] #[cfg(feature = "std")]
@ -26,9 +37,10 @@ pub mod std;
// Platform-aware Instant: web_time on wasm, std::time on native. // Platform-aware Instant: web_time on wasm, std::time on native.
// web_time is a no-op re-export of std::time::Instant on non-wasm targets. // web_time is a no-op re-export of std::time::Instant on non-wasm targets.
// In no_std (without wasm), Instant is not available — timing returns 0.
#[cfg(feature = "wasm")] #[cfg(feature = "wasm")]
pub(crate) use web_time::Instant; pub(crate) use web_time::Instant;
#[cfg(not(feature = "wasm"))] #[cfg(all(not(feature = "wasm"), feature = "std"))]
pub(crate) use ::std::time::Instant; pub(crate) use ::std::time::Instant;
#[cfg(feature = "getrandom")] #[cfg(feature = "getrandom")]
@ -36,7 +48,7 @@ pub(crate) fn get_random(buf: &mut [u8]) {
getrandom::getrandom(buf).unwrap() getrandom::getrandom(buf).unwrap()
} }
#[cfg(all(feature = "no_random", not(feature = "getrandom")))] #[cfg(not(feature = "getrandom"))]
pub(crate) fn get_random(buf: &mut [u8]) { pub(crate) fn get_random(buf: &mut [u8]) {
use core::sync::atomic::{AtomicUsize, Ordering}; use core::sync::atomic::{AtomicUsize, Ordering};

View file

@ -1,11 +1,16 @@
use std::any::Any; #[cfg(not(feature = "std"))]
use std::cell::RefCell; use crate::compat::{Box, Vec};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock}; use core::any::Any;
#[cfg(not(target_arch = "wasm32"))] use core::cell::RefCell;
use core::sync::atomic::{AtomicBool, Ordering};
use crate::compat::Arc;
#[cfg(feature = "std")]
use crate::compat::OnceLock;
#[cfg(all(not(target_arch = "wasm32"), feature = "std"))]
use std::thread::{self, JoinHandle}; use std::thread::{self, JoinHandle};
#[cfg(feature = "std")]
use std::thread::Thread; use std::thread::Thread;
use crate::Instant;
use crate::actor::{Actor, ActorAddress, ActorInterface, AnyActor, Environment, ExitValue, Message, ResumeSignal, SpawnRequest, StopSignal, StopWithSignal, SystemInfo}; use crate::actor::{Actor, ActorAddress, ActorInterface, AnyActor, Environment, ExitValue, Message, ResumeSignal, SpawnRequest, StopSignal, StopWithSignal, SystemInfo};
use crate::channel::{Receiver, Sender}; use crate::channel::{Receiver, Sender};
@ -69,13 +74,13 @@ impl<R: Message> Ask<R> {
} }
/// Handle for dealing with a runtime that has started via the `Runtime::run()` method. /// Handle for dealing with a runtime that has started via the `Runtime::run()` method.
#[cfg(not(target_arch = "wasm32"))] #[cfg(all(not(target_arch = "wasm32"), feature = "std"))]
pub struct RuntimeHandle { pub struct RuntimeHandle {
pub runtime: Arc<Runtime>, pub runtime: Arc<Runtime>,
threads: Vec<JoinHandle<()>>, threads: Vec<JoinHandle<()>>,
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(all(not(target_arch = "wasm32"), feature = "std"))]
impl RuntimeHandle { impl RuntimeHandle {
pub fn join(self) { pub fn join(self) {
for handle in self.threads { for handle in self.threads {
@ -110,11 +115,13 @@ pub struct Runtime {
/// Workers available for tick(). run() drains this and moves workers to threads. /// Workers available for tick(). run() drains this and moves workers to threads.
tick_workers: RefCell<Vec<Worker>>, tick_workers: RefCell<Vec<Worker>>,
/// Thread handles for waking parked workers. Set by workers on startup via OnceLock. /// Thread handles for waking parked workers. Set by workers on startup via OnceLock.
#[cfg(feature = "std")]
worker_threads: Arc<Vec<OnceLock<Thread>>>, worker_threads: Arc<Vec<OnceLock<Thread>>>,
created_at: Instant, #[cfg(feature = "std")]
#[cfg(feature = "transport")] created_at: crate::Instant,
#[cfg(all(feature = "transport", feature = "std"))]
codec_registry: Option<Arc<crate::transport::CodecRegistry>>, codec_registry: Option<Arc<crate::transport::CodecRegistry>>,
#[cfg(feature = "transport")] #[cfg(all(feature = "transport", feature = "std"))]
transport_router: Option<Arc<crate::transport::TransportRouter>>, transport_router: Option<Arc<crate::transport::TransportRouter>>,
} }
@ -129,8 +136,8 @@ unsafe impl Sync for Runtime {}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RuntimeAddress(pub [u8; 32]); pub struct RuntimeAddress(pub [u8; 32]);
impl std::fmt::Display for RuntimeAddress { impl core::fmt::Display for RuntimeAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
for b in &self.0[..8] { for b in &self.0[..8] {
write!(f, "{:02x}", b)?; write!(f, "{:02x}", b)?;
} }
@ -155,6 +162,7 @@ impl RuntimeAddress {
pub struct ExternalSender { pub struct ExternalSender {
address_map: Arc<AddressMap>, address_map: Arc<AddressMap>,
transfer_txs: Vec<Sender<Envelope>>, transfer_txs: Vec<Sender<Envelope>>,
#[cfg(feature = "std")]
worker_threads: Arc<Vec<OnceLock<Thread>>>, worker_threads: Arc<Vec<OnceLock<Thread>>>,
} }
@ -163,6 +171,7 @@ impl Clone for ExternalSender {
Self { Self {
address_map: self.address_map.clone(), address_map: self.address_map.clone(),
transfer_txs: self.transfer_txs.clone(), transfer_txs: self.transfer_txs.clone(),
#[cfg(feature = "std")]
worker_threads: self.worker_threads.clone(), worker_threads: self.worker_threads.clone(),
} }
} }
@ -182,6 +191,7 @@ impl ExternalSender {
Some(wid) => { Some(wid) => {
self.transfer_txs[wid.as_usize()] self.transfer_txs[wid.as_usize()]
.send(Envelope::new(addr, Box::new(msg))); .send(Envelope::new(addr, Box::new(msg)));
#[cfg(feature = "std")]
notify_worker(&self.worker_threads, wid.as_usize()); notify_worker(&self.worker_threads, wid.as_usize());
Ok(()) Ok(())
} }
@ -232,6 +242,7 @@ impl Runtime {
let placement = Placement::new(num_workers, worker_stats.clone()); let placement = Placement::new(num_workers, worker_stats.clone());
#[cfg(feature = "std")]
let worker_threads: Arc<Vec<OnceLock<Thread>>> = let worker_threads: Arc<Vec<OnceLock<Thread>>> =
Arc::new((0..num_workers).map(|_| OnceLock::new()).collect()); Arc::new((0..num_workers).map(|_| OnceLock::new()).collect());
@ -247,11 +258,13 @@ impl Runtime {
worker_stats, worker_stats,
stats_hook: None, stats_hook: None,
tick_workers: RefCell::new(workers), tick_workers: RefCell::new(workers),
#[cfg(feature = "std")]
worker_threads, worker_threads,
created_at: Instant::now(), #[cfg(feature = "std")]
#[cfg(feature = "transport")] created_at: crate::Instant::now(),
#[cfg(all(feature = "transport", feature = "std"))]
codec_registry: None, codec_registry: None,
#[cfg(feature = "transport")] #[cfg(all(feature = "transport", feature = "std"))]
transport_router: None, transport_router: None,
}; };
@ -369,6 +382,7 @@ impl Runtime {
ExternalSender { ExternalSender {
address_map: self.address_map.clone(), address_map: self.address_map.clone(),
transfer_txs: self.transfer_txs.iter().cloned().collect(), transfer_txs: self.transfer_txs.iter().cloned().collect(),
#[cfg(feature = "std")]
worker_threads: self.worker_threads.clone(), worker_threads: self.worker_threads.clone(),
} }
} }
@ -383,12 +397,14 @@ impl Runtime {
config: &self.config, config: &self.config,
extension: self.extension.as_deref(), extension: self.extension.as_deref(),
stats_hook: self.stats_hook.as_deref(), stats_hook: self.stats_hook.as_deref(),
#[cfg(feature = "std")]
worker_threads: &self.worker_threads, worker_threads: &self.worker_threads,
worker_stats: &self.worker_stats, worker_stats: &self.worker_stats,
#[cfg(feature = "std")]
created_at: self.created_at, created_at: self.created_at,
#[cfg(feature = "transport")] #[cfg(all(feature = "transport", feature = "std"))]
codec_registry: self.codec_registry.as_deref(), codec_registry: self.codec_registry.as_deref(),
#[cfg(feature = "transport")] #[cfg(all(feature = "transport", feature = "std"))]
transport_router: self.transport_router.as_deref(), transport_router: self.transport_router.as_deref(),
} }
} }
@ -414,7 +430,7 @@ impl Runtime {
/// In single-threaded mode, one background thread is spawned. /// In single-threaded mode, one background thread is spawned.
/// ///
/// Not available on wasm32 — use the browser crate's Web Worker-based run instead. /// Not available on wasm32 — use the browser crate's Web Worker-based run instead.
#[cfg(not(target_arch = "wasm32"))] #[cfg(all(not(target_arch = "wasm32"), feature = "std"))]
pub fn run(self) -> Result<RuntimeHandle, Error> { pub fn run(self) -> Result<RuntimeHandle, Error> {
self.is_running.store(true, Ordering::Release); self.is_running.store(true, Ordering::Release);
@ -464,7 +480,10 @@ impl Runtime {
.map(|ws| ws.drain_tick_timings()) .map(|ws| ws.drain_tick_timings())
.collect(); .collect();
#[cfg(feature = "std")]
let uptime_ms = self.created_at.elapsed().as_millis() as u64; let uptime_ms = self.created_at.elapsed().as_millis() as u64;
#[cfg(not(feature = "std"))]
let uptime_ms = 0u64;
RuntimeStats { num_workers, uptime_ms, actors, workers, actor_details: Vec::new(), tick_timings } RuntimeStats { num_workers, uptime_ms, actors, workers, actor_details: Vec::new(), tick_timings }
} }
@ -480,6 +499,7 @@ impl Runtime {
Some(wid) => { Some(wid) => {
self.transfer_txs[wid.as_usize()] self.transfer_txs[wid.as_usize()]
.send(Envelope::new(addr, Box::new(StopSignal))); .send(Envelope::new(addr, Box::new(StopSignal)));
#[cfg(feature = "std")]
notify_worker(&self.worker_threads, wid.as_usize()); notify_worker(&self.worker_threads, wid.as_usize());
Ok(()) Ok(())
} }
@ -494,9 +514,12 @@ impl Runtime {
self.is_running.store(false, Ordering::Release); self.is_running.store(false, Ordering::Release);
// Wake all parked workers so they see the shutdown flag immediately // Wake all parked workers so they see the shutdown flag immediately
for thread in self.worker_threads.iter() { #[cfg(feature = "std")]
if let Some(t) = thread.get() { {
t.unpark(); for thread in self.worker_threads.iter() {
if let Some(t) = thread.get() {
t.unpark();
}
} }
} }
} }
@ -509,13 +532,13 @@ impl Runtime {
} }
/// Set the codec registry for remote transport. /// Set the codec registry for remote transport.
#[cfg(feature = "transport")] #[cfg(all(feature = "transport", feature = "std"))]
pub fn set_codec_registry(&mut self, registry: Arc<crate::transport::CodecRegistry>) { pub fn set_codec_registry(&mut self, registry: Arc<crate::transport::CodecRegistry>) {
self.codec_registry = Some(registry); self.codec_registry = Some(registry);
} }
/// Set the transport router for remote message delivery. /// Set the transport router for remote message delivery.
#[cfg(feature = "transport")] #[cfg(all(feature = "transport", feature = "std"))]
pub fn set_transport_router(&mut self, router: Arc<crate::transport::TransportRouter>) { pub fn set_transport_router(&mut self, router: Arc<crate::transport::TransportRouter>) {
self.transport_router = Some(router); self.transport_router = Some(router);
} }
@ -524,7 +547,7 @@ impl Runtime {
/// ///
/// Used by [`CodecRegistry::receive`](crate::transport::CodecRegistry::receive) /// Used by [`CodecRegistry::receive`](crate::transport::CodecRegistry::receive)
/// to inject incoming messages from remote runtimes. /// to inject incoming messages from remote runtimes.
#[cfg(feature = "transport")] #[cfg(all(feature = "transport", feature = "std"))]
pub fn deliver_raw( pub fn deliver_raw(
&self, &self,
addr: ActorAddress, addr: ActorAddress,
@ -543,6 +566,7 @@ impl Runtime {
/// Wake a parked worker thread so it can process new work. /// 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). /// No-op if the thread handle hasn't been registered yet (single-threaded tick mode).
#[cfg(feature = "std")]
#[inline] #[inline]
pub(crate) fn notify_worker(threads: &[OnceLock<Thread>], wid: usize) { pub(crate) fn notify_worker(threads: &[OnceLock<Thread>], wid: usize) {
if let Some(t) = threads.get(wid).and_then(|o| o.get()) { if let Some(t) = threads.get(wid).and_then(|o| o.get()) {
@ -557,6 +581,7 @@ impl ContextInner for Runtime {
Some(wid) => { Some(wid) => {
self.transfer_txs[wid.as_usize()] self.transfer_txs[wid.as_usize()]
.send(Envelope::new(addr, msg)); .send(Envelope::new(addr, msg));
#[cfg(feature = "std")]
notify_worker(&self.worker_threads, wid.as_usize()); notify_worker(&self.worker_threads, wid.as_usize());
Ok(()) Ok(())
} }
@ -569,6 +594,7 @@ impl ContextInner for Runtime {
self.address_map.insert(request.addr, worker_id); self.address_map.insert(request.addr, worker_id);
self.spawn_txs[worker_id.as_usize()] self.spawn_txs[worker_id.as_usize()]
.send(request); .send(request);
#[cfg(feature = "std")]
notify_worker(&self.worker_threads, worker_id.as_usize()); notify_worker(&self.worker_threads, worker_id.as_usize());
} }
@ -577,6 +603,7 @@ impl ContextInner for Runtime {
if let Some(wid) = self.address_map.lookup(&addr) { if let Some(wid) = self.address_map.lookup(&addr) {
self.transfer_txs[wid.as_usize()] self.transfer_txs[wid.as_usize()]
.send(Envelope::new(addr, Box::new(StopSignal))); .send(Envelope::new(addr, Box::new(StopSignal)));
#[cfg(feature = "std")]
notify_worker(&self.worker_threads, wid.as_usize()); notify_worker(&self.worker_threads, wid.as_usize());
} }
} }
@ -585,19 +612,22 @@ impl ContextInner for Runtime {
if let Some(wid) = self.address_map.lookup(&addr) { if let Some(wid) = self.address_map.lookup(&addr) {
self.transfer_txs[wid.as_usize()] self.transfer_txs[wid.as_usize()]
.send(Envelope::new(addr, Box::new(StopWithSignal(value)))); .send(Envelope::new(addr, Box::new(StopWithSignal(value))));
#[cfg(feature = "std")]
notify_worker(&self.worker_threads, wid.as_usize()); notify_worker(&self.worker_threads, wid.as_usize());
} }
} }
fn request_suspend(&self, addr: ActorAddress) { fn request_suspend(&self, _addr: ActorAddress) {
// Outside worker context — not supported (suspend is per-actor, from handler) // Outside worker context — not supported (suspend is per-actor, from handler)
eprintln!("swactor: request_suspend called outside worker context for {addr} — ignored"); #[cfg(feature = "std")]
eprintln!("swactor: request_suspend called outside worker context for {_addr} — ignored");
} }
fn request_resume(&self, addr: ActorAddress) { fn request_resume(&self, addr: ActorAddress) {
if let Some(wid) = self.address_map.lookup(&addr) { if let Some(wid) = self.address_map.lookup(&addr) {
self.transfer_txs[wid.as_usize()] self.transfer_txs[wid.as_usize()]
.send(Envelope::new(addr, Box::new(ResumeSignal))); .send(Envelope::new(addr, Box::new(ResumeSignal)));
#[cfg(feature = "std")]
notify_worker(&self.worker_threads, wid.as_usize()); notify_worker(&self.worker_threads, wid.as_usize());
} }
} }
@ -605,6 +635,7 @@ impl ContextInner for Runtime {
fn post_worker_request(&self, _request: Box<dyn Any + Send>) { fn post_worker_request(&self, _request: Box<dyn Any + Send>) {
// Worker requests (e.g., timers) are per-worker; posting from outside // Worker requests (e.g., timers) are per-worker; posting from outside
// a worker context (e.g., rt.spawn() callback) is not supported. // a worker context (e.g., rt.spawn() callback) is not supported.
#[cfg(feature = "std")]
eprintln!("swactor: post_worker_request called outside worker context — ignored"); eprintln!("swactor: post_worker_request called outside worker context — ignored");
} }
@ -617,11 +648,15 @@ impl ContextInner for Runtime {
let total_actors: usize = self.worker_stats.iter() let total_actors: usize = self.worker_stats.iter()
.map(|ws| ws.num_actors.load(Ordering::Relaxed)) .map(|ws| ws.num_actors.load(Ordering::Relaxed))
.sum(); .sum();
#[cfg(feature = "std")]
let uptime_ms = self.created_at.elapsed().as_millis() as u64;
#[cfg(not(feature = "std"))]
let uptime_ms = 0u64;
SystemInfo { SystemInfo {
worker_id: 0, worker_id: 0,
num_workers, num_workers,
total_actors, total_actors,
uptime_ms: self.created_at.elapsed().as_millis() as u64, uptime_ms,
} }
} }
} }

View file

@ -1,4 +1,7 @@
use std::sync::atomic::{AtomicU64, AtomicUsize}; #[cfg(not(feature = "std"))]
use crate::compat::{String, Vec};
use core::sync::atomic::{AtomicU64, AtomicUsize};
use crossbeam_queue::ArrayQueue; use crossbeam_queue::ArrayQueue;
@ -83,7 +86,7 @@ impl WorkerStats {
/// Create a point-in-time snapshot as a [`WorkerInfo`]. /// Create a point-in-time snapshot as a [`WorkerInfo`].
pub fn snapshot(&self, id: usize) -> WorkerInfo { pub fn snapshot(&self, id: usize) -> WorkerInfo {
use std::sync::atomic::Ordering::Relaxed; use core::sync::atomic::Ordering::Relaxed;
WorkerInfo { WorkerInfo {
id, id,
num_actors: self.num_actors.load(Relaxed), num_actors: self.num_actors.load(Relaxed),

230
src/sync_impl.rs Normal file
View file

@ -0,0 +1,230 @@
//! Spin-based sync primitives for `#[cfg(not(feature = "std"))]`.
//!
//! These are minimal implementations sufficient for the swactor runtime
//! in single-threaded cooperative (no_std) environments. They use atomic
//! spin loops and are NOT suitable for highly contended multi-threaded use.
#![allow(dead_code)]
use core::cell::UnsafeCell;
use core::ops::{Deref, DerefMut};
use core::sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering};
// ─── SpinMutex ──────────────────────────────────────────────────────────────
/// A simple spin-lock mutex.
///
/// Returns `Result<Guard, E>` from `lock()` to match `std::sync::Mutex` API
/// (the codebase calls `.lock().unwrap()`). The `Err` variant is never produced.
pub struct SpinMutex<T> {
locked: AtomicBool,
data: UnsafeCell<T>,
}
unsafe impl<T: Send> Send for SpinMutex<T> {}
unsafe impl<T: Send> Sync for SpinMutex<T> {}
impl<T> SpinMutex<T> {
pub const fn new(value: T) -> Self {
Self {
locked: AtomicBool::new(false),
data: UnsafeCell::new(value),
}
}
pub fn lock(&self) -> Result<SpinMutexGuard<'_, T>, core::convert::Infallible> {
while self
.locked
.compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
core::hint::spin_loop();
}
Ok(SpinMutexGuard { lock: self })
}
}
pub struct SpinMutexGuard<'a, T> {
lock: &'a SpinMutex<T>,
}
impl<T> Deref for SpinMutexGuard<'_, T> {
type Target = T;
fn deref(&self) -> &T {
unsafe { &*self.lock.data.get() }
}
}
impl<T> DerefMut for SpinMutexGuard<'_, T> {
fn deref_mut(&mut self) -> &mut T {
unsafe { &mut *self.lock.data.get() }
}
}
impl<T> Drop for SpinMutexGuard<'_, T> {
fn drop(&mut self) {
self.lock.locked.store(false, Ordering::Release);
}
}
// ─── SpinRwLock ─────────────────────────────────────────────────────────────
/// A simple spin-lock reader-writer lock.
///
/// State encoding in `state: AtomicUsize`:
/// - 0 = unlocked
/// - 1..usize::MAX-1 = N active readers
/// - usize::MAX = writer holds the lock
const WRITER: usize = usize::MAX;
pub struct SpinRwLock<T> {
state: AtomicUsize,
data: UnsafeCell<T>,
}
unsafe impl<T: Send> Send for SpinRwLock<T> {}
unsafe impl<T: Send + Sync> Sync for SpinRwLock<T> {}
impl<T> SpinRwLock<T> {
pub const fn new(value: T) -> Self {
Self {
state: AtomicUsize::new(0),
data: UnsafeCell::new(value),
}
}
pub fn read(&self) -> Result<SpinRwLockReadGuard<'_, T>, core::convert::Infallible> {
loop {
let s = self.state.load(Ordering::Relaxed);
if s == WRITER {
core::hint::spin_loop();
continue;
}
if self
.state
.compare_exchange_weak(s, s + 1, Ordering::Acquire, Ordering::Relaxed)
.is_ok()
{
return Ok(SpinRwLockReadGuard { lock: self });
}
core::hint::spin_loop();
}
}
pub fn write(&self) -> Result<SpinRwLockWriteGuard<'_, T>, core::convert::Infallible> {
loop {
if self
.state
.compare_exchange_weak(0, WRITER, Ordering::Acquire, Ordering::Relaxed)
.is_ok()
{
return Ok(SpinRwLockWriteGuard { lock: self });
}
core::hint::spin_loop();
}
}
}
pub struct SpinRwLockReadGuard<'a, T> {
lock: &'a SpinRwLock<T>,
}
impl<T> Deref for SpinRwLockReadGuard<'_, T> {
type Target = T;
fn deref(&self) -> &T {
unsafe { &*self.lock.data.get() }
}
}
impl<T> Drop for SpinRwLockReadGuard<'_, T> {
fn drop(&mut self) {
self.lock.state.fetch_sub(1, Ordering::Release);
}
}
pub struct SpinRwLockWriteGuard<'a, T> {
lock: &'a SpinRwLock<T>,
}
impl<T> Deref for SpinRwLockWriteGuard<'_, T> {
type Target = T;
fn deref(&self) -> &T {
unsafe { &*self.lock.data.get() }
}
}
impl<T> DerefMut for SpinRwLockWriteGuard<'_, T> {
fn deref_mut(&mut self) -> &mut T {
unsafe { &mut *self.lock.data.get() }
}
}
impl<T> Drop for SpinRwLockWriteGuard<'_, T> {
fn drop(&mut self) {
self.lock.state.store(0, Ordering::Release);
}
}
// ─── SpinOnceLock ───────────────────────────────────────────────────────────
const EMPTY: u8 = 0;
const INITIALIZING: u8 = 1;
const READY: u8 = 2;
/// A spin-based `OnceLock` matching the `std::sync::OnceLock` API surface.
pub struct SpinOnceLock<T> {
state: AtomicU8,
data: UnsafeCell<Option<T>>,
}
unsafe impl<T: Send + Sync> Send for SpinOnceLock<T> {}
unsafe impl<T: Send + Sync> Sync for SpinOnceLock<T> {}
impl<T> SpinOnceLock<T> {
pub const fn new() -> Self {
Self {
state: AtomicU8::new(EMPTY),
data: UnsafeCell::new(None),
}
}
pub fn get(&self) -> Option<&T> {
if self.state.load(Ordering::Acquire) == READY {
unsafe { (*self.data.get()).as_ref() }
} else {
None
}
}
pub fn set(&self, value: T) -> Result<(), T> {
if self
.state
.compare_exchange(EMPTY, INITIALIZING, Ordering::Acquire, Ordering::Relaxed)
.is_ok()
{
unsafe { *self.data.get() = Some(value) };
self.state.store(READY, Ordering::Release);
Ok(())
} else {
Err(value)
}
}
pub fn get_or_init<F: FnOnce() -> T>(&self, f: F) -> &T {
if self.state.load(Ordering::Acquire) == READY {
return unsafe { (*self.data.get()).as_ref().unwrap() };
}
if self
.state
.compare_exchange(EMPTY, INITIALIZING, Ordering::Acquire, Ordering::Relaxed)
.is_ok()
{
unsafe { *self.data.get() = Some(f()) };
self.state.store(READY, Ordering::Release);
} else {
while self.state.load(Ordering::Acquire) != READY {
core::hint::spin_loop();
}
}
unsafe { (*self.data.get()).as_ref().unwrap() }
}
}

View file

@ -8,7 +8,7 @@
//! - **[`Codec<M>`]**: HOW bytes are encoded — gRPC/protobuf, bincode, custom, etc. //! - **[`Codec<M>`]**: HOW bytes are encoded — gRPC/protobuf, bincode, custom, etc.
//! - **[`Transport`]**: WHERE bytes are sent — in-memory, gRPC channel, etc. //! - **[`Transport`]**: WHERE bytes are sent — in-memory, gRPC channel, etc.
use std::any::{Any, TypeId}; use core::any::{Any, TypeId};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};

View file

@ -1,15 +1,22 @@
use std::any::Any; #[cfg(not(feature = "std"))]
use std::cell::RefCell; use crate::compat::{Box, Vec};
use std::collections::{HashMap, VecDeque};
use std::sync::atomic::{AtomicBool, Ordering}; use core::any::Any;
use std::sync::Arc; use core::cell::RefCell;
use core::sync::atomic::Ordering;
#[cfg(feature = "std")]
use core::sync::atomic::AtomicBool;
use crate::compat::{Arc, HashMap, VecDeque};
#[cfg(feature = "std")]
use std::thread; use std::thread;
#[cfg(feature = "std")]
use crate::Instant; use crate::Instant;
use crate::actor::{ActorAddress, AnyActor, ContextInner, Ctx, Environment, ExitValue, ResumeSignal, SpawnRequest, StopReason, StopSignal, StopWithSignal, SystemInfo}; use crate::actor::{ActorAddress, AnyActor, ContextInner, Ctx, Environment, ExitValue, ResumeSignal, SpawnRequest, StopReason, StopSignal, StopWithSignal, SystemInfo};
use crate::channel::Receiver; use crate::channel::Receiver;
use crate::config::MailboxOverflow; use crate::config::MailboxOverflow;
use crate::delivery::{AddrBuildHasher, AddrMap, Envelope, TickContext, WorkerId}; use crate::delivery::{AddrMap, Envelope, TickContext, WorkerId};
use swactor_core::{ActorPhase, LifecycleEvent, lifecycle_transition};
use crate::stats::{ActorSnapshot, TickTiming, WorkerStats}; use crate::stats::{ActorSnapshot, TickTiming, WorkerStats};
use crate::Error; use crate::Error;
@ -29,6 +36,7 @@ fn route_to_pool_or_remote(
match tc.address_map.lookup(&dest) { match tc.address_map.lookup(&dest) {
Some(wid) => { Some(wid) => {
tc.transfer_txs[wid.as_usize()].send(Envelope::new(dest, msg)); tc.transfer_txs[wid.as_usize()].send(Envelope::new(dest, msg));
#[cfg(feature = "std")]
crate::runtime::notify_worker(tc.worker_threads, wid.as_usize()); crate::runtime::notify_worker(tc.worker_threads, wid.as_usize());
} }
None => { None => {
@ -86,7 +94,11 @@ impl Worker {
let mut spawn_count: usize = 0; let mut spawn_count: usize = 0;
while let Some(mut req) = self.spawn_rx.try_recv() { while let Some(mut req) = self.spawn_rx.try_recv() {
if let Some(ext) = tc.extension { if let Some(ext) = tc.extension {
req.env = ext.on_spawn(req.addr, req.parent, req.env, tc.created_at.elapsed().as_millis() as u64); #[cfg(feature = "std")]
let uptime_ms = tc.created_at.elapsed().as_millis() as u64;
#[cfg(not(feature = "std"))]
let uptime_ms = 0u64;
req.env = ext.on_spawn(req.addr, req.parent, req.env, uptime_ms);
} }
self.pool.insert(req); self.pool.insert(req);
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
@ -169,10 +181,12 @@ impl Worker {
} }
let mut did_work = false; let mut did_work = false;
#[cfg(feature = "std")]
let t0 = Instant::now(); let t0 = Instant::now();
// 1. Drain spawn queue → add actors to pool // 1. Drain spawn queue → add actors to pool
did_work |= self.drain_spawns(tc); did_work |= self.drain_spawns(tc);
#[cfg(feature = "std")]
let t1 = Instant::now(); let t1 = Instant::now();
// 2. Drain transfer queue → deliver envelopes to actors // 2. Drain transfer queue → deliver envelopes to actors
@ -182,6 +196,7 @@ impl Worker {
self.pool.deliver(&dest, payload); self.pool.deliver(&dest, payload);
did_work = true; did_work = true;
} }
#[cfg(feature = "std")]
let t2 = Instant::now(); let t2 = Instant::now();
// 2.5. Fire per-worker extension (e.g., timers) → deliver before tick_all // 2.5. Fire per-worker extension (e.g., timers) → deliver before tick_all
@ -218,6 +233,7 @@ impl Worker {
did_work = true; did_work = true;
} }
} }
#[cfg(feature = "std")]
let t3 = Instant::now(); let t3 = Instant::now();
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
@ -232,6 +248,7 @@ impl Worker {
// 4. Drain spawn queue again — actors spawned during step 3 // 4. Drain spawn queue again — actors spawned during step 3
// must be in the pool before pending_local delivery. // must be in the pool before pending_local delivery.
did_work |= self.drain_spawns(tc); did_work |= self.drain_spawns(tc);
#[cfg(feature = "std")]
let t4 = Instant::now(); let t4 = Instant::now();
// 5. Drain pending_local buffer → deliver to local actors // 5. Drain pending_local buffer → deliver to local actors
@ -250,6 +267,7 @@ impl Worker {
} }
} }
#[cfg(feature = "std")]
let t5 = Instant::now(); let t5 = Instant::now();
// 6. Publish stats (skip entirely when idle to avoid allocation + mutex) // 6. Publish stats (skip entirely when idle to avoid allocation + mutex)
@ -268,10 +286,12 @@ impl Worker {
} }
} }
#[cfg(feature = "std")]
let t6 = Instant::now(); let t6 = Instant::now();
// Record tick timing // Record tick timing
let timing = TickTiming { let timing = TickTiming {
#[cfg(feature = "std")]
phase_us: [ phase_us: [
t1.duration_since(t0).as_micros() as u64, t1.duration_since(t0).as_micros() as u64,
t2.duration_since(t1).as_micros() as u64, t2.duration_since(t1).as_micros() as u64,
@ -280,6 +300,8 @@ impl Worker {
t5.duration_since(t4).as_micros() as u64, t5.duration_since(t4).as_micros() as u64,
t6.duration_since(t5).as_micros() as u64, t6.duration_since(t5).as_micros() as u64,
], ],
#[cfg(not(feature = "std"))]
phase_us: [0; 6],
messages_processed: processed, messages_processed: processed,
did_work, did_work,
}; };
@ -303,6 +325,7 @@ impl Worker {
did_work did_work
} }
#[cfg(feature = "std")]
pub(crate) fn run(&mut self, tc: &TickContext, is_running: &AtomicBool) { pub(crate) fn run(&mut self, tc: &TickContext, is_running: &AtomicBool) {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
let _span = tracing::info_span!("worker.run", worker_id = self.id.0).entered(); let _span = tracing::info_span!("worker.run", worker_id = self.id.0).entered();
@ -343,6 +366,7 @@ impl ContextInner for WorkerContext<'_> {
Some(wid) => { Some(wid) => {
self.stats.cross_sends.fetch_add(1, Ordering::Relaxed); self.stats.cross_sends.fetch_add(1, Ordering::Relaxed);
self.tc.transfer_txs[wid.as_usize()].send(Envelope::new(addr, msg)); self.tc.transfer_txs[wid.as_usize()].send(Envelope::new(addr, msg));
#[cfg(feature = "std")]
crate::runtime::notify_worker(self.tc.worker_threads, wid.as_usize()); crate::runtime::notify_worker(self.tc.worker_threads, wid.as_usize());
Ok(()) Ok(())
} }
@ -358,6 +382,7 @@ impl ContextInner for WorkerContext<'_> {
self.tc.address_map.insert(request.addr, worker_id); self.tc.address_map.insert(request.addr, worker_id);
self.tc.spawn_txs[worker_id.as_usize()] self.tc.spawn_txs[worker_id.as_usize()]
.send(request); .send(request);
#[cfg(feature = "std")]
crate::runtime::notify_worker(self.tc.worker_threads, worker_id.as_usize()); crate::runtime::notify_worker(self.tc.worker_threads, worker_id.as_usize());
} }
@ -392,11 +417,15 @@ impl ContextInner for WorkerContext<'_> {
let total_actors: usize = self.tc.worker_stats.iter() let total_actors: usize = self.tc.worker_stats.iter()
.map(|ws| ws.num_actors.load(Ordering::Relaxed)) .map(|ws| ws.num_actors.load(Ordering::Relaxed))
.sum(); .sum();
#[cfg(feature = "std")]
let uptime_ms = self.tc.created_at.elapsed().as_millis() as u64;
#[cfg(not(feature = "std"))]
let uptime_ms = 0u64;
SystemInfo { SystemInfo {
worker_id: self.worker_id.0, worker_id: self.worker_id.0,
num_workers, num_workers,
total_actors, total_actors,
uptime_ms: self.tc.created_at.elapsed().as_millis() as u64, uptime_ms,
} }
} }
} }
@ -404,13 +433,8 @@ impl ContextInner for WorkerContext<'_> {
struct ActorSlot { struct ActorSlot {
mailbox: VecDeque<Box<dyn Any + Send>>, mailbox: VecDeque<Box<dyn Any + Send>>,
actor: Box<dyn AnyActor>, actor: Box<dyn AnyActor>,
poisoned: bool, /// Lifecycle phase — replaces the old `started`, `poisoned`, `stopping`, `suspended` booleans.
/// Graceful stop requested (via StopSignal). phase: ActorPhase,
stopping: bool,
/// Whether on_start has been called for this actor.
started: bool,
/// Actor is suspended — messages queue but are not processed.
suspended: bool,
last_msg_type: Option<&'static str>, last_msg_type: Option<&'static str>,
messages_processed: u64, messages_processed: u64,
/// Per-message-type counters (bounded to 32 entries). /// Per-message-type counters (bounded to 32 entries).
@ -438,7 +462,10 @@ pub(crate) struct ActorPool {
impl ActorPool { impl ActorPool {
pub fn new(default_mailbox_capacity: usize, default_overflow_policy: MailboxOverflow) -> Self { pub fn new(default_mailbox_capacity: usize, default_overflow_policy: MailboxOverflow) -> Self {
Self { Self {
actors: HashMap::with_hasher(AddrBuildHasher), #[cfg(feature = "std")]
actors: std::collections::HashMap::with_hasher(crate::delivery::AddrBuildHasher),
#[cfg(not(feature = "std"))]
actors: AddrMap::new(),
default_mailbox_capacity, default_mailbox_capacity,
default_overflow_policy, default_overflow_policy,
drops_this_tick: 0, drops_this_tick: 0,
@ -451,10 +478,7 @@ impl ActorPool {
self.actors.insert(req.addr, ActorSlot { self.actors.insert(req.addr, ActorSlot {
mailbox: VecDeque::with_capacity(prealloc), mailbox: VecDeque::with_capacity(prealloc),
actor: req.actor, actor: req.actor,
poisoned: false, phase: ActorPhase::Unstarted,
stopping: false,
started: false,
suspended: false,
last_msg_type: None, last_msg_type: None,
messages_processed: 0, messages_processed: 0,
msg_type_counts: HashMap::new(), msg_type_counts: HashMap::new(),
@ -472,13 +496,13 @@ impl ActorPool {
if let Some(slot) = self.actors.get_mut(addr) { if let Some(slot) = self.actors.get_mut(addr) {
// Intercept control signals for suspended actors: they skip tick_all // Intercept control signals for suspended actors: they skip tick_all
// so we must handle resume/stop at delivery time. // so we must handle resume/stop at delivery time.
if slot.suspended { if slot.phase == ActorPhase::Suspended {
if msg.is::<ResumeSignal>() { if msg.is::<ResumeSignal>() {
slot.suspended = false; slot.phase = lifecycle_transition(slot.phase, LifecycleEvent::Resumed);
return true; return true;
} }
if msg.is::<StopSignal>() { if msg.is::<StopSignal>() {
slot.stopping = true; slot.phase = lifecycle_transition(slot.phase, LifecycleEvent::StopRequested);
slot.mailbox.clear(); slot.mailbox.clear();
return true; return true;
} }
@ -486,21 +510,20 @@ impl ActorPool {
if let Ok(sig) = msg.downcast::<StopWithSignal>() { if let Ok(sig) = msg.downcast::<StopWithSignal>() {
slot.exit_value = Some(sig.0); slot.exit_value = Some(sig.0);
} }
slot.stopping = true; slot.phase = lifecycle_transition(slot.phase, LifecycleEvent::StopWithRequested);
slot.mailbox.clear(); slot.mailbox.clear();
return true; return true;
} }
} }
if slot.mailbox_capacity > 0 && slot.mailbox.len() >= slot.mailbox_capacity { match swactor_core::mailbox_accept(slot.mailbox.len(), slot.mailbox_capacity, slot.overflow_policy) {
match slot.overflow_policy { swactor_core::MailboxDecision::Accept => {}
MailboxOverflow::DropNewest => { swactor_core::MailboxDecision::RejectNewest => {
self.drops_this_tick += 1; self.drops_this_tick += 1;
return true; return true;
} }
MailboxOverflow::DropOldest => { swactor_core::MailboxDecision::EvictOldest => {
slot.mailbox.pop_front(); slot.mailbox.pop_front();
self.drops_this_tick += 1; self.drops_this_tick += 1;
}
} }
} }
slot.mailbox.push_back(msg); slot.mailbox.push_back(msg);
@ -512,7 +535,7 @@ impl ActorPool {
/// Take and reset the drop counter for this tick. /// Take and reset the drop counter for this tick.
pub fn take_drops(&mut self) -> usize { pub fn take_drops(&mut self) -> usize {
std::mem::replace(&mut self.drops_this_tick, 0) core::mem::replace(&mut self.drops_this_tick, 0)
} }
/// Tick all actors in the pool. Returns the number of messages processed. /// Tick all actors in the pool. Returns the number of messages processed.
@ -530,14 +553,11 @@ impl ActorPool {
) -> usize { ) -> usize {
let mut count = 0; let mut count = 0;
for (&addr, slot) in self.actors.iter_mut() { for (&addr, slot) in self.actors.iter_mut() {
if slot.poisoned || slot.stopping { if !swactor_core::should_tick_actor(slot.phase) {
// Discard all messages for poisoned/stopping actors // Discard mailbox for poisoned/stopping; suspended keeps queueing
slot.mailbox.clear(); if slot.phase == ActorPhase::Poisoned || slot.phase == ActorPhase::Stopping {
continue; slot.mailbox.clear();
} }
// Skip suspended actors — messages keep queueing
if slot.suspended {
continue; continue;
} }
@ -554,26 +574,30 @@ impl ActorPool {
let ctx = Ctx::new(inner, addr, slot.parent_addr, slot.env.clone(), snap_processed, snap_depth, snap_type_counts); let ctx = Ctx::new(inner, addr, slot.parent_addr, slot.env.clone(), snap_processed, snap_depth, snap_type_counts);
// Call on_start once, before first message // Call on_start once, before first message
if !slot.started { if slot.phase == ActorPhase::Unstarted {
#[cfg(feature = "std")]
let start_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let start_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
slot.actor.on_start(&ctx); slot.actor.on_start(&ctx);
})); }));
slot.started = true; #[cfg(not(feature = "std"))]
let start_result: Result<(), ()> = { slot.actor.on_start(&ctx); Ok(()) };
if start_result.is_err() { if start_result.is_err() {
stats.panics.fetch_add(1, Ordering::Relaxed); stats.panics.fetch_add(1, Ordering::Relaxed);
#[cfg(feature = "std")]
eprintln!("swactor: actor {addr} panicked in on_start — poisoned"); eprintln!("swactor: actor {addr} panicked in on_start — poisoned");
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::error!(actor_addr = %addr, "actor.on_start_panicked"); tracing::error!(actor_addr = %addr, "actor.on_start_panicked");
slot.poisoned = true; slot.phase = lifecycle_transition(slot.phase, LifecycleEvent::StartPanicked);
slot.mailbox.clear(); slot.mailbox.clear();
continue; continue;
} }
slot.phase = lifecycle_transition(slot.phase, LifecycleEvent::Started);
// Check if on_start requested stop or stop_with // Check if on_start requested stop or stop_with
{ {
let stops = stop_requests.borrow(); let stops = stop_requests.borrow();
if !stops.is_empty() && stops.contains(&addr) { if !stops.is_empty() && stops.contains(&addr) {
drop(stops); drop(stops);
slot.stopping = true; slot.phase = lifecycle_transition(slot.phase, LifecycleEvent::StopRequested);
stats.stops.fetch_add(1, Ordering::Relaxed); stats.stops.fetch_add(1, Ordering::Relaxed);
slot.mailbox.clear(); slot.mailbox.clear();
// Check for stop_with value // Check for stop_with value
@ -591,7 +615,7 @@ impl ActorPool {
if let Some(pos) = sws.iter().position(|(a, _)| *a == addr) { if let Some(pos) = sws.iter().position(|(a, _)| *a == addr) {
let (_, val) = sws.swap_remove(pos); let (_, val) = sws.swap_remove(pos);
slot.exit_value = Some(val); slot.exit_value = Some(val);
slot.stopping = true; slot.phase = lifecycle_transition(slot.phase, LifecycleEvent::StopWithRequested);
stats.stops.fetch_add(1, Ordering::Relaxed); stats.stops.fetch_add(1, Ordering::Relaxed);
slot.mailbox.clear(); slot.mailbox.clear();
continue; continue;
@ -602,7 +626,7 @@ impl ActorPool {
let suspends = suspend_requests.borrow(); let suspends = suspend_requests.borrow();
if !suspends.is_empty() && suspends.contains(&addr) { if !suspends.is_empty() && suspends.contains(&addr) {
drop(suspends); drop(suspends);
slot.suspended = true; slot.phase = lifecycle_transition(slot.phase, LifecycleEvent::SuspendRequested);
continue; continue;
} }
} }
@ -612,7 +636,7 @@ impl ActorPool {
while let Some(msg) = slot.mailbox.pop_front() { while let Some(msg) = slot.mailbox.pop_front() {
// Intercept StopSignal (from external runtime.stop_actor) // Intercept StopSignal (from external runtime.stop_actor)
if msg.is::<StopSignal>() { if msg.is::<StopSignal>() {
slot.stopping = true; slot.phase = lifecycle_transition(slot.phase, LifecycleEvent::StopRequested);
stats.stops.fetch_add(1, Ordering::Relaxed); stats.stops.fetch_add(1, Ordering::Relaxed);
slot.mailbox.clear(); slot.mailbox.clear();
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
@ -625,15 +649,18 @@ impl ActorPool {
if let Ok(sig) = msg.downcast::<StopWithSignal>() { if let Ok(sig) = msg.downcast::<StopWithSignal>() {
slot.exit_value = Some(sig.0); slot.exit_value = Some(sig.0);
} }
slot.stopping = true; slot.phase = lifecycle_transition(slot.phase, LifecycleEvent::StopWithRequested);
stats.stops.fetch_add(1, Ordering::Relaxed); stats.stops.fetch_add(1, Ordering::Relaxed);
slot.mailbox.clear(); slot.mailbox.clear();
break; break;
} }
#[cfg(feature = "std")]
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
slot.actor.handle_any(&ctx, msg) slot.actor.handle_any(&ctx, msg)
})); }));
#[cfg(not(feature = "std"))]
let result: Result<Option<&'static str>, ()> = Ok(slot.actor.handle_any(&ctx, msg));
match result { match result {
Ok(None) => { Ok(None) => {
stats.type_mismatches.fetch_add(1, Ordering::Relaxed); stats.type_mismatches.fetch_add(1, Ordering::Relaxed);
@ -641,10 +668,11 @@ impl ActorPool {
Err(_) => { Err(_) => {
stats.panics.fetch_add(1, Ordering::Relaxed); stats.panics.fetch_add(1, Ordering::Relaxed);
slot.mailbox.clear(); slot.mailbox.clear();
#[cfg(feature = "std")]
eprintln!("swactor: actor {addr} panicked — poisoned, future messages will be discarded"); eprintln!("swactor: actor {addr} panicked — poisoned, future messages will be discarded");
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::error!(actor_addr = %addr, "actor.panicked"); tracing::error!(actor_addr = %addr, "actor.panicked");
slot.poisoned = true; slot.phase = lifecycle_transition(slot.phase, LifecycleEvent::MessagePanicked);
slot.mailbox.clear(); slot.mailbox.clear();
break; break;
} }
@ -675,7 +703,7 @@ impl ActorPool {
slot.exit_value = Some(val); slot.exit_value = Some(val);
} }
drop(sws); drop(sws);
slot.stopping = true; slot.phase = lifecycle_transition(slot.phase, LifecycleEvent::StopRequested);
stats.stops.fetch_add(1, Ordering::Relaxed); stats.stops.fetch_add(1, Ordering::Relaxed);
slot.mailbox.clear(); slot.mailbox.clear();
break; break;
@ -688,12 +716,12 @@ impl ActorPool {
let suspends = suspend_requests.borrow(); let suspends = suspend_requests.borrow();
if !suspends.is_empty() && suspends.contains(&addr) { if !suspends.is_empty() && suspends.contains(&addr) {
drop(suspends); drop(suspends);
slot.suspended = true; slot.phase = lifecycle_transition(slot.phase, LifecycleEvent::SuspendRequested);
break; // stop processing this actor's messages this tick break; // stop processing this actor's messages this tick
} }
} }
if budget > 0 && actor_count >= budget { if swactor_core::budget_exhausted(actor_count, budget) {
break; break;
} }
} }
@ -723,21 +751,15 @@ impl ActorPool {
let dead_addrs: Vec<ActorAddress> = self let dead_addrs: Vec<ActorAddress> = self
.actors .actors
.iter() .iter()
.filter(|(_, slot)| slot.poisoned || slot.stopping) .filter(|(_, slot)| slot.phase == ActorPhase::Poisoned || slot.phase == ActorPhase::Stopping)
.map(|(&addr, _)| addr) .map(|(&addr, _)| addr)
.collect(); .collect();
let mut dead = Vec::with_capacity(dead_addrs.len()); let mut dead = Vec::with_capacity(dead_addrs.len());
for addr in dead_addrs { for addr in dead_addrs {
if let Some(mut slot) = self.actors.remove(&addr) { if let Some(mut slot) = self.actors.remove(&addr) {
let reason = if slot.poisoned { let reason = swactor_core::cleanup_stop_reason(slot.phase, slot.exit_value.is_some());
StopReason::Panicked
} else if slot.exit_value.is_some() {
StopReason::Completed
} else {
StopReason::Normal
};
// Call on_stop for gracefully stopping actors only // Call on_stop for gracefully stopping actors only
if slot.stopping && !slot.poisoned { if slot.phase == ActorPhase::Stopping {
let mut type_counts: Vec<(&'static str, u64)> = let mut type_counts: Vec<(&'static str, u64)> =
slot.msg_type_counts.iter().map(|(&k, &v)| (k, v)).collect(); slot.msg_type_counts.iter().map(|(&k, &v)| (k, v)).collect();
type_counts.sort_by(|a, b| b.1.cmp(&a.1)); type_counts.sort_by(|a, b| b.1.cmp(&a.1));
@ -747,9 +769,12 @@ impl ActorPool {
slot.mailbox.len(), slot.mailbox.len(),
type_counts, type_counts,
); );
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { #[cfg(feature = "std")]
{ let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
slot.actor.on_stop(&ctx); slot.actor.on_stop(&ctx);
})); })); }
#[cfg(not(feature = "std"))]
{ slot.actor.on_stop(&ctx); }
} }
dead.push((addr, reason, slot.exit_value.take())); dead.push((addr, reason, slot.exit_value.take()));
// slot is dropped here — actor resources freed // slot is dropped here — actor resources freed
@ -770,7 +795,7 @@ impl ActorPool {
mailbox_depth: slot.mailbox.len(), mailbox_depth: slot.mailbox.len(),
last_msg_type: slot.last_msg_type, last_msg_type: slot.last_msg_type,
messages_processed: slot.messages_processed, messages_processed: slot.messages_processed,
poisoned: slot.poisoned, poisoned: slot.phase == ActorPhase::Poisoned,
message_type_counts: type_counts, message_type_counts: type_counts,
} }
})); }));