Skip to main content

actor_kit/
scheduler.rs

1//! Work-stealing scheduler for efficient actor execution.
2//!
3//! Implements a multi-worker scheduler with work stealing for load balancing.
4
5use parking_lot::{Mutex, RwLock};
6use std::panic::AssertUnwindSafe;
7use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
8use std::sync::Arc;
9use std::thread::{self, JoinHandle};
10
11#[cfg(test)]
12use crate::executor::NullExecutor;
13use crate::executor::{ActorExecutor, ExecutionResult};
14use crate::policy::ResourcePolicy;
15use crate::queue::{create_local_queue, PriorityQueue, Task, WorkQueue, WorkStealer};
16#[cfg(feature = "serde")]
17use crate::rpc::RpcClient;
18use crate::supervisor::ResourceMonitor;
19use crate::Error;
20use crate::{ActorId, ActorRegistry, ActorState, MailboxConfig, Message, MessagePayload, Priority};
21
22/// Type alias for worker stealer entry (worker_id, stealer)
23type WorkerStealer = (usize, crossbeam_deque::Stealer<Task>);
24
25/// Shared stealer registry for work stealing between workers.
26#[derive(Clone)]
27pub struct StealerRegistry {
28    stealers: Arc<RwLock<Vec<WorkerStealer>>>,
29    version: Arc<AtomicU64>,
30}
31
32impl StealerRegistry {
33    /// Create an empty stealer registry.
34    pub fn new() -> Self {
35        Self {
36            stealers: Arc::new(RwLock::new(Vec::new())),
37            version: Arc::new(AtomicU64::new(0)),
38        }
39    }
40
41    /// Register a worker's stealer under the given worker ID.
42    pub fn add_stealer(&self, worker_id: usize, stealer: crossbeam_deque::Stealer<Task>) {
43        let mut stealers = self.stealers.write();
44        stealers.push((worker_id, stealer));
45        self.version.fetch_add(1, Ordering::Release);
46    }
47
48    /// Get stealers for all workers except `exclude_worker_id`.
49    pub fn get_stealers(&self, exclude_worker_id: usize) -> Vec<crossbeam_deque::Stealer<Task>> {
50        let stealers = self.stealers.read();
51        stealers
52            .iter()
53            .filter(|(id, _)| *id != exclude_worker_id)
54            .map(|(_, s)| s.clone())
55            .collect()
56    }
57
58    /// Get the registry version (bumped on every `add_stealer`).
59    pub fn version(&self) -> u64 {
60        self.version.load(Ordering::Acquire)
61    }
62}
63
64impl Default for StealerRegistry {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70/// Configuration for the actor scheduler.
71#[derive(Debug, Clone)]
72pub struct SchedulerConfig {
73    /// Number of worker threads (0 = auto-detect)
74    pub workers: usize,
75    /// Mailbox configuration
76    pub mailbox_config: MailboxConfig,
77    /// Enable priority scheduling
78    pub priority_scheduling: bool,
79    /// Maximum steal batch size
80    pub max_steal_batch: usize,
81    /// Idle sleep duration in microseconds
82    pub idle_sleep_us: u64,
83    /// Stealer refresh interval (in iterations)
84    pub stealer_refresh_interval: u32,
85}
86
87impl Default for SchedulerConfig {
88    fn default() -> Self {
89        Self {
90            workers: 0,
91            mailbox_config: MailboxConfig::default(),
92            priority_scheduling: true,
93            max_steal_batch: 32,
94            idle_sleep_us: 100,
95            stealer_refresh_interval: 1000,
96        }
97    }
98}
99
100impl SchedulerConfig {
101    /// Create a new scheduler config.
102    pub fn new() -> Self {
103        Self::default()
104    }
105
106    /// Set the number of workers.
107    pub fn workers(mut self, workers: usize) -> Self {
108        self.workers = workers;
109        self
110    }
111
112    /// Get the effective number of workers.
113    pub fn effective_workers(&self) -> usize {
114        if self.workers == 0 {
115            thread::available_parallelism()
116                .map(|p| p.get())
117                .unwrap_or(4)
118        } else {
119            self.workers
120        }
121    }
122}
123
124/// Per-worker statistics.
125///
126/// Cache-line isolation: `WorkerStats` instances live in separate `Arc`
127/// allocations, but an `Arc` allocation is a 16-byte header plus these three
128/// 8-byte counters (~40 bytes). The allocator typically places consecutive
129/// allocations 48 bytes apart, so worker *i*'s counters and worker *i+1*'s
130/// counters can share a 64-byte cache line even though no field is
131/// logically shared. Every message processed does three `fetch_add`s on
132/// this struct by the owning worker, so a shared line would ping-pong
133/// between workers on every message (false sharing). `repr(align(64))`
134/// pins the counters to their own line per instance. Each instance has a
135/// single writer (the owning worker); `stats()` readers tolerate the
136/// slightly larger footprint.
137#[derive(Debug, Default)]
138#[repr(align(64))]
139struct WorkerStats {
140    /// Tasks processed by this worker
141    processed: AtomicU64,
142    /// Tasks stolen by this worker
143    stolen: AtomicU64,
144    /// Number of batch tasks processed by this worker
145    batches_processed: AtomicU64,
146}
147
148/// Work-stealing actor scheduler.
149pub struct ActorScheduler {
150    /// Configuration
151    config: SchedulerConfig,
152    /// Global work queue
153    global_queue: Arc<WorkQueue>,
154    /// Priority queue for high-priority tasks
155    priority_queue: Arc<PriorityQueue>,
156    /// Actor registry
157    registry: Arc<ActorRegistry>,
158    /// Shared stealer registry
159    stealer_registry: StealerRegistry,
160    /// Worker thread handles
161    worker_handles: Mutex<Vec<JoinHandle<()>>>,
162    /// Running flag
163    running: Arc<AtomicBool>,
164    /// Total actors spawned
165    total_actors: AtomicU64,
166    /// Per-worker statistics
167    worker_stats: Vec<Arc<WorkerStats>>,
168    /// Optional executor for WASM execution
169    executor: Option<Arc<dyn ActorExecutor>>,
170    /// Optional admission-control policy (spawns and message sends).
171    ///
172    /// Hook point replacing the upstream `QuotaEnforcer` dependency: install a
173    /// custom policy via [`ActorScheduler::with_resource_policy`] to enforce
174    /// actor-count limits, message rates, or per-tenant budgets. See
175    /// [`crate::policy::ResourcePolicy`].
176    resource_policy: Option<Arc<dyn ResourcePolicy>>,
177    /// Optional degradation controller for graceful admission control.
178    /// `Arc<dyn ResourceMonitor>` is the monitor; the `f64` is the
179    /// `elevated_batch_factor` passed to `DegradationController`.
180    degradation: Option<(Arc<dyn ResourceMonitor>, f64)>,
181}
182
183impl ActorScheduler {
184    /// Create a new actor scheduler.
185    pub fn new(config: SchedulerConfig) -> Self {
186        Self::with_options(config, None, None)
187    }
188
189    /// Create a new scheduler with an executor.
190    pub fn with_executor(config: SchedulerConfig, executor: Arc<dyn ActorExecutor>) -> Self {
191        Self::with_options(config, Some(executor), None)
192    }
193
194    /// Create a new scheduler with an executor and a resource policy.
195    ///
196    /// The policy gates actor spawns and message sends; see
197    /// [`crate::policy::ResourcePolicy`] for the (cheap, non-blocking)
198    /// contract.
199    pub fn with_resource_policy(
200        config: SchedulerConfig,
201        executor: Arc<dyn ActorExecutor>,
202        resource_policy: Arc<dyn ResourcePolicy>,
203    ) -> Self {
204        Self::with_options(config, Some(executor), Some(resource_policy))
205    }
206
207    /// Create a new scheduler with optional executor and optional resource policy.
208    fn with_options(
209        config: SchedulerConfig,
210        executor: Option<Arc<dyn ActorExecutor>>,
211        resource_policy: Option<Arc<dyn ResourcePolicy>>,
212    ) -> Self {
213        let worker_count = config.effective_workers();
214        let worker_stats: Vec<_> = (0..worker_count)
215            .map(|_| Arc::new(WorkerStats::default()))
216            .collect();
217
218        // Honor the configured mailbox capacity: the registry creates a
219        // mailbox per actor, so it must see `config.mailbox_config`.
220        // (`ActorRegistry::new()` silently fell back to the 10_000-message
221        // default, making `SchedulerConfig::mailbox_config` a dead knob.)
222        let registry = Arc::new(ActorRegistry::with_mailbox_config(
223            config.mailbox_config.clone(),
224        ));
225
226        Self {
227            config,
228            global_queue: Arc::new(WorkQueue::new()),
229            priority_queue: Arc::new(PriorityQueue::new()),
230            registry,
231            stealer_registry: StealerRegistry::new(),
232            worker_handles: Mutex::new(Vec::new()),
233            running: Arc::new(AtomicBool::new(false)),
234            total_actors: AtomicU64::new(0),
235            worker_stats,
236            executor,
237            resource_policy,
238            degradation: None,
239        }
240    }
241
242    /// Set the executor for WASM execution.
243    pub fn set_executor(&mut self, executor: Arc<dyn ActorExecutor>) {
244        self.executor = Some(executor);
245    }
246
247    /// Set the degradation controller for OS-level admission control.
248    ///
249    /// When set, `spawn_named` will check resource pressure before creating a
250    /// new actor. `Critical` pressure returns an error; `Elevated` logs a
251    /// debug message but allows the spawn.
252    pub fn set_degradation_with_monitor(
253        &mut self,
254        monitor: Arc<dyn ResourceMonitor>,
255        elevated_batch_factor: f64,
256    ) {
257        self.degradation = Some((monitor, elevated_batch_factor));
258    }
259
260    /// Start the scheduler.
261    pub fn start(&self) -> Result<(), Error> {
262        if self.running.swap(true, Ordering::SeqCst) {
263            return Ok(());
264        }
265
266        let mut handles = self.worker_handles.lock();
267
268        for (id, stats) in self.worker_stats.iter().enumerate() {
269            let (worker, stealer) = create_local_queue();
270            self.stealer_registry.add_stealer(id, stealer);
271
272            let global_queue = self.global_queue.clone();
273            let priority_queue = self.priority_queue.clone();
274            let registry = self.registry.clone();
275            let config = self.config.clone();
276            let running_flag = self.running.clone();
277            let stats = stats.clone();
278            let stealer_registry = self.stealer_registry.clone();
279            let executor = self.executor.clone();
280
281            let handle = thread::Builder::new()
282                .name(format!("actor-kit-worker-{}", id))
283                .spawn(move || {
284                    Self::worker_loop(
285                        id,
286                        worker,
287                        global_queue,
288                        priority_queue,
289                        registry,
290                        config,
291                        running_flag,
292                        stats,
293                        stealer_registry,
294                        executor.as_ref(),
295                    );
296                });
297
298            match handle {
299                Ok(h) => handles.push(h),
300                Err(e) => {
301                    tracing::error!("Failed to spawn worker thread {}: {}", id, e);
302                    return Err(Error::internal(format!(
303                        "Failed to spawn worker thread {}: {}",
304                        id, e
305                    )));
306                }
307            }
308        }
309
310        Ok(())
311    }
312
313    /// Stop the scheduler.
314    pub fn stop(&self) {
315        self.running.store(false, Ordering::SeqCst);
316
317        let mut handles = self.worker_handles.lock();
318        for handle in handles.drain(..) {
319            let _ = handle.join();
320        }
321    }
322
323    /// Spawn a new actor.
324    pub fn spawn(&self) -> crate::Result<ActorId> {
325        self.spawn_named(None)
326    }
327
328    /// Spawn a new actor with a name.
329    pub fn spawn_named(&self, name: Option<String>) -> crate::Result<ActorId> {
330        self.check_degradation()?;
331
332        if let Some(ref policy) = self.resource_policy {
333            policy.admit_actor()?;
334        }
335        let id = ActorId::new();
336        self.registry.register_named(id, name)?;
337        self.total_actors.fetch_add(1, Ordering::Relaxed);
338        Ok(id)
339    }
340
341    /// Check the degradation controller before spawning.
342    ///
343    /// Returns `Err` on `Reject`; logs a warning on `Throttle` but allows the spawn.
344    fn check_degradation(&self) -> crate::Result<()> {
345        let (monitor, _) = match &self.degradation {
346            Some(d) => d,
347            None => return Ok(()),
348        };
349
350        match monitor.current_pressure() {
351            crate::supervisor::PressureLevel::Normal => Ok(()),
352            crate::supervisor::PressureLevel::Elevated => {
353                tracing::debug!("actor spawn throttled due to elevated resource pressure");
354                Ok(())
355            }
356            crate::supervisor::PressureLevel::Critical => {
357                tracing::warn!("actor spawn rejected due to critical resource pressure");
358                Err(Error::resource_exhausted(
359                    "system resources critically low; actor spawn rejected",
360                ))
361            }
362        }
363    }
364
365    /// Kill an actor.
366    pub fn kill(&self, id: &ActorId) -> crate::Result<()> {
367        self.registry.set_state(id, ActorState::Stopped)?;
368        if let Some(m) = self.registry.get_mailbox(id) {
369            m.clear()
370        }
371        self.registry.unregister(id)?;
372        if let Some(ref policy) = self.resource_policy {
373            policy.release_actor();
374        }
375        Ok(())
376    }
377
378    /// Set an actor to running state.
379    pub fn set_actor_running(&self, id: &ActorId) -> crate::Result<()> {
380        self.registry.set_state(id, ActorState::Running)
381    }
382
383    /// Set an actor state.
384    pub fn set_actor_state(&self, id: &ActorId, state: ActorState) -> crate::Result<()> {
385        self.registry.set_state(id, state)
386    }
387
388    /// Send a message to an actor.
389    pub async fn send(&self, target: ActorId, message: Message) -> crate::Result<()> {
390        if let Some(ref policy) = self.resource_policy {
391            policy.admit_message(1)?;
392        }
393
394        let mailbox = self
395            .registry
396            .get_mailbox(&target)
397            .ok_or_else(|| Error::actor(format!("actor {:?} not found", target)))?;
398
399        let state = self.registry.get_state(&target);
400        match state {
401            Some(ActorState::Stopped) | Some(ActorState::Failed) => {
402                return Err(Error::actor(format!("actor {:?} is not running", target)));
403            }
404            Some(ActorState::Suspended) => {
405                // Message queued but actor won't process until resumed
406            }
407            _ => {}
408        }
409
410        let priority = message.priority;
411
412        // Clone once for mailbox, move original into task.
413        // Previous implementation cloned twice (mailbox + task).
414        mailbox.send(message.clone()).await?;
415
416        let task = Task {
417            actor_id: target,
418            message, // moved, not cloned
419            priority,
420            additional_messages: Vec::new(),
421        };
422
423        if self.config.priority_scheduling && task.priority >= Priority::High {
424            self.priority_queue.push(task);
425        } else {
426            self.global_queue.push(task);
427        }
428
429        Ok(())
430    }
431
432    /// Send a batch of messages to an actor in a single queue operation.
433    ///
434    /// This is more efficient than calling `send` in a loop because it performs
435    /// one quota/rate-limit check and pushes a single `Task` to the work-stealing
436    /// queue for the entire batch.
437    pub async fn send_batch(&self, target: ActorId, messages: Vec<Message>) -> crate::Result<()> {
438        if messages.is_empty() {
439            return Ok(());
440        }
441
442        if let Some(ref policy) = self.resource_policy {
443            policy.admit_message(messages.len())?;
444        }
445
446        let mailbox = self
447            .registry
448            .get_mailbox(&target)
449            .ok_or_else(|| Error::actor(format!("actor {:?} not found", target)))?;
450
451        let state = self.registry.get_state(&target);
452        match state {
453            Some(ActorState::Stopped) | Some(ActorState::Failed) => {
454                return Err(Error::actor(format!("actor {:?} is not running", target)));
455            }
456            Some(ActorState::Suspended) => {}
457            _ => {}
458        }
459
460        let max_priority = messages
461            .iter()
462            .map(|m| m.priority)
463            .max()
464            .unwrap_or(Priority::Normal);
465
466        for msg in &messages {
467            mailbox.send(msg.clone()).await?;
468        }
469
470        let mut messages_into_iter = messages.into_iter();
471        let first = match messages_into_iter.next() {
472            Some(msg) => msg,
473            None => return Ok(()),
474        };
475        let additional: Vec<Message> = messages_into_iter.collect();
476
477        let task = Task {
478            actor_id: target,
479            message: first,
480            priority: max_priority,
481            additional_messages: additional,
482        };
483
484        if self.config.priority_scheduling && task.priority >= Priority::High {
485            self.priority_queue.push(task);
486        } else {
487            self.global_queue.push(task);
488        }
489
490        Ok(())
491    }
492
493    /// Try to send a message (non-blocking).
494    pub fn try_send(&self, target: ActorId, message: Message) -> crate::Result<()> {
495        let mailbox = self
496            .registry
497            .get_mailbox(&target)
498            .ok_or_else(|| Error::actor(format!("actor {:?} not found", target)))?;
499
500        let priority = message.priority;
501
502        // Clone once for mailbox, move original into task.
503        mailbox.try_send(message.clone()).map_err(|(_, e)| e)?;
504
505        let task = Task {
506            actor_id: target,
507            message, // moved, not cloned
508            priority,
509            additional_messages: Vec::new(),
510        };
511
512        if self.config.priority_scheduling && task.priority >= Priority::High {
513            self.priority_queue.push(task);
514        } else {
515            self.global_queue.push(task);
516        }
517
518        Ok(())
519    }
520
521    /// Get the actor registry.
522    pub fn registry(&self) -> &Arc<ActorRegistry> {
523        &self.registry
524    }
525
526    /// Get scheduler statistics.
527    pub fn stats(&self) -> SchedulerStats {
528        let mut worker_stats = Vec::new();
529        let mut total_processed = 0u64;
530        let mut total_stolen = 0u64;
531        let mut total_batches = 0u64;
532
533        for (id, stats) in self.worker_stats.iter().enumerate() {
534            let processed = stats.processed.load(Ordering::Relaxed);
535            let stolen = stats.stolen.load(Ordering::Relaxed);
536            let batches = stats.batches_processed.load(Ordering::Relaxed);
537
538            total_processed += processed;
539            total_stolen += stolen;
540            total_batches += batches;
541
542            worker_stats.push(WorkerStatsInfo {
543                id,
544                processed,
545                stolen,
546                batches_processed: batches,
547            });
548        }
549
550        SchedulerStats {
551            running: self.running.load(Ordering::Relaxed),
552            total_actors: self.total_actors.load(Ordering::Relaxed),
553            active_actors: self.registry.stats().running,
554            total_messages_processed: total_processed,
555            total_batches_processed: total_batches,
556            total_stolen,
557            worker_count: self.worker_stats.len(),
558            workers: worker_stats,
559        }
560    }
561
562    /// Get an RPC client for making typed RPC calls to actors.
563    #[cfg(feature = "serde")]
564    pub fn rpc_client(self: &Arc<Self>) -> RpcClient {
565        RpcClient::new(self.clone())
566    }
567
568    #[allow(clippy::too_many_arguments)]
569    fn worker_loop(
570        worker_id: usize,
571        worker: crossbeam_deque::Worker<Task>,
572        global_queue: Arc<WorkQueue>,
573        priority_queue: Arc<PriorityQueue>,
574        registry: Arc<ActorRegistry>,
575        config: SchedulerConfig,
576        running: Arc<AtomicBool>,
577        stats: Arc<WorkerStats>,
578        stealer_registry: StealerRegistry,
579        executor: Option<&Arc<dyn ActorExecutor>>,
580    ) {
581        let mut stealer = WorkStealer::new(Vec::new());
582        let mut last_version = 0u64;
583        let mut iteration = 0u32;
584
585        let mut consecutive_empty = 0u32;
586
587        while running.load(Ordering::Acquire) {
588            iteration = iteration.wrapping_add(1);
589
590            if iteration % config.stealer_refresh_interval == 0 {
591                let current_version = stealer_registry.version();
592                if current_version != last_version {
593                    let stealers = stealer_registry.get_stealers(worker_id);
594                    stealer = WorkStealer::new(stealers);
595                    last_version = current_version;
596                }
597            }
598
599            // Try priority queue first
600            if let Some(task) = priority_queue.pop() {
601                Self::process_task_safe(&registry, task, &stats, executor);
602                consecutive_empty = 0;
603                continue;
604            }
605
606            // Try local queue
607            if let Some(task) = worker.pop() {
608                Self::process_task_safe(&registry, task, &stats, executor);
609                consecutive_empty = 0;
610                continue;
611            }
612
613            // Try global queue
614            if let Some(task) = global_queue.steal_global() {
615                Self::process_task_safe(&registry, task, &stats, executor);
616                consecutive_empty = 0;
617                continue;
618            }
619
620            // Try stealing from other workers
621            if let Some(task) = stealer.steal() {
622                stats.stolen.fetch_add(1, Ordering::Relaxed);
623                Self::process_task_safe(&registry, task, &stats, executor);
624                consecutive_empty = 0;
625                continue;
626            }
627
628            // Steal batch from other workers
629            let stolen = stealer.steal_batch(&worker, config.max_steal_batch);
630            if stolen > 0 {
631                stats.stolen.fetch_add(stolen as u64, Ordering::Relaxed);
632                continue;
633            }
634
635            // No work found, back off
636            consecutive_empty += 1;
637            if consecutive_empty > 100 {
638                std::thread::sleep(std::time::Duration::from_micros(config.idle_sleep_us));
639            } else if consecutive_empty > 10 {
640                std::hint::spin_loop();
641            }
642        }
643    }
644
645    /// Process a task with panic protection.
646    ///
647    /// Wraps `process_task` in `catch_unwind` so that a panic in the executor
648    /// or handler does not kill the worker thread. On panic, the actor is marked
649    /// as `Failed` and its mailbox is drained to prevent further processing of
650    /// messages by the panicked actor.
651    fn process_task_safe(
652        registry: &ActorRegistry,
653        task: Task,
654        stats: &WorkerStats,
655        executor: Option<&Arc<dyn ActorExecutor>>,
656    ) {
657        let actor_id = task.actor_id;
658        let batch_size = 1 + task.additional_messages.len();
659        let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
660            Self::process_task(registry, task, stats, executor);
661        }));
662
663        if let Err(panic_payload) = result {
664            if batch_size > 1 {
665                stats.batches_processed.fetch_add(1, Ordering::Relaxed);
666            }
667            tracing::error!(
668                actor_id = ?actor_id,
669                batch_size,
670                "worker panicked while processing task; marking actor as Failed and draining mailbox"
671            );
672            let _ = registry.set_state(&actor_id, ActorState::Failed);
673            if let Some(mailbox) = registry.get_mailbox(&actor_id) {
674                mailbox.clear();
675            }
676            if let Some(s) = panic_payload.downcast_ref::<&str>() {
677                tracing::error!(panic_message = %s, "panic details");
678            } else if let Some(s) = panic_payload.downcast_ref::<String>() {
679                tracing::error!(panic_message = %s, "panic details");
680            }
681        }
682    }
683
684    /// Deterministic-simulation seam (feature `sim`).
685    ///
686    /// Runs one task through the *real* processing pipeline
687    /// ([`Self::process_task_safe`] → panic containment → state transitions →
688    /// mailbox slot release) against the real registry and mailboxes — without
689    /// worker threads. The sim owns task selection and calls this exactly once
690    /// per scheduling decision; this is the only place the simulation touches
691    /// production execution code.
692    ///
693    /// Returns the number of messages the runtime counted as processed.
694    #[cfg(feature = "sim")]
695    pub(crate) fn sim_process_task(
696        registry: &ActorRegistry,
697        task: Task,
698        executor: Option<&Arc<dyn ActorExecutor>>,
699    ) -> u64 {
700        let stats = WorkerStats::default();
701        Self::process_task_safe(registry, task, &stats, executor);
702        stats.processed.load(Ordering::Relaxed)
703    }
704
705    fn process_task(
706        registry: &ActorRegistry,
707        mut task: Task,
708        stats: &WorkerStats,
709        executor: Option<&Arc<dyn ActorExecutor>>,
710    ) {
711        if task.additional_messages.is_empty() {
712            // Fast path: single message
713            Self::process_single_message(registry, task.actor_id, &task.message, stats, executor);
714        } else {
715            // Batch path: process primary message, then additional ones
716            stats.batches_processed.fetch_add(1, Ordering::Relaxed);
717
718            Self::process_single_message(registry, task.actor_id, &task.message, stats, executor);
719
720            // Early exit if the actor failed during the first message
721            if matches!(
722                registry.get_state(&task.actor_id),
723                Some(ActorState::Failed) | Some(ActorState::Stopped)
724            ) {
725                return;
726            }
727
728            for message in task.additional_messages.drain(..) {
729                Self::process_single_message(registry, task.actor_id, &message, stats, executor);
730
731                // Stop processing remaining batch if actor is no longer viable
732                if matches!(
733                    registry.get_state(&task.actor_id),
734                    Some(ActorState::Failed) | Some(ActorState::Stopped)
735                ) {
736                    break;
737                }
738            }
739        }
740    }
741
742    fn process_single_message(
743        registry: &ActorRegistry,
744        actor_id: ActorId,
745        message: &Message,
746        stats: &WorkerStats,
747        executor: Option<&Arc<dyn ActorExecutor>>,
748    ) {
749        let actor_state = registry.get_state(&actor_id);
750
751        match actor_state {
752            Some(ActorState::Running) | Some(ActorState::Creating) => {
753                let should_count = if let Some(exec) = executor {
754                    match exec.execute(&actor_id, message) {
755                        ExecutionResult::Success { .. } => {
756                            if matches!(message.payload, MessagePayload::Start) {
757                                let _ = registry.set_state(&actor_id, ActorState::Running);
758                            } else if matches!(message.payload, MessagePayload::Stop) {
759                                let _ = registry.set_state(&actor_id, ActorState::Stopped);
760                            }
761                            true
762                        }
763                        ExecutionResult::FuelExhausted { .. } => {
764                            let _ = registry.set_state(&actor_id, ActorState::Failed);
765                            if let Some(mailbox) = registry.get_mailbox(&actor_id) {
766                                mailbox.clear();
767                            }
768                            false
769                        }
770                        ExecutionResult::Failed { error } => {
771                            tracing::warn!("Actor execution failed: {}", error);
772                            let _ = registry.set_state(&actor_id, ActorState::Failed);
773                            if let Some(mailbox) = registry.get_mailbox(&actor_id) {
774                                mailbox.clear();
775                            }
776                            false
777                        }
778                        ExecutionResult::NotReady => {
779                            Self::handle_state_change_for(actor_id, message, registry);
780                            true
781                        }
782                    }
783                } else {
784                    Self::handle_state_change_for(actor_id, message, registry);
785                    true
786                };
787
788                if should_count {
789                    stats.processed.fetch_add(1, Ordering::Relaxed);
790                    registry.record_processed(&actor_id);
791                    // The message is now consumed: release the mailbox slot
792                    // it parked in. `Mailbox::send`/`try_send` acquire one
793                    // capacity permit per message and `forget()` it so the
794                    // slot stays held until consumption; the worker processes
795                    // the `Task` copy from the work queue, so without this
796                    // pop the permit and slot leak forever. After `capacity`
797                    // cumulative messages the semaphore exhausted and every
798                    // further send stalled (the 0.1.0 drain-stall bug).
799                    if let Some(mailbox) = registry.get_mailbox(&actor_id) {
800                        mailbox.try_recv();
801                    }
802                }
803            }
804            Some(ActorState::Suspended) => {
805                if let Some(mailbox) = registry.get_mailbox(&actor_id) {
806                    let _ = mailbox.try_send(message.clone());
807                }
808            }
809            _ => {}
810        }
811    }
812
813    fn handle_state_change_for(actor_id: ActorId, message: &Message, registry: &ActorRegistry) {
814        if matches!(message.payload, MessagePayload::Start) {
815            let _ = registry.set_state(&actor_id, ActorState::Running);
816        } else if matches!(message.payload, MessagePayload::Stop) {
817            let _ = registry.set_state(&actor_id, ActorState::Stopped);
818        } else if let Some(MessagePayload::Signal(signal)) = Some(&message.payload) {
819            match signal {
820                crate::Signal::Pause => {
821                    let _ = registry.set_state(&actor_id, ActorState::Suspended);
822                }
823                crate::Signal::Resume => {
824                    let _ = registry.set_state(&actor_id, ActorState::Running);
825                }
826                crate::Signal::Restart => {
827                    let _ = registry.set_state(&actor_id, ActorState::Creating);
828                }
829            }
830        }
831    }
832}
833
834impl Drop for ActorScheduler {
835    fn drop(&mut self) {
836        self.stop();
837    }
838}
839
840/// Statistics for the scheduler.
841#[derive(Debug)]
842pub struct SchedulerStats {
843    /// Whether the scheduler is running
844    pub running: bool,
845    /// Total actors spawned
846    pub total_actors: u64,
847    /// Currently active actors
848    pub active_actors: usize,
849    /// Total messages processed
850    pub total_messages_processed: u64,
851    /// Total batch tasks processed
852    pub total_batches_processed: u64,
853    /// Total tasks stolen
854    pub total_stolen: u64,
855    /// Number of workers
856    pub worker_count: usize,
857    /// Per-worker statistics
858    pub workers: Vec<WorkerStatsInfo>,
859}
860
861/// Statistics for a worker.
862#[derive(Debug)]
863pub struct WorkerStatsInfo {
864    /// Worker ID
865    pub id: usize,
866    /// Tasks processed
867    pub processed: u64,
868    /// Tasks stolen from others
869    pub stolen: u64,
870    /// Batch tasks processed
871    pub batches_processed: u64,
872}
873
874#[cfg(test)]
875mod tests {
876    use super::*;
877    use crate::policy::{NoopPolicy, ResourcePolicy};
878
879    /// Test policy admitting at most `max_actors` live actors, mirroring the
880    /// upstream `QuotaEnforcer` max-actors behavior.
881    struct CountingPolicy {
882        max_actors: usize,
883        live: std::sync::atomic::AtomicUsize,
884    }
885
886    impl CountingPolicy {
887        fn new(max_actors: usize) -> Self {
888            Self {
889                max_actors,
890                live: std::sync::atomic::AtomicUsize::new(0),
891            }
892        }
893    }
894
895    impl ResourcePolicy for CountingPolicy {
896        fn admit_actor(&self) -> crate::Result<()> {
897            let prev = self.live.fetch_add(1, Ordering::SeqCst);
898            if prev >= self.max_actors {
899                self.live.fetch_sub(1, Ordering::SeqCst);
900                return Err(crate::Error::ResourceExhausted(format!(
901                    "actor limit exceeded ({}/{})",
902                    prev, self.max_actors
903                )));
904            }
905            Ok(())
906        }
907
908        fn release_actor(&self) {
909            self.live.fetch_sub(1, Ordering::SeqCst);
910        }
911    }
912
913    #[test]
914    fn test_scheduler_creation() {
915        let config = SchedulerConfig::new().workers(2);
916        let scheduler = ActorScheduler::new(config);
917
918        assert!(!scheduler.running.load(Ordering::Relaxed));
919        assert_eq!(scheduler.worker_stats.len(), 2);
920    }
921
922    #[tokio::test]
923    async fn test_scheduler_spawn_actor() {
924        let scheduler = ActorScheduler::new(SchedulerConfig::new().workers(1));
925        scheduler.start().unwrap();
926
927        let id = scheduler.spawn().unwrap();
928        assert!(scheduler.registry().get_state(&id).is_some());
929
930        scheduler.stop();
931    }
932
933    #[tokio::test]
934    async fn test_scheduler_send_message() {
935        let scheduler = ActorScheduler::new(SchedulerConfig::new().workers(1));
936        scheduler.start().unwrap();
937
938        let id = scheduler.spawn().unwrap();
939
940        scheduler.set_actor_running(&id).unwrap();
941
942        let msg = Message {
943            sender: None,
944            payload: MessagePayload::Custom(vec![1, 2, 3]),
945            priority: Priority::Normal,
946        };
947
948        scheduler.send(id, msg).await.unwrap();
949
950        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
951
952        scheduler.stop();
953    }
954
955    #[test]
956    fn test_config_effective_workers() {
957        let config = SchedulerConfig::new().workers(0);
958        assert!(config.effective_workers() > 0);
959
960        let config = SchedulerConfig::new().workers(8);
961        assert_eq!(config.effective_workers(), 8);
962    }
963
964    #[test]
965    fn test_stealer_registry() {
966        let registry = StealerRegistry::new();
967        let (worker1, stealer1) = create_local_queue();
968        let (worker2, stealer2) = create_local_queue();
969
970        registry.add_stealer(0, stealer1);
971        registry.add_stealer(1, stealer2);
972
973        let stealers_for_0 = registry.get_stealers(0);
974        assert_eq!(stealers_for_0.len(), 1);
975
976        let stealers_for_1 = registry.get_stealers(1);
977        assert_eq!(stealers_for_1.len(), 1);
978
979        drop(worker1);
980        drop(worker2);
981    }
982
983    #[tokio::test]
984    async fn test_scheduler_with_null_executor() {
985        let executor = Arc::new(NullExecutor::new());
986        let scheduler =
987            ActorScheduler::with_executor(SchedulerConfig::new().workers(2), executor.clone());
988        scheduler.start().unwrap();
989
990        let id = scheduler.spawn().unwrap();
991        scheduler.set_actor_running(&id).unwrap();
992
993        let msg = Message {
994            sender: None,
995            payload: MessagePayload::Custom(vec![1, 2, 3]),
996            priority: Priority::Normal,
997        };
998
999        scheduler.send(id, msg).await.unwrap();
1000
1001        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
1002
1003        let stats = scheduler.stats();
1004        assert!(stats.total_messages_processed > 0 || stats.total_stolen > 0);
1005
1006        scheduler.stop();
1007    }
1008
1009    #[tokio::test]
1010    async fn test_work_stealing_between_workers() {
1011        let scheduler = ActorScheduler::new(SchedulerConfig::new().workers(4));
1012        scheduler.start().unwrap();
1013
1014        let mut actors = Vec::new();
1015        for _ in 0..10 {
1016            let id = scheduler.spawn().unwrap();
1017            scheduler.set_actor_running(&id).unwrap();
1018            actors.push(id);
1019        }
1020
1021        for actor_id in &actors {
1022            let msg = Message {
1023                sender: None,
1024                payload: MessagePayload::Custom(vec![1, 2, 3]),
1025                priority: Priority::Normal,
1026            };
1027            scheduler.send(*actor_id, msg).await.unwrap();
1028        }
1029
1030        tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
1031
1032        let stats = scheduler.stats();
1033        assert!(stats.total_messages_processed >= 5);
1034        assert_eq!(stats.worker_count, 4);
1035
1036        scheduler.stop();
1037    }
1038
1039    #[tokio::test]
1040    async fn test_scheduler_resource_policy_rejects_over_limit() {
1041        let policy = Arc::new(CountingPolicy::new(1));
1042        let scheduler = ActorScheduler::with_resource_policy(
1043            SchedulerConfig::new().workers(1),
1044            Arc::new(NullExecutor::new()),
1045            policy,
1046        );
1047
1048        let first = scheduler.spawn().unwrap();
1049        assert!(scheduler.registry().get_state(&first).is_some());
1050
1051        let result = scheduler.spawn();
1052        assert!(result.is_err());
1053        let err_msg = result.unwrap_err().to_string();
1054        assert!(err_msg.contains("actor limit exceeded"), "got: {err_msg}");
1055
1056        // Releasing the killed actor frees a slot.
1057        scheduler.kill(&first).unwrap();
1058        let second = scheduler.spawn().unwrap();
1059        assert!(scheduler.registry().get_state(&second).is_some());
1060
1061        scheduler.stop();
1062    }
1063
1064    #[test]
1065    fn test_scheduler_noop_policy_admits_all() {
1066        let policy = NoopPolicy;
1067        assert!(policy.admit_actor().is_ok());
1068        assert!(policy.admit_message(100).is_ok());
1069        policy.release_actor();
1070    }
1071}