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)]
138#[repr(align(64))]
139struct WorkerStats {
140 processed: AtomicU64,
142 stolen: AtomicU64,
144 batches_processed: AtomicU64,
146}
147
148pub struct ActorScheduler {
150 config: SchedulerConfig,
152 global_queue: Arc<WorkQueue>,
154 priority_queue: Arc<PriorityQueue>,
156 registry: Arc<ActorRegistry>,
158 stealer_registry: StealerRegistry,
160 worker_handles: Mutex<Vec<JoinHandle<()>>>,
162 running: Arc<AtomicBool>,
164 total_actors: AtomicU64,
166 worker_stats: Vec<Arc<WorkerStats>>,
168 executor: Option<Arc<dyn ActorExecutor>>,
170 resource_policy: Option<Arc<dyn ResourcePolicy>>,
177 degradation: Option<(Arc<dyn ResourceMonitor>, f64)>,
181}
182
183impl ActorScheduler {
184 pub fn new(config: SchedulerConfig) -> Self {
186 Self::with_options(config, None, None)
187 }
188
189 pub fn with_executor(config: SchedulerConfig, executor: Arc<dyn ActorExecutor>) -> Self {
191 Self::with_options(config, Some(executor), None)
192 }
193
194 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 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 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 pub fn set_executor(&mut self, executor: Arc<dyn ActorExecutor>) {
244 self.executor = Some(executor);
245 }
246
247 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 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 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 pub fn spawn(&self) -> crate::Result<ActorId> {
325 self.spawn_named(None)
326 }
327
328 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 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 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 pub fn set_actor_running(&self, id: &ActorId) -> crate::Result<()> {
380 self.registry.set_state(id, ActorState::Running)
381 }
382
383 pub fn set_actor_state(&self, id: &ActorId, state: ActorState) -> crate::Result<()> {
385 self.registry.set_state(id, state)
386 }
387
388 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 }
407 _ => {}
408 }
409
410 let priority = message.priority;
411
412 mailbox.send(message.clone()).await?;
415
416 let task = Task {
417 actor_id: target,
418 message, 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 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 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 mailbox.try_send(message.clone()).map_err(|(_, e)| e)?;
504
505 let task = Task {
506 actor_id: target,
507 message, 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 pub fn registry(&self) -> &Arc<ActorRegistry> {
523 &self.registry
524 }
525
526 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 #[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 if let Some(task) = priority_queue.pop() {
601 Self::process_task_safe(®istry, task, &stats, executor);
602 consecutive_empty = 0;
603 continue;
604 }
605
606 if let Some(task) = worker.pop() {
608 Self::process_task_safe(®istry, task, &stats, executor);
609 consecutive_empty = 0;
610 continue;
611 }
612
613 if let Some(task) = global_queue.steal_global() {
615 Self::process_task_safe(®istry, task, &stats, executor);
616 consecutive_empty = 0;
617 continue;
618 }
619
620 if let Some(task) = stealer.steal() {
622 stats.stolen.fetch_add(1, Ordering::Relaxed);
623 Self::process_task_safe(®istry, task, &stats, executor);
624 consecutive_empty = 0;
625 continue;
626 }
627
628 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 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 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 #[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 Self::process_single_message(registry, task.actor_id, &task.message, stats, executor);
714 } else {
715 stats.batches_processed.fetch_add(1, Ordering::Relaxed);
717
718 Self::process_single_message(registry, task.actor_id, &task.message, stats, executor);
719
720 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 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 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#[derive(Debug)]
842pub struct SchedulerStats {
843 pub running: bool,
845 pub total_actors: u64,
847 pub active_actors: usize,
849 pub total_messages_processed: u64,
851 pub total_batches_processed: u64,
853 pub total_stolen: u64,
855 pub worker_count: usize,
857 pub workers: Vec<WorkerStatsInfo>,
859}
860
861#[derive(Debug)]
863pub struct WorkerStatsInfo {
864 pub id: usize,
866 pub processed: u64,
868 pub stolen: u64,
870 pub batches_processed: u64,
872}
873
874#[cfg(test)]
875mod tests {
876 use super::*;
877 use crate::policy::{NoopPolicy, ResourcePolicy};
878
879 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 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}