1use std::future::Future;
43use std::pin::Pin;
44use std::sync::Arc;
45use std::sync::atomic::{AtomicU8, Ordering};
46use std::time::Duration;
47
48use crate::channel::mpsc;
49use crate::channel::mpsc::SendError;
50use crate::cx::Cx;
51use crate::runtime::{JoinError, SpawnError};
52use crate::types::{CxInner, Outcome, RegionId, TaskId, Time};
53
54#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
60pub struct ActorId(TaskId);
61
62impl ActorId {
63 #[must_use]
65 #[inline]
66 pub const fn from_task(task_id: TaskId) -> Self {
67 Self(task_id)
68 }
69
70 #[must_use]
72 #[inline]
73 pub const fn task_id(self) -> TaskId {
74 self.0
75 }
76}
77
78impl std::fmt::Debug for ActorId {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 f.debug_tuple("ActorId").field(&self.0).finish()
81 }
82}
83
84impl std::fmt::Display for ActorId {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 write!(f, "{}", self.0)
89 }
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum ActorState {
95 Created,
97 Running,
99 Stopping,
101 Stopped,
103}
104
105#[derive(Debug)]
106struct ActorStateCell {
107 state: AtomicU8,
108}
109
110impl ActorStateCell {
111 #[inline]
112 fn new(state: ActorState) -> Self {
113 Self {
114 state: AtomicU8::new(Self::encode(state)),
115 }
116 }
117
118 #[inline]
119 fn load(&self) -> ActorState {
120 Self::decode(self.state.load(Ordering::Acquire))
121 }
122
123 #[inline]
124 fn store(&self, state: ActorState) {
125 self.state.store(Self::encode(state), Ordering::Release);
126 }
127
128 #[inline]
131 fn compare_and_swap(&self, current: ActorState, new: ActorState) -> bool {
132 self.state
133 .compare_exchange(
134 Self::encode(current),
135 Self::encode(new),
136 Ordering::AcqRel,
137 Ordering::Acquire,
138 )
139 .is_ok()
140 }
141
142 #[inline]
143 const fn encode(state: ActorState) -> u8 {
144 match state {
145 ActorState::Created => 0,
146 ActorState::Running => 1,
147 ActorState::Stopping => 2,
148 ActorState::Stopped => 3,
149 }
150 }
151
152 #[inline]
153 const fn decode(value: u8) -> ActorState {
154 match value {
155 0 => ActorState::Created,
156 1 => ActorState::Running,
157 2 => ActorState::Stopping,
158 _ => ActorState::Stopped,
159 }
160 }
161}
162
163struct ActorCell<M> {
168 mailbox: mpsc::Receiver<M>,
169 state: Arc<ActorStateCell>,
170}
171
172pub trait Actor: Send + 'static {
186 type Message: Send + 'static;
188
189 fn on_start(&mut self, _cx: &Cx) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
194 Box::pin(async {})
195 }
196
197 fn handle(
202 &mut self,
203 cx: &Cx,
204 msg: Self::Message,
205 ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
206
207 fn on_stop(&mut self, _cx: &Cx) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
211 Box::pin(async {})
212 }
213}
214
215#[derive(Debug)]
224pub struct ActorHandle<A: Actor> {
225 actor_id: ActorId,
226 sender: mpsc::Sender<A::Message>,
227 state: Arc<ActorStateCell>,
228 task_id: TaskId,
229 receiver: crate::channel::oneshot::Receiver<Result<A, JoinError>>,
230 inner: std::sync::Weak<parking_lot::RwLock<CxInner>>,
231 completed: bool,
232}
233
234impl<A: Actor> ActorHandle<A> {
235 pub async fn send(&self, cx: &Cx, msg: A::Message) -> Outcome<(), SendError<A::Message>> {
239 match self.sender.send(cx, msg).await {
240 Ok(()) => Outcome::ok(()),
241 Err(e) => Outcome::err(e),
242 }
243 }
244
245 pub fn try_send(&self, msg: A::Message) -> Result<(), SendError<A::Message>> {
250 self.sender.try_send(msg)
251 }
252
253 #[must_use]
255 pub fn sender(&self) -> ActorRef<A::Message> {
256 ActorRef {
257 actor_id: self.actor_id,
258 sender: self.sender.clone(),
259 state: Arc::clone(&self.state),
260 }
261 }
262
263 #[must_use]
265 pub const fn actor_id(&self) -> ActorId {
266 self.actor_id
267 }
268
269 #[must_use]
271 pub fn task_id(&self) -> crate::types::TaskId {
272 self.task_id
273 }
274
275 pub fn stop(&self) {
285 self.state.store(ActorState::Stopping);
286 self.sender.close_receiver();
287 }
288
289 #[must_use]
291 pub fn is_finished(&self) -> bool {
292 self.completed || self.receiver.is_ready() || self.receiver.is_closed()
293 }
294
295 pub fn join<'a>(&'a mut self, _cx: &'a Cx) -> ActorJoinFuture<'a, A> {
300 let cx_inner = self.inner.clone();
301 let receiver = &mut self.receiver;
302 let terminal_state = &mut self.completed;
303 ActorJoinFuture {
304 inner: receiver.recv_uninterruptible(),
305 cx_inner,
306 sender: self.sender.clone(),
307 state: Arc::clone(&self.state),
308 terminal_state,
309 drop_abort_defused: false,
310 }
311 }
312
313 pub fn abort(&self) {
319 self.state.store(ActorState::Stopping);
320 self.sender.close_receiver();
321 if let Some(inner) = self.inner.upgrade() {
322 let cancel_wakers = {
323 let mut guard = inner.write();
324 guard.cancel_requested = true;
325 guard
326 .fast_cancel
327 .store(true, std::sync::atomic::Ordering::Release);
328 if guard.cancel_reason.is_none() {
329 guard.cancel_reason = Some(crate::types::CancelReason::user("actor aborted"));
330 }
331 guard.cancel_waker_snapshot()
332 };
333 for waker in cancel_wakers {
334 waker.wake_by_ref();
335 }
336 }
337 }
338}
339
340pub struct ActorJoinFuture<'a, A: Actor> {
345 inner: crate::channel::oneshot::RecvUninterruptibleFuture<'a, Result<A, JoinError>>,
346 cx_inner: std::sync::Weak<parking_lot::RwLock<CxInner>>,
347 sender: mpsc::Sender<A::Message>,
348 state: Arc<ActorStateCell>,
349 terminal_state: &'a mut bool,
350 drop_abort_defused: bool,
351}
352
353impl<A: Actor> ActorJoinFuture<'_, A> {
354 fn closed_reason(&self) -> crate::types::CancelReason {
355 self.cx_inner
356 .upgrade()
357 .and_then(|inner| inner.read().cancel_reason.clone())
358 .unwrap_or_else(|| crate::types::CancelReason::user("join channel closed"))
359 }
360
361 fn abort(&self) {
362 self.state.store(ActorState::Stopping);
363 self.sender.close_receiver();
364 if let Some(inner) = self.cx_inner.upgrade() {
365 let cancel_wakers = {
366 let mut guard = inner.write();
367 guard.cancel_requested = true;
368 guard
369 .fast_cancel
370 .store(true, std::sync::atomic::Ordering::Release);
371 if guard.cancel_reason.is_none() {
372 guard.cancel_reason = Some(crate::types::CancelReason::user("actor aborted"));
373 }
374 guard.cancel_waker_snapshot()
375 };
376 for waker in cancel_wakers {
377 waker.wake_by_ref();
378 }
379 }
380 }
381}
382
383impl<A: Actor> std::future::Future for ActorJoinFuture<'_, A> {
384 type Output = Result<A, JoinError>;
385
386 fn poll(
387 mut self: Pin<&mut Self>,
388 cx: &mut std::task::Context<'_>,
389 ) -> std::task::Poll<Self::Output> {
390 let this = &mut *self;
391 if *this.terminal_state {
392 return std::task::Poll::Ready(Err(JoinError::PolledAfterCompletion));
393 }
394
395 match Pin::new(&mut this.inner).poll(cx) {
396 std::task::Poll::Ready(Ok(res)) => {
397 *this.terminal_state = true;
398 this.drop_abort_defused = true;
399 std::task::Poll::Ready(res)
400 }
401 std::task::Poll::Ready(Err(crate::channel::oneshot::RecvError::Closed)) => {
402 *this.terminal_state = true;
403 this.drop_abort_defused = true;
404 let reason = this.closed_reason();
405 std::task::Poll::Ready(Err(JoinError::Cancelled(reason)))
406 }
407 std::task::Poll::Ready(Err(crate::channel::oneshot::RecvError::Cancelled)) => {
408 unreachable!(
409 "RecvUninterruptibleFuture does not consult Cx cancellation and only resolves \
410 to Ok(value), Closed, or PolledAfterCompletion"
411 );
412 }
413 std::task::Poll::Ready(Err(
414 crate::channel::oneshot::RecvError::PolledAfterCompletion,
415 )) => {
416 unreachable!(
417 "ActorJoinFuture sets terminal_state before returning Ready, so a repoll \
418 fails closed before the inner oneshot future can be polled again"
419 )
420 }
421 std::task::Poll::Pending => std::task::Poll::Pending,
422 }
423 }
424}
425
426impl<A: Actor> Drop for ActorJoinFuture<'_, A> {
427 fn drop(&mut self) {
428 if !*self.terminal_state && !self.drop_abort_defused {
429 if self.inner.receiver_finished() {
430 return;
431 }
432 self.abort();
433 }
434 }
435}
436
437#[derive(Debug)]
442pub struct ActorRef<M> {
443 actor_id: ActorId,
444 sender: mpsc::Sender<M>,
445 state: Arc<ActorStateCell>,
446}
447
448impl<M> Clone for ActorRef<M> {
452 fn clone(&self) -> Self {
453 Self {
454 actor_id: self.actor_id,
455 sender: self.sender.clone(),
456 state: Arc::clone(&self.state),
457 }
458 }
459}
460
461impl<M: Send + 'static> ActorRef<M> {
462 pub async fn send(&self, cx: &Cx, msg: M) -> Outcome<(), SendError<M>> {
464 match self.sender.send(cx, msg).await {
465 Ok(()) => Outcome::ok(()),
466 Err(e) => Outcome::err(e),
467 }
468 }
469
470 #[must_use]
472 pub fn reserve<'a>(&'a self, cx: &'a Cx) -> mpsc::Reserve<'a, M> {
473 self.sender.reserve(cx)
474 }
475
476 pub fn try_send(&self, msg: M) -> Result<(), SendError<M>> {
478 self.sender.try_send(msg)
479 }
480
481 #[must_use]
483 pub fn is_closed(&self) -> bool {
484 self.sender.is_closed()
485 }
486
487 #[must_use]
491 pub fn is_alive(&self) -> bool {
492 self.state.load() != ActorState::Stopped
493 }
494
495 #[must_use]
497 pub const fn actor_id(&self) -> ActorId {
498 self.actor_id
499 }
500}
501
502#[derive(Debug, Clone, Copy)]
508pub struct MailboxConfig {
509 pub capacity: usize,
511 pub backpressure: bool,
513}
514
515impl Default for MailboxConfig {
516 fn default() -> Self {
517 Self {
518 capacity: DEFAULT_MAILBOX_CAPACITY,
519 backpressure: true,
520 }
521 }
522}
523
524impl MailboxConfig {
525 #[must_use]
527 pub const fn with_capacity(capacity: usize) -> Self {
528 Self {
529 capacity,
530 backpressure: true,
531 }
532 }
533}
534
535#[derive(Debug, Clone)]
537pub enum SupervisorMessage {
538 ChildFailed {
540 child_id: ActorId,
542 reason: String,
544 },
545 ChildStopped {
547 child_id: ActorId,
549 },
550}
551
552pub struct ActorContext<'a, M: Send + 'static> {
577 cx: &'a Cx,
579 self_ref: ActorRef<M>,
581 actor_id: ActorId,
583 parent: Option<ActorRef<SupervisorMessage>>,
585 children: Vec<ActorId>,
587 stopping: bool,
589}
590
591#[allow(clippy::elidable_lifetime_names)]
592impl<'a, M: Send + 'static> ActorContext<'a, M> {
593 #[must_use]
597 pub fn new(
598 cx: &'a Cx,
599 self_ref: ActorRef<M>,
600 actor_id: ActorId,
601 parent: Option<ActorRef<SupervisorMessage>>,
602 ) -> Self {
603 Self {
604 cx,
605 self_ref,
606 actor_id,
607 parent,
608 children: Vec::new(),
609 stopping: false,
610 }
611 }
612
613 #[must_use]
618 pub const fn self_actor_id(&self) -> ActorId {
619 self.actor_id
620 }
621
622 #[must_use]
624 pub const fn actor_id(&self) -> ActorId {
625 self.actor_id
626 }
627
628 pub fn register_child(&mut self, child_id: ActorId) {
636 self.children.push(child_id);
637 }
638
639 pub fn unregister_child(&mut self, child_id: ActorId) -> bool {
643 if let Some(pos) = self.children.iter().position(|&id| id == child_id) {
644 self.children.swap_remove(pos);
645 true
646 } else {
647 false
648 }
649 }
650
651 #[must_use]
653 pub fn children(&self) -> &[ActorId] {
654 &self.children
655 }
656
657 #[must_use]
659 pub fn has_children(&self) -> bool {
660 !self.children.is_empty()
661 }
662
663 #[must_use]
665 pub fn child_count(&self) -> usize {
666 self.children.len()
667 }
668
669 pub fn stop_self(&mut self) {
678 self.stopping = true;
679 }
680
681 #[must_use]
683 pub fn is_stopping(&self) -> bool {
684 self.stopping
685 }
686
687 #[must_use]
695 pub fn parent(&self) -> Option<&ActorRef<SupervisorMessage>> {
696 self.parent.as_ref()
697 }
698
699 #[must_use]
701 pub fn has_parent(&self) -> bool {
702 self.parent.is_some()
703 }
704
705 pub async fn escalate(&self, reason: String) {
710 if let Some(parent) = &self.parent {
711 let msg = SupervisorMessage::ChildFailed {
712 child_id: self.actor_id,
713 reason,
714 };
715 let _ = parent.send(self.cx, msg).await;
717 }
718 }
719
720 #[allow(clippy::result_large_err)]
729 pub fn checkpoint(&self) -> Result<(), crate::error::Error> {
730 if self.stopping {
731 let reason = crate::types::CancelReason::user("actor stopping")
732 .with_region(self.cx.region_id())
733 .with_task(self.cx.task_id());
734 return Err(crate::error::Error::cancelled(&reason));
735 }
736 self.cx.checkpoint()
737 }
738
739 #[must_use]
743 pub fn is_cancel_requested(&self) -> bool {
744 self.stopping || self.cx.checkpoint().is_err()
745 }
746
747 #[must_use]
749 pub fn budget(&self) -> crate::types::Budget {
750 self.cx.budget()
751 }
752
753 #[must_use]
755 pub fn deadline(&self) -> Option<Time> {
756 self.cx.budget().deadline
757 }
758
759 pub fn trace(&self, event: &str) {
761 self.cx.trace(event);
762 }
763
764 #[must_use]
769 pub fn self_ref(&self) -> ActorRef<M> {
770 self.self_ref.clone()
771 }
772
773 #[must_use]
775 pub const fn cx(&self) -> &Cx {
776 self.cx
777 }
778}
779
780impl<M: Send + 'static> std::ops::Deref for ActorContext<'_, M> {
781 type Target = Cx;
782
783 fn deref(&self) -> &Self::Target {
784 self.cx
785 }
786}
787
788impl<M: Send + 'static> std::fmt::Debug for ActorContext<'_, M> {
789 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
790 f.debug_struct("ActorContext")
791 .field("actor_id", &self.actor_id)
792 .field("children", &self.children.len())
793 .field("stopping", &self.stopping)
794 .field("has_parent", &self.parent.is_some())
795 .finish()
796 }
797}
798
799pub const DEFAULT_MAILBOX_CAPACITY: usize = 64;
801
802struct OnStopMaskGuard(Arc<parking_lot::RwLock<CxInner>>);
803
804impl Drop for OnStopMaskGuard {
805 fn drop(&mut self) {
806 let mut g = self.0.write();
807 g.mask_depth = g.mask_depth.saturating_sub(1);
808 }
809}
810
811async fn run_actor_loop<A: Actor>(mut actor: A, cx: Cx, cell: &mut ActorCell<A::Message>) -> A {
820 use crate::tracing_compat::debug;
821
822 cell.state
827 .compare_and_swap(ActorState::Created, ActorState::Running);
828
829 cx.trace("actor::on_start");
834 actor.on_start(&cx).await;
835
836 let mut messages_processed = 0u32;
839 const YIELD_INTERVAL: u32 = 8; loop {
842 if cx.checkpoint().is_err() {
844 cx.trace("actor::cancel_requested");
845 break;
846 }
847
848 let recv_result = std::future::poll_fn(|task_cx| {
849 match cell.mailbox.poll_recv(&cx, task_cx) {
850 std::task::Poll::Pending if cell.state.load() == ActorState::Stopping => {
851 std::task::Poll::Ready(Err(crate::channel::mpsc::RecvError::Disconnected))
853 }
854 other => other,
855 }
856 })
857 .await;
858
859 match recv_result {
860 Ok(msg) => {
861 actor.handle(&cx, msg).await;
862
863 messages_processed += 1;
865 if messages_processed >= YIELD_INTERVAL {
866 messages_processed = 0;
867 if cx.budget().poll_quota == 0 {
870 cx.trace("actor::yield_on_budget_exhaustion");
871 }
873 }
874 }
875 Err(crate::channel::mpsc::RecvError::Disconnected) => {
876 cx.trace("actor::mailbox_disconnected");
878 break;
879 }
880 Err(crate::channel::mpsc::RecvError::Cancelled) => {
881 cx.trace("actor::recv_cancelled");
883 break;
884 }
885 Err(crate::channel::mpsc::RecvError::Empty) => {
886 break;
888 }
889 }
890 }
891
892 cell.state.store(ActorState::Stopping);
893
894 let is_aborted = cx.checkpoint().is_err();
895
896 cell.mailbox.close();
902
903 if is_aborted {
904 while let Ok(_msg) = cell.mailbox.try_recv() {}
905 } else {
906 let mut drained: u64 = 0;
907 let mut drain_yield_counter = 0u32;
908 while let Ok(msg) = cell.mailbox.try_recv() {
909 actor.handle(&cx, msg).await;
910 drained += 1;
911
912 drain_yield_counter += 1;
914 if drain_yield_counter >= YIELD_INTERVAL {
915 drain_yield_counter = 0;
916 if cx.budget().poll_quota == 0 {
917 cx.trace("actor::yield_during_drain");
918 }
919 }
920 }
921 if drained > 0 {
922 debug!(drained = drained, "actor::mailbox_drained");
923 cx.trace("actor::mailbox_drained");
924 }
925 }
926
927 cx.trace("actor::on_stop");
932 let inner = cx.inner.clone();
933 {
934 let mut guard = inner.write();
935 assert!(
938 guard.mask_depth < crate::types::task_context::MAX_MASK_DEPTH,
939 "mask depth exceeded MAX_MASK_DEPTH ({}) in actor::on_stop: \
940 this violates INV-MASK-BOUNDED and prevents cancellation from ever \
941 being observed. Reduce nesting of masked sections.",
942 crate::types::task_context::MAX_MASK_DEPTH
943 );
944 guard.mask_depth += 1;
945 }
946 let mask_guard = OnStopMaskGuard(inner);
947 actor.on_stop(&cx).await;
948 drop(mask_guard);
949
950 actor
951}
952
953fn actor_cancel_join_error(cx: &Cx) -> JoinError {
954 JoinError::Cancelled(
955 cx.cancel_reason()
956 .unwrap_or_else(|| crate::types::CancelReason::user("actor supervision cancelled")),
957 )
958}
959
960fn supervised_restart_timestamp(cx: &Cx) -> u64 {
961 cx.timer_driver().map_or_else(
962 || crate::time::wall_now().as_nanos(),
963 |td| td.now().as_nanos(),
964 )
965}
966
967#[inline]
968fn try_commit_supervised_restart(state: &ActorStateCell) -> bool {
969 state.compare_and_swap(ActorState::Running, ActorState::Created)
970}
971
972async fn wait_supervised_restart_delay(cx: &Cx, delay: Duration) -> Outcome<(), JoinError> {
973 if cx.checkpoint().is_err() {
974 return Outcome::err(actor_cancel_join_error(cx));
975 }
976 if delay.is_zero() {
977 return Outcome::ok(());
978 }
979
980 let mut sleeper = cx.timer_driver().map_or_else(
981 || crate::time::sleep(crate::time::wall_now(), delay),
982 |driver| {
983 let delay_nanos = u64::try_from(delay.as_nanos()).unwrap_or(u64::MAX);
984 let deadline = driver.now().saturating_add_nanos(delay_nanos);
985 crate::time::Sleep::with_timer_driver(deadline, driver)
986 },
987 );
988 std::future::poll_fn(|task_cx| {
989 if cx.checkpoint().is_err() {
990 return std::task::Poll::Ready(Outcome::err(actor_cancel_join_error(cx)));
991 }
992 Pin::new(&mut sleeper)
993 .poll(task_cx)
994 .map(|()| Outcome::ok(()))
995 })
996 .await
997}
998
999fn join_result_to_task_outcome<A>(result: &Result<A, JoinError>) -> Outcome<(), ()> {
1000 match result {
1001 Ok(_) => Outcome::Ok(()),
1002 Err(JoinError::Cancelled(reason)) => Outcome::Cancelled(reason.clone()),
1003 Err(JoinError::Panicked(payload)) => Outcome::Panicked(payload.clone()),
1004 Err(JoinError::PolledAfterCompletion) => {
1005 Outcome::Err(())
1009 }
1010 }
1011}
1012
1013impl<P: crate::types::Policy> crate::cx::Scope<'_, P> {
1015 pub fn spawn_actor<A: Actor>(
1032 &self,
1033 state: &mut crate::runtime::state::RuntimeState,
1034 cx: &Cx,
1035 actor: A,
1036 mailbox_capacity: usize,
1037 ) -> Result<(ActorHandle<A>, crate::runtime::stored_task::StoredTask), SpawnError> {
1038 use crate::channel::oneshot;
1039 use crate::cx::scope::CatchUnwind;
1040 use crate::runtime::stored_task::StoredTask;
1041 use crate::tracing_compat::{debug, debug_span};
1042
1043 let (msg_tx, msg_rx) = mpsc::channel::<A::Message>(mailbox_capacity);
1045
1046 let (result_tx, result_rx) = oneshot::channel::<Result<A, JoinError>>();
1048
1049 let task_id = self.create_task_record(state)?;
1051 let actor_id = ActorId::from_task(task_id);
1052 let actor_state = Arc::new(ActorStateCell::new(ActorState::Created));
1053 let region_id = self.region_id();
1054
1055 let (_, child_cx) = self.build_child_task_cx(state, cx, task_id);
1057
1058 if let Some(record) = state.task_mut(task_id) {
1060 record.set_cx_inner(child_cx.inner.clone());
1061 record.set_cx(child_cx.clone());
1062 }
1063 let spawned_at = state
1064 .timer_driver()
1065 .map_or(state.now, crate::time::TimerDriverHandle::now);
1066 let spawn_effects = state.prepare_task_spawn_effects(
1067 task_id,
1068 region_id,
1069 self.budget(),
1070 crate::runtime::state::TaskSpawnSource::Scope,
1071 spawned_at,
1072 );
1073
1074 let inner_weak = Arc::downgrade(&child_cx.inner);
1075 let state_for_task = Arc::clone(&actor_state);
1076
1077 let mut cell = ActorCell {
1078 mailbox: msg_rx,
1079 state: Arc::clone(&actor_state),
1080 };
1081
1082 let wrapped = async move {
1084 spawn_effects.dispatch();
1085 let result = CatchUnwind {
1086 inner: Box::pin(async move {
1087 {
1088 let _span = debug_span!(
1089 "actor_spawn",
1090 task_id = ?task_id,
1091 region_id = ?region_id,
1092 mailbox_capacity = mailbox_capacity,
1093 )
1094 .entered();
1095 debug!(
1096 task_id = ?task_id,
1097 region_id = ?region_id,
1098 mailbox_capacity = mailbox_capacity,
1099 "actor spawned"
1100 );
1101 }
1102 run_actor_loop(actor, child_cx, &mut cell).await
1103 }),
1104 }
1105 .await;
1106 state_for_task.store(ActorState::Stopped);
1107 match result {
1108 Ok(actor_final) => {
1109 let _ = result_tx.send_blocking(Ok(actor_final));
1110 Outcome::Ok(())
1111 }
1112 Err(payload) => {
1113 let msg = crate::cx::scope::payload_to_string(&payload);
1114 std::mem::forget(payload);
1115 let panic_payload = crate::types::PanicPayload::new(msg);
1116 let _ =
1117 result_tx.send_blocking(Err(JoinError::Panicked(panic_payload.clone())));
1118 Outcome::Panicked(panic_payload)
1119 }
1120 }
1121 };
1122
1123 let stored = StoredTask::new_with_id(wrapped, task_id);
1124
1125 let handle = ActorHandle {
1126 actor_id,
1127 sender: msg_tx,
1128 state: actor_state,
1129 task_id,
1130 receiver: result_rx,
1131 inner: inner_weak,
1132 completed: false,
1133 };
1134
1135 Ok((handle, stored))
1136 }
1137
1138 pub fn spawn_supervised_actor<A, F>(
1159 &self,
1160 state: &mut crate::runtime::state::RuntimeState,
1161 cx: &Cx,
1162 mut factory: F,
1163 strategy: crate::supervision::SupervisionStrategy,
1164 mailbox_capacity: usize,
1165 ) -> Result<(ActorHandle<A>, crate::runtime::stored_task::StoredTask), SpawnError>
1166 where
1167 A: Actor,
1168 F: FnMut() -> A + Send + 'static,
1169 {
1170 use crate::channel::oneshot;
1171 use crate::runtime::stored_task::StoredTask;
1172 use crate::supervision::Supervisor;
1173 use crate::tracing_compat::{debug, debug_span};
1174
1175 let (msg_tx, msg_rx) = mpsc::channel::<A::Message>(mailbox_capacity);
1176 let (result_tx, result_rx) = oneshot::channel::<Result<A, JoinError>>();
1177 let task_id = self.create_task_record(state)?;
1178 let actor_id = ActorId::from_task(task_id);
1179 let actor_state = Arc::new(ActorStateCell::new(ActorState::Created));
1180 let region_id = self.region_id();
1181
1182 let (_, child_cx) = self.build_child_task_cx(state, cx, task_id);
1183
1184 if let Some(record) = state.task_mut(task_id) {
1185 record.set_cx_inner(child_cx.inner.clone());
1186 record.set_cx(child_cx.clone());
1187 }
1188 let spawned_at = state
1189 .timer_driver()
1190 .map_or(state.now, crate::time::TimerDriverHandle::now);
1191 let spawn_effects = state.prepare_task_spawn_effects(
1192 task_id,
1193 region_id,
1194 self.budget(),
1195 crate::runtime::state::TaskSpawnSource::Scope,
1196 spawned_at,
1197 );
1198
1199 let inner_weak = Arc::downgrade(&child_cx.inner);
1200 let state_for_task = Arc::clone(&actor_state);
1201
1202 let mut cell = ActorCell {
1203 mailbox: msg_rx,
1204 state: Arc::clone(&actor_state),
1205 };
1206
1207 let wrapped = async move {
1208 spawn_effects.dispatch();
1209 let result = match (crate::cx::scope::CatchUnwind {
1210 inner: Box::pin(async move {
1211 {
1212 let _span = debug_span!(
1213 "supervised_actor_spawn",
1214 task_id = ?task_id,
1215 region_id = ?region_id,
1216 mailbox_capacity = mailbox_capacity,
1217 )
1218 .entered();
1219 debug!(
1220 task_id = ?task_id,
1221 region_id = ?region_id,
1222 "supervised actor spawned"
1223 );
1224 }
1225 let actor = factory();
1226 run_supervised_loop(
1227 actor,
1228 &mut factory,
1229 child_cx,
1230 &mut cell,
1231 Supervisor::new(strategy),
1232 task_id,
1233 region_id,
1234 )
1235 .await
1236 }),
1237 })
1238 .await
1239 {
1240 Ok(result) => result,
1241 Err(payload) => {
1242 let message = crate::cx::scope::payload_to_string(&payload);
1243 std::mem::forget(payload);
1244 Err(JoinError::Panicked(crate::types::PanicPayload::new(
1245 message,
1246 )))
1247 }
1248 };
1249 state_for_task.store(ActorState::Stopped);
1250 let outcome = join_result_to_task_outcome(&result).map_err(|_| ());
1251 let _ = result_tx.send_blocking(result);
1252 outcome
1253 };
1254
1255 let stored = StoredTask::new_with_id(wrapped, task_id);
1256
1257 let handle = ActorHandle {
1258 actor_id,
1259 sender: msg_tx,
1260 state: actor_state,
1261 task_id,
1262 receiver: result_rx,
1263 inner: inner_weak,
1264 completed: false,
1265 };
1266
1267 Ok((handle, stored))
1268 }
1269}
1270
1271#[derive(Debug)]
1273pub enum SupervisedOutcome {
1274 Stopped,
1276 RestartBudgetExhausted {
1278 total_restarts: u32,
1280 },
1281 Escalated,
1283}
1284
1285async fn run_supervised_loop<A, F>(
1290 initial_actor: A,
1291 factory: &mut F,
1292 cx: Cx,
1293 cell: &mut ActorCell<A::Message>,
1294 mut supervisor: crate::supervision::Supervisor,
1295 task_id: TaskId,
1296 region_id: RegionId,
1297) -> Result<A, JoinError>
1298where
1299 A: Actor,
1300 F: FnMut() -> A,
1301{
1302 use crate::cx::scope::CatchUnwind;
1303 use crate::supervision::SupervisionDecision;
1304 use crate::types::Outcome;
1305
1306 let mut current_actor = initial_actor;
1307
1308 loop {
1309 let result = CatchUnwind {
1311 inner: Box::pin(run_actor_loop(current_actor, cx.clone(), cell)),
1312 }
1313 .await;
1314
1315 match result {
1316 Ok(actor_final) => {
1317 return Ok(actor_final);
1319 }
1320 Err(payload) => {
1321 let msg = crate::cx::scope::payload_to_string(&payload);
1322 let panic_payload = crate::types::PanicPayload::new(msg);
1323 cx.trace("supervised_actor::failure");
1324
1325 if cell.state.load() == ActorState::Stopping || cx.checkpoint().is_err() {
1329 cx.trace("supervised_actor::shutdown_panic");
1330 return Err(JoinError::Panicked(panic_payload));
1331 }
1332
1333 let outcome = Outcome::Err(());
1339 let now = supervised_restart_timestamp(&cx);
1340 let decision = supervisor.on_failure(task_id, region_id, None, &outcome, now);
1341
1342 match decision {
1343 SupervisionDecision::Restart { delay, .. } => {
1344 cx.trace("supervised_actor::restart");
1345
1346 if cell.state.load() == ActorState::Stopping {
1351 cx.trace("supervised_actor::restart_suppressed");
1352 return Err(JoinError::Panicked(panic_payload));
1353 }
1354
1355 if let Some(backoff) = delay {
1357 match wait_supervised_restart_delay(&cx, backoff).await {
1358 Outcome::Ok(()) => {}
1359 Outcome::Err(err) => return Err(err),
1360 Outcome::Cancelled(_) => return Err(actor_cancel_join_error(&cx)),
1361 Outcome::Panicked(payload) => {
1362 return Err(JoinError::Panicked(payload));
1363 }
1364 }
1365 }
1366
1367 if cx.checkpoint().is_err() {
1368 cx.trace("supervised_actor::restart_suppressed");
1369 return Err(JoinError::Panicked(panic_payload));
1370 }
1371
1372 if !try_commit_supervised_restart(&cell.state) {
1377 cx.trace("supervised_actor::restart_suppressed");
1378 return Err(JoinError::Panicked(panic_payload));
1379 }
1380 current_actor = factory();
1381 }
1382 SupervisionDecision::Stop { .. } => {
1383 cx.trace("supervised_actor::stopped");
1384 return Err(JoinError::Panicked(panic_payload));
1385 }
1386 SupervisionDecision::Escalate { .. } => {
1387 cx.trace("supervised_actor::escalated");
1388 return Err(JoinError::Panicked(panic_payload));
1389 }
1390 }
1391 }
1392 }
1393 }
1394}
1395
1396#[cfg(test)]
1397mod tests {
1398 #![allow(
1399 clippy::pedantic,
1400 clippy::nursery,
1401 clippy::expect_fun_call,
1402 clippy::map_unwrap_or,
1403 clippy::cast_possible_wrap,
1404 clippy::future_not_send
1405 )]
1406 use super::*;
1407 use crate::cx::macaroon::MacaroonToken;
1408 use crate::cx::registry::{RegistryCap, RegistryHandle};
1409 use crate::remote::{NodeId, RemoteCap};
1410 use crate::runtime::state::RuntimeState;
1411 use crate::security::key::AuthKey;
1412 use crate::types::Budget;
1413 use crate::types::SystemPressure;
1414 use crate::types::policy::FailFast;
1415 use std::sync::Arc;
1416 use std::task::{Context, Poll, Waker};
1417
1418 fn init_test(name: &str) {
1419 crate::test_utils::init_test_logging();
1420 crate::test_phase!(name);
1421 }
1422
1423 fn actor_join_future_from_receiver<'a, A: Actor>(
1424 receiver: &'a mut crate::channel::oneshot::Receiver<Result<A, JoinError>>,
1425 terminal_state: &'a mut bool,
1426 ) -> ActorJoinFuture<'a, A> {
1427 let (sender, _mailbox_rx) = mpsc::channel::<A::Message>(4);
1428 ActorJoinFuture {
1429 inner: receiver.recv_uninterruptible(),
1430 cx_inner: std::sync::Weak::new(),
1431 sender,
1432 state: Arc::new(ActorStateCell::new(ActorState::Running)),
1433 terminal_state,
1434 drop_abort_defused: false,
1435 }
1436 }
1437
1438 fn counting_waker(counter: Arc<std::sync::atomic::AtomicUsize>) -> Waker {
1439 struct CountingWaker {
1440 counter: Arc<std::sync::atomic::AtomicUsize>,
1441 }
1442
1443 impl std::task::Wake for CountingWaker {
1444 fn wake(self: Arc<Self>) {
1445 self.counter.fetch_add(1, Ordering::Relaxed);
1446 }
1447
1448 fn wake_by_ref(self: &Arc<Self>) {
1449 self.counter.fetch_add(1, Ordering::Relaxed);
1450 }
1451 }
1452
1453 Waker::from(Arc::new(CountingWaker { counter }))
1454 }
1455
1456 fn terminal_state_waker(
1457 state: Arc<ActorStateCell>,
1458 counter: Arc<std::sync::atomic::AtomicUsize>,
1459 ) -> Waker {
1460 struct TerminalStateWaker {
1461 state: Arc<ActorStateCell>,
1462 counter: Arc<std::sync::atomic::AtomicUsize>,
1463 }
1464
1465 impl TerminalStateWaker {
1466 fn record_wake(&self) {
1467 assert_eq!(
1468 self.state.load(),
1469 ActorState::Stopped,
1470 "join receiver must not wake before actor state is terminal"
1471 );
1472 self.counter.fetch_add(1, Ordering::Relaxed);
1473 }
1474 }
1475
1476 impl std::task::Wake for TerminalStateWaker {
1477 fn wake(self: Arc<Self>) {
1478 self.record_wake();
1479 }
1480
1481 fn wake_by_ref(self: &Arc<Self>) {
1482 self.record_wake();
1483 }
1484 }
1485
1486 Waker::from(Arc::new(TerminalStateWaker { state, counter }))
1487 }
1488
1489 #[derive(Debug)]
1491 struct Counter {
1492 count: u64,
1493 started: bool,
1494 stopped: bool,
1495 }
1496
1497 impl Counter {
1498 fn new() -> Self {
1499 Self {
1500 count: 0,
1501 started: false,
1502 stopped: false,
1503 }
1504 }
1505 }
1506
1507 impl Actor for Counter {
1508 type Message = u64;
1509
1510 fn on_start(&mut self, _cx: &Cx) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
1511 self.started = true;
1512 Box::pin(async {})
1513 }
1514
1515 fn handle(&mut self, _cx: &Cx, msg: u64) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
1516 self.count += msg;
1517 Box::pin(async {})
1518 }
1519
1520 fn on_stop(&mut self, _cx: &Cx) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
1521 self.stopped = true;
1522 Box::pin(async {})
1523 }
1524 }
1525
1526 fn assert_actor<A: Actor>() {}
1527
1528 #[derive(Debug, Clone, PartialEq, Eq)]
1529 struct CapabilitySnapshot {
1530 same_registry: bool,
1531 same_remote: bool,
1532 same_io: bool,
1533 same_pressure: bool,
1534 same_macaroon: bool,
1535 has_timer: bool,
1536 }
1537
1538 struct CapabilityProbeActor {
1539 snapshot: Arc<parking_lot::Mutex<Option<CapabilitySnapshot>>>,
1540 expected_registry: Arc<dyn RegistryCap>,
1541 expected_remote_node: String,
1542 expected_io: Arc<dyn crate::io::IoCap>,
1543 expected_pressure: Arc<SystemPressure>,
1544 expected_macaroon: Arc<MacaroonToken>,
1545 }
1546
1547 impl Actor for CapabilityProbeActor {
1548 type Message = ();
1549
1550 fn on_start(&mut self, cx: &Cx) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
1551 let child_registry = cx
1552 .registry_handle()
1553 .expect("actor child Cx must inherit registry")
1554 .as_arc();
1555 let child_io = cx
1556 .io_cap_handle()
1557 .expect("actor child Cx must inherit io capability");
1558 let child_pressure = cx
1559 .pressure_handle()
1560 .expect("actor child Cx must inherit system pressure");
1561 let child_macaroon = cx
1562 .macaroon_handle()
1563 .expect("actor child Cx must inherit macaroon");
1564 let remote_node = cx
1565 .remote()
1566 .map(|remote| remote.local_node().as_str().to_owned());
1567
1568 *self.snapshot.lock() = Some(CapabilitySnapshot {
1569 same_registry: Arc::ptr_eq(&child_registry, &self.expected_registry),
1570 same_remote: remote_node.as_deref() == Some(self.expected_remote_node.as_str()),
1571 same_io: Arc::ptr_eq(&child_io, &self.expected_io),
1572 same_pressure: Arc::ptr_eq(&child_pressure, &self.expected_pressure),
1573 same_macaroon: Arc::ptr_eq(&child_macaroon, &self.expected_macaroon),
1574 has_timer: cx.has_timer(),
1575 });
1576
1577 Box::pin(async {})
1578 }
1579
1580 fn handle(&mut self, _cx: &Cx, _msg: ()) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
1581 Box::pin(async {})
1582 }
1583 }
1584
1585 fn capability_rich_parent_cx(
1586 runtime: &crate::lab::LabRuntime,
1587 region: crate::types::RegionId,
1588 ) -> (
1589 Cx,
1590 Arc<dyn RegistryCap>,
1591 Arc<dyn crate::io::IoCap>,
1592 Arc<SystemPressure>,
1593 Arc<MacaroonToken>,
1594 ) {
1595 let registry = crate::cx::NameRegistry::new();
1596 let registry_handle = RegistryHandle::new(Arc::new(registry));
1597 let registry_arc = registry_handle.as_arc();
1598 let io_cap: Arc<dyn crate::io::IoCap> = Arc::new(crate::io::LabIoCap::new_for_tests());
1599 let pressure = Arc::new(SystemPressure::with_headroom(0.25));
1600 let macaroon_token =
1601 MacaroonToken::mint(&AuthKey::from_seed(7), "scope:actor", "actor/tests");
1602
1603 let parent_cx = Cx::new_with_drivers(
1604 region,
1605 crate::types::TaskId::new_for_test(77, 0),
1606 Budget::INFINITE,
1607 None,
1608 None,
1609 Some(Arc::clone(&io_cap)),
1610 runtime.state.timer_driver_handle(),
1611 None,
1612 )
1613 .with_registry_handle(Some(registry_handle))
1614 .with_remote_cap(RemoteCap::new().with_local_node(NodeId::new("actor-origin")))
1615 .with_pressure(Arc::clone(&pressure))
1616 .with_macaroon(macaroon_token);
1617
1618 let macaroon = parent_cx
1619 .macaroon_handle()
1620 .expect("parent actor test Cx must retain macaroon");
1621
1622 (parent_cx, registry_arc, io_cap, pressure, macaroon)
1623 }
1624
1625 #[test]
1626 fn actor_trait_object_safety() {
1627 init_test("actor_trait_object_safety");
1628
1629 assert_actor::<Counter>();
1631
1632 crate::test_complete!("actor_trait_object_safety");
1633 }
1634
1635 #[test]
1636 fn actor_handle_creation() {
1637 init_test("actor_handle_creation");
1638
1639 let mut state = RuntimeState::new();
1640 let root = state.create_root_region(Budget::INFINITE);
1641 let cx: Cx = Cx::for_testing();
1642 let scope = crate::cx::Scope::<FailFast>::new(root, Budget::INFINITE);
1643
1644 let result = scope.spawn_actor(&mut state, &cx, Counter::new(), 32);
1645 assert!(result.is_ok(), "spawn_actor should succeed");
1646
1647 let (handle, stored) = result.unwrap();
1648 state.store_spawned_task(handle.task_id(), stored);
1649
1650 let _tid = handle.task_id();
1652
1653 assert!(!handle.is_finished());
1655
1656 crate::test_complete!("actor_handle_creation");
1657 }
1658
1659 #[test]
1660 fn spawn_actor_inherits_child_cx_capabilities() {
1661 init_test("spawn_actor_inherits_child_cx_capabilities");
1662
1663 let mut runtime = crate::lab::LabRuntime::new(crate::lab::LabConfig::default());
1664 let region = runtime.state.create_root_region(Budget::INFINITE);
1665 let scope = crate::cx::Scope::<FailFast>::new(region, Budget::INFINITE);
1666 let (parent_cx, registry_arc, io_cap, pressure, macaroon) =
1667 capability_rich_parent_cx(&runtime, region);
1668 let snapshot = Arc::new(parking_lot::Mutex::new(None));
1669
1670 let actor = CapabilityProbeActor {
1671 snapshot: Arc::clone(&snapshot),
1672 expected_registry: registry_arc,
1673 expected_remote_node: "actor-origin".to_string(),
1674 expected_io: io_cap,
1675 expected_pressure: pressure,
1676 expected_macaroon: macaroon,
1677 };
1678
1679 let (handle, stored) = scope
1680 .spawn_actor(&mut runtime.state, &parent_cx, actor, 8)
1681 .expect("spawn actor");
1682 let task_id = handle.task_id();
1683 runtime.state.store_spawned_task(task_id, stored);
1684
1685 runtime.scheduler.lock().schedule(task_id, 0);
1686 runtime.run_until_idle();
1687
1688 let observed = snapshot
1689 .lock()
1690 .clone()
1691 .expect("actor on_start should capture inherited capabilities");
1692 assert_eq!(
1693 observed,
1694 CapabilitySnapshot {
1695 same_registry: true,
1696 same_remote: true,
1697 same_io: true,
1698 same_pressure: true,
1699 same_macaroon: true,
1700 has_timer: true,
1701 }
1702 );
1703
1704 drop(handle);
1705 runtime.run_until_quiescent();
1706
1707 crate::test_complete!("spawn_actor_inherits_child_cx_capabilities");
1708 }
1709
1710 #[test]
1711 fn spawn_supervised_actor_inherits_child_cx_capabilities() {
1712 init_test("spawn_supervised_actor_inherits_child_cx_capabilities");
1713
1714 let mut runtime = crate::lab::LabRuntime::new(crate::lab::LabConfig::default());
1715 let region = runtime.state.create_root_region(Budget::INFINITE);
1716 let scope = crate::cx::Scope::<FailFast>::new(region, Budget::INFINITE);
1717 let (parent_cx, registry_arc, io_cap, pressure, macaroon) =
1718 capability_rich_parent_cx(&runtime, region);
1719 let snapshot = Arc::new(parking_lot::Mutex::new(None));
1720
1721 let snapshot_for_factory = Arc::clone(&snapshot);
1722 let strategy = crate::supervision::SupervisionStrategy::Stop;
1723 let (handle, stored) = scope
1724 .spawn_supervised_actor(
1725 &mut runtime.state,
1726 &parent_cx,
1727 move || CapabilityProbeActor {
1728 snapshot: Arc::clone(&snapshot_for_factory),
1729 expected_registry: Arc::clone(®istry_arc),
1730 expected_remote_node: "actor-origin".to_string(),
1731 expected_io: Arc::clone(&io_cap),
1732 expected_pressure: Arc::clone(&pressure),
1733 expected_macaroon: Arc::clone(&macaroon),
1734 },
1735 strategy,
1736 8,
1737 )
1738 .expect("spawn supervised actor");
1739 let task_id = handle.task_id();
1740 runtime.state.store_spawned_task(task_id, stored);
1741
1742 runtime.scheduler.lock().schedule(task_id, 0);
1743 runtime.run_until_idle();
1744
1745 let observed = snapshot
1746 .lock()
1747 .clone()
1748 .expect("supervised actor on_start should capture inherited capabilities");
1749 assert_eq!(
1750 observed,
1751 CapabilitySnapshot {
1752 same_registry: true,
1753 same_remote: true,
1754 same_io: true,
1755 same_pressure: true,
1756 same_macaroon: true,
1757 has_timer: true,
1758 }
1759 );
1760
1761 drop(handle);
1762 runtime.run_until_quiescent();
1763
1764 crate::test_complete!("spawn_supervised_actor_inherits_child_cx_capabilities");
1765 }
1766
1767 #[test]
1768 fn actor_id_generation_distinct() {
1769 init_test("actor_id_generation_distinct");
1770
1771 let id1 = ActorId::from_task(TaskId::new_for_test(1, 1));
1772 let id2 = ActorId::from_task(TaskId::new_for_test(1, 2));
1773 assert!(id1 != id2, "generation must distinguish actor reuse");
1774
1775 crate::test_complete!("actor_id_generation_distinct");
1776 }
1777
1778 #[test]
1779 fn actor_ref_is_cloneable() {
1780 init_test("actor_ref_is_cloneable");
1781
1782 let mut state = RuntimeState::new();
1783 let root = state.create_root_region(Budget::INFINITE);
1784 let cx: Cx = Cx::for_testing();
1785 let scope = crate::cx::Scope::<FailFast>::new(root, Budget::INFINITE);
1786
1787 let (handle, stored) = scope
1788 .spawn_actor(&mut state, &cx, Counter::new(), 32)
1789 .unwrap();
1790 state.store_spawned_task(handle.task_id(), stored);
1791
1792 let ref1 = handle.sender();
1794 let ref2 = ref1.clone();
1795
1796 assert_eq!(ref1.actor_id(), handle.actor_id());
1798 assert_eq!(ref2.actor_id(), handle.actor_id());
1799
1800 assert!(ref1.is_alive());
1802 assert!(ref2.is_alive());
1803
1804 assert!(!ref1.is_closed());
1806 assert!(!ref2.is_closed());
1807
1808 crate::test_complete!("actor_ref_is_cloneable");
1809 }
1810
1811 use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
1814
1815 struct ObservableCounter {
1818 count: u64,
1819 on_stop_count: Arc<AtomicU64>,
1820 started: Arc<AtomicBool>,
1821 stopped: Arc<AtomicBool>,
1822 }
1823
1824 impl ObservableCounter {
1825 fn new(
1826 on_stop_count: Arc<AtomicU64>,
1827 started: Arc<AtomicBool>,
1828 stopped: Arc<AtomicBool>,
1829 ) -> Self {
1830 Self {
1831 count: 0,
1832 on_stop_count,
1833 started,
1834 stopped,
1835 }
1836 }
1837 }
1838
1839 impl Actor for ObservableCounter {
1840 type Message = u64;
1841
1842 fn on_start(&mut self, _cx: &Cx) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
1843 self.started.store(true, Ordering::SeqCst);
1844 Box::pin(async {})
1845 }
1846
1847 fn handle(&mut self, _cx: &Cx, msg: u64) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
1848 self.count += msg;
1849 Box::pin(async {})
1850 }
1851
1852 fn on_stop(&mut self, _cx: &Cx) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
1853 self.on_stop_count.store(self.count, Ordering::SeqCst);
1854 self.stopped.store(true, Ordering::SeqCst);
1855 Box::pin(async {})
1856 }
1857 }
1858
1859 fn observable_state() -> (Arc<AtomicU64>, Arc<AtomicBool>, Arc<AtomicBool>) {
1860 (
1861 Arc::new(AtomicU64::new(u64::MAX)),
1862 Arc::new(AtomicBool::new(false)),
1863 Arc::new(AtomicBool::new(false)),
1864 )
1865 }
1866
1867 #[test]
1870 fn actor_processes_all_messages() {
1871 init_test("actor_processes_all_messages");
1872
1873 let mut runtime = crate::lab::LabRuntime::new(crate::lab::LabConfig::default());
1874 let region = runtime.state.create_root_region(Budget::INFINITE);
1875 let cx: Cx = Cx::for_testing();
1876 let scope = crate::cx::Scope::<FailFast>::new(region, Budget::INFINITE);
1877
1878 let (on_stop_count, started, stopped) = observable_state();
1879 let actor = ObservableCounter::new(on_stop_count.clone(), started.clone(), stopped.clone());
1880
1881 let (handle, stored) = scope
1882 .spawn_actor(&mut runtime.state, &cx, actor, 32)
1883 .unwrap();
1884 let task_id = handle.task_id();
1885 runtime.state.store_spawned_task(task_id, stored);
1886
1887 for _ in 0..5 {
1889 handle.try_send(1).unwrap();
1890 }
1891
1892 drop(handle);
1895
1896 runtime.scheduler.lock().schedule(task_id, 0);
1897 runtime.run_until_quiescent();
1898
1899 assert_eq!(
1900 on_stop_count.load(Ordering::SeqCst),
1901 5,
1902 "all messages processed"
1903 );
1904 assert!(started.load(Ordering::SeqCst), "on_start was called");
1905 assert!(stopped.load(Ordering::SeqCst), "on_stop was called");
1906
1907 crate::test_complete!("actor_processes_all_messages");
1908 }
1909
1910 #[test]
1914 fn actor_drains_mailbox_on_cancel() {
1915 init_test("actor_drains_mailbox_on_cancel");
1916
1917 let mut runtime = crate::lab::LabRuntime::new(crate::lab::LabConfig::default());
1918 let region = runtime.state.create_root_region(Budget::INFINITE);
1919 let cx: Cx = Cx::for_testing();
1920 let scope = crate::cx::Scope::<FailFast>::new(region, Budget::INFINITE);
1921
1922 let (on_stop_count, started, stopped) = observable_state();
1923 let actor = ObservableCounter::new(on_stop_count.clone(), started.clone(), stopped.clone());
1924
1925 let (handle, stored) = scope
1926 .spawn_actor(&mut runtime.state, &cx, actor, 32)
1927 .unwrap();
1928 let task_id = handle.task_id();
1929 runtime.state.store_spawned_task(task_id, stored);
1930
1931 for _ in 0..5 {
1933 handle.try_send(1).unwrap();
1934 }
1935
1936 handle.stop();
1939 let stopped_ref = handle.sender();
1940 assert!(
1941 stopped_ref.is_closed(),
1942 "stop() seals the mailbox immediately"
1943 );
1944 assert!(
1945 matches!(handle.try_send(99), Err(SendError::Disconnected(99))),
1946 "stop() must reject new messages instead of extending shutdown"
1947 );
1948
1949 runtime.scheduler.lock().schedule(task_id, 0);
1950 runtime.run_until_quiescent();
1951
1952 assert_eq!(
1954 on_stop_count.load(Ordering::SeqCst),
1955 5,
1956 "drain processed all messages"
1957 );
1958 assert!(started.load(Ordering::SeqCst), "on_start was called");
1959 assert!(stopped.load(Ordering::SeqCst), "on_stop was called");
1960
1961 crate::test_complete!("actor_drains_mailbox_on_cancel");
1962 }
1963
1964 #[test]
1966 fn actor_ref_is_alive_transitions() {
1967 init_test("actor_ref_is_alive_transitions");
1968
1969 let mut runtime = crate::lab::LabRuntime::new(crate::lab::LabConfig::default());
1970 let region = runtime.state.create_root_region(Budget::INFINITE);
1971 let cx: Cx = Cx::for_testing();
1972 let scope = crate::cx::Scope::<FailFast>::new(region, Budget::INFINITE);
1973
1974 let (on_stop_count, started, stopped) = observable_state();
1975 let actor = ObservableCounter::new(on_stop_count.clone(), started.clone(), stopped.clone());
1976
1977 let (handle, stored) = scope
1978 .spawn_actor(&mut runtime.state, &cx, actor, 32)
1979 .unwrap();
1980 let task_id = handle.task_id();
1981 runtime.state.store_spawned_task(task_id, stored);
1982
1983 let actor_ref = handle.sender();
1984 assert!(actor_ref.is_alive(), "created actor should be alive");
1985 assert_eq!(actor_ref.actor_id(), handle.actor_id());
1986
1987 handle.stop();
1988 assert!(actor_ref.is_alive(), "stopping actor is still alive");
1989
1990 runtime.scheduler.lock().schedule(task_id, 0);
1991 runtime.run_until_quiescent();
1992
1993 assert!(
1994 handle.is_finished(),
1995 "actor should be finished after stop + run"
1996 );
1997 assert!(!actor_ref.is_alive(), "finished actor is not alive");
1998
1999 assert!(started.load(Ordering::SeqCst), "on_start was called");
2001 assert!(stopped.load(Ordering::SeqCst), "on_stop was called");
2002 assert_ne!(
2003 on_stop_count.load(Ordering::SeqCst),
2004 u64::MAX,
2005 "on_stop_count updated"
2006 );
2007
2008 crate::test_complete!("actor_ref_is_alive_transitions");
2009 }
2010
2011 #[test]
2012 fn dropped_join_future_marks_actor_stopping_like_abort() {
2013 init_test("dropped_join_future_marks_actor_stopping_like_abort");
2014
2015 let mut runtime = crate::lab::LabRuntime::new(crate::lab::LabConfig::default());
2016 let region = runtime.state.create_root_region(Budget::INFINITE);
2017 let cx: Cx = Cx::for_testing();
2018 let scope = crate::cx::Scope::<FailFast>::new(region, Budget::INFINITE);
2019
2020 let (on_stop_count, started, stopped) = observable_state();
2021 let actor = ObservableCounter::new(on_stop_count.clone(), started.clone(), stopped.clone());
2022
2023 let (mut handle, stored) = scope
2024 .spawn_actor(&mut runtime.state, &cx, actor, 32)
2025 .unwrap();
2026 let task_id = handle.task_id();
2027 runtime.state.store_spawned_task(task_id, stored);
2028
2029 runtime.scheduler.lock().schedule(task_id, 0);
2030 runtime.run_until_idle();
2031 assert_eq!(
2032 handle.state.load(),
2033 ActorState::Running,
2034 "actor should be running before join drop requests abort"
2035 );
2036
2037 drop(handle.join(&cx));
2038
2039 assert_eq!(
2040 handle.state.load(),
2041 ActorState::Stopping,
2042 "dropping join future should mirror ActorHandle::abort state transition"
2043 );
2044 assert!(
2045 matches!(handle.try_send(1), Err(SendError::Disconnected(1))),
2046 "join-drop abort must seal the mailbox immediately"
2047 );
2048
2049 runtime.run_until_quiescent();
2050 assert!(
2051 handle.is_finished(),
2052 "actor should stop after join future drop"
2053 );
2054 assert!(started.load(Ordering::SeqCst), "on_start should have run");
2055 assert!(stopped.load(Ordering::SeqCst), "on_stop should have run");
2056 assert_eq!(
2057 on_stop_count.load(Ordering::SeqCst),
2058 0,
2059 "idle actor should stop without processing phantom messages"
2060 );
2061
2062 crate::test_complete!("dropped_join_future_marks_actor_stopping_like_abort");
2063 }
2064
2065 #[test]
2066 fn actor_stop_unblocks_pending_sender_with_disconnect() {
2067 init_test("actor_stop_unblocks_pending_sender_with_disconnect");
2068
2069 let mut state = RuntimeState::new();
2070 let root = state.create_root_region(Budget::INFINITE);
2071 let cx: Cx = Cx::for_testing();
2072 let scope = crate::cx::Scope::<FailFast>::new(root, Budget::INFINITE);
2073
2074 let (handle, stored) = scope
2075 .spawn_actor(&mut state, &cx, Counter::new(), 1)
2076 .unwrap();
2077 state.store_spawned_task(handle.task_id(), stored);
2078
2079 handle.try_send(1).expect("fill mailbox");
2080 let sender = handle.sender();
2081 let mut send_fut = Box::pin(sender.send(&cx, 2));
2082 let wake_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2083 let waker = counting_waker(Arc::clone(&wake_count));
2084 let mut task_cx = Context::from_waker(&waker);
2085
2086 let first_poll = send_fut.as_mut().poll(&mut task_cx);
2087 assert!(
2088 matches!(first_poll, Poll::Pending),
2089 "send should wait while the mailbox is full"
2090 );
2091
2092 handle.stop();
2093
2094 assert_eq!(
2095 wake_count.load(Ordering::SeqCst),
2096 1,
2097 "stop() must wake a sender blocked on mailbox capacity"
2098 );
2099 let second_poll = send_fut.as_mut().poll(&mut task_cx);
2100 assert!(
2101 matches!(
2102 second_poll,
2103 Poll::Ready(Outcome::Err(SendError::Disconnected(2)))
2104 ),
2105 "pending sender must fail fast once stop seals the mailbox"
2106 );
2107
2108 crate::test_complete!("actor_stop_unblocks_pending_sender_with_disconnect");
2109 }
2110
2111 #[test]
2113 fn supervised_actor_panic_restarts_under_restart_strategy() {
2114 use std::sync::atomic::AtomicU32;
2115
2116 #[derive(Debug)]
2117 struct PanickingCounter {
2118 count: u64,
2119 panic_on: u64,
2120 final_count: Arc<AtomicU64>,
2121 }
2122
2123 impl Actor for PanickingCounter {
2124 type Message = u64;
2125
2126 fn handle(
2127 &mut self,
2128 _cx: &Cx,
2129 msg: u64,
2130 ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
2131 assert!(msg != self.panic_on, "threshold exceeded: {msg}");
2132 self.count += msg;
2133 Box::pin(async {})
2134 }
2135
2136 fn on_stop(&mut self, _cx: &Cx) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
2137 self.final_count.store(self.count, Ordering::SeqCst);
2138 Box::pin(async {})
2139 }
2140 }
2141
2142 init_test("supervised_actor_panic_restarts_under_restart_strategy");
2143
2144 let mut runtime = crate::lab::LabRuntime::new(crate::lab::LabConfig::default());
2145 let region = runtime.state.create_root_region(Budget::INFINITE);
2146 let cx: Cx = Cx::for_testing();
2147 let scope = crate::cx::Scope::<FailFast>::new(region, Budget::INFINITE);
2148
2149 let final_count = Arc::new(AtomicU64::new(u64::MAX));
2150 let restart_count = Arc::new(AtomicU32::new(0));
2151 let fc = final_count.clone();
2152 let rc = restart_count.clone();
2153
2154 let strategy = crate::supervision::SupervisionStrategy::Restart(
2155 crate::supervision::RestartConfig::new(3, std::time::Duration::from_secs(60))
2156 .with_backoff(crate::supervision::BackoffStrategy::None),
2157 );
2158
2159 let (mut handle, stored) = scope
2160 .spawn_supervised_actor(
2161 &mut runtime.state,
2162 &cx,
2163 move || {
2164 rc.fetch_add(1, Ordering::Relaxed);
2165 PanickingCounter {
2166 count: 0,
2167 panic_on: 999,
2168 final_count: fc.clone(),
2169 }
2170 },
2171 strategy,
2172 32,
2173 )
2174 .unwrap();
2175 let task_id = handle.task_id();
2176 runtime.state.store_spawned_task(task_id, stored);
2177
2178 handle.try_send(1).unwrap();
2183 handle.try_send(999).unwrap(); handle.try_send(1).unwrap();
2185
2186 runtime.scheduler.lock().schedule(task_id, 0);
2187 runtime.run_until_idle();
2188 handle.abort();
2189 runtime.run_until_quiescent();
2190
2191 let join = futures_lite::future::block_on(handle.join(&cx));
2192 let actor = join.expect("aborting the restarted actor should still return final state");
2193 assert_eq!(
2194 restart_count.load(Ordering::SeqCst),
2195 2,
2196 "panic must trigger exactly one supervised restart, got {} factory calls",
2197 restart_count.load(Ordering::SeqCst)
2198 );
2199 assert_eq!(
2200 actor.count, 1,
2201 "restarted actor should keep the post-crash message count"
2202 );
2203 assert_eq!(
2204 final_count.load(Ordering::SeqCst),
2205 1,
2206 "restarted actor should process the queued post-crash message before abort"
2207 );
2208
2209 crate::test_complete!("supervised_actor_panic_restarts_under_restart_strategy");
2210 }
2211
2212 #[test]
2213 fn supervised_restart_window_expires_without_timer_driver() {
2214 use std::thread;
2215
2216 init_test("supervised_restart_window_expires_without_timer_driver");
2217
2218 let region_id = RegionId::new_for_test(0, 1);
2219 let cx = Cx::new(region_id, TaskId::new_for_test(1, 1), Budget::INFINITE);
2220 let mut supervisor =
2221 crate::supervision::Supervisor::new(crate::supervision::SupervisionStrategy::Restart(
2222 crate::supervision::RestartConfig::new(1, Duration::from_millis(2))
2223 .with_backoff(crate::supervision::BackoffStrategy::None),
2224 ));
2225 let outcome = Outcome::Err(());
2226 let task_id = TaskId::new_for_test(2, 1);
2227
2228 let first = supervisor.on_failure(
2229 task_id,
2230 region_id,
2231 None,
2232 &outcome,
2233 supervised_restart_timestamp(&cx),
2234 );
2235 assert!(
2236 matches!(
2237 first,
2238 crate::supervision::SupervisionDecision::Restart { attempt: 1, .. }
2239 ),
2240 "first failure should allow a restart"
2241 );
2242
2243 thread::sleep(Duration::from_millis(5));
2244
2245 let second = supervisor.on_failure(
2246 task_id,
2247 region_id,
2248 None,
2249 &outcome,
2250 supervised_restart_timestamp(&cx),
2251 );
2252 assert!(
2253 matches!(
2254 second,
2255 crate::supervision::SupervisionDecision::Restart { attempt: 1, .. }
2256 ),
2257 "wall-clock fallback must let the restart window expire without a timer driver"
2258 );
2259
2260 crate::test_complete!("supervised_restart_window_expires_without_timer_driver");
2261 }
2262
2263 #[test]
2264 fn supervised_restart_delay_uses_explicit_timer_driver_without_ambient_cx() {
2265 init_test("supervised_restart_delay_uses_explicit_timer_driver_without_ambient_cx");
2266
2267 let clock = Arc::new(crate::time::VirtualClock::new());
2268 let timer = crate::time::TimerDriverHandle::with_virtual_clock(Arc::clone(&clock));
2269 let cx = Cx::new_with_drivers(
2270 RegionId::new_for_test(4, 0),
2271 TaskId::new_for_test(4, 0),
2272 Budget::INFINITE,
2273 None,
2274 None,
2275 None,
2276 Some(timer.clone()),
2277 None,
2278 );
2279 let mut wait = Box::pin(wait_supervised_restart_delay(&cx, Duration::from_millis(5)));
2280 let mut task_cx = Context::from_waker(Waker::noop());
2281
2282 assert!(matches!(
2283 Future::poll(wait.as_mut(), &mut task_cx),
2284 Poll::Pending
2285 ));
2286
2287 clock.advance(5_000_000);
2288 assert_eq!(
2289 timer.process_timers(),
2290 1,
2291 "restart delay must register with the explicit timer driver"
2292 );
2293
2294 assert!(matches!(
2295 Future::poll(wait.as_mut(), &mut task_cx),
2296 Poll::Ready(Outcome::Ok(()))
2297 ));
2298
2299 crate::test_complete!(
2300 "supervised_restart_delay_uses_explicit_timer_driver_without_ambient_cx"
2301 );
2302 }
2303
2304 #[test]
2305 fn supervised_actor_stop_prevents_restart_after_panic() {
2306 use std::sync::atomic::AtomicU32;
2307
2308 #[derive(Debug)]
2309 struct StopThenPanicActor;
2310
2311 impl Actor for StopThenPanicActor {
2312 type Message = ();
2313
2314 fn handle(
2315 &mut self,
2316 _cx: &Cx,
2317 _msg: (),
2318 ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
2319 panic!("panic during shutdown");
2320 }
2321 }
2322
2323 init_test("supervised_actor_stop_prevents_restart_after_panic");
2324
2325 let mut runtime = crate::lab::LabRuntime::new(crate::lab::LabConfig::default());
2326 let region = runtime.state.create_root_region(Budget::INFINITE);
2327 let cx: Cx = Cx::for_testing();
2328 let scope = crate::cx::Scope::<FailFast>::new(region, Budget::INFINITE);
2329
2330 let restart_count = Arc::new(AtomicU32::new(0));
2331 let rc = Arc::clone(&restart_count);
2332 let strategy = crate::supervision::SupervisionStrategy::Restart(
2333 crate::supervision::RestartConfig::new(3, Duration::from_secs(60))
2334 .with_backoff(crate::supervision::BackoffStrategy::None),
2335 );
2336
2337 let (mut handle, stored) = scope
2338 .spawn_supervised_actor(
2339 &mut runtime.state,
2340 &cx,
2341 move || {
2342 rc.fetch_add(1, Ordering::Relaxed);
2343 StopThenPanicActor
2344 },
2345 strategy,
2346 8,
2347 )
2348 .expect("spawn supervised actor");
2349 let task_id = handle.task_id();
2350 runtime.state.store_spawned_task(task_id, stored);
2351
2352 handle.try_send(()).expect("queue panic message");
2353 handle.stop();
2354
2355 runtime.scheduler.lock().schedule(task_id, 0);
2356 runtime.run_until_quiescent();
2357
2358 assert_eq!(
2359 restart_count.load(Ordering::SeqCst),
2360 1,
2361 "explicit stop must suppress supervised restarts"
2362 );
2363
2364 let join = futures_lite::future::block_on(handle.join(&cx));
2365 match join {
2366 Err(JoinError::Panicked(payload)) => {
2367 assert_eq!(
2368 payload.message(),
2369 "panic during shutdown",
2370 "shutdown panic should surface without restarting"
2371 );
2372 }
2373 other => panic!("expected shutdown panic without restart, got {other:?}"),
2374 }
2375
2376 crate::test_complete!("supervised_actor_stop_prevents_restart_after_panic");
2377 }
2378
2379 #[test]
2380 fn supervised_actor_stop_during_restart_backoff_prevents_new_instance() {
2381 use std::sync::atomic::AtomicU32;
2382
2383 #[derive(Debug)]
2384 struct DelayedRestartActor {
2385 starts: Arc<AtomicU32>,
2386 }
2387
2388 impl Actor for DelayedRestartActor {
2389 type Message = ();
2390
2391 fn on_start(&mut self, _cx: &Cx) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
2392 let starts = Arc::clone(&self.starts);
2393 Box::pin(async move {
2394 starts.fetch_add(1, Ordering::Relaxed);
2395 })
2396 }
2397
2398 fn handle(
2399 &mut self,
2400 _cx: &Cx,
2401 _msg: (),
2402 ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
2403 panic!("panic before delayed restart");
2404 }
2405 }
2406
2407 init_test("supervised_actor_stop_during_restart_backoff_prevents_new_instance");
2408
2409 let mut runtime = crate::lab::LabRuntime::new(crate::lab::LabConfig::default());
2410 let region = runtime.state.create_root_region(Budget::INFINITE);
2411 let cx: Cx = Cx::for_testing();
2412 let scope = crate::cx::Scope::<FailFast>::new(region, Budget::INFINITE);
2413
2414 let factory_count = Arc::new(AtomicU32::new(0));
2415 let starts = Arc::new(AtomicU32::new(0));
2416 let fc = Arc::clone(&factory_count);
2417 let starts_for_factory = Arc::clone(&starts);
2418 let strategy = crate::supervision::SupervisionStrategy::Restart(
2419 crate::supervision::RestartConfig::new(3, Duration::from_secs(60)).with_backoff(
2420 crate::supervision::BackoffStrategy::Fixed(Duration::from_secs(5)),
2421 ),
2422 );
2423
2424 let (mut handle, stored) = scope
2425 .spawn_supervised_actor(
2426 &mut runtime.state,
2427 &cx,
2428 move || {
2429 fc.fetch_add(1, Ordering::Relaxed);
2430 DelayedRestartActor {
2431 starts: Arc::clone(&starts_for_factory),
2432 }
2433 },
2434 strategy,
2435 8,
2436 )
2437 .expect("spawn supervised actor");
2438 let task_id = handle.task_id();
2439 runtime.state.store_spawned_task(task_id, stored);
2440
2441 handle.try_send(()).expect("queue panic message");
2442
2443 runtime.scheduler.lock().schedule(task_id, 0);
2444 runtime.run_until_idle();
2445 assert_eq!(
2446 runtime.pending_timer_count(),
2447 1,
2448 "supervised actor should be waiting on restart backoff"
2449 );
2450
2451 handle.stop();
2452 let report = runtime.run_with_auto_advance();
2453
2454 assert!(
2455 matches!(
2456 report.termination,
2457 crate::lab::AutoAdvanceTermination::Quiescent
2458 ),
2459 "runtime should quiesce after stop suppresses restart: {report:?}"
2460 );
2461 assert_eq!(
2462 factory_count.load(Ordering::SeqCst),
2463 1,
2464 "graceful stop during backoff must prevent a replacement actor from being constructed"
2465 );
2466 assert_eq!(
2467 starts.load(Ordering::SeqCst),
2468 1,
2469 "graceful stop during backoff must prevent restarted actor lifecycle hooks from running"
2470 );
2471
2472 let join = futures_lite::future::block_on(handle.join(&cx));
2473 match join {
2474 Err(JoinError::Panicked(payload)) => {
2475 assert_eq!(
2476 payload.message(),
2477 "panic before delayed restart",
2478 "original panic should surface when restart is suppressed"
2479 );
2480 }
2481 other => panic!(
2482 "expected original panic when stop suppresses delayed restart, got {other:?}"
2483 ),
2484 }
2485
2486 crate::test_complete!("supervised_actor_stop_during_restart_backoff_prevents_new_instance");
2487 }
2488
2489 #[test]
2490 fn spawn_actor_panic_surfaces_as_task_outcome() {
2491 init_test("spawn_actor_panic_surfaces_as_task_outcome");
2492
2493 #[derive(Debug)]
2494 struct PanicActor;
2495
2496 impl Actor for PanicActor {
2497 type Message = ();
2498
2499 fn handle(
2500 &mut self,
2501 _cx: &Cx,
2502 _msg: (),
2503 ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
2504 panic!("actor boom");
2505 }
2506 }
2507
2508 let mut state = RuntimeState::new();
2509 let root = state.create_root_region(Budget::INFINITE);
2510 let cx: Cx = Cx::for_testing();
2511 let scope = crate::cx::Scope::<FailFast>::new(root, Budget::INFINITE);
2512
2513 let (mut handle, mut stored) = scope
2514 .spawn_actor(&mut state, &cx, PanicActor, 8)
2515 .expect("spawn actor");
2516 handle.try_send(()).expect("queue panic message");
2517
2518 let wake_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2519 let waker = terminal_state_waker(Arc::clone(&handle.state), Arc::clone(&wake_count));
2520 let mut poll_cx = Context::from_waker(&waker);
2521 let join = std::pin::pin!(handle.join(&cx));
2522 let mut join = join;
2523 assert!(matches!(join.as_mut().poll(&mut poll_cx), Poll::Pending));
2524
2525 match stored.poll(&mut poll_cx) {
2526 Poll::Ready(Outcome::Panicked(payload)) => {
2527 assert_eq!(payload.message(), "actor boom", "panic payload preserved");
2528 }
2529 other => panic!("panicking actor task must return Outcome::Panicked: {other:?}"),
2530 }
2531 assert_eq!(wake_count.load(Ordering::Relaxed), 1);
2532
2533 match join.as_mut().poll(&mut poll_cx) {
2534 Poll::Ready(Err(JoinError::Panicked(payload))) => {
2535 assert_eq!(
2536 payload.message(),
2537 "actor boom",
2538 "join preserves panic payload"
2539 );
2540 }
2541 other => panic!("join must surface actor panic: {other:?}"),
2542 }
2543
2544 crate::test_complete!("spawn_actor_panic_surfaces_as_task_outcome");
2545 }
2546
2547 #[test]
2548 fn spawn_supervised_actor_panic_surfaces_as_task_outcome() {
2549 init_test("spawn_supervised_actor_panic_surfaces_as_task_outcome");
2550
2551 #[derive(Debug)]
2552 struct PanicActor;
2553
2554 impl Actor for PanicActor {
2555 type Message = ();
2556
2557 fn handle(
2558 &mut self,
2559 _cx: &Cx,
2560 _msg: (),
2561 ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
2562 panic!("supervised actor boom");
2563 }
2564 }
2565
2566 let mut state = RuntimeState::new();
2567 let root = state.create_root_region(Budget::INFINITE);
2568 let cx: Cx = Cx::for_testing();
2569 let scope = crate::cx::Scope::<FailFast>::new(root, Budget::INFINITE);
2570
2571 let (mut handle, mut stored) = scope
2572 .spawn_supervised_actor(
2573 &mut state,
2574 &cx,
2575 || PanicActor,
2576 crate::supervision::SupervisionStrategy::Stop,
2577 8,
2578 )
2579 .expect("spawn supervised actor");
2580 handle.try_send(()).expect("queue panic message");
2581
2582 let wake_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2583 let waker = terminal_state_waker(Arc::clone(&handle.state), Arc::clone(&wake_count));
2584 let mut poll_cx = Context::from_waker(&waker);
2585 let join = std::pin::pin!(handle.join(&cx));
2586 let mut join = join;
2587 assert!(matches!(join.as_mut().poll(&mut poll_cx), Poll::Pending));
2588
2589 match stored.poll(&mut poll_cx) {
2590 Poll::Ready(Outcome::Panicked(payload)) => {
2591 assert_eq!(
2592 payload.message(),
2593 "supervised actor boom",
2594 "panic payload preserved"
2595 );
2596 }
2597 other => {
2598 panic!("panicking supervised actor task must return Outcome::Panicked: {other:?}")
2599 }
2600 }
2601 assert_eq!(wake_count.load(Ordering::Relaxed), 1);
2602
2603 match join.as_mut().poll(&mut poll_cx) {
2604 Poll::Ready(Err(JoinError::Panicked(payload))) => {
2605 assert_eq!(
2606 payload.message(),
2607 "supervised actor boom",
2608 "join preserves panic payload"
2609 );
2610 }
2611 other => panic!("join must surface supervised actor panic: {other:?}"),
2612 }
2613
2614 crate::test_complete!("spawn_supervised_actor_panic_surfaces_as_task_outcome");
2615 }
2616
2617 #[test]
2618 fn supervised_restart_factory_panic_reaches_join_and_marks_stopped() {
2619 use std::sync::atomic::AtomicU32;
2620
2621 init_test("supervised_restart_factory_panic_reaches_join_and_marks_stopped");
2622
2623 #[derive(Debug)]
2624 struct PanicActor;
2625
2626 impl Actor for PanicActor {
2627 type Message = ();
2628
2629 fn handle(
2630 &mut self,
2631 _cx: &Cx,
2632 _msg: (),
2633 ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
2634 panic!("actor panic before restart factory");
2635 }
2636 }
2637
2638 let mut state = RuntimeState::new();
2639 let root = state.create_root_region(Budget::INFINITE);
2640 let cx: Cx = Cx::for_testing();
2641 let scope = crate::cx::Scope::<FailFast>::new(root, Budget::INFINITE);
2642 let factory_calls = Arc::new(AtomicU32::new(0));
2643 let calls = Arc::clone(&factory_calls);
2644
2645 let (mut handle, mut stored) = scope
2646 .spawn_supervised_actor(
2647 &mut state,
2648 &cx,
2649 move || {
2650 let attempt = calls.fetch_add(1, Ordering::Relaxed);
2651 if attempt == 0 {
2652 PanicActor
2653 } else {
2654 panic!("restart factory boom");
2655 }
2656 },
2657 crate::supervision::SupervisionStrategy::Restart(
2658 crate::supervision::RestartConfig::new(1, Duration::from_secs(60))
2659 .with_backoff(crate::supervision::BackoffStrategy::None),
2660 ),
2661 8,
2662 )
2663 .expect("spawn supervised actor");
2664 assert_eq!(
2665 factory_calls.load(Ordering::Relaxed),
2666 0,
2667 "supervised actor factory must stay lazy until the stored task's first poll"
2668 );
2669 handle.try_send(()).expect("queue panic message");
2670
2671 let actor_ref = handle.sender();
2672 let wake_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2673 let waker = terminal_state_waker(Arc::clone(&handle.state), Arc::clone(&wake_count));
2674 let mut poll_cx = Context::from_waker(&waker);
2675 let join = std::pin::pin!(handle.join(&cx));
2676 let mut join = join;
2677 assert!(matches!(join.as_mut().poll(&mut poll_cx), Poll::Pending));
2678
2679 match stored.poll(&mut poll_cx) {
2680 Poll::Ready(Outcome::Panicked(payload)) => {
2681 assert_eq!(payload.message(), "restart factory boom");
2682 }
2683 other => panic!("restart factory panic must terminate the task: {other:?}"),
2684 }
2685
2686 assert_eq!(factory_calls.load(Ordering::Relaxed), 2);
2687 assert_eq!(wake_count.load(Ordering::Relaxed), 1);
2688 assert!(
2689 !actor_ref.is_alive(),
2690 "restart factory panic must publish the terminal actor state"
2691 );
2692
2693 match join.as_mut().poll(&mut poll_cx) {
2694 Poll::Ready(Err(JoinError::Panicked(payload))) => {
2695 assert_eq!(payload.message(), "restart factory boom");
2696 }
2697 other => panic!("join must preserve the restart factory panic: {other:?}"),
2698 }
2699
2700 crate::test_complete!("supervised_restart_factory_panic_reaches_join_and_marks_stopped");
2701 }
2702
2703 #[test]
2704 fn supervised_restart_delay_honors_cancellation() {
2705 init_test("supervised_restart_delay_honors_cancellation");
2706
2707 let cx = Cx::for_testing();
2708 cx.cancel_fast(crate::types::CancelKind::User);
2709
2710 let mut delay = std::pin::pin!(wait_supervised_restart_delay(
2711 &cx,
2712 std::time::Duration::from_secs(60),
2713 ));
2714 let first_poll =
2715 futures_lite::future::block_on(futures_lite::future::poll_once(&mut delay));
2716
2717 match first_poll {
2718 Some(Outcome::Err(JoinError::Cancelled(reason))) => {
2719 assert_eq!(reason.kind, crate::types::CancelKind::User);
2720 }
2721 other => panic!("expected immediate cancellation, got {other:?}"),
2722 }
2723
2724 crate::test_complete!("supervised_restart_delay_honors_cancellation");
2725 }
2726
2727 #[test]
2729 fn actor_deterministic_replay() {
2730 fn run_scenario(seed: u64) -> u64 {
2731 let config = crate::lab::LabConfig::new(seed);
2732 let mut runtime = crate::lab::LabRuntime::new(config);
2733 let region = runtime.state.create_root_region(Budget::INFINITE);
2734 let cx: Cx = Cx::for_testing();
2735 let scope = crate::cx::Scope::<FailFast>::new(region, Budget::INFINITE);
2736
2737 let (on_stop_count, started, stopped) = observable_state();
2738 let actor = ObservableCounter::new(on_stop_count.clone(), started, stopped);
2739
2740 let (handle, stored) = scope
2741 .spawn_actor(&mut runtime.state, &cx, actor, 32)
2742 .unwrap();
2743 let task_id = handle.task_id();
2744 runtime.state.store_spawned_task(task_id, stored);
2745
2746 for i in 1..=10 {
2747 handle.try_send(i).unwrap();
2748 }
2749 drop(handle);
2750
2751 runtime.scheduler.lock().schedule(task_id, 0);
2752 runtime.run_until_quiescent();
2753
2754 on_stop_count.load(Ordering::SeqCst)
2755 }
2756
2757 init_test("actor_deterministic_replay");
2758
2759 let result1 = run_scenario(0xDEAD_BEEF);
2761 let result2 = run_scenario(0xDEAD_BEEF);
2762
2763 assert_eq!(
2764 result1, result2,
2765 "deterministic replay: same seed → same result"
2766 );
2767 assert_eq!(result1, 55, "sum of 1..=10");
2768
2769 crate::test_complete!("actor_deterministic_replay");
2770 }
2771
2772 #[test]
2775 fn actor_context_self_reference() {
2776 init_test("actor_context_self_reference");
2777
2778 let mut state = RuntimeState::new();
2779 let root = state.create_root_region(Budget::INFINITE);
2780 let cx: Cx = Cx::for_testing();
2781 let scope = crate::cx::Scope::<FailFast>::new(root, Budget::INFINITE);
2782
2783 let (handle, stored) = scope
2784 .spawn_actor(&mut state, &cx, Counter::new(), 32)
2785 .unwrap();
2786 state.store_spawned_task(handle.task_id(), stored);
2787
2788 let actor_ref = handle.sender();
2790 let actor_id = handle.actor_id();
2791 let ctx: ActorContext<'_, u64> = ActorContext::new(&cx, actor_ref, actor_id, None);
2792
2793 assert_eq!(ctx.self_actor_id(), actor_id);
2795 assert_eq!(ctx.actor_id(), actor_id);
2796
2797 crate::test_complete!("actor_context_self_reference");
2798 }
2799
2800 #[test]
2801 fn actor_context_child_management() {
2802 init_test("actor_context_child_management");
2803
2804 let cx: Cx = Cx::for_testing();
2805 let (sender, _receiver) = mpsc::channel::<u64>(32);
2806 let actor_id = ActorId::from_task(TaskId::new_for_test(1, 1));
2807 let actor_ref = ActorRef {
2808 actor_id,
2809 sender,
2810 state: Arc::new(ActorStateCell::new(ActorState::Running)),
2811 };
2812
2813 let mut ctx = ActorContext::new(&cx, actor_ref, actor_id, None);
2814
2815 assert!(!ctx.has_children());
2817 assert_eq!(ctx.child_count(), 0);
2818 assert!(ctx.children().is_empty());
2819
2820 let child1 = ActorId::from_task(TaskId::new_for_test(2, 1));
2822 let child2 = ActorId::from_task(TaskId::new_for_test(3, 1));
2823
2824 ctx.register_child(child1);
2825 assert!(ctx.has_children());
2826 assert_eq!(ctx.child_count(), 1);
2827
2828 ctx.register_child(child2);
2829 assert_eq!(ctx.child_count(), 2);
2830
2831 assert!(ctx.unregister_child(child1));
2833 assert_eq!(ctx.child_count(), 1);
2834
2835 assert!(!ctx.unregister_child(child1));
2837
2838 crate::test_complete!("actor_context_child_management");
2839 }
2840
2841 #[test]
2842 fn actor_context_stopping() {
2843 init_test("actor_context_stopping");
2844
2845 let cx: Cx = Cx::for_testing();
2846 let (sender, _receiver) = mpsc::channel::<u64>(32);
2847 let actor_id = ActorId::from_task(TaskId::new_for_test(1, 1));
2848 let actor_ref = ActorRef {
2849 actor_id,
2850 sender,
2851 state: Arc::new(ActorStateCell::new(ActorState::Running)),
2852 };
2853
2854 let mut ctx = ActorContext::new(&cx, actor_ref, actor_id, None);
2855
2856 assert!(!ctx.is_stopping());
2858 assert!(ctx.checkpoint().is_ok());
2859
2860 ctx.stop_self();
2862 assert!(ctx.is_stopping());
2863 assert!(ctx.checkpoint().is_err());
2864 assert!(cx.checkpoint().is_ok());
2865 assert!(ctx.is_cancel_requested());
2866
2867 crate::test_complete!("actor_context_stopping");
2868 }
2869
2870 #[test]
2871 fn actor_context_parent_none() {
2872 init_test("actor_context_parent_none");
2873
2874 let cx: Cx = Cx::for_testing();
2875 let (sender, _receiver) = mpsc::channel::<u64>(32);
2876 let actor_id = ActorId::from_task(TaskId::new_for_test(1, 1));
2877 let actor_ref = ActorRef {
2878 actor_id,
2879 sender,
2880 state: Arc::new(ActorStateCell::new(ActorState::Running)),
2881 };
2882
2883 let ctx = ActorContext::new(&cx, actor_ref, actor_id, None);
2884
2885 assert!(!ctx.has_parent());
2887 assert!(ctx.parent().is_none());
2888
2889 crate::test_complete!("actor_context_parent_none");
2890 }
2891
2892 #[test]
2893 fn actor_context_cx_delegation() {
2894 init_test("actor_context_cx_delegation");
2895
2896 let cx: Cx = Cx::for_testing();
2897 let (sender, _receiver) = mpsc::channel::<u64>(32);
2898 let actor_id = ActorId::from_task(TaskId::new_for_test(1, 1));
2899 let actor_ref = ActorRef {
2900 actor_id,
2901 sender,
2902 state: Arc::new(ActorStateCell::new(ActorState::Running)),
2903 };
2904
2905 let ctx = ActorContext::new(&cx, actor_ref, actor_id, None);
2906
2907 let _budget = ctx.budget();
2909 ctx.trace("test_event");
2910
2911 let _cx_ref = ctx.cx();
2913
2914 crate::test_complete!("actor_context_cx_delegation");
2915 }
2916
2917 #[test]
2918 fn actor_context_debug() {
2919 init_test("actor_context_debug");
2920
2921 let cx: Cx = Cx::for_testing();
2922 let (sender, _receiver) = mpsc::channel::<u64>(32);
2923 let actor_id = ActorId::from_task(TaskId::new_for_test(1, 1));
2924 let actor_ref = ActorRef {
2925 actor_id,
2926 sender,
2927 state: Arc::new(ActorStateCell::new(ActorState::Running)),
2928 };
2929
2930 let ctx = ActorContext::new(&cx, actor_ref, actor_id, None);
2931
2932 let debug_str = format!("{ctx:?}");
2934 assert!(debug_str.contains("ActorContext"));
2935 assert!(debug_str.contains("actor_id"));
2936
2937 crate::test_complete!("actor_context_debug");
2938 }
2939
2940 #[test]
2945 fn actor_state_cell_encode_decode_roundtrip() {
2946 init_test("actor_state_cell_encode_decode_roundtrip");
2947
2948 let states = [
2949 ActorState::Created,
2950 ActorState::Running,
2951 ActorState::Stopping,
2952 ActorState::Stopped,
2953 ];
2954
2955 for &state in &states {
2956 let cell = ActorStateCell::new(state);
2957 let loaded = cell.load();
2958 crate::assert_with_log!(loaded == state, "roundtrip", state, loaded);
2959 }
2960
2961 for raw in 4_u8..=10 {
2963 let decoded = ActorStateCell::decode(raw);
2964 let is_stopped = decoded == ActorState::Stopped;
2965 crate::assert_with_log!(is_stopped, "unknown u8 -> Stopped", true, is_stopped);
2966 }
2967
2968 crate::test_complete!("actor_state_cell_encode_decode_roundtrip");
2969 }
2970
2971 #[test]
2974 fn mailbox_config_defaults() {
2975 init_test("mailbox_config_defaults");
2976
2977 let config = MailboxConfig::default();
2978 crate::assert_with_log!(
2979 config.capacity == DEFAULT_MAILBOX_CAPACITY,
2980 "default capacity",
2981 DEFAULT_MAILBOX_CAPACITY,
2982 config.capacity
2983 );
2984 crate::assert_with_log!(
2985 config.backpressure,
2986 "backpressure enabled by default",
2987 true,
2988 config.backpressure
2989 );
2990
2991 let custom = MailboxConfig::with_capacity(8);
2992 crate::assert_with_log!(
2993 custom.capacity == 8,
2994 "custom capacity",
2995 8usize,
2996 custom.capacity
2997 );
2998 crate::assert_with_log!(
2999 custom.backpressure,
3000 "with_capacity enables backpressure",
3001 true,
3002 custom.backpressure
3003 );
3004
3005 crate::test_complete!("mailbox_config_defaults");
3006 }
3007
3008 #[test]
3011 fn actor_try_send_full_mailbox_returns_error() {
3012 init_test("actor_try_send_full_mailbox_returns_error");
3013
3014 let mut state = RuntimeState::new();
3015 let root = state.create_root_region(Budget::INFINITE);
3016 let cx: Cx = Cx::for_testing();
3017 let scope = crate::cx::Scope::<FailFast>::new(root, Budget::INFINITE);
3018
3019 let (handle, stored) = scope
3021 .spawn_actor(&mut state, &cx, Counter::new(), 2)
3022 .unwrap();
3023 state.store_spawned_task(handle.task_id(), stored);
3024
3025 let ok1 = handle.try_send(1).is_ok();
3027 crate::assert_with_log!(ok1, "first send ok", true, ok1);
3028 let ok2 = handle.try_send(2).is_ok();
3029 crate::assert_with_log!(ok2, "second send ok", true, ok2);
3030
3031 let result = handle.try_send(3);
3033 let is_full = result.is_err();
3034 crate::assert_with_log!(is_full, "third send fails (full)", true, is_full);
3035
3036 crate::test_complete!("actor_try_send_full_mailbox_returns_error");
3037 }
3038
3039 #[test]
3042 fn actor_context_with_parent_supervisor() {
3043 init_test("actor_context_with_parent_supervisor");
3044
3045 let cx: Cx = Cx::for_testing();
3046
3047 let (parent_sender, _parent_receiver) = mpsc::channel::<SupervisorMessage>(8);
3049 let parent_id = ActorId::from_task(TaskId::new_for_test(10, 1));
3050 let parent_ref = ActorRef {
3051 actor_id: parent_id,
3052 sender: parent_sender,
3053 state: Arc::new(ActorStateCell::new(ActorState::Running)),
3054 };
3055
3056 let (child_sender, _child_receiver) = mpsc::channel::<u64>(32);
3058 let child_id = ActorId::from_task(TaskId::new_for_test(20, 1));
3059 let child_ref = ActorRef {
3060 actor_id: child_id,
3061 sender: child_sender,
3062 state: Arc::new(ActorStateCell::new(ActorState::Running)),
3063 };
3064
3065 let ctx = ActorContext::new(&cx, child_ref, child_id, Some(parent_ref));
3066
3067 let has_parent = ctx.has_parent();
3068 crate::assert_with_log!(has_parent, "has parent", true, has_parent);
3069
3070 let parent = ctx.parent().expect("parent should be Some");
3071 let parent_id_matches = parent.actor_id() == parent_id;
3072 crate::assert_with_log!(
3073 parent_id_matches,
3074 "parent id matches",
3075 true,
3076 parent_id_matches
3077 );
3078
3079 crate::test_complete!("actor_context_with_parent_supervisor");
3080 }
3081
3082 #[test]
3085 fn actor_id_debug_format() {
3086 let id = ActorId::from_task(TaskId::new_for_test(5, 3));
3087 let dbg = format!("{id:?}");
3088 assert!(dbg.contains("ActorId"), "{dbg}");
3089 }
3090
3091 #[test]
3092 fn actor_id_display_delegates_to_task_id() {
3093 let tid = TaskId::new_for_test(7, 2);
3094 let aid = ActorId::from_task(tid);
3095 assert_eq!(format!("{aid}"), format!("{tid}"));
3096 }
3097
3098 #[test]
3099 fn actor_id_from_task_roundtrip() {
3100 let tid = TaskId::new_for_test(3, 1);
3101 let aid = ActorId::from_task(tid);
3102 assert_eq!(aid.task_id(), tid);
3103 }
3104
3105 #[test]
3106 fn actor_id_copy_clone() {
3107 let id = ActorId::from_task(TaskId::new_for_test(1, 1));
3108 let copied = id; let cloned = id;
3110 assert_eq!(id, copied);
3111 assert_eq!(id, cloned);
3112 }
3113
3114 #[test]
3115 fn actor_id_hash_consistency() {
3116 use crate::util::DetHasher;
3117 use std::hash::{Hash, Hasher};
3118
3119 let id1 = ActorId::from_task(TaskId::new_for_test(4, 2));
3120 let id2 = ActorId::from_task(TaskId::new_for_test(4, 2));
3121 assert_eq!(id1, id2);
3122
3123 let mut h1 = DetHasher::default();
3124 let mut h2 = DetHasher::default();
3125 id1.hash(&mut h1);
3126 id2.hash(&mut h2);
3127 assert_eq!(h1.finish(), h2.finish(), "equal IDs must hash equal");
3128 }
3129
3130 #[test]
3131 fn actor_state_debug_all_variants() {
3132 for (state, expected) in [
3133 (ActorState::Created, "Created"),
3134 (ActorState::Running, "Running"),
3135 (ActorState::Stopping, "Stopping"),
3136 (ActorState::Stopped, "Stopped"),
3137 ] {
3138 let dbg = format!("{state:?}");
3139 assert_eq!(dbg, expected, "ActorState::{expected}");
3140 }
3141 }
3142
3143 #[test]
3144 fn actor_state_clone_copy_eq() {
3145 let s = ActorState::Running;
3146 let copied = s;
3147 let cloned = s;
3148 assert_eq!(s, copied);
3149 assert_eq!(s, cloned);
3150 }
3151
3152 #[test]
3153 fn actor_state_exhaustive_inequality() {
3154 let all = [
3155 ActorState::Created,
3156 ActorState::Running,
3157 ActorState::Stopping,
3158 ActorState::Stopped,
3159 ];
3160 for (i, a) in all.iter().enumerate() {
3161 for (j, b) in all.iter().enumerate() {
3162 if i == j {
3163 assert_eq!(a, b);
3164 } else {
3165 assert_ne!(a, b);
3166 }
3167 }
3168 }
3169 }
3170
3171 #[test]
3172 fn actor_state_cell_sequential_transitions() {
3173 let cell = ActorStateCell::new(ActorState::Created);
3174 assert_eq!(cell.load(), ActorState::Created);
3175
3176 cell.store(ActorState::Running);
3177 assert_eq!(cell.load(), ActorState::Running);
3178
3179 cell.store(ActorState::Stopping);
3180 assert_eq!(cell.load(), ActorState::Stopping);
3181
3182 cell.store(ActorState::Stopped);
3183 assert_eq!(cell.load(), ActorState::Stopped);
3184 }
3185
3186 #[test]
3187 fn supervised_restart_commit_preserves_a_winning_stop() {
3188 let restart_wins = ActorStateCell::new(ActorState::Running);
3189 assert!(try_commit_supervised_restart(&restart_wins));
3190 assert_eq!(restart_wins.load(), ActorState::Created);
3191
3192 let stop_wins = ActorStateCell::new(ActorState::Running);
3193 stop_wins.store(ActorState::Stopping);
3194 assert!(
3195 !try_commit_supervised_restart(&stop_wins),
3196 "restart commit must fail after a concurrent stop linearizes"
3197 );
3198 assert_eq!(
3199 stop_wins.load(),
3200 ActorState::Stopping,
3201 "failed restart commit must not overwrite the stop request"
3202 );
3203 }
3204
3205 #[test]
3206 fn supervisor_message_debug_child_failed() {
3207 let msg = SupervisorMessage::ChildFailed {
3208 child_id: ActorId::from_task(TaskId::new_for_test(1, 1)),
3209 reason: "panicked".to_string(),
3210 };
3211 let dbg = format!("{msg:?}");
3212 assert!(dbg.contains("ChildFailed"), "{dbg}");
3213 assert!(dbg.contains("panicked"), "{dbg}");
3214 }
3215
3216 #[test]
3217 fn supervisor_message_debug_child_stopped() {
3218 let msg = SupervisorMessage::ChildStopped {
3219 child_id: ActorId::from_task(TaskId::new_for_test(2, 1)),
3220 };
3221 let dbg = format!("{msg:?}");
3222 assert!(dbg.contains("ChildStopped"), "{dbg}");
3223 }
3224
3225 #[test]
3226 fn supervisor_message_clone() {
3227 let msg = SupervisorMessage::ChildFailed {
3228 child_id: ActorId::from_task(TaskId::new_for_test(1, 1)),
3229 reason: "boom".to_string(),
3230 };
3231 let cloned = msg.clone();
3232 let (a, b) = (format!("{msg:?}"), format!("{cloned:?}"));
3233 assert_eq!(a, b);
3234 }
3235
3236 #[test]
3237 fn supervised_outcome_debug_all_variants() {
3238 let variants: Vec<SupervisedOutcome> = vec![
3239 SupervisedOutcome::Stopped,
3240 SupervisedOutcome::RestartBudgetExhausted { total_restarts: 5 },
3241 SupervisedOutcome::Escalated,
3242 ];
3243 for v in &variants {
3244 let dbg = format!("{v:?}");
3245 assert!(!dbg.is_empty());
3246 }
3247 assert!(format!("{variants0:?}", variants0 = variants[0]).contains("Stopped"));
3248 assert!(format!("{variants1:?}", variants1 = variants[1]).contains('5'));
3249 assert!(format!("{variants2:?}", variants2 = variants[2]).contains("Escalated"));
3250 }
3251
3252 #[test]
3253 fn mailbox_config_debug_clone_copy() {
3254 let cfg = MailboxConfig::default();
3255 let dbg = format!("{cfg:?}");
3256 assert!(dbg.contains("MailboxConfig"), "{dbg}");
3257 assert!(dbg.contains("64"), "{dbg}");
3258
3259 let copied = cfg;
3260 let cloned = cfg;
3261 assert_eq!(copied.capacity, cfg.capacity);
3262 assert_eq!(cloned.backpressure, cfg.backpressure);
3263 }
3264
3265 #[test]
3266 fn mailbox_config_zero_capacity() {
3267 let cfg = MailboxConfig::with_capacity(0);
3268 assert_eq!(cfg.capacity, 0);
3269 assert!(cfg.backpressure);
3270 }
3271
3272 #[test]
3273 fn mailbox_config_max_capacity() {
3274 let cfg = MailboxConfig::with_capacity(usize::MAX);
3275 assert_eq!(cfg.capacity, usize::MAX);
3276 }
3277
3278 #[test]
3279 fn default_mailbox_capacity_is_64() {
3280 assert_eq!(DEFAULT_MAILBOX_CAPACITY, 64);
3281 }
3282
3283 #[test]
3284 fn actor_context_duplicate_child_registration() {
3285 let cx: Cx = Cx::for_testing();
3286 let (sender, _receiver) = mpsc::channel::<u64>(32);
3287 let actor_id = ActorId::from_task(TaskId::new_for_test(1, 1));
3288 let actor_ref = ActorRef {
3289 actor_id,
3290 sender,
3291 state: Arc::new(ActorStateCell::new(ActorState::Running)),
3292 };
3293
3294 let mut ctx = ActorContext::new(&cx, actor_ref, actor_id, None);
3295 let child = ActorId::from_task(TaskId::new_for_test(2, 1));
3296
3297 ctx.register_child(child);
3298 ctx.register_child(child); assert_eq!(ctx.child_count(), 2, "register_child does not dedup");
3300
3301 assert!(ctx.unregister_child(child));
3303 assert_eq!(ctx.child_count(), 1, "one copy remains");
3304 assert!(ctx.unregister_child(child));
3305 assert_eq!(ctx.child_count(), 0);
3306 assert!(!ctx.unregister_child(child), "nothing left to remove");
3307 }
3308
3309 #[test]
3310 fn actor_context_stop_self_is_idempotent() {
3311 let cx: Cx = Cx::for_testing();
3312 let (sender, _receiver) = mpsc::channel::<u64>(32);
3313 let actor_id = ActorId::from_task(TaskId::new_for_test(1, 1));
3314 let actor_ref = ActorRef {
3315 actor_id,
3316 sender,
3317 state: Arc::new(ActorStateCell::new(ActorState::Running)),
3318 };
3319
3320 let mut ctx = ActorContext::new(&cx, actor_ref, actor_id, None);
3321 ctx.stop_self();
3322 assert!(ctx.is_stopping());
3323 ctx.stop_self(); assert!(ctx.is_stopping());
3325 }
3326
3327 #[test]
3328 fn actor_context_self_ref_returns_working_ref() {
3329 let cx: Cx = Cx::for_testing();
3330 let (sender, _receiver) = mpsc::channel::<u64>(32);
3331 let actor_id = ActorId::from_task(TaskId::new_for_test(1, 1));
3332 let actor_ref = ActorRef {
3333 actor_id,
3334 sender,
3335 state: Arc::new(ActorStateCell::new(ActorState::Running)),
3336 };
3337
3338 let ctx = ActorContext::new(&cx, actor_ref, actor_id, None);
3339 let self_ref = ctx.self_ref();
3340 assert_eq!(self_ref.actor_id(), actor_id);
3341 assert!(self_ref.is_alive());
3342 }
3343
3344 #[test]
3345 fn actor_context_deadline_reflects_budget() {
3346 let cx: Cx = Cx::for_testing();
3347 let (sender, _receiver) = mpsc::channel::<u64>(32);
3348 let actor_id = ActorId::from_task(TaskId::new_for_test(1, 1));
3349 let actor_ref = ActorRef {
3350 actor_id,
3351 sender,
3352 state: Arc::new(ActorStateCell::new(ActorState::Running)),
3353 };
3354
3355 let ctx = ActorContext::new(&cx, actor_ref, actor_id, None);
3356 assert!(ctx.deadline().is_none());
3358 }
3359
3360 #[test]
3361 fn actor_handle_debug() {
3362 let mut state = RuntimeState::new();
3363 let root = state.create_root_region(Budget::INFINITE);
3364 let cx: Cx = Cx::for_testing();
3365 let scope = crate::cx::Scope::<FailFast>::new(root, Budget::INFINITE);
3366
3367 let (handle, stored) = scope
3368 .spawn_actor(&mut state, &cx, Counter::new(), 32)
3369 .unwrap();
3370 state.store_spawned_task(handle.task_id(), stored);
3371
3372 let dbg = format!("{handle:?}");
3373 assert!(dbg.contains("ActorHandle"), "{dbg}");
3374 }
3375
3376 #[test]
3377 fn actor_handle_second_join_fails_closed() {
3378 init_test("actor_handle_second_join_fails_closed");
3379
3380 let mut runtime = crate::lab::LabRuntime::new(crate::lab::LabConfig::default());
3381 let region = runtime.state.create_root_region(Budget::INFINITE);
3382 let cx = Cx::for_testing();
3383 let scope = crate::cx::Scope::<FailFast>::new(region, Budget::INFINITE);
3384
3385 let (mut handle, stored) = scope
3386 .spawn_actor(&mut runtime.state, &cx, Counter::new(), 32)
3387 .unwrap();
3388 let task_id = handle.task_id();
3389 runtime.state.store_spawned_task(task_id, stored);
3390
3391 handle.stop();
3392 runtime.scheduler.lock().schedule(task_id, 0);
3393 runtime.run_until_quiescent();
3394 assert!(handle.is_finished(), "stopped actor should report finished");
3395
3396 let final_state = futures_lite::future::block_on(handle.join(&cx)).expect("first join");
3397 assert_eq!(final_state.count, 0, "join should return final actor state");
3398
3399 let second = futures_lite::future::block_on(handle.join(&cx));
3400 assert!(
3401 matches!(second, Err(JoinError::PolledAfterCompletion)),
3402 "second join must fail closed, got {second:?}"
3403 );
3404
3405 crate::test_complete!("actor_handle_second_join_fails_closed");
3406 }
3407
3408 #[test]
3409 fn actor_join_future_closed_inner_maps_to_cancelled_reason() {
3410 init_test("actor_join_future_closed_inner_maps_to_cancelled_reason");
3411
3412 let (result_tx, mut result_rx) =
3413 crate::channel::oneshot::channel::<Result<Counter, JoinError>>();
3414 drop(result_tx);
3415 let mut terminal_state = false;
3416 let poll_result = {
3417 let mut join = std::pin::pin!(actor_join_future_from_receiver::<Counter>(
3418 &mut result_rx,
3419 &mut terminal_state,
3420 ));
3421 let waker = counting_waker(Arc::new(std::sync::atomic::AtomicUsize::new(0)));
3422 let mut poll_cx = Context::from_waker(&waker);
3423 join.as_mut().poll(&mut poll_cx)
3424 };
3425
3426 match poll_result {
3427 Poll::Ready(Err(JoinError::Cancelled(reason))) => {
3428 assert_eq!(reason.kind, crate::types::CancelKind::User);
3429 assert_eq!(
3430 reason.message.as_deref(),
3431 Some("join channel closed"),
3432 "closed inner oneshot should surface the explicit join-channel reason"
3433 );
3434 }
3435 other => panic!("closed inner join future must map to Cancelled, got {other:?}"),
3436 }
3437
3438 assert!(
3439 terminal_state,
3440 "closed join future should mark terminal state"
3441 );
3442 crate::test_complete!("actor_join_future_closed_inner_maps_to_cancelled_reason");
3443 }
3444
3445 #[test]
3446 fn actor_join_future_repoll_fails_before_inner_polled_after_completion() {
3447 init_test("actor_join_future_repoll_fails_before_inner_polled_after_completion");
3448
3449 let (result_tx, mut result_rx) =
3450 crate::channel::oneshot::channel::<Result<Counter, JoinError>>();
3451 let cx: Cx = Cx::for_testing();
3452 result_tx
3453 .send(&cx, Ok(Counter::new()))
3454 .expect("seed join result");
3455 let mut terminal_state = false;
3456 let (first_poll, second_poll) = {
3457 let mut join = std::pin::pin!(actor_join_future_from_receiver::<Counter>(
3458 &mut result_rx,
3459 &mut terminal_state,
3460 ));
3461 let waker = counting_waker(Arc::new(std::sync::atomic::AtomicUsize::new(0)));
3462 let mut poll_cx = Context::from_waker(&waker);
3463 let first_poll = join.as_mut().poll(&mut poll_cx);
3464 let second_poll = join.as_mut().poll(&mut poll_cx);
3465 (first_poll, second_poll)
3466 };
3467
3468 match first_poll {
3469 Poll::Ready(Ok(actor)) => {
3470 assert_eq!(actor.count, 0, "seeded actor state should round-trip");
3471 }
3472 other => panic!("first poll should return actor state, got {other:?}"),
3473 }
3474
3475 assert!(terminal_state, "successful join should mark terminal state");
3476
3477 match second_poll {
3478 Poll::Ready(Err(JoinError::PolledAfterCompletion)) => {}
3479 other => panic!(
3480 "re-poll should fail closed before the inner oneshot can return PolledAfterCompletion, got {other:?}"
3481 ),
3482 }
3483
3484 crate::test_complete!(
3485 "actor_join_future_repoll_fails_before_inner_polled_after_completion"
3486 );
3487 }
3488
3489 #[test]
3490 fn actor_ref_debug() {
3491 let mut state = RuntimeState::new();
3492 let root = state.create_root_region(Budget::INFINITE);
3493 let cx: Cx = Cx::for_testing();
3494 let scope = crate::cx::Scope::<FailFast>::new(root, Budget::INFINITE);
3495
3496 let (handle, stored) = scope
3497 .spawn_actor(&mut state, &cx, Counter::new(), 32)
3498 .unwrap();
3499 state.store_spawned_task(handle.task_id(), stored);
3500
3501 let actor_ref = handle.sender();
3502 let dbg = format!("{actor_ref:?}");
3503 assert!(dbg.contains("ActorRef"), "{dbg}");
3504 }
3505
3506 #[test]
3507 fn actor_state_cell_debug() {
3508 let cell = ActorStateCell::new(ActorState::Running);
3509 let dbg = format!("{cell:?}");
3510 assert!(dbg.contains("ActorStateCell"), "{dbg}");
3511 }
3512
3513 #[test]
3514 fn actor_id_clone_copy_eq_hash() {
3515 use std::collections::HashSet;
3516
3517 let id = ActorId::from_task(TaskId::new_for_test(1, 0));
3518 let dbg = format!("{id:?}");
3519 assert!(dbg.contains("ActorId"));
3520
3521 let id2 = id;
3522 assert_eq!(id, id2);
3523
3524 let id3 = id;
3526 assert_eq!(id, id3);
3527
3528 let mut set = HashSet::new();
3530 set.insert(id);
3531 set.insert(ActorId::from_task(TaskId::new_for_test(2, 0)));
3532 assert_eq!(set.len(), 2);
3533 }
3534
3535 #[test]
3536 fn actor_state_debug_clone_copy_eq() {
3537 let s = ActorState::Running;
3538 let dbg = format!("{s:?}");
3539 assert!(dbg.contains("Running"));
3540
3541 let s2 = s;
3542 assert_eq!(s, s2);
3543
3544 let s3 = s;
3545 assert_eq!(s, s3);
3546
3547 assert_ne!(ActorState::Created, ActorState::Stopped);
3548 }
3549
3550 #[test]
3551 fn mailbox_config_debug_clone_copy_default() {
3552 let c = MailboxConfig::default();
3553 let dbg = format!("{c:?}");
3554 assert!(dbg.contains("MailboxConfig"));
3555
3556 let c2 = c;
3557 assert_eq!(c2.capacity, c.capacity);
3558 assert_eq!(c2.backpressure, c.backpressure);
3559
3560 let c3 = c;
3562 assert_eq!(c3.capacity, c.capacity);
3563 }
3564}
3565
3566#[cfg(test)]
3571#[path = "actor_conformance_tests.rs"]
3572mod actor_conformance_tests;
3573
3574#[cfg(test)]
3575mod conformance_integration {
3576 use super::actor_conformance_tests::{ActorConformanceHarness, TestVerdict};
3577
3578 #[test]
3579 fn actor_conformance_suite() {
3580 crate::test_utils::init_test_logging();
3581
3582 let mut harness = ActorConformanceHarness::new();
3583
3584 let results = harness.run_full_suite();
3586
3587 let mut failures = Vec::new();
3588 let mut passes = 0;
3589
3590 for result in results {
3591 match result.verdict {
3592 TestVerdict::Pass => {
3593 passes += 1;
3594 }
3595 TestVerdict::Fail(reason) => {
3596 failures.push(format!("{}: {}", result.test_name, reason));
3597 }
3598 }
3599 }
3600
3601 assert!(
3602 failures.is_empty(),
3603 "Actor conformance failures:\n{}",
3604 failures.join("\n")
3605 );
3606
3607 assert!(
3608 passes > 0,
3609 "No conformance tests passed - harness may be broken"
3610 );
3611
3612 crate::test_complete!("actor_conformance_suite");
3613 }
3614}