1use std::future;
30use std::panic::AssertUnwindSafe;
31use std::sync::Arc;
32#[cfg(test)]
33use std::sync::OnceLock;
34use std::sync::atomic::{AtomicU32, Ordering};
35use std::time::Duration;
36
37use anyhow::{Context, Result, anyhow};
38use futures::FutureExt;
39#[cfg(test)]
40use parking_lot::Mutex;
41use tokio::sync::{broadcast, mpsc, oneshot};
42use tokio::task::{JoinError, JoinHandle};
43use tracing::{Instrument, instrument::WithSubscriber};
44
45use crate::actor::action::ActionDispatchError;
46use crate::actor::connection::ConnHandle;
47use crate::actor::context::ActorContext;
48use crate::actor::factory::ActorFactory;
49use crate::actor::lifecycle_hooks::{ActorEvents, ActorStart, Reply};
50use crate::actor::messages::{
51 ActorEvent, ActorHttpResponse, QueueSendResult, Request, SerializeStateReason, StateDelta,
52 WorkflowKvWrite,
53};
54use crate::actor::metrics::startup_phase::StartupPhase;
55use crate::actor::state::{PersistedActor, RequestSaveOpts};
56use crate::actor::task_types::ShutdownKind;
57use crate::actor::work_registry::ActorWorkKind;
58use crate::error::{ActorLifecycle as ActorLifecycleError, ActorRuntime};
59use crate::runtime::RuntimeSpawner;
60#[cfg(test)]
61use crate::time::sleep;
62use crate::time::{Instant, sleep_until, timeout};
63use crate::types::{SaveStateOpts, format_actor_key};
64use crate::websocket::WebSocket;
65
66pub type ActionDispatchResult = std::result::Result<Vec<u8>, ActionDispatchError>;
67pub type HttpDispatchResult = Result<ActorHttpResponse>;
68
69const SERIALIZE_STATE_SHUTDOWN_SANITY_CAP: Duration = Duration::from_secs(15);
70#[cfg(test)]
71const LONG_SHUTDOWN_DRAIN_WARNING_THRESHOLD: Duration = Duration::from_secs(1);
72const INSPECTOR_SERIALIZE_STATE_INTERVAL: Duration = Duration::from_millis(50);
73const INSPECTOR_OVERLAY_CHANNEL_CAPACITY: usize = 32;
74
75pub use crate::actor::task_types::LifecycleState;
76
77#[cfg(test)]
79#[path = "../../tests/task.rs"]
80mod tests;
81
82#[cfg(test)]
83#[path = "../../tests/modules/task_lifecycle.rs"]
84mod lifecycle_tests;
85
86#[cfg(test)]
87type ShutdownCleanupHook = Arc<dyn Fn(&ActorContext, &'static str) + Send + Sync>;
88
89#[cfg(test)]
90static SHUTDOWN_CLEANUP_HOOK: OnceLock<Mutex<Option<ShutdownCleanupHook>>> = OnceLock::new();
92
93#[cfg(test)]
94pub(crate) struct ShutdownCleanupHookGuard;
95
96#[cfg(test)]
97type ShutdownReplyHook = Arc<dyn Fn(&ActorContext, ShutdownKind) + Send + Sync>;
98
99#[cfg(test)]
100static SHUTDOWN_REPLY_HOOK: OnceLock<Mutex<Option<ShutdownReplyHook>>> = OnceLock::new();
102
103#[cfg(test)]
104pub(crate) struct ShutdownReplyHookGuard;
105
106#[cfg(test)]
107pub(crate) fn install_shutdown_cleanup_hook(hook: ShutdownCleanupHook) -> ShutdownCleanupHookGuard {
108 *SHUTDOWN_CLEANUP_HOOK
109 .get_or_init(|| Mutex::new(None))
110 .lock() = Some(hook);
111 ShutdownCleanupHookGuard
112}
113
114#[cfg(test)]
115impl Drop for ShutdownCleanupHookGuard {
116 fn drop(&mut self) {
117 if let Some(hooks) = SHUTDOWN_CLEANUP_HOOK.get() {
118 *hooks.lock() = None;
119 }
120 }
121}
122
123#[cfg(test)]
124fn run_shutdown_cleanup_hook(ctx: &ActorContext, reason: &'static str) {
125 let hook = SHUTDOWN_CLEANUP_HOOK
126 .get_or_init(|| Mutex::new(None))
127 .lock()
128 .clone();
129 if let Some(hook) = hook {
130 hook(ctx, reason);
131 }
132}
133
134#[cfg(test)]
135pub(crate) fn install_shutdown_reply_hook(hook: ShutdownReplyHook) -> ShutdownReplyHookGuard {
136 *SHUTDOWN_REPLY_HOOK.get_or_init(|| Mutex::new(None)).lock() = Some(hook);
137 ShutdownReplyHookGuard
138}
139
140#[cfg(test)]
141impl Drop for ShutdownReplyHookGuard {
142 fn drop(&mut self) {
143 if let Some(hooks) = SHUTDOWN_REPLY_HOOK.get() {
144 *hooks.lock() = None;
145 }
146 }
147}
148
149#[cfg(test)]
150fn run_shutdown_reply_hook(ctx: &ActorContext, reason: ShutdownKind) {
151 let hook = SHUTDOWN_REPLY_HOOK
152 .get_or_init(|| Mutex::new(None))
153 .lock()
154 .clone();
155 if let Some(hook) = hook {
156 hook(ctx, reason);
157 }
158}
159
160pub enum LifecycleCommand {
161 Start {
162 reply: oneshot::Sender<Result<()>>,
163 },
164 Stop {
165 reason: ShutdownKind,
166 reply: oneshot::Sender<Result<()>>,
167 },
168 FireAlarm {
169 reply: oneshot::Sender<Result<()>>,
170 },
171}
172
173impl LifecycleCommand {
174 fn kind(&self) -> &'static str {
175 match self {
176 Self::Start { .. } => "start",
177 Self::Stop { .. } => "stop",
178 Self::FireAlarm { .. } => "fire_alarm",
179 }
180 }
181
182 fn stop_reason(&self) -> Option<&'static str> {
183 match self {
184 Self::Stop { reason, .. } => Some(shutdown_reason_label(*reason)),
185 Self::Start { .. } => None,
186 Self::FireAlarm { .. } => None,
187 }
188 }
189}
190
191pub(crate) fn try_send_lifecycle_command(
192 sender: &mpsc::UnboundedSender<LifecycleCommand>,
193 command: LifecycleCommand,
194) -> Result<()> {
195 sender
196 .send(command)
197 .map_err(|_| ActorLifecycleError::NotReady.build())
198}
199
200pub enum DispatchCommand {
201 Action {
202 name: String,
203 args: Vec<u8>,
204 conn: ConnHandle,
205 reply: oneshot::Sender<Result<Vec<u8>>>,
206 },
207 QueueSend {
208 name: String,
209 body: Vec<u8>,
210 conn: ConnHandle,
211 request: Request,
212 wait: bool,
213 timeout_ms: Option<u64>,
214 reply: oneshot::Sender<Result<QueueSendResult>>,
215 },
216 Http {
217 request: Request,
218 reply: oneshot::Sender<HttpDispatchResult>,
219 },
220 OpenWebSocket {
221 conn: ConnHandle,
222 ws: WebSocket,
223 request: Option<Request>,
224 reply: oneshot::Sender<Result<()>>,
225 },
226 WorkflowHistory {
227 reply: oneshot::Sender<Result<Option<Vec<u8>>>>,
228 },
229 WorkflowReplay {
230 entry_id: Option<String>,
231 reply: oneshot::Sender<Result<Option<Vec<u8>>>>,
232 },
233}
234
235impl DispatchCommand {
236 fn kind(&self) -> &'static str {
237 match self {
238 Self::Action { .. } => "action",
239 Self::QueueSend { .. } => "queue_send",
240 Self::Http { .. } => "http",
241 Self::OpenWebSocket { .. } => "open_websocket",
242 Self::WorkflowHistory { .. } => "workflow_history",
243 Self::WorkflowReplay { .. } => "workflow_replay",
244 }
245 }
246}
247
248pub(crate) fn try_send_dispatch_command(
249 sender: &mpsc::UnboundedSender<DispatchCommand>,
250 command: DispatchCommand,
251) -> Result<()> {
252 sender
253 .send(command)
254 .map_err(|_| ActorLifecycleError::NotReady.build())
255}
256
257#[derive(Debug)]
258pub enum LifecycleEvent {
259 SaveRequested {
260 immediate: bool,
261 },
262 WorkflowFlushRequested {
263 writes: Vec<WorkflowKvWrite>,
264 reply: Reply<()>,
265 },
266 InspectorSerializeRequested,
267 InspectorAttachmentsChanged,
268 SleepTick,
269}
270
271impl LifecycleEvent {
272 fn kind(&self) -> &'static str {
273 match self {
274 Self::SaveRequested { .. } => "save_requested",
275 Self::WorkflowFlushRequested { .. } => "workflow_flush_requested",
276 Self::InspectorSerializeRequested => "inspector_serialize_requested",
277 Self::InspectorAttachmentsChanged => "inspector_attachments_changed",
278 Self::SleepTick => "sleep_tick",
279 }
280 }
281}
282
283impl PartialEq for LifecycleEvent {
284 fn eq(&self, other: &Self) -> bool {
285 match self {
286 Self::SaveRequested { immediate: left } => {
287 if let Self::SaveRequested { immediate: right } = other {
288 left == right
289 } else {
290 false
291 }
292 }
293 Self::WorkflowFlushRequested { .. } => false,
294 Self::InspectorSerializeRequested => {
295 matches!(other, Self::InspectorSerializeRequested)
296 }
297 Self::InspectorAttachmentsChanged => {
298 matches!(other, Self::InspectorAttachmentsChanged)
299 }
300 Self::SleepTick => matches!(other, Self::SleepTick),
301 }
302 }
303}
304
305impl Eq for LifecycleEvent {}
306
307enum LiveExit {
308 Shutdown { reason: ShutdownKind },
309 Terminated,
310}
311
312struct SleepGraceState {
313 deadline: Instant,
314 reason: ShutdownKind,
315}
316
317struct PersistedStartup {
318 actor: PersistedActor,
319 last_pushed_alarm: Option<i64>,
320 run_wake_at: Option<i64>,
321}
322
323struct PendingLifecycleReply {
324 command: &'static str,
325 reason: Option<&'static str>,
326 reply: oneshot::Sender<Result<()>>,
327}
328
329pub struct ActorTask {
330 pub actor_id: String,
332 pub generation: u32,
333
334 pub lifecycle_inbox: mpsc::UnboundedReceiver<LifecycleCommand>,
338 pub dispatch_inbox: mpsc::UnboundedReceiver<DispatchCommand>,
342 pub lifecycle_events: mpsc::UnboundedReceiver<LifecycleEvent>,
346
347 pub lifecycle: LifecycleState,
349 pub factory: Arc<ActorFactory>,
350 pub ctx: ActorContext,
351
352 pub start_input: Option<Vec<u8>>,
354
355 actor_event_tx: Option<mpsc::UnboundedSender<ActorEvent>>,
359 actor_event_rx: Option<mpsc::UnboundedReceiver<ActorEvent>>,
363 run_handle: Option<JoinHandle<Result<()>>>,
366
367 inspector_attach_count: Arc<AtomicU32>,
371 inspector_overlay_tx: broadcast::Sender<Arc<Vec<u8>>>,
374
375 pub state_save_deadline: Option<Instant>,
379 pub inspector_serialize_state_deadline: Option<Instant>,
382 pub sleep_deadline: Option<Instant>,
385
386 shutdown_reply: Option<PendingLifecycleReply>,
390 sleep_grace: Option<SleepGraceState>,
393}
394
395impl ActorTask {
396 pub fn new(
397 actor_id: String,
398 generation: u32,
399 lifecycle_inbox: mpsc::UnboundedReceiver<LifecycleCommand>,
400 dispatch_inbox: mpsc::UnboundedReceiver<DispatchCommand>,
401 lifecycle_events: mpsc::UnboundedReceiver<LifecycleEvent>,
402 factory: Arc<ActorFactory>,
403 ctx: ActorContext,
404 start_input: Option<Vec<u8>>,
405 ) -> Self {
406 let (actor_event_tx, actor_event_rx) = mpsc::unbounded_channel();
407 let (inspector_overlay_tx, _) = broadcast::channel(INSPECTOR_OVERLAY_CHANNEL_CAPACITY);
408 let inspector_attach_count = Arc::new(AtomicU32::new(0));
409 ctx.configure_inspector_runtime(
410 Arc::clone(&inspector_attach_count),
411 inspector_overlay_tx.clone(),
412 );
413 let inspector_ctx = ctx.downgrade();
414 let inspector_attach_count_for_hook = Arc::clone(&inspector_attach_count);
415 ctx.on_request_save(Box::new(move |_opts| {
416 if inspector_attach_count_for_hook.load(Ordering::SeqCst) > 0 {
417 if let Some(ctx) = ActorContext::from_weak(&inspector_ctx) {
418 ctx.notify_inspector_serialize_requested();
419 }
420 }
421 }));
422 Self {
423 actor_id,
424 generation,
425 lifecycle_inbox,
426 dispatch_inbox,
427 lifecycle_events,
428 lifecycle: LifecycleState::default(),
429 factory,
430 ctx,
431 start_input,
432 actor_event_tx: Some(actor_event_tx),
433 actor_event_rx: Some(actor_event_rx),
434 run_handle: None,
435 inspector_attach_count,
436 inspector_overlay_tx,
437 state_save_deadline: None,
438 inspector_serialize_state_deadline: None,
439 sleep_deadline: None,
440 shutdown_reply: None,
441 sleep_grace: None,
442 }
443 }
444
445 #[tracing::instrument(
446 skip_all,
447 fields(
448 actor_id = %self.actor_id,
449 generation = self.generation,
450 actor_key = %format_actor_key(self.ctx.key()),
451 ),
452 )]
453 pub async fn run(mut self) -> Result<()> {
454 let exit = self.run_live().await;
455 let LiveExit::Shutdown { reason } = exit else {
456 self.record_inbox_depths();
457 self.ctx.metrics().record_actor_stopped();
458 return Ok(());
459 };
460
461 let result = match AssertUnwindSafe(self.run_shutdown(reason))
462 .catch_unwind()
463 .await
464 {
465 Ok(result) => result,
466 Err(_) => Err(anyhow!("shutdown panicked during {reason:?}")),
467 };
468 self.deliver_shutdown_reply(reason, &result);
469 self.transition_to(LifecycleState::Terminated);
470 self.record_inbox_depths();
471 self.ctx.metrics().record_actor_stopped();
472 result
473 }
474
475 async fn run_live(&mut self) -> LiveExit {
476 let activity_notify = self.ctx.sleep_activity_notify();
477
478 loop {
479 if self.ctx.acknowledge_activity_dirty() {
480 if let Some(exit) = self.on_activity_signal().await {
481 return exit;
482 }
483 }
484 self.record_inbox_depths();
486 tokio::select! {
487 biased;
488 lifecycle_command = self.lifecycle_inbox.recv() => {
489 match lifecycle_command {
490 Some(command) => {
491 if let Some(exit) = self.handle_lifecycle(command).await {
492 return exit;
493 }
494 }
495 None => {
496 self.log_closed_channel(
497 "lifecycle_inbox",
498 "actor task terminating because lifecycle command inbox closed",
499 );
500 return LiveExit::Terminated;
501 }
502 }
503 }
504 lifecycle_event = self.lifecycle_events.recv() => {
505 match lifecycle_event {
506 Some(event) => self.handle_event(event).await,
507 None => {
508 self.log_closed_channel(
509 "lifecycle_events",
510 "actor task terminating because lifecycle event inbox closed",
511 );
512 return LiveExit::Terminated;
513 }
514 }
515 }
516 _ = activity_notify.notified() => {
517 self.ctx.acknowledge_activity_dirty();
518 if let Some(exit) = self.on_activity_signal().await {
519 return exit;
520 }
521 }
522 _ = Self::sleep_grace_tick(self.sleep_grace.as_ref().map(|grace| grace.deadline)), if self.sleep_grace.is_some() => {
523 if let Some(exit) = self.on_sleep_grace_deadline().await {
524 return exit;
525 }
526 }
527 dispatch_command = self.dispatch_inbox.recv(), if self.accepting_dispatch() => {
528 match dispatch_command {
529 Some(command) => self.handle_dispatch(command).await,
530 None => {
531 self.log_closed_channel(
532 "dispatch_inbox",
533 "actor task terminating because dispatch inbox closed",
534 );
535 return LiveExit::Terminated;
536 }
537 }
538 }
539 outcome = Self::wait_for_run_handle(self.run_handle.as_mut()), if self.run_handle.is_some() => {
540 if let Some(exit) = self.handle_run_handle_outcome(outcome) {
541 return exit;
542 }
543 }
544 _ = Self::state_save_tick(self.state_save_deadline), if self.state_save_timer_active() => {
545 self.on_state_save_tick().await;
546 }
547 _ = Self::inspector_serialize_state_tick(self.inspector_serialize_state_deadline), if self.inspector_serialize_timer_active() => {
548 self.on_inspector_serialize_state_tick().await;
549 }
550 _ = Self::sleep_tick(self.sleep_deadline), if self.sleep_timer_active() => {
551 self.on_sleep_tick().await;
552 }
553 }
554
555 if self.should_terminate() {
556 return LiveExit::Terminated;
557 }
558 }
559 }
560
561 async fn handle_lifecycle(&mut self, command: LifecycleCommand) -> Option<LiveExit> {
562 let command_kind = command.kind();
563 let reason = command.stop_reason();
564 self.log_lifecycle_command_received(command_kind, reason);
565 if matches!(
566 self.lifecycle,
567 LifecycleState::SleepGrace | LifecycleState::DestroyGrace
568 ) {
569 return self
570 .handle_sleep_grace_lifecycle(command, command_kind, reason)
571 .await;
572 }
573 match command {
574 LifecycleCommand::Start { reply } => {
575 let result = self.start_actor().await;
576 let failed = result.is_err();
577 if failed {
578 self.finish_failed_startup().await;
579 }
580 self.reply_lifecycle_command(command_kind, reason, reply, result);
581 failed.then_some(LiveExit::Terminated)
582 }
583 LifecycleCommand::Stop { reason, reply } => {
584 self.begin_stop(
585 reason,
586 command_kind,
587 Some(shutdown_reason_label(reason)),
588 reply,
589 )
590 .await
591 }
592 LifecycleCommand::FireAlarm { reply } => {
593 let result = self.fire_due_alarms().await;
594 self.reply_lifecycle_command(command_kind, reason, reply, result);
595 None
596 }
597 }
598 }
599
600 async fn handle_sleep_grace_lifecycle(
601 &mut self,
602 command: LifecycleCommand,
603 command_kind: &'static str,
604 command_reason: Option<&'static str>,
605 ) -> Option<LiveExit> {
606 match command {
607 LifecycleCommand::Start { reply } => {
608 self.reply_lifecycle_command(
609 command_kind,
610 command_reason,
611 reply,
612 Err(ActorLifecycleError::Stopping.build()),
613 );
614 None
615 }
616 LifecycleCommand::Stop { reason, reply } => {
617 let current_reason = self.sleep_grace.as_ref().map(|grace| grace.reason);
618 if current_reason != Some(reason) {
619 debug_assert!(false, "engine actor2 sends one Stop per actor instance");
620 tracing::warn!(
621 actor_id = %self.ctx.actor_id(),
622 reason = shutdown_reason_label(reason),
623 current_reason = ?current_reason,
624 "conflicting Stop during grace, ignoring"
625 );
626 }
627 self.reply_lifecycle_command(command_kind, command_reason, reply, Ok(()));
628 None
629 }
630 LifecycleCommand::FireAlarm { reply } => {
631 let result = self.fire_due_alarms().await;
632 self.reply_lifecycle_command(command_kind, command_reason, reply, result);
633 None
634 }
635 }
636 }
637
638 #[cfg(test)]
639 async fn handle_stop(&mut self, reason: ShutdownKind) -> Result<()> {
640 let (reply_tx, reply_rx) = oneshot::channel();
641 self.register_shutdown_reply("stop", Some(shutdown_reason_label(reason)), reply_tx);
642 self.begin_grace(reason).await;
643 loop {
644 if self.ctx.acknowledge_activity_dirty() {
645 if let Some(exit) = self.on_activity_signal().await {
646 let LiveExit::Shutdown { reason } = exit else {
647 return Ok(());
648 };
649 let result = match AssertUnwindSafe(self.run_shutdown(reason))
650 .catch_unwind()
651 .await
652 {
653 Ok(result) => result,
654 Err(_) => Err(anyhow!("shutdown panicked during {reason:?}")),
655 };
656 self.deliver_shutdown_reply(reason, &result);
657 self.transition_to(LifecycleState::Terminated);
658 return match reply_rx.await {
659 Ok(result) => result,
660 Err(_) => Err(ActorLifecycleError::DroppedReply.build()),
661 };
662 }
663 }
664
665 let Some(deadline) = self.sleep_grace.as_ref().map(|grace| grace.deadline) else {
666 return Err(anyhow!("stop grace ended without shutdown exit"));
667 };
668 let activity_notify = self.ctx.sleep_activity_notify();
669 let activity = activity_notify.notified();
670 tokio::pin!(activity);
671
672 tokio::select! {
673 _ = &mut activity => {}
674 _ = Self::sleep_grace_tick(Some(deadline)) => {
675 if let Some(exit) = self.on_sleep_grace_deadline().await {
676 let LiveExit::Shutdown { reason } = exit else {
677 return Ok(());
678 };
679 let result = match AssertUnwindSafe(self.run_shutdown(reason))
680 .catch_unwind()
681 .await
682 {
683 Ok(result) => result,
684 Err(_) => Err(anyhow!("shutdown panicked during {reason:?}")),
685 };
686 self.deliver_shutdown_reply(reason, &result);
687 self.transition_to(LifecycleState::Terminated);
688 return match reply_rx.await {
689 Ok(result) => result,
690 Err(_) => Err(ActorLifecycleError::DroppedReply.build()),
691 };
692 }
693 }
694 }
695 }
696 }
697
698 async fn begin_stop(
699 &mut self,
700 reason: ShutdownKind,
701 command: &'static str,
702 command_reason: Option<&'static str>,
703 reply: oneshot::Sender<Result<()>>,
704 ) -> Option<LiveExit> {
705 match self.lifecycle {
706 LifecycleState::Started => {
707 self.register_shutdown_reply(command, command_reason, reply);
708 self.drain_accepted_dispatch().await;
709 self.begin_grace(reason).await;
710 self.try_finish_grace()
711 }
712 LifecycleState::SleepGrace | LifecycleState::DestroyGrace => {
713 let current_reason = self.sleep_grace.as_ref().map(|grace| grace.reason);
714 if current_reason == Some(reason) {
715 self.reply_lifecycle_command(command, command_reason, reply, Ok(()));
716 None
717 } else {
718 debug_assert!(false, "engine actor2 sends one Stop per actor instance");
719 tracing::warn!(
720 actor_id = %self.ctx.actor_id(),
721 reason = shutdown_reason_label(reason),
722 current_reason = ?current_reason,
723 "conflicting Stop during grace, ignoring"
724 );
725 self.reply_lifecycle_command(command, command_reason, reply, Ok(()));
726 None
727 }
728 }
729 LifecycleState::SleepFinalize | LifecycleState::Destroying => {
730 debug_assert!(false, "engine actor2 sends one Stop per actor instance");
731 tracing::warn!(
732 actor_id = %self.ctx.actor_id(),
733 reason = shutdown_reason_label(reason),
734 "duplicate Stop after shutdown started, ignoring"
735 );
736 self.reply_lifecycle_command(command, command_reason, reply, Ok(()));
737 None
738 }
739 LifecycleState::Terminated => {
740 self.reply_lifecycle_command(command, command_reason, reply, Ok(()));
741 None
742 }
743 LifecycleState::Loading => {
744 self.reply_lifecycle_command(
745 command,
746 command_reason,
747 reply,
748 Err(ActorLifecycleError::NotReady.build()),
749 );
750 None
751 }
752 }
753 }
754
755 async fn drain_accepted_dispatch(&mut self) {
756 while self.accepting_dispatch() {
757 let Ok(command) = self.dispatch_inbox.try_recv() else {
758 break;
759 };
760 self.handle_dispatch(command).await;
761 }
762 }
763
764 async fn begin_grace(&mut self, reason: ShutdownKind) {
765 tracing::debug!(
766 actor_id = %self.ctx.actor_id(),
767 reason = shutdown_reason_label(reason),
768 "actor grace shutdown started"
769 );
770 self.ctx.suspend_alarm_dispatch();
771 self.ctx.cancel_local_alarm_timeouts();
772 self.ctx.set_local_alarm_callback(None);
773 self.transition_to(match reason {
774 ShutdownKind::Sleep => LifecycleState::SleepGrace,
775 ShutdownKind::Destroy => LifecycleState::DestroyGrace,
776 });
777 self.start_grace(reason);
778 self.emit_grace_events(reason);
779 }
780
781 fn emit_grace_events(&mut self, reason: ShutdownKind) {
782 let conns: Vec<_> = self.ctx.conns().collect();
783 for conn in conns {
784 let hibernatable_sleep =
785 matches!(reason, ShutdownKind::Sleep) && conn.is_hibernatable();
786 if hibernatable_sleep {
787 self.ctx.request_hibernation_transport_save(conn.id());
788 continue;
789 }
790 self.ctx.begin_core_dispatched_hook();
791 let reply = self.core_dispatched_hook_reply("disconnect_conn");
792 let conn_id = conn.id().to_owned();
793 if let Err(error) = self.send_actor_event(
794 "grace_disconnect_conn",
795 ActorEvent::DisconnectConn { conn_id, reply },
796 ) {
797 tracing::error!(?error, "failed to enqueue disconnect cleanup event");
798 }
799 }
800
801 self.ctx.begin_core_dispatched_hook();
802 let reply = self.core_dispatched_hook_reply("run_graceful_cleanup");
803 if let Err(error) = self.send_actor_event(
804 "grace_run_cleanup",
805 ActorEvent::RunGracefulCleanup { reason, reply },
806 ) {
807 tracing::error!(?error, "failed to enqueue run cleanup event");
808 }
809 self.ctx.reset_sleep_timer();
810 }
811
812 fn core_dispatched_hook_reply(&self, operation: &'static str) -> Reply<()> {
813 let (tx, rx) = oneshot::channel();
814 let ctx = self.ctx.clone();
815 let task = async move {
816 match rx.await {
817 Ok(Ok(())) => {}
818 Ok(Err(error)) => {
819 tracing::error!(?error, operation, "core dispatched hook failed");
820 }
821 Err(error) => {
822 tracing::error!(?error, operation, "core dispatched hook reply dropped");
823 }
824 }
825 ctx.mark_core_dispatched_hook_completed();
826 }
827 .in_current_span();
828 RuntimeSpawner::spawn(task);
829 tx.into()
830 }
831
832 async fn handle_event(&mut self, event: LifecycleEvent) {
833 tracing::debug!(
834 actor_id = %self.ctx.actor_id(),
835 event = event.kind(),
836 "actor lifecycle event drained"
837 );
838 match event {
839 LifecycleEvent::SaveRequested { immediate } => {
840 self.schedule_state_save(immediate);
841 self.sync_inspector_serialize_deadline();
842 }
843 LifecycleEvent::WorkflowFlushRequested { writes, reply } => {
844 reply.send(self.flush_workflow_state(writes).await);
845 }
846 LifecycleEvent::InspectorSerializeRequested
847 | LifecycleEvent::InspectorAttachmentsChanged => {
848 self.sync_inspector_serialize_deadline();
849 }
850 LifecycleEvent::SleepTick => {
851 self.on_sleep_tick().await;
852 }
853 }
854 }
855
856 async fn flush_workflow_state(&mut self, writes: Vec<WorkflowKvWrite>) -> Result<()> {
857 if !matches!(
858 self.lifecycle,
859 LifecycleState::Started | LifecycleState::SleepGrace
860 ) {
861 return Err(ActorLifecycleError::NotReady.build());
862 }
863 loop {
864 let save_request_revision = self.ctx.save_request_revision();
865 let state_transaction_epoch = self.ctx.state_transaction_epoch();
866 let (reply_tx, reply_rx) = oneshot::channel();
867 self.send_actor_event(
868 "workflow_flush_serialize_state",
869 ActorEvent::SerializeState {
870 reason: SerializeStateReason::Save,
871 reply: Reply::from(reply_tx),
872 },
873 )?;
874 let deltas = reply_rx
875 .await
876 .context("receive workflow flush serialize-state reply")??;
877 if self
878 .ctx
879 .save_state_and_workflow_batch_at_transaction_epoch(
880 deltas,
881 writes.clone(),
882 save_request_revision,
883 state_transaction_epoch,
884 )
885 .await?
886 {
887 return Ok(());
888 }
889 }
890 }
891
892 async fn handle_dispatch(&mut self, command: DispatchCommand) {
893 let command_kind = command.kind();
894 tracing::debug!(
895 actor_id = %self.ctx.actor_id(),
896 command = command_kind,
897 "actor dispatch command received"
898 );
899 if let Some(error) = self.dispatch_lifecycle_error() {
900 self.reply_dispatch_error(command, error);
901 self.log_dispatch_command_handled(command_kind, "rejected_lifecycle");
902 return;
903 }
904
905 match command {
906 DispatchCommand::Action {
907 name,
908 args,
909 conn,
910 reply,
911 } => {
912 tracing::info!(
913 actor_id = %self.ctx.actor_id(),
914 action_name = %name,
915 conn_id = ?conn.id(),
916 args_len = args.len(),
917 "actor task: handling DispatchCommand::Action"
918 );
919 let (tracked_reply_tx, tracked_reply_rx) = oneshot::channel();
920 let action_name_for_log = name.clone();
921 match self.send_actor_event(
922 "dispatch_action",
923 ActorEvent::Action {
924 name,
925 args,
926 conn: Some(conn),
927 scheduled_fire: None,
928 reply: Reply::from(tracked_reply_tx),
929 },
930 ) {
931 Ok(()) => {
932 tracing::info!(
933 actor_id = %self.ctx.actor_id(),
934 action_name = %action_name_for_log,
935 "actor task: ActorEvent::Action enqueued"
936 );
937 self.log_dispatch_command_handled(command_kind, "enqueued");
938 let actor_id = self.ctx.actor_id().to_owned();
939 let ctx = self.ctx.clone();
940 self.ctx.spawn_work(ActorWorkKind::Action, async move {
941 match tracked_reply_rx.await {
942 Ok(result) => {
943 let result =
944 result.map_err(|error| ctx.attach_actor_to_error(error));
945 tracing::info!(
946 actor_id = %actor_id,
947 action_name = %action_name_for_log,
948 ok = result.is_ok(),
949 "actor task: tracked reply received, forwarding"
950 );
951 let _ = reply.send(result);
952 }
953 Err(_) => {
954 tracing::warn!(
955 actor_id = %actor_id,
956 action_name = %action_name_for_log,
957 "actor task: tracked reply dropped before completion"
958 );
959 let error = ctx.attach_actor_to_error(
960 ActorLifecycleError::DroppedReply.build(),
961 );
962 let _ = reply.send(Err(error));
963 }
964 }
965 });
966 }
967 Err(error) => {
968 tracing::warn!(
969 actor_id = %self.ctx.actor_id(),
970 action_name = %action_name_for_log,
971 ?error,
972 "actor task: failed to enqueue ActorEvent::Action"
973 );
974 let _ = reply.send(Err(self.attach_actor_to_error(error)));
975 self.log_dispatch_command_handled(command_kind, "enqueue_failed");
976 }
977 }
978 }
979 DispatchCommand::QueueSend {
980 name,
981 body,
982 conn,
983 request,
984 wait,
985 timeout_ms,
986 reply,
987 } => match self.send_actor_event(
988 "dispatch_queue_send",
989 ActorEvent::QueueSend {
990 name,
991 body,
992 conn,
993 request,
994 wait,
995 timeout_ms,
996 reply: Reply::from(reply),
997 },
998 ) {
999 Ok(()) => {
1000 self.log_dispatch_command_handled(command_kind, "enqueued");
1001 }
1002 Err(_error) => {
1003 self.log_dispatch_command_handled(command_kind, "enqueue_failed");
1004 }
1005 },
1006 DispatchCommand::Http { request, reply } => {
1007 match self.send_actor_event(
1008 "dispatch_http",
1009 ActorEvent::HttpRequest {
1010 request,
1011 reply: Reply::from(reply),
1012 },
1013 ) {
1014 Ok(()) => {
1015 self.log_dispatch_command_handled(command_kind, "enqueued");
1016 }
1017 Err(_error) => {
1018 self.log_dispatch_command_handled(command_kind, "enqueue_failed");
1019 }
1020 }
1021 }
1022 DispatchCommand::OpenWebSocket {
1023 conn,
1024 ws,
1025 request,
1026 reply,
1027 } => {
1028 match self.send_actor_event(
1029 "dispatch_websocket_open",
1030 ActorEvent::WebSocketOpen {
1031 conn,
1032 ws,
1033 request,
1034 reply: Reply::from(reply),
1035 },
1036 ) {
1037 Ok(()) => {
1038 self.log_dispatch_command_handled(command_kind, "enqueued");
1039 }
1040 Err(_error) => {
1041 self.log_dispatch_command_handled(command_kind, "enqueue_failed");
1042 }
1043 }
1044 }
1045 DispatchCommand::WorkflowHistory { reply } => {
1046 match self.send_actor_event(
1047 "dispatch_workflow_history",
1048 ActorEvent::WorkflowHistoryRequested {
1049 reply: Reply::from(reply),
1050 },
1051 ) {
1052 Ok(()) => {
1053 self.log_dispatch_command_handled(command_kind, "enqueued");
1054 }
1055 Err(_error) => {
1056 self.log_dispatch_command_handled(command_kind, "enqueue_failed");
1057 }
1058 }
1059 }
1060 DispatchCommand::WorkflowReplay { entry_id, reply } => {
1061 match self.send_actor_event(
1062 "dispatch_workflow_replay",
1063 ActorEvent::WorkflowReplayRequested {
1064 entry_id,
1065 reply: Reply::from(reply),
1066 },
1067 ) {
1068 Ok(()) => {
1069 self.log_dispatch_command_handled(command_kind, "enqueued");
1070 }
1071 Err(_error) => {
1072 self.log_dispatch_command_handled(command_kind, "enqueue_failed");
1073 }
1074 }
1075 }
1076 }
1077 }
1078
1079 fn log_dispatch_command_handled(&self, command: &'static str, outcome: &'static str) {
1080 tracing::debug!(
1081 actor_id = %self.ctx.actor_id(),
1082 command,
1083 outcome,
1084 "actor dispatch command handled"
1085 );
1086 }
1087
1088 fn send_actor_event(&self, operation: &'static str, event: ActorEvent) -> Result<()> {
1089 let sender = self
1090 .actor_event_tx
1091 .as_ref()
1092 .ok_or_else(|| ActorLifecycleError::NotReady.build())?;
1093 tracing::debug!(
1094 actor_id = %self.ctx.actor_id(),
1095 operation,
1096 event = event.kind(),
1097 "actor event enqueued"
1098 );
1099 sender
1100 .send(event)
1101 .map_err(|_| ActorLifecycleError::NotReady.build())
1102 }
1103
1104 fn reply_dispatch_error(&self, command: DispatchCommand, error: anyhow::Error) {
1105 let error = self.ctx.attach_actor_to_error(error);
1106 match command {
1107 DispatchCommand::Action { reply, .. } => {
1108 let _ = reply.send(Err(error));
1109 }
1110 DispatchCommand::QueueSend { reply, .. } => {
1111 let _ = reply.send(Err(error));
1112 }
1113 DispatchCommand::Http { reply, .. } => {
1114 let _ = reply.send(Err(error));
1115 }
1116 DispatchCommand::OpenWebSocket { reply, .. } => {
1117 let _ = reply.send(Err(error));
1118 }
1119 DispatchCommand::WorkflowHistory { reply } => {
1120 let _ = reply.send(Err(error));
1121 }
1122 DispatchCommand::WorkflowReplay { reply, .. } => {
1123 let _ = reply.send(Err(error));
1124 }
1125 }
1126 }
1127
1128 fn attach_actor_to_error(&self, error: anyhow::Error) -> anyhow::Error {
1129 self.ctx.attach_actor_to_error(error)
1130 }
1131
1132 fn dispatch_lifecycle_error(&self) -> Option<anyhow::Error> {
1133 if self.ctx.destroy_requested() {
1135 self.ctx.warn_work_sent_to_stopping_instance("dispatch");
1136 return Some(ActorLifecycleError::Destroying.build());
1137 }
1138
1139 match self.lifecycle {
1140 LifecycleState::Started | LifecycleState::SleepGrace => None,
1141 LifecycleState::SleepFinalize | LifecycleState::DestroyGrace => {
1142 self.ctx.warn_work_sent_to_stopping_instance("dispatch");
1143 Some(ActorLifecycleError::Stopping.build())
1144 }
1145 LifecycleState::Destroying | LifecycleState::Terminated => {
1146 self.ctx.warn_work_sent_to_stopping_instance("dispatch");
1147 Some(ActorLifecycleError::Destroying.build())
1148 }
1149 LifecycleState::Loading => {
1150 self.ctx.warn_self_call_risk("dispatch");
1151 Some(ActorLifecycleError::NotReady.build())
1152 }
1153 }
1154 }
1155
1156 async fn start_actor(&mut self) -> Result<()> {
1157 let mut startup_timer = self.ctx.metrics().begin_startup_timer();
1158 let actor_id = self.ctx.actor_id().to_owned();
1159 if !self.ctx.started() {
1160 self.ctx.configure_sleep(self.factory.config().clone());
1161 self.ctx
1162 .configure_connection_runtime(self.factory.config().clone());
1163 }
1164 self.ensure_actor_event_channel();
1165 self.ctx.configure_actor_events(self.actor_event_tx.clone());
1166
1167 let schema_started_at = Instant::now();
1168 crate::actor::internal_storage::schema::ensure_internal_schema(self.ctx.sql())
1169 .await
1170 .context("initialize internal sqlite schema")?;
1171 tracing::debug!(
1172 actor_id = %actor_id,
1173 duration_ms = duration_ms_f64(schema_started_at.elapsed()),
1174 "perf internal: initInternalSqliteSchemaMs"
1175 );
1176
1177 let load_state_started_at = Instant::now();
1178 let load_state_result = self.load_persisted_startup().await;
1179 let persisted = self.ctx.metrics().observe_startup_phase_result(
1180 StartupPhase::LoadPersisted,
1181 None,
1182 load_state_started_at,
1183 load_state_result,
1184 )?;
1185 let is_new = !persisted.actor.has_initialized;
1186 startup_timer.set_is_new(is_new);
1187 tracing::debug!(
1188 actor_id = %actor_id,
1189 duration_ms = duration_ms_f64(load_state_started_at.elapsed()),
1190 "perf internal: loadStateMs"
1191 );
1192
1193 self.ctx.metrics().set_startup_phase(StartupPhase::CoreInit);
1194 let core_init_started_at = Instant::now();
1195 let core_init_result: Result<()> = async {
1196 self.ctx.load_persisted_actor(persisted.actor);
1197 self.ctx.load_last_pushed_alarm(persisted.last_pushed_alarm);
1198 self.ctx.load_run_wake_at(persisted.run_wake_at);
1199 if !is_new || !self.factory.requires_manual_startup_ready() {
1203 self.ctx.set_has_initialized(true);
1204 self.ctx
1205 .persist_state(SaveStateOpts { immediate: true })
1206 .await
1207 .context("persist actor initialization")?;
1208 }
1209 let init_inspector_token_started_at = Instant::now();
1210 crate::inspector::auth::init_inspector_token(&self.ctx)
1211 .await
1212 .context("initialize inspector token")?;
1213 tracing::debug!(
1214 actor_id = %actor_id,
1215 duration_ms = duration_ms_f64(init_inspector_token_started_at.elapsed()),
1216 "perf internal: initInspectorTokenMs"
1217 );
1218 self.ctx
1219 .restore_hibernatable_connections()
1220 .await
1221 .context("restore hibernatable connections")?;
1222 Self::settle_hibernated_connections(self.ctx.clone())
1223 .await
1224 .context("settle hibernated connections")?;
1225 self.ctx.init_alarms().await;
1226 Ok(())
1227 }
1228 .await;
1229 self.ctx.metrics().observe_startup_phase_result(
1230 StartupPhase::CoreInit,
1231 Some(is_new),
1232 core_init_started_at,
1233 core_init_result,
1234 )?;
1235
1236 self.transition_to(LifecycleState::Started);
1237 self.ctx
1238 .metrics()
1239 .set_startup_phase(StartupPhase::RuntimePreamble);
1240 let runtime_preamble_started_at = Instant::now();
1241 let runtime_preamble_result = self.spawn_run_handle(is_new).await;
1242 self.ctx.metrics().observe_startup_phase_result(
1243 StartupPhase::RuntimePreamble,
1244 Some(is_new),
1245 runtime_preamble_started_at,
1246 runtime_preamble_result,
1247 )?;
1248
1249 self.ctx
1250 .metrics()
1251 .set_startup_phase(StartupPhase::PostReady);
1252 let post_ready_started_at = Instant::now();
1253 let post_ready_result: Result<()> = async {
1254 if is_new {
1255 if !self.ctx.persisted_actor().has_initialized {
1259 self.ctx.set_has_initialized(true);
1260 }
1261 self.ctx
1262 .persist_state(SaveStateOpts { immediate: true })
1263 .await
1264 .context("persist actor startup state")?;
1265 }
1266 self.reset_sleep_deadline().await;
1267 self.ctx.drain_overdue_scheduled_events().await?;
1268 Ok(())
1269 }
1270 .await;
1271 self.ctx.metrics().observe_startup_phase_result(
1272 StartupPhase::PostReady,
1273 Some(is_new),
1274 post_ready_started_at,
1275 post_ready_result,
1276 )?;
1277 let startup_elapsed = startup_timer.finish_success();
1278 tracing::debug!(
1279 actor_id = %actor_id,
1280 duration_ms = duration_ms_f64(startup_elapsed),
1281 is_new,
1282 "perf internal: startupTotalMs"
1283 );
1284 Ok(())
1285 }
1286
1287 async fn load_persisted_startup(&mut self) -> Result<PersistedStartup> {
1288 crate::actor::migrate_kv_to_sqlite::import_core_state_if_needed(&self.ctx)
1289 .await
1290 .context("import legacy core actor storage to sqlite")?;
1291 if let Some(snapshot) = crate::actor::internal_storage::load_actor_snapshot(self.ctx.sql())
1292 .await
1293 .context("load persisted actor startup data from sqlite")?
1294 {
1295 return Ok(PersistedStartup {
1296 actor: snapshot.actor,
1297 last_pushed_alarm: snapshot.last_pushed_alarm,
1298 run_wake_at: snapshot.run_wake_at,
1299 });
1300 }
1301 Ok(PersistedStartup {
1302 actor: PersistedActor {
1303 input: self.start_input.clone(),
1304 ..PersistedActor::default()
1305 },
1306 last_pushed_alarm: None,
1307 run_wake_at: None,
1308 })
1309 }
1310
1311 fn ensure_actor_event_channel(&mut self) {
1312 if self.actor_event_tx.is_some() && self.actor_event_rx.is_some() {
1313 return;
1314 }
1315
1316 let (actor_event_tx, actor_event_rx) = mpsc::unbounded_channel();
1317 self.actor_event_tx = Some(actor_event_tx);
1318 self.actor_event_rx = Some(actor_event_rx);
1319 }
1320
1321 async fn spawn_run_handle(&mut self, is_new: bool) -> Result<()> {
1322 if self.run_handle.is_some() {
1323 return Ok(());
1324 }
1325
1326 let Some(actor_events) = self.actor_event_rx.take() else {
1327 return Ok(());
1328 };
1329 let requires_manual_startup_ready = self.factory.requires_manual_startup_ready();
1330 let (startup_ready_tx, startup_ready_rx) = if requires_manual_startup_ready {
1331 let (tx, rx) = oneshot::channel();
1332 (Some(tx), Some(rx))
1333 } else {
1334 (None, None)
1335 };
1336 let start = ActorStart {
1337 ctx: self.ctx.clone(),
1338 input: self.ctx.persisted_actor().input.clone(),
1339 is_new,
1340 snapshot: (!is_new).then(|| self.ctx.state()),
1341 hibernated: self
1342 .ctx
1343 .conns()
1344 .filter(|conn| conn.is_hibernatable())
1345 .map(|conn| {
1346 let bytes = conn.state();
1347 (conn, bytes)
1348 })
1349 .collect(),
1350 events: ActorEvents::new(self.ctx.actor_id().to_owned(), actor_events),
1351 startup_ready: startup_ready_tx,
1352 };
1353 let factory = self.factory.clone();
1354 let run_dispatch = tracing::dispatcher::get_default(Clone::clone);
1355 self.run_handle = Some(RuntimeSpawner::spawn(
1356 async move {
1357 match AssertUnwindSafe(factory.start(start)).catch_unwind().await {
1358 Ok(result) => result,
1359 Err(_) => Err(ActorRuntime::Panicked {
1360 operation: "run handler".to_owned(),
1361 }
1362 .build()),
1363 }
1364 }
1365 .in_current_span()
1366 .with_subscriber(run_dispatch),
1367 ));
1368 if let Some(startup_ready_rx) = startup_ready_rx {
1369 startup_ready_rx
1370 .await
1371 .context("receive runtime startup ready reply")?
1372 .context("runtime startup preamble")?;
1373 }
1374 Ok(())
1375 }
1376
1377 async fn settle_hibernated_connections(ctx: ActorContext) -> Result<()> {
1378 let actor_id = ctx.actor_id().to_owned();
1379 let mut dead_conn_ids = Vec::new();
1380 for conn in ctx.conns().filter(|conn| conn.is_hibernatable()) {
1381 let hibernation = conn.hibernation();
1382 let Some(hibernation) = hibernation else {
1383 tracing::debug!(
1384 actor_id = %actor_id,
1385 conn_id = conn.id(),
1386 outcome = "dead_missing_hibernation_metadata",
1387 "hibernated connection settled"
1388 );
1389 dead_conn_ids.push(conn.id().to_owned());
1390 continue;
1391 };
1392 let is_live = ctx
1393 .hibernated_connection_is_live(&hibernation.gateway_id, &hibernation.request_id)?;
1394 if is_live {
1395 tracing::debug!(
1396 actor_id = %actor_id,
1397 conn_id = conn.id(),
1398 outcome = "live",
1399 "hibernated connection settled"
1400 );
1401 continue;
1402 }
1403 tracing::debug!(
1404 actor_id = %actor_id,
1405 conn_id = conn.id(),
1406 outcome = "dead_not_live",
1407 "hibernated connection settled"
1408 );
1409 dead_conn_ids.push(conn.id().to_owned());
1410 }
1411
1412 for conn_id in dead_conn_ids {
1413 ctx.request_hibernation_transport_removal(conn_id.clone());
1414 ctx.remove_conn(&conn_id);
1415 tracing::debug!(
1416 actor_id = %actor_id,
1417 conn_id = %conn_id,
1418 "dead hibernated connection removed"
1419 );
1420 }
1421
1422 Ok(())
1423 }
1424
1425 async fn fire_due_alarms(&mut self) -> Result<()> {
1426 if !matches!(
1427 self.lifecycle,
1428 LifecycleState::Started | LifecycleState::SleepGrace | LifecycleState::DestroyGrace
1429 ) {
1430 return Ok(());
1431 }
1432
1433 let due_run_wake = self.ctx.consume_due_run_wake().await?;
1434 if let Err(error) = self.ctx.drain_overdue_scheduled_events().await {
1435 if self.lifecycle != LifecycleState::DestroyGrace
1436 && let Some((wake_at, wake_revision)) = due_run_wake
1437 && let Err(restore_error) = self
1438 .ctx
1439 .restore_run_wake_at_if_unchanged(wake_at, wake_revision)
1440 .await
1441 {
1442 tracing::error!(
1443 ?restore_error,
1444 wake_at,
1445 "failed to restore run wake after schedule alarm dispatch failed",
1446 );
1447 }
1448 return Err(error);
1449 }
1450 if self.lifecycle == LifecycleState::DestroyGrace {
1454 return Ok(());
1455 }
1456 if let Some((wake_at, wake_revision)) = due_run_wake {
1457 let (reply_tx, reply_rx) = oneshot::channel();
1458 if let Err(error) = self.ctx.try_send_actor_event(
1459 ActorEvent::RunWake {
1460 wake_at,
1461 wake_revision,
1462 reply: Reply::from(reply_tx),
1463 },
1464 "run_wake",
1465 ) {
1466 self.ctx
1467 .restore_run_wake_at_if_unchanged(wake_at, wake_revision)
1468 .await?;
1469 return Err(error).context("dispatch due run wake");
1470 }
1471 let restart_result = match reply_rx.await {
1472 Ok(result) => result,
1473 Err(error) => {
1474 self.ctx
1475 .restore_run_wake_at_if_unchanged(wake_at, wake_revision)
1476 .await?;
1477 return Err(error).context("receive due run wake reply");
1478 }
1479 };
1480 if let Err(error) = restart_result {
1481 self.ctx
1482 .restore_run_wake_at_if_unchanged(wake_at, wake_revision)
1483 .await?;
1484 return Err(error).context("ensure run handler active for due wake");
1485 }
1486 }
1487 Ok(())
1488 }
1489
1490 fn handle_run_handle_outcome(
1491 &mut self,
1492 outcome: std::result::Result<Result<()>, JoinError>,
1493 ) -> Option<LiveExit> {
1494 self.run_handle = None;
1495 let (clean_exit, crash_message) = match outcome {
1496 Ok(Ok(())) => (true, None),
1497 Ok(Err(error)) => {
1498 log_actor_error(&error, "actor run handler failed");
1499 (false, Some(format!("{error:#}")))
1500 }
1501 Err(error) => {
1502 tracing::error!(?error, "actor run handler join failed");
1503 let message =
1506 (!error.is_cancelled()).then(|| "actor run handler panicked".to_string());
1507 (false, message)
1508 }
1509 };
1510
1511 if clean_exit && self.lifecycle == LifecycleState::Started {
1512 tracing::debug!(
1513 actor_id = %self.ctx.actor_id(),
1514 "actor run handler exited cleanly while awaiting engine stop"
1515 );
1516 return None;
1517 }
1518
1519 if self.lifecycle == LifecycleState::Started {
1520 if let Some(message) = crash_message {
1527 match self.ctx.stop_with_error(message.clone()) {
1528 Ok(()) => return None,
1529 Err(error) => {
1530 if self.ctx.destroy_requested() {
1531 tracing::debug!(
1532 ?error,
1533 actor_id = %self.ctx.actor_id(),
1534 "run handler failed while a stop was already requested"
1535 );
1536 return None;
1537 }
1538 tracing::error!(
1539 ?error,
1540 dropped_message = %message,
1541 actor_id = %self.ctx.actor_id(),
1542 "failed to report run handler error to engine"
1543 );
1544 }
1545 }
1546 }
1547 self.transition_to(LifecycleState::Terminated);
1548 }
1549
1550 self.ctx.reset_sleep_timer();
1551 self.state_save_deadline = None;
1552 self.inspector_serialize_state_deadline = None;
1553 self.close_actor_event_channel();
1554
1555 None
1556 }
1557
1558 async fn wait_for_run_handle(
1559 run_handle: Option<&mut JoinHandle<Result<()>>>,
1560 ) -> std::result::Result<Result<()>, JoinError> {
1561 let Some(run_handle) = run_handle else {
1562 future::pending::<()>().await;
1563 unreachable!();
1564 };
1565 run_handle.await
1566 }
1567
1568 fn close_actor_event_channel(&mut self) {
1569 self.actor_event_tx = None;
1570 self.ctx.configure_actor_events(None);
1571 }
1572
1573 fn start_grace(&mut self, reason: ShutdownKind) {
1574 let grace_period = self.factory.config().effective_sleep_grace_period();
1575 self.sleep_deadline = None;
1576 self.ctx.cancel_sleep_timer();
1577 self.ctx.cancel_actor_abort_signal();
1578 self.sleep_grace = Some(SleepGraceState {
1579 deadline: Instant::now() + grace_period,
1580 reason,
1581 });
1582 self.ctx.reset_sleep_timer();
1583 }
1584
1585 async fn sleep_grace_tick(deadline: Option<Instant>) {
1586 let Some(deadline) = deadline else {
1587 future::pending::<()>().await;
1588 return;
1589 };
1590
1591 sleep_until(deadline).await;
1592 }
1593
1594 async fn on_activity_signal(&mut self) -> Option<LiveExit> {
1595 match self.lifecycle {
1596 LifecycleState::Started => {
1597 self.reset_sleep_deadline().await;
1598 None
1599 }
1600 LifecycleState::SleepGrace | LifecycleState::DestroyGrace => self.try_finish_grace(),
1601 LifecycleState::Loading
1605 | LifecycleState::SleepFinalize
1606 | LifecycleState::Destroying
1607 | LifecycleState::Terminated => None,
1608 }
1609 }
1610
1611 fn try_finish_grace(&mut self) -> Option<LiveExit> {
1612 let Some(grace) = self.sleep_grace.as_ref() else {
1613 return None;
1614 };
1615 if self.ctx.can_finalize_shutdown(grace.reason) {
1616 let reason = grace.reason;
1617 self.sleep_grace = None;
1618 return Some(LiveExit::Shutdown { reason });
1619 }
1620 None
1621 }
1622
1623 async fn on_sleep_grace_deadline(&mut self) -> Option<LiveExit> {
1624 let Some(grace) = self.sleep_grace.take() else {
1625 return None;
1626 };
1627 if let Some(run_handle) = self.run_handle.as_mut() {
1628 run_handle.abort();
1629 }
1630 self.ctx.cancel_shutdown_deadline();
1631 self.ctx.mark_shutdown_deadline_reached();
1635 self.ctx.record_shutdown_timeout(grace.reason);
1636 tracing::warn!(
1637 actor_id = %self.ctx.actor_id(),
1638 reason = shutdown_reason_label(grace.reason),
1639 deadline_missed_by_ms = Instant::now()
1640 .saturating_duration_since(grace.deadline)
1641 .as_millis() as u64,
1642 core_dispatched_hook_count = self.ctx.core_dispatched_hook_count(),
1643 shutdown_task_count = self.ctx.shutdown_task_count(),
1644 sleep_keep_awake_count = self.ctx.sleep_keep_awake_count(),
1645 sleep_internal_keep_awake_count = self.ctx.sleep_internal_keep_awake_count(),
1646 active_http_request_count = self.ctx.active_http_request_count(),
1647 websocket_callback_count = self.ctx.websocket_callback_count(),
1648 pending_disconnect_count = self.ctx.pending_disconnect_count(),
1649 connection_count = self.ctx.conns().len(),
1650 "actor shutdown reached the grace deadline"
1651 );
1652 Some(LiveExit::Shutdown {
1653 reason: grace.reason,
1654 })
1655 }
1656
1657 async fn join_aborted_run_handle(&mut self) {
1658 let Some(mut run_handle) = self.run_handle.take() else {
1659 return;
1660 };
1661 match (&mut run_handle).await {
1662 Ok(Ok(())) => {}
1663 Ok(Err(error)) => {
1664 log_actor_error(&error, "actor run handler failed during shutdown");
1665 }
1666 Err(error) => {
1667 if !error.is_cancelled() {
1668 tracing::error!(?error, "actor run handler join failed during shutdown");
1669 }
1670 }
1671 };
1672 }
1673
1674 #[cfg(test)]
1675 async fn drain_tracked_work(
1676 &mut self,
1677 reason: ShutdownKind,
1678 phase: &'static str,
1679 deadline: Instant,
1680 ) -> bool {
1681 Self::drain_tracked_work_with_ctx(self.ctx.clone(), reason, phase, deadline).await
1682 }
1683
1684 #[cfg(test)]
1685 async fn drain_tracked_work_with_ctx(
1686 ctx: ActorContext,
1687 reason: ShutdownKind,
1688 phase: &'static str,
1689 deadline: Instant,
1690 ) -> bool {
1691 let started_at = Instant::now();
1692 tokio::select! {
1693 result = ctx.wait_for_shutdown_tasks(deadline) => result,
1694 _ = sleep(LONG_SHUTDOWN_DRAIN_WARNING_THRESHOLD) => {
1695 if ctx.wait_for_shutdown_tasks(Instant::now()).await {
1696 true
1697 } else {
1698 tracing::warn!(
1699 actor_id = %ctx.actor_id(),
1700 reason = reason.as_metric_label(),
1701 phase,
1702 elapsed_ms = Instant::now().duration_since(started_at).as_millis() as u64,
1703 "actor shutdown drain is taking longer than expected"
1704 );
1705 ctx.wait_for_shutdown_tasks(deadline).await
1706 }
1707 }
1708 }
1709 }
1710
1711 fn log_lifecycle_command_received(&self, command: &'static str, reason: Option<&'static str>) {
1712 tracing::debug!(
1713 actor_id = %self.ctx.actor_id(),
1714 command,
1715 reason,
1716 "actor lifecycle command received"
1717 );
1718 }
1719
1720 fn reply_lifecycle_command(
1721 &self,
1722 command: &'static str,
1723 reason: Option<&'static str>,
1724 reply: oneshot::Sender<Result<()>>,
1725 result: Result<()>,
1726 ) {
1727 let result = result.map_err(|error| self.attach_actor_to_error(error));
1728 let outcome = result_outcome(&result);
1729 let delivered = reply.send(result).is_ok();
1730 tracing::debug!(
1731 actor_id = %self.ctx.actor_id(),
1732 command,
1733 reason,
1734 outcome,
1735 delivered,
1736 "actor lifecycle command replied"
1737 );
1738 }
1739
1740 fn register_shutdown_reply(
1741 &mut self,
1742 command: &'static str,
1743 reason: Option<&'static str>,
1744 reply: oneshot::Sender<Result<()>>,
1745 ) {
1746 if self.shutdown_reply.is_some() {
1747 debug_assert!(false, "engine actor2 sends one Stop per actor instance");
1748 tracing::warn!(
1749 actor_id = %self.ctx.actor_id(),
1750 command,
1751 reason,
1752 "duplicate Stop after shutdown reply was registered, dropping new reply"
1753 );
1754 return;
1755 }
1756 self.shutdown_reply = Some(PendingLifecycleReply {
1757 command,
1758 reason,
1759 reply,
1760 });
1761 }
1762
1763 fn deliver_shutdown_reply(&mut self, reason: ShutdownKind, result: &Result<()>) {
1764 #[cfg(test)]
1765 run_shutdown_reply_hook(&self.ctx, reason);
1766
1767 let Some(pending) = self.shutdown_reply.take() else {
1768 return;
1769 };
1770 let outcome = result_outcome(result);
1771 let delivered = pending.reply.send(clone_shutdown_result(result)).is_ok();
1772 tracing::debug!(
1773 actor_id = %self.ctx.actor_id(),
1774 command = pending.command,
1775 reason = pending.reason,
1776 shutdown_reason = shutdown_reason_label(reason),
1777 outcome,
1778 delivered,
1779 "actor lifecycle command replied"
1780 );
1781 }
1782
1783 async fn run_shutdown(&mut self, reason: ShutdownKind) -> Result<()> {
1784 self.sleep_grace = None;
1785 let started_at = Instant::now();
1786 self.state_save_deadline = None;
1787 self.inspector_serialize_state_deadline = None;
1788 self.sleep_deadline = None;
1789 self.transition_to(match reason {
1790 ShutdownKind::Sleep => LifecycleState::SleepFinalize,
1791 ShutdownKind::Destroy => LifecycleState::Destroying,
1792 });
1793 let result: Result<()> = async {
1794 self.save_final_state().await?;
1795 self.close_actor_event_channel();
1796 self.join_aborted_run_handle().await;
1797 Self::finish_shutdown_cleanup_with_ctx(self.ctx.clone(), reason).await
1798 }
1799 .await;
1800 if result.is_ok() && matches!(reason, ShutdownKind::Destroy) {
1801 self.ctx.mark_destroy_completed();
1802 }
1803 self.ctx.record_shutdown_wait(reason, started_at.elapsed());
1804 result
1805 }
1806
1807 async fn finish_failed_startup(&mut self) {
1808 self.ctx.cancel_actor_abort_signal();
1809 self.ctx.cancel_sleep_timer();
1810 self.ctx.suspend_alarm_dispatch();
1811 self.ctx.cancel_local_alarm_timeouts();
1812 self.ctx.set_local_alarm_callback(None);
1813 self.ctx.configure_lifecycle_events(None);
1814 self.close_actor_event_channel();
1815 if let Some(run_handle) = self.run_handle.as_mut() {
1816 run_handle.abort();
1817 }
1818 self.join_aborted_run_handle().await;
1819 self.ctx.mark_shutdown_deadline_reached();
1822 self.ctx.teardown_sleep_state().await;
1823 self.ctx.wait_for_pending_state_writes().await;
1824 self.ctx.abandon_pending_alarm_writes();
1825 #[cfg(feature = "sqlite-local")]
1826 self.ctx.shutdown_actor_runtime_socket().await;
1827 if let Err(error) = self.ctx.sql().close_and_wait().await {
1828 tracing::error!(
1829 actor_id = %self.ctx.actor_id(),
1830 ?error,
1831 "actor resource cleanup failed after startup failure",
1832 );
1833 }
1834 trim_native_allocator_after_shutdown(self.ctx.actor_id(), "startup_failed");
1835 self.transition_to(LifecycleState::Terminated);
1836 }
1837
1838 async fn save_final_state(&mut self) -> Result<()> {
1839 let (reply_tx, reply_rx) = oneshot::channel();
1840 if let Err(error) = self.send_actor_event(
1841 "shutdown_serialize_state",
1842 ActorEvent::SerializeState {
1843 reason: SerializeStateReason::Save,
1844 reply: Reply::from(reply_tx),
1845 },
1846 ) {
1847 tracing::error!(?error, "shutdown serialize-state enqueue failed");
1848 return self.ctx.save_state(Vec::new()).await;
1849 }
1850
1851 let cap = SERIALIZE_STATE_SHUTDOWN_SANITY_CAP
1856 .max(self.factory.config().effective_sleep_grace_period());
1857 let deltas = match timeout(cap, reply_rx).await {
1858 Ok(Ok(Ok(deltas))) => deltas,
1859 Ok(Ok(Err(error))) => {
1860 tracing::error!(?error, "serializeState callback returned error");
1861 Vec::new()
1862 }
1863 Ok(Err(error)) => {
1864 tracing::error!(?error, "serializeState reply dropped");
1865 Vec::new()
1866 }
1867 Err(_) => {
1868 tracing::error!(
1869 actor_id = %self.ctx.actor_id(),
1870 cap_ms = cap.as_millis() as u64,
1871 "serializeState timed out; saving with empty deltas, prior persisted state retained"
1872 );
1873 Vec::new()
1874 }
1875 };
1876
1877 self.ctx.save_state(deltas).await
1878 }
1879
1880 async fn finish_shutdown_cleanup_with_ctx(
1881 ctx: ActorContext,
1882 reason: ShutdownKind,
1883 ) -> Result<()> {
1884 let reason_label = shutdown_reason_label(reason);
1885 let actor_id = ctx.actor_id().to_owned();
1886 ctx.teardown_sleep_state().await;
1887 tracing::debug!(
1888 actor_id = %actor_id,
1889 reason = reason_label,
1890 step = "teardown_sleep_state",
1891 "actor shutdown cleanup step completed"
1892 );
1893 #[cfg(test)]
1894 run_shutdown_cleanup_hook(&ctx, reason_label);
1895 ctx.wait_for_pending_state_writes().await;
1896 tracing::debug!(
1897 actor_id = %actor_id,
1898 reason = reason_label,
1899 step = "wait_for_pending_state_writes",
1900 "actor shutdown cleanup step completed"
1901 );
1902 ctx.sync_alarm_logged().await;
1903 tracing::debug!(
1904 actor_id = %actor_id,
1905 reason = reason_label,
1906 step = "sync_alarm",
1907 "actor shutdown cleanup step completed"
1908 );
1909 match reason {
1912 ShutdownKind::Destroy => {
1913 ctx.cancel_driver_alarm_logged();
1914 tracing::debug!(
1915 actor_id = %actor_id,
1916 reason = reason_label,
1917 step = "cancel_driver_alarm",
1918 "actor shutdown cleanup step completed"
1919 );
1920 }
1921 ShutdownKind::Sleep => {}
1922 }
1923 ctx.wait_for_pending_alarm_writes().await;
1924 tracing::debug!(
1925 actor_id = %actor_id,
1926 reason = reason_label,
1927 step = "wait_for_pending_alarm_writes",
1928 "actor shutdown cleanup step completed"
1929 );
1930 #[cfg(feature = "sqlite-local")]
1931 ctx.shutdown_actor_runtime_socket().await;
1932 ctx.sql()
1933 .cleanup_for_shutdown(reason == ShutdownKind::Sleep)
1934 .await
1935 .with_context(|| format!("cleanup sqlite during {reason_label} shutdown"))?;
1936 trim_native_allocator_after_shutdown(&actor_id, reason_label);
1937 tracing::debug!(
1938 actor_id = %actor_id,
1939 reason = reason_label,
1940 step = "cleanup_sqlite",
1941 "actor shutdown cleanup step completed"
1942 );
1943 match reason {
1944 ShutdownKind::Sleep => {
1948 ctx.cancel_local_alarm_timeouts();
1949 tracing::debug!(
1950 actor_id = %actor_id,
1951 reason = reason_label,
1952 step = "cancel_local_alarm_timeouts",
1953 "actor shutdown cleanup step completed"
1954 );
1955 }
1956 ShutdownKind::Destroy => {}
1957 }
1958 Ok(())
1959 }
1960
1961 fn record_inbox_depths(&self) {
1962 self.ctx
1963 .metrics()
1964 .set_lifecycle_inbox_depth(self.lifecycle_inbox.len());
1965 self.ctx
1966 .metrics()
1967 .set_dispatch_inbox_depth(self.dispatch_inbox.len());
1968 self.ctx
1969 .metrics()
1970 .set_lifecycle_event_inbox_depth(self.lifecycle_events.len());
1971 }
1972
1973 fn accepting_dispatch(&self) -> bool {
1974 matches!(
1975 self.lifecycle,
1976 LifecycleState::Started | LifecycleState::SleepGrace | LifecycleState::DestroyGrace
1977 )
1978 }
1979
1980 fn sleep_timer_active(&self) -> bool {
1981 self.sleep_deadline.is_some()
1982 }
1983
1984 fn state_save_timer_active(&self) -> bool {
1985 self.state_save_deadline.is_some()
1986 }
1987
1988 fn inspector_serialize_timer_active(&self) -> bool {
1989 self.inspector_serialize_state_deadline.is_some()
1990 }
1991
1992 fn schedule_state_save(&mut self, immediate: bool) {
1993 if !matches!(
1994 self.lifecycle,
1995 LifecycleState::Started | LifecycleState::SleepGrace
1996 ) || !self.ctx.save_requested()
1997 {
1998 self.state_save_deadline = None;
1999 return;
2000 }
2001
2002 let next_deadline = self.ctx.save_deadline(immediate);
2003 self.state_save_deadline = Some(match self.state_save_deadline {
2004 Some(existing) => existing.min(next_deadline),
2005 None => next_deadline,
2006 });
2007 }
2008
2009 async fn sleep_tick(deadline: Option<Instant>) {
2010 let Some(deadline) = deadline else {
2011 future::pending::<()>().await;
2012 return;
2013 };
2014
2015 sleep_until(deadline).await;
2016 }
2017
2018 async fn state_save_tick(deadline: Option<Instant>) {
2019 let Some(deadline) = deadline else {
2020 future::pending::<()>().await;
2021 return;
2022 };
2023
2024 sleep_until(deadline).await;
2025 }
2026
2027 async fn inspector_serialize_state_tick(deadline: Option<Instant>) {
2028 let Some(deadline) = deadline else {
2029 future::pending::<()>().await;
2030 return;
2031 };
2032
2033 sleep_until(deadline).await;
2034 }
2035
2036 async fn on_state_save_tick(&mut self) {
2037 self.state_save_deadline = None;
2038 self.inspector_serialize_state_deadline = None;
2039 if !matches!(
2040 self.lifecycle,
2041 LifecycleState::Started | LifecycleState::SleepGrace
2042 ) || !self.ctx.save_requested()
2043 {
2044 return;
2045 }
2046
2047 let save_request_revision = self.ctx.save_request_revision();
2048 let state_transaction_epoch = self.ctx.state_transaction_epoch();
2049 let (reply_tx, reply_rx) = oneshot::channel();
2050 match self.send_actor_event(
2051 "save_tick",
2052 ActorEvent::SerializeState {
2053 reason: SerializeStateReason::Save,
2054 reply: Reply::from(reply_tx),
2055 },
2056 ) {
2057 Ok(()) => {}
2058 Err(error) => {
2059 tracing::warn!(?error, "failed to enqueue save tick");
2060 self.schedule_state_save(true);
2061 return;
2062 }
2063 }
2064
2065 match reply_rx.await {
2066 Ok(Ok(deltas)) => {
2067 let serialized_bytes = state_delta_payload_bytes(&deltas);
2068 tracing::debug!(
2069 actor_id = %self.ctx.actor_id(),
2070 reason = SerializeStateReason::Save.label(),
2071 delta_count = deltas.len(),
2072 serialized_bytes,
2073 save_request_revision,
2074 "actor serializeState completed"
2075 );
2076 match self
2081 .ctx
2082 .save_state_with_revision_at_transaction_epoch(
2083 deltas,
2084 save_request_revision,
2085 state_transaction_epoch,
2086 )
2087 .await
2088 {
2089 Ok(true) => {
2090 if self.ctx.save_requested() {
2091 self.schedule_state_save(self.ctx.save_requested_immediate());
2092 self.sync_inspector_serialize_deadline();
2093 }
2094 }
2095 Ok(false) => {
2096 self.ctx.request_save(RequestSaveOpts {
2097 immediate: true,
2098 max_wait_ms: None,
2099 });
2100 self.schedule_state_save(true);
2101 self.sync_inspector_serialize_deadline();
2102 }
2103 Err(error) => {
2104 tracing::error!(?error, "failed to persist actor save tick");
2105 self.schedule_state_save(true);
2106 self.sync_inspector_serialize_deadline();
2107 }
2108 }
2109 }
2110 Ok(Err(error)) => {
2111 tracing::error!(?error, "actor save tick failed");
2112 self.schedule_state_save(true);
2113 self.sync_inspector_serialize_deadline();
2114 }
2115 Err(error) => {
2116 tracing::error!(?error, "actor save tick reply dropped");
2117 self.schedule_state_save(true);
2118 self.sync_inspector_serialize_deadline();
2119 }
2120 }
2121 }
2122
2123 async fn on_inspector_serialize_state_tick(&mut self) {
2124 self.inspector_serialize_state_deadline = None;
2125 if !matches!(
2126 self.lifecycle,
2127 LifecycleState::Started | LifecycleState::SleepGrace
2128 ) || self.inspector_attach_count.load(Ordering::SeqCst) == 0
2129 || !self.ctx.save_requested()
2130 {
2131 return;
2132 }
2133
2134 let (reply_tx, reply_rx) = oneshot::channel();
2135 match self.send_actor_event(
2136 "inspector_serialize_state",
2137 ActorEvent::SerializeState {
2138 reason: SerializeStateReason::Inspector,
2139 reply: Reply::from(reply_tx),
2140 },
2141 ) {
2142 Ok(()) => {}
2143 Err(error) => {
2144 tracing::warn!(?error, "failed to enqueue inspector serialize tick");
2145 self.sync_inspector_serialize_deadline();
2146 return;
2147 }
2148 }
2149
2150 match reply_rx.await {
2151 Ok(Ok(deltas)) => {
2152 tracing::debug!(
2153 actor_id = %self.ctx.actor_id(),
2154 reason = SerializeStateReason::Inspector.label(),
2155 delta_count = deltas.len(),
2156 serialized_bytes = state_delta_payload_bytes(&deltas),
2157 "actor serializeState completed"
2158 );
2159 self.broadcast_inspector_overlay(&deltas);
2160 }
2161 Ok(Err(error)) => {
2162 tracing::error!(?error, "actor inspector serialize tick failed");
2163 self.sync_inspector_serialize_deadline();
2164 }
2165 Err(error) => {
2166 tracing::error!(?error, "actor inspector serialize tick reply dropped");
2167 self.sync_inspector_serialize_deadline();
2168 }
2169 }
2170 }
2171
2172 async fn on_sleep_tick(&mut self) {
2173 self.sleep_deadline = None;
2174 if self.lifecycle != LifecycleState::Started {
2175 return;
2176 }
2177
2178 let can_sleep = self.ctx.can_sleep().await;
2179 if can_sleep == crate::actor::sleep::CanSleep::Yes {
2180 tracing::debug!(
2181 actor_id = %self.ctx.actor_id(),
2182 sleep_timeout_ms = self.factory.config().sleep_timeout.as_millis() as u64,
2183 "sleep idle deadline elapsed"
2184 );
2185 if let Err(err) = self.ctx.sleep() {
2186 tracing::debug!(
2187 actor_id = %self.ctx.actor_id(),
2188 ?err,
2189 "sleep idle deadline request suppressed"
2190 );
2191 }
2192 } else {
2193 tracing::warn!(
2194 actor_id = %self.ctx.actor_id(),
2195 reason = ?can_sleep,
2196 "sleep idle deadline elapsed but actor stayed awake"
2197 );
2198 self.reset_sleep_deadline().await;
2199 }
2200 }
2201
2202 async fn reset_sleep_deadline(&mut self) {
2203 if self.lifecycle != LifecycleState::Started {
2204 self.sleep_deadline = None;
2205 tracing::debug!(
2206 actor_id = %self.ctx.actor_id(),
2207 lifecycle = ?self.lifecycle,
2208 "sleep activity reset skipped outside started state"
2209 );
2210 return;
2211 }
2212
2213 let can_sleep = self.ctx.can_sleep().await;
2214 if can_sleep == crate::actor::sleep::CanSleep::Yes {
2215 let deadline = Instant::now() + self.factory.config().sleep_timeout;
2216 self.sleep_deadline = Some(deadline);
2217 tracing::debug!(
2218 actor_id = %self.ctx.actor_id(),
2219 sleep_timeout_ms = self.factory.config().sleep_timeout.as_millis() as u64,
2220 "sleep activity reset"
2221 );
2222 } else {
2223 self.sleep_deadline = None;
2224 tracing::debug!(
2225 actor_id = %self.ctx.actor_id(),
2226 reason = ?can_sleep,
2227 "sleep activity reset skipped"
2228 );
2229 }
2230 }
2231
2232 fn sync_inspector_serialize_deadline(&mut self) {
2233 if !matches!(
2234 self.lifecycle,
2235 LifecycleState::Started | LifecycleState::SleepGrace
2236 ) || self.inspector_attach_count.load(Ordering::SeqCst) == 0
2237 || !self.ctx.save_requested()
2238 {
2239 self.inspector_serialize_state_deadline = None;
2240 return;
2241 }
2242
2243 self.inspector_serialize_state_deadline
2244 .get_or_insert_with(|| Instant::now() + INSPECTOR_SERIALIZE_STATE_INTERVAL);
2245 }
2246
2247 fn broadcast_inspector_overlay(&self, deltas: &[StateDelta]) {
2248 if self.inspector_attach_count.load(Ordering::SeqCst) == 0 || deltas.is_empty() {
2249 return;
2250 }
2251
2252 let mut payload = Vec::new();
2253 if let Err(error) = ciborium::into_writer(deltas, &mut payload) {
2254 tracing::error!(?error, "failed to encode inspector overlay deltas");
2255 return;
2256 }
2257
2258 let payload = Arc::new(payload);
2259 let payload_bytes = payload.len();
2260 match self.inspector_overlay_tx.send(payload) {
2261 Ok(receiver_count) => {
2262 tracing::debug!(
2263 actor_id = %self.ctx.actor_id(),
2264 delta_count = deltas.len(),
2265 payload_bytes,
2266 receiver_count,
2267 "inspector overlay broadcast"
2268 );
2269 }
2270 Err(error) => {
2271 tracing::debug!(
2272 actor_id = %self.ctx.actor_id(),
2273 delta_count = deltas.len(),
2274 payload_bytes,
2275 error = ?error,
2276 "inspector overlay broadcast dropped"
2277 );
2278 }
2279 }
2280 }
2281
2282 fn should_terminate(&self) -> bool {
2283 matches!(self.lifecycle, LifecycleState::Terminated)
2284 }
2285
2286 fn log_closed_channel(&self, channel: &'static str, message: &'static str) {
2287 tracing::warn!(
2288 actor_id = %self.ctx.actor_id(),
2289 channel,
2290 reason = "all senders dropped",
2291 "{message}"
2292 );
2293 }
2294
2295 fn transition_to(&mut self, lifecycle: LifecycleState) {
2296 let old = self.lifecycle;
2297 tracing::info!(
2298 actor_id = %self.ctx.actor_id(),
2299 old = ?old,
2300 new = ?lifecycle,
2301 "actor lifecycle transition"
2302 );
2303 self.lifecycle = lifecycle;
2304 if matches!(lifecycle, LifecycleState::Started) {
2305 self.ctx.reset_abort_signal_for_start();
2308 self.ctx.clear_sleep_requested();
2309 }
2310 self.ctx.set_started(matches!(
2311 lifecycle,
2312 LifecycleState::Started | LifecycleState::SleepGrace
2313 ));
2314 }
2315}
2316
2317fn shutdown_reason_label(reason: ShutdownKind) -> &'static str {
2318 match reason {
2319 ShutdownKind::Sleep => "sleep",
2320 ShutdownKind::Destroy => "destroy",
2321 }
2322}
2323
2324#[cfg(all(unix, target_env = "gnu"))]
2325fn trim_native_allocator_after_shutdown(actor_id: &str, reason: &str) {
2326 unsafe extern "C" {
2327 fn malloc_trim(pad: usize) -> i32;
2328 }
2329
2330 let rc = unsafe { malloc_trim(0) };
2331 tracing::debug!(
2332 actor_id,
2333 reason,
2334 rc,
2335 "trimmed native allocator after actor shutdown"
2336 );
2337}
2338
2339#[cfg(not(all(unix, target_env = "gnu")))]
2340fn trim_native_allocator_after_shutdown(_actor_id: &str, _reason: &str) {}
2341
2342fn clone_shutdown_result(result: &Result<()>) -> Result<()> {
2343 match result {
2344 Ok(()) => Ok(()),
2345 Err(error) => {
2346 let error = rivet_error::RivetError::extract(error);
2347 Err(anyhow::Error::new(error))
2348 }
2349 }
2350}
2351
2352fn log_actor_error(error: &anyhow::Error, log_message: &'static str) {
2353 let structured = rivet_error::RivetError::extract(error);
2354 tracing::error!(
2355 ?error,
2356 group = structured.group(),
2357 code = structured.code(),
2358 message = %structured.message(),
2359 metadata = ?structured.metadata(),
2360 "{log_message}"
2361 );
2362}
2363
2364fn result_outcome<T>(result: &Result<T>) -> &'static str {
2365 match result {
2366 Ok(_) => "ok",
2367 Err(_) => "error",
2368 }
2369}
2370
2371fn state_delta_payload_bytes(deltas: &[StateDelta]) -> usize {
2372 deltas.iter().map(StateDelta::payload_len).sum()
2373}
2374
2375fn duration_ms_f64(duration: Duration) -> f64 {
2376 duration.as_secs_f64() * 1000.0
2377}