1use 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
22type WorkerStealer = (usize, crossbeam_deque::Stealer<Task>);
24
25#[derive(Clone)]
27pub struct StealerRegistry {
28 stealers: Arc<RwLock<Vec<WorkerStealer>>>,
29 version: Arc<AtomicU64>,
30}
31
32impl StealerRegistry {
33 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 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 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 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#[derive(Debug, Clone)]
72pub struct SchedulerConfig {
73 pub workers: usize,
75 pub mailbox_config: MailboxConfig,
77 pub priority_scheduling: bool,
79 pub max_steal_batch: usize,
81 pub idle_sleep_us: u64,
83 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 pub fn new() -> Self {
103 Self::default()
104 }
105
106 pub fn workers(mut self, workers: usize) -> Self {
108 self.workers = workers;
109 self
110 }
111
112 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#[derive(Debug, Default)]
126struct WorkerStats {
127 processed: AtomicU64,
129 stolen: AtomicU64,
131 batches_processed: AtomicU64,
133}
134
135pub struct ActorScheduler {
137 config: SchedulerConfig,
139 global_queue: Arc<WorkQueue>,
141 priority_queue: Arc<PriorityQueue>,
143 registry: Arc<ActorRegistry>,
145 stealer_registry: StealerRegistry,
147 worker_handles: Mutex<Vec<JoinHandle<()>>>,
149 running: Arc<AtomicBool>,
151 total_actors: AtomicU64,
153 total_processed: Arc<AtomicU64>,
155 worker_stats: Vec<Arc<WorkerStats>>,
157 executor: Option<Arc<dyn ActorExecutor>>,
159 resource_policy: Option<Arc<dyn ResourcePolicy>>,
166 degradation: Option<(Arc<dyn ResourceMonitor>, f64)>,
170}
171
172impl ActorScheduler {
173 pub fn new(config: SchedulerConfig) -> Self {
175 Self::with_options(config, None, None)
176 }
177
178 pub fn with_executor(config: SchedulerConfig, executor: Arc<dyn ActorExecutor>) -> Self {
180 Self::with_options(config, Some(executor), None)
181 }
182
183 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 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 let registry = Arc::new(ActorRegistry::with_mailbox_config(
212 config.mailbox_config.clone(),
213 ));
214
215 Self {
216 config,
217 global_queue: Arc::new(WorkQueue::new()),
218 priority_queue: Arc::new(PriorityQueue::new()),
219 registry,
220 stealer_registry: StealerRegistry::new(),
221 worker_handles: Mutex::new(Vec::new()),
222 running: Arc::new(AtomicBool::new(false)),
223 total_actors: AtomicU64::new(0),
224 total_processed: Arc::new(AtomicU64::new(0)),
225 worker_stats,
226 executor,
227 resource_policy,
228 degradation: None,
229 }
230 }
231
232 pub fn set_executor(&mut self, executor: Arc<dyn ActorExecutor>) {
234 self.executor = Some(executor);
235 }
236
237 pub fn set_degradation_with_monitor(
243 &mut self,
244 monitor: Arc<dyn ResourceMonitor>,
245 elevated_batch_factor: f64,
246 ) {
247 self.degradation = Some((monitor, elevated_batch_factor));
248 }
249
250 pub fn start(&self) -> Result<(), Error> {
252 if self.running.swap(true, Ordering::SeqCst) {
253 return Ok(());
254 }
255
256 let mut handles = self.worker_handles.lock();
257
258 for (id, stats) in self.worker_stats.iter().enumerate() {
259 let (worker, stealer) = create_local_queue();
260 self.stealer_registry.add_stealer(id, stealer);
261
262 let global_queue = self.global_queue.clone();
263 let priority_queue = self.priority_queue.clone();
264 let registry = self.registry.clone();
265 let config = self.config.clone();
266 let running_flag = self.running.clone();
267 let total_processed = self.total_processed.clone();
268 let stats = stats.clone();
269 let stealer_registry = self.stealer_registry.clone();
270 let executor = self.executor.clone();
271
272 let handle = thread::Builder::new()
273 .name(format!("actor-kit-worker-{}", id))
274 .spawn(move || {
275 Self::worker_loop(
276 id,
277 worker,
278 global_queue,
279 priority_queue,
280 registry,
281 config,
282 running_flag,
283 total_processed,
284 stats,
285 stealer_registry,
286 executor.as_ref(),
287 );
288 });
289
290 match handle {
291 Ok(h) => handles.push(h),
292 Err(e) => {
293 tracing::error!("Failed to spawn worker thread {}: {}", id, e);
294 return Err(Error::internal(format!(
295 "Failed to spawn worker thread {}: {}",
296 id, e
297 )));
298 }
299 }
300 }
301
302 Ok(())
303 }
304
305 pub fn stop(&self) {
307 self.running.store(false, Ordering::SeqCst);
308
309 let mut handles = self.worker_handles.lock();
310 for handle in handles.drain(..) {
311 let _ = handle.join();
312 }
313 }
314
315 pub fn spawn(&self) -> crate::Result<ActorId> {
317 self.spawn_named(None)
318 }
319
320 pub fn spawn_named(&self, name: Option<String>) -> crate::Result<ActorId> {
322 self.check_degradation()?;
323
324 if let Some(ref policy) = self.resource_policy {
325 policy.admit_actor()?;
326 }
327 let id = ActorId::new();
328 self.registry.register_named(id, name)?;
329 self.total_actors.fetch_add(1, Ordering::Relaxed);
330 Ok(id)
331 }
332
333 fn check_degradation(&self) -> crate::Result<()> {
337 let (monitor, _) = match &self.degradation {
338 Some(d) => d,
339 None => return Ok(()),
340 };
341
342 match monitor.current_pressure() {
343 crate::supervisor::PressureLevel::Normal => Ok(()),
344 crate::supervisor::PressureLevel::Elevated => {
345 tracing::debug!("actor spawn throttled due to elevated resource pressure");
346 Ok(())
347 }
348 crate::supervisor::PressureLevel::Critical => {
349 tracing::warn!("actor spawn rejected due to critical resource pressure");
350 Err(Error::resource_exhausted(
351 "system resources critically low; actor spawn rejected",
352 ))
353 }
354 }
355 }
356
357 pub fn kill(&self, id: &ActorId) -> crate::Result<()> {
359 self.registry.set_state(id, ActorState::Stopped)?;
360 if let Some(m) = self.registry.get_mailbox(id) {
361 m.clear()
362 }
363 self.registry.unregister(id)?;
364 if let Some(ref policy) = self.resource_policy {
365 policy.release_actor();
366 }
367 Ok(())
368 }
369
370 pub fn set_actor_running(&self, id: &ActorId) -> crate::Result<()> {
372 self.registry.set_state(id, ActorState::Running)
373 }
374
375 pub fn set_actor_state(&self, id: &ActorId, state: ActorState) -> crate::Result<()> {
377 self.registry.set_state(id, state)
378 }
379
380 pub async fn send(&self, target: ActorId, message: Message) -> crate::Result<()> {
382 if let Some(ref policy) = self.resource_policy {
383 policy.admit_message(1)?;
384 }
385
386 let mailbox = self
387 .registry
388 .get_mailbox(&target)
389 .ok_or_else(|| Error::actor(format!("actor {:?} not found", target)))?;
390
391 let state = self.registry.get_state(&target);
392 match state {
393 Some(ActorState::Stopped) | Some(ActorState::Failed) => {
394 return Err(Error::actor(format!("actor {:?} is not running", target)));
395 }
396 Some(ActorState::Suspended) => {
397 }
399 _ => {}
400 }
401
402 let priority = message.priority;
403
404 mailbox.send(message.clone()).await?;
407
408 let task = Task {
409 actor_id: target,
410 message, priority,
412 additional_messages: Vec::new(),
413 };
414
415 if self.config.priority_scheduling && task.priority >= Priority::High {
416 self.priority_queue.push(task);
417 } else {
418 self.global_queue.push(task);
419 }
420
421 Ok(())
422 }
423
424 pub async fn send_batch(&self, target: ActorId, messages: Vec<Message>) -> crate::Result<()> {
430 if messages.is_empty() {
431 return Ok(());
432 }
433
434 if let Some(ref policy) = self.resource_policy {
435 policy.admit_message(messages.len())?;
436 }
437
438 let mailbox = self
439 .registry
440 .get_mailbox(&target)
441 .ok_or_else(|| Error::actor(format!("actor {:?} not found", target)))?;
442
443 let state = self.registry.get_state(&target);
444 match state {
445 Some(ActorState::Stopped) | Some(ActorState::Failed) => {
446 return Err(Error::actor(format!("actor {:?} is not running", target)));
447 }
448 Some(ActorState::Suspended) => {}
449 _ => {}
450 }
451
452 let max_priority = messages
453 .iter()
454 .map(|m| m.priority)
455 .max()
456 .unwrap_or(Priority::Normal);
457
458 for msg in &messages {
459 mailbox.send(msg.clone()).await?;
460 }
461
462 let mut messages_into_iter = messages.into_iter();
463 let first = match messages_into_iter.next() {
464 Some(msg) => msg,
465 None => return Ok(()),
466 };
467 let additional: Vec<Message> = messages_into_iter.collect();
468
469 let task = Task {
470 actor_id: target,
471 message: first,
472 priority: max_priority,
473 additional_messages: additional,
474 };
475
476 if self.config.priority_scheduling && task.priority >= Priority::High {
477 self.priority_queue.push(task);
478 } else {
479 self.global_queue.push(task);
480 }
481
482 Ok(())
483 }
484
485 pub fn try_send(&self, target: ActorId, message: Message) -> crate::Result<()> {
487 let mailbox = self
488 .registry
489 .get_mailbox(&target)
490 .ok_or_else(|| Error::actor(format!("actor {:?} not found", target)))?;
491
492 let priority = message.priority;
493
494 mailbox.try_send(message.clone()).map_err(|(_, e)| e)?;
496
497 let task = Task {
498 actor_id: target,
499 message, priority,
501 additional_messages: Vec::new(),
502 };
503
504 if self.config.priority_scheduling && task.priority >= Priority::High {
505 self.priority_queue.push(task);
506 } else {
507 self.global_queue.push(task);
508 }
509
510 Ok(())
511 }
512
513 pub fn registry(&self) -> &Arc<ActorRegistry> {
515 &self.registry
516 }
517
518 pub fn stats(&self) -> SchedulerStats {
520 let mut worker_stats = Vec::new();
521 let mut total_processed = 0u64;
522 let mut total_stolen = 0u64;
523 let mut total_batches = 0u64;
524
525 for (id, stats) in self.worker_stats.iter().enumerate() {
526 let processed = stats.processed.load(Ordering::Relaxed);
527 let stolen = stats.stolen.load(Ordering::Relaxed);
528 let batches = stats.batches_processed.load(Ordering::Relaxed);
529
530 total_processed += processed;
531 total_stolen += stolen;
532 total_batches += batches;
533
534 worker_stats.push(WorkerStatsInfo {
535 id,
536 processed,
537 stolen,
538 batches_processed: batches,
539 });
540 }
541
542 SchedulerStats {
543 running: self.running.load(Ordering::Relaxed),
544 total_actors: self.total_actors.load(Ordering::Relaxed),
545 active_actors: self.registry.stats().running,
546 total_messages_processed: total_processed,
547 total_batches_processed: total_batches,
548 total_stolen,
549 worker_count: self.worker_stats.len(),
550 workers: worker_stats,
551 }
552 }
553
554 #[cfg(feature = "serde")]
556 pub fn rpc_client(self: &Arc<Self>) -> RpcClient {
557 RpcClient::new(self.clone())
558 }
559
560 #[allow(clippy::too_many_arguments)]
561 fn worker_loop(
562 worker_id: usize,
563 worker: crossbeam_deque::Worker<Task>,
564 global_queue: Arc<WorkQueue>,
565 priority_queue: Arc<PriorityQueue>,
566 registry: Arc<ActorRegistry>,
567 config: SchedulerConfig,
568 running: Arc<AtomicBool>,
569 total_processed: Arc<AtomicU64>,
570 stats: Arc<WorkerStats>,
571 stealer_registry: StealerRegistry,
572 executor: Option<&Arc<dyn ActorExecutor>>,
573 ) {
574 let mut stealer = WorkStealer::new(Vec::new());
575 let mut last_version = 0u64;
576 let mut iteration = 0u32;
577
578 let mut consecutive_empty = 0u32;
579
580 while running.load(Ordering::Acquire) {
581 iteration = iteration.wrapping_add(1);
582
583 if iteration % config.stealer_refresh_interval == 0 {
584 let current_version = stealer_registry.version();
585 if current_version != last_version {
586 let stealers = stealer_registry.get_stealers(worker_id);
587 stealer = WorkStealer::new(stealers);
588 last_version = current_version;
589 }
590 }
591
592 if let Some(task) = priority_queue.pop() {
594 Self::process_task_safe(®istry, task, &total_processed, &stats, executor);
595 consecutive_empty = 0;
596 continue;
597 }
598
599 if let Some(task) = worker.pop() {
601 Self::process_task_safe(®istry, task, &total_processed, &stats, executor);
602 consecutive_empty = 0;
603 continue;
604 }
605
606 if let Some(task) = global_queue.steal_global() {
608 Self::process_task_safe(®istry, task, &total_processed, &stats, executor);
609 consecutive_empty = 0;
610 continue;
611 }
612
613 if let Some(task) = stealer.steal() {
615 stats.stolen.fetch_add(1, Ordering::Relaxed);
616 Self::process_task_safe(®istry, task, &total_processed, &stats, executor);
617 consecutive_empty = 0;
618 continue;
619 }
620
621 let stolen = stealer.steal_batch(&worker, config.max_steal_batch);
623 if stolen > 0 {
624 stats.stolen.fetch_add(stolen as u64, Ordering::Relaxed);
625 continue;
626 }
627
628 consecutive_empty += 1;
630 if consecutive_empty > 100 {
631 std::thread::sleep(std::time::Duration::from_micros(config.idle_sleep_us));
632 } else if consecutive_empty > 10 {
633 std::hint::spin_loop();
634 }
635 }
636 }
637
638 fn process_task_safe(
645 registry: &ActorRegistry,
646 task: Task,
647 total_processed: &AtomicU64,
648 stats: &WorkerStats,
649 executor: Option<&Arc<dyn ActorExecutor>>,
650 ) {
651 let actor_id = task.actor_id;
652 let batch_size = 1 + task.additional_messages.len();
653 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
654 Self::process_task(registry, task, total_processed, stats, executor);
655 }));
656
657 if let Err(panic_payload) = result {
658 if batch_size > 1 {
659 stats.batches_processed.fetch_add(1, Ordering::Relaxed);
660 }
661 tracing::error!(
662 actor_id = ?actor_id,
663 batch_size,
664 "worker panicked while processing task; marking actor as Failed and draining mailbox"
665 );
666 let _ = registry.set_state(&actor_id, ActorState::Failed);
667 if let Some(mailbox) = registry.get_mailbox(&actor_id) {
668 mailbox.clear();
669 }
670 if let Some(s) = panic_payload.downcast_ref::<&str>() {
671 tracing::error!(panic_message = %s, "panic details");
672 } else if let Some(s) = panic_payload.downcast_ref::<String>() {
673 tracing::error!(panic_message = %s, "panic details");
674 }
675 }
676 }
677
678 fn process_task(
679 registry: &ActorRegistry,
680 mut task: Task,
681 total_processed: &AtomicU64,
682 stats: &WorkerStats,
683 executor: Option<&Arc<dyn ActorExecutor>>,
684 ) {
685 if task.additional_messages.is_empty() {
686 Self::process_single_message(
688 registry,
689 task.actor_id,
690 &task.message,
691 total_processed,
692 stats,
693 executor,
694 );
695 } else {
696 stats.batches_processed.fetch_add(1, Ordering::Relaxed);
698
699 Self::process_single_message(
700 registry,
701 task.actor_id,
702 &task.message,
703 total_processed,
704 stats,
705 executor,
706 );
707
708 if matches!(
710 registry.get_state(&task.actor_id),
711 Some(ActorState::Failed) | Some(ActorState::Stopped)
712 ) {
713 return;
714 }
715
716 for message in task.additional_messages.drain(..) {
717 Self::process_single_message(
718 registry,
719 task.actor_id,
720 &message,
721 total_processed,
722 stats,
723 executor,
724 );
725
726 if matches!(
728 registry.get_state(&task.actor_id),
729 Some(ActorState::Failed) | Some(ActorState::Stopped)
730 ) {
731 break;
732 }
733 }
734 }
735 }
736
737 fn process_single_message(
738 registry: &ActorRegistry,
739 actor_id: ActorId,
740 message: &Message,
741 total_processed: &AtomicU64,
742 stats: &WorkerStats,
743 executor: Option<&Arc<dyn ActorExecutor>>,
744 ) {
745 let actor_state = registry.get_state(&actor_id);
746
747 match actor_state {
748 Some(ActorState::Running) | Some(ActorState::Creating) => {
749 let should_count = if let Some(exec) = executor {
750 match exec.execute(&actor_id, message) {
751 ExecutionResult::Success { .. } => {
752 if matches!(message.payload, MessagePayload::Start) {
753 let _ = registry.set_state(&actor_id, ActorState::Running);
754 } else if matches!(message.payload, MessagePayload::Stop) {
755 let _ = registry.set_state(&actor_id, ActorState::Stopped);
756 }
757 true
758 }
759 ExecutionResult::FuelExhausted { .. } => {
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::Failed { error } => {
767 tracing::warn!("Actor execution failed: {}", error);
768 let _ = registry.set_state(&actor_id, ActorState::Failed);
769 if let Some(mailbox) = registry.get_mailbox(&actor_id) {
770 mailbox.clear();
771 }
772 false
773 }
774 ExecutionResult::NotReady => {
775 Self::handle_state_change_for(actor_id, message, registry);
776 true
777 }
778 }
779 } else {
780 Self::handle_state_change_for(actor_id, message, registry);
781 true
782 };
783
784 if should_count {
785 stats.processed.fetch_add(1, Ordering::Relaxed);
786 total_processed.fetch_add(1, Ordering::Relaxed);
787 registry.record_processed(&actor_id);
788 if let Some(mailbox) = registry.get_mailbox(&actor_id) {
797 mailbox.try_recv();
798 }
799 }
800 }
801 Some(ActorState::Suspended) => {
802 if let Some(mailbox) = registry.get_mailbox(&actor_id) {
803 let _ = mailbox.try_send(message.clone());
804 }
805 }
806 _ => {}
807 }
808 }
809
810 fn handle_state_change_for(actor_id: ActorId, message: &Message, registry: &ActorRegistry) {
811 if matches!(message.payload, MessagePayload::Start) {
812 let _ = registry.set_state(&actor_id, ActorState::Running);
813 } else if matches!(message.payload, MessagePayload::Stop) {
814 let _ = registry.set_state(&actor_id, ActorState::Stopped);
815 } else if let Some(MessagePayload::Signal(signal)) = Some(&message.payload) {
816 match signal {
817 crate::Signal::Pause => {
818 let _ = registry.set_state(&actor_id, ActorState::Suspended);
819 }
820 crate::Signal::Resume => {
821 let _ = registry.set_state(&actor_id, ActorState::Running);
822 }
823 crate::Signal::Restart => {
824 let _ = registry.set_state(&actor_id, ActorState::Creating);
825 }
826 }
827 }
828 }
829}
830
831impl Drop for ActorScheduler {
832 fn drop(&mut self) {
833 self.stop();
834 }
835}
836
837#[derive(Debug)]
839pub struct SchedulerStats {
840 pub running: bool,
842 pub total_actors: u64,
844 pub active_actors: usize,
846 pub total_messages_processed: u64,
848 pub total_batches_processed: u64,
850 pub total_stolen: u64,
852 pub worker_count: usize,
854 pub workers: Vec<WorkerStatsInfo>,
856}
857
858#[derive(Debug)]
860pub struct WorkerStatsInfo {
861 pub id: usize,
863 pub processed: u64,
865 pub stolen: u64,
867 pub batches_processed: u64,
869}
870
871#[cfg(test)]
872mod tests {
873 use super::*;
874 use crate::policy::{NoopPolicy, ResourcePolicy};
875
876 struct CountingPolicy {
879 max_actors: usize,
880 live: std::sync::atomic::AtomicUsize,
881 }
882
883 impl CountingPolicy {
884 fn new(max_actors: usize) -> Self {
885 Self {
886 max_actors,
887 live: std::sync::atomic::AtomicUsize::new(0),
888 }
889 }
890 }
891
892 impl ResourcePolicy for CountingPolicy {
893 fn admit_actor(&self) -> crate::Result<()> {
894 let prev = self.live.fetch_add(1, Ordering::SeqCst);
895 if prev >= self.max_actors {
896 self.live.fetch_sub(1, Ordering::SeqCst);
897 return Err(crate::Error::ResourceExhausted(format!(
898 "actor limit exceeded ({}/{})",
899 prev, self.max_actors
900 )));
901 }
902 Ok(())
903 }
904
905 fn release_actor(&self) {
906 self.live.fetch_sub(1, Ordering::SeqCst);
907 }
908 }
909
910 #[test]
911 fn test_scheduler_creation() {
912 let config = SchedulerConfig::new().workers(2);
913 let scheduler = ActorScheduler::new(config);
914
915 assert!(!scheduler.running.load(Ordering::Relaxed));
916 assert_eq!(scheduler.worker_stats.len(), 2);
917 }
918
919 #[tokio::test]
920 async fn test_scheduler_spawn_actor() {
921 let scheduler = ActorScheduler::new(SchedulerConfig::new().workers(1));
922 scheduler.start().unwrap();
923
924 let id = scheduler.spawn().unwrap();
925 assert!(scheduler.registry().get_state(&id).is_some());
926
927 scheduler.stop();
928 }
929
930 #[tokio::test]
931 async fn test_scheduler_send_message() {
932 let scheduler = ActorScheduler::new(SchedulerConfig::new().workers(1));
933 scheduler.start().unwrap();
934
935 let id = scheduler.spawn().unwrap();
936
937 scheduler.set_actor_running(&id).unwrap();
938
939 let msg = Message {
940 sender: None,
941 payload: MessagePayload::Custom(vec![1, 2, 3]),
942 priority: Priority::Normal,
943 };
944
945 scheduler.send(id, msg).await.unwrap();
946
947 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
948
949 scheduler.stop();
950 }
951
952 #[test]
953 fn test_config_effective_workers() {
954 let config = SchedulerConfig::new().workers(0);
955 assert!(config.effective_workers() > 0);
956
957 let config = SchedulerConfig::new().workers(8);
958 assert_eq!(config.effective_workers(), 8);
959 }
960
961 #[test]
962 fn test_stealer_registry() {
963 let registry = StealerRegistry::new();
964 let (worker1, stealer1) = create_local_queue();
965 let (worker2, stealer2) = create_local_queue();
966
967 registry.add_stealer(0, stealer1);
968 registry.add_stealer(1, stealer2);
969
970 let stealers_for_0 = registry.get_stealers(0);
971 assert_eq!(stealers_for_0.len(), 1);
972
973 let stealers_for_1 = registry.get_stealers(1);
974 assert_eq!(stealers_for_1.len(), 1);
975
976 drop(worker1);
977 drop(worker2);
978 }
979
980 #[tokio::test]
981 async fn test_scheduler_with_null_executor() {
982 let executor = Arc::new(NullExecutor::new());
983 let scheduler =
984 ActorScheduler::with_executor(SchedulerConfig::new().workers(2), executor.clone());
985 scheduler.start().unwrap();
986
987 let id = scheduler.spawn().unwrap();
988 scheduler.set_actor_running(&id).unwrap();
989
990 let msg = Message {
991 sender: None,
992 payload: MessagePayload::Custom(vec![1, 2, 3]),
993 priority: Priority::Normal,
994 };
995
996 scheduler.send(id, msg).await.unwrap();
997
998 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
999
1000 let stats = scheduler.stats();
1001 assert!(stats.total_messages_processed > 0 || stats.total_stolen > 0);
1002
1003 scheduler.stop();
1004 }
1005
1006 #[tokio::test]
1007 async fn test_work_stealing_between_workers() {
1008 let scheduler = ActorScheduler::new(SchedulerConfig::new().workers(4));
1009 scheduler.start().unwrap();
1010
1011 let mut actors = Vec::new();
1012 for _ in 0..10 {
1013 let id = scheduler.spawn().unwrap();
1014 scheduler.set_actor_running(&id).unwrap();
1015 actors.push(id);
1016 }
1017
1018 for actor_id in &actors {
1019 let msg = Message {
1020 sender: None,
1021 payload: MessagePayload::Custom(vec![1, 2, 3]),
1022 priority: Priority::Normal,
1023 };
1024 scheduler.send(*actor_id, msg).await.unwrap();
1025 }
1026
1027 tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
1028
1029 let stats = scheduler.stats();
1030 assert!(stats.total_messages_processed >= 5);
1031 assert_eq!(stats.worker_count, 4);
1032
1033 scheduler.stop();
1034 }
1035
1036 #[tokio::test]
1037 async fn test_scheduler_resource_policy_rejects_over_limit() {
1038 let policy = Arc::new(CountingPolicy::new(1));
1039 let scheduler = ActorScheduler::with_resource_policy(
1040 SchedulerConfig::new().workers(1),
1041 Arc::new(NullExecutor::new()),
1042 policy,
1043 );
1044
1045 let first = scheduler.spawn().unwrap();
1046 assert!(scheduler.registry().get_state(&first).is_some());
1047
1048 let result = scheduler.spawn();
1049 assert!(result.is_err());
1050 let err_msg = result.unwrap_err().to_string();
1051 assert!(err_msg.contains("actor limit exceeded"), "got: {err_msg}");
1052
1053 scheduler.kill(&first).unwrap();
1055 let second = scheduler.spawn().unwrap();
1056 assert!(scheduler.registry().get_state(&second).is_some());
1057
1058 scheduler.stop();
1059 }
1060
1061 #[test]
1062 fn test_scheduler_noop_policy_admits_all() {
1063 let policy = NoopPolicy;
1064 assert!(policy.admit_actor().is_ok());
1065 assert!(policy.admit_message(100).is_ok());
1066 policy.release_actor();
1067 }
1068}