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