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 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 pub fn set_executor(&mut self, executor: Arc<dyn ActorExecutor>) {
226 self.executor = Some(executor);
227 }
228
229 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 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 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 pub fn spawn(&self) -> crate::Result<ActorId> {
309 self.spawn_named(None)
310 }
311
312 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 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 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 pub fn set_actor_running(&self, id: &ActorId) -> crate::Result<()> {
364 self.registry.set_state(id, ActorState::Running)
365 }
366
367 pub fn set_actor_state(&self, id: &ActorId, state: ActorState) -> crate::Result<()> {
369 self.registry.set_state(id, state)
370 }
371
372 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 }
391 _ => {}
392 }
393
394 let priority = message.priority;
395
396 mailbox.send(message.clone()).await?;
399
400 let task = Task {
401 actor_id: target,
402 message, 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 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 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 mailbox.try_send(message.clone()).map_err(|(_, e)| e)?;
488
489 let task = Task {
490 actor_id: target,
491 message, 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 pub fn registry(&self) -> &Arc<ActorRegistry> {
507 &self.registry
508 }
509
510 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 #[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 if let Some(task) = priority_queue.pop() {
586 Self::process_task_safe(®istry, task, &total_processed, &stats, executor);
587 consecutive_empty = 0;
588 continue;
589 }
590
591 if let Some(task) = worker.pop() {
593 Self::process_task_safe(®istry, task, &total_processed, &stats, executor);
594 consecutive_empty = 0;
595 continue;
596 }
597
598 if let Some(task) = global_queue.steal_global() {
600 Self::process_task_safe(®istry, task, &total_processed, &stats, executor);
601 consecutive_empty = 0;
602 continue;
603 }
604
605 if let Some(task) = stealer.steal() {
607 stats.stolen.fetch_add(1, Ordering::Relaxed);
608 Self::process_task_safe(®istry, task, &total_processed, &stats, executor);
609 consecutive_empty = 0;
610 continue;
611 }
612
613 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 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 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 Self::process_single_message(
680 registry,
681 task.actor_id,
682 &task.message,
683 total_processed,
684 stats,
685 executor,
686 );
687 } else {
688 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 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 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#[derive(Debug)]
820pub struct SchedulerStats {
821 pub running: bool,
823 pub total_actors: u64,
825 pub active_actors: usize,
827 pub total_messages_processed: u64,
829 pub total_batches_processed: u64,
831 pub total_stolen: u64,
833 pub worker_count: usize,
835 pub workers: Vec<WorkerStatsInfo>,
837}
838
839#[derive(Debug)]
841pub struct WorkerStatsInfo {
842 pub id: usize,
844 pub processed: u64,
846 pub stolen: u64,
848 pub batches_processed: u64,
850}
851
852#[cfg(test)]
853mod tests {
854 use super::*;
855 use crate::policy::{NoopPolicy, ResourcePolicy};
856
857 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 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}