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 self.reply_lifecycle_command(command_kind, reason, reply, result);
577 None
578 }
579 LifecycleCommand::Stop { reason, reply } => {
580 self.begin_stop(
581 reason,
582 command_kind,
583 Some(shutdown_reason_label(reason)),
584 reply,
585 )
586 .await
587 }
588 LifecycleCommand::FireAlarm { reply } => {
589 let result = self.fire_due_alarms().await;
590 self.reply_lifecycle_command(command_kind, reason, reply, result);
591 None
592 }
593 }
594 }
595
596 async fn handle_sleep_grace_lifecycle(
597 &mut self,
598 command: LifecycleCommand,
599 command_kind: &'static str,
600 command_reason: Option<&'static str>,
601 ) -> Option<LiveExit> {
602 match command {
603 LifecycleCommand::Start { reply } => {
604 self.reply_lifecycle_command(
605 command_kind,
606 command_reason,
607 reply,
608 Err(ActorLifecycleError::Stopping.build()),
609 );
610 None
611 }
612 LifecycleCommand::Stop { reason, reply } => {
613 let current_reason = self.sleep_grace.as_ref().map(|grace| grace.reason);
614 if current_reason != Some(reason) {
615 debug_assert!(false, "engine actor2 sends one Stop per actor instance");
616 tracing::warn!(
617 actor_id = %self.ctx.actor_id(),
618 reason = shutdown_reason_label(reason),
619 current_reason = ?current_reason,
620 "conflicting Stop during grace, ignoring"
621 );
622 }
623 self.reply_lifecycle_command(command_kind, command_reason, reply, Ok(()));
624 None
625 }
626 LifecycleCommand::FireAlarm { reply } => {
627 let result = self.fire_due_alarms().await;
628 self.reply_lifecycle_command(command_kind, command_reason, reply, result);
629 None
630 }
631 }
632 }
633
634 #[cfg(test)]
635 async fn handle_stop(&mut self, reason: ShutdownKind) -> Result<()> {
636 let (reply_tx, reply_rx) = oneshot::channel();
637 self.register_shutdown_reply("stop", Some(shutdown_reason_label(reason)), reply_tx);
638 self.begin_grace(reason).await;
639 loop {
640 if self.ctx.acknowledge_activity_dirty() {
641 if let Some(exit) = self.on_activity_signal().await {
642 let LiveExit::Shutdown { reason } = exit else {
643 return Ok(());
644 };
645 let result = match AssertUnwindSafe(self.run_shutdown(reason))
646 .catch_unwind()
647 .await
648 {
649 Ok(result) => result,
650 Err(_) => Err(anyhow!("shutdown panicked during {reason:?}")),
651 };
652 self.deliver_shutdown_reply(reason, &result);
653 self.transition_to(LifecycleState::Terminated);
654 return match reply_rx.await {
655 Ok(result) => result,
656 Err(_) => Err(ActorLifecycleError::DroppedReply.build()),
657 };
658 }
659 }
660
661 let Some(deadline) = self.sleep_grace.as_ref().map(|grace| grace.deadline) else {
662 return Err(anyhow!("stop grace ended without shutdown exit"));
663 };
664 let activity_notify = self.ctx.sleep_activity_notify();
665 let activity = activity_notify.notified();
666 tokio::pin!(activity);
667
668 tokio::select! {
669 _ = &mut activity => {}
670 _ = Self::sleep_grace_tick(Some(deadline)) => {
671 if let Some(exit) = self.on_sleep_grace_deadline().await {
672 let LiveExit::Shutdown { reason } = exit else {
673 return Ok(());
674 };
675 let result = match AssertUnwindSafe(self.run_shutdown(reason))
676 .catch_unwind()
677 .await
678 {
679 Ok(result) => result,
680 Err(_) => Err(anyhow!("shutdown panicked during {reason:?}")),
681 };
682 self.deliver_shutdown_reply(reason, &result);
683 self.transition_to(LifecycleState::Terminated);
684 return match reply_rx.await {
685 Ok(result) => result,
686 Err(_) => Err(ActorLifecycleError::DroppedReply.build()),
687 };
688 }
689 }
690 }
691 }
692 }
693
694 async fn begin_stop(
695 &mut self,
696 reason: ShutdownKind,
697 command: &'static str,
698 command_reason: Option<&'static str>,
699 reply: oneshot::Sender<Result<()>>,
700 ) -> Option<LiveExit> {
701 match self.lifecycle {
702 LifecycleState::Started => {
703 self.register_shutdown_reply(command, command_reason, reply);
704 self.drain_accepted_dispatch().await;
705 self.begin_grace(reason).await;
706 self.try_finish_grace()
707 }
708 LifecycleState::SleepGrace | LifecycleState::DestroyGrace => {
709 let current_reason = self.sleep_grace.as_ref().map(|grace| grace.reason);
710 if current_reason == Some(reason) {
711 self.reply_lifecycle_command(command, command_reason, reply, Ok(()));
712 None
713 } else {
714 debug_assert!(false, "engine actor2 sends one Stop per actor instance");
715 tracing::warn!(
716 actor_id = %self.ctx.actor_id(),
717 reason = shutdown_reason_label(reason),
718 current_reason = ?current_reason,
719 "conflicting Stop during grace, ignoring"
720 );
721 self.reply_lifecycle_command(command, command_reason, reply, Ok(()));
722 None
723 }
724 }
725 LifecycleState::SleepFinalize | LifecycleState::Destroying => {
726 debug_assert!(false, "engine actor2 sends one Stop per actor instance");
727 tracing::warn!(
728 actor_id = %self.ctx.actor_id(),
729 reason = shutdown_reason_label(reason),
730 "duplicate Stop after shutdown started, ignoring"
731 );
732 self.reply_lifecycle_command(command, command_reason, reply, Ok(()));
733 None
734 }
735 LifecycleState::Terminated => {
736 self.reply_lifecycle_command(command, command_reason, reply, Ok(()));
737 None
738 }
739 LifecycleState::Loading => {
740 self.reply_lifecycle_command(
741 command,
742 command_reason,
743 reply,
744 Err(ActorLifecycleError::NotReady.build()),
745 );
746 None
747 }
748 }
749 }
750
751 async fn drain_accepted_dispatch(&mut self) {
752 while self.accepting_dispatch() {
753 let Ok(command) = self.dispatch_inbox.try_recv() else {
754 break;
755 };
756 self.handle_dispatch(command).await;
757 }
758 }
759
760 async fn begin_grace(&mut self, reason: ShutdownKind) {
761 tracing::debug!(
762 actor_id = %self.ctx.actor_id(),
763 reason = shutdown_reason_label(reason),
764 "actor grace shutdown started"
765 );
766 self.ctx.suspend_alarm_dispatch();
767 self.ctx.cancel_local_alarm_timeouts();
768 self.ctx.set_local_alarm_callback(None);
769 self.transition_to(match reason {
770 ShutdownKind::Sleep => LifecycleState::SleepGrace,
771 ShutdownKind::Destroy => LifecycleState::DestroyGrace,
772 });
773 self.start_grace(reason);
774 self.emit_grace_events(reason);
775 }
776
777 fn emit_grace_events(&mut self, reason: ShutdownKind) {
778 let conns: Vec<_> = self.ctx.conns().collect();
779 for conn in conns {
780 let hibernatable_sleep =
781 matches!(reason, ShutdownKind::Sleep) && conn.is_hibernatable();
782 if hibernatable_sleep {
783 self.ctx.request_hibernation_transport_save(conn.id());
784 continue;
785 }
786 self.ctx.begin_core_dispatched_hook();
787 let reply = self.core_dispatched_hook_reply("disconnect_conn");
788 let conn_id = conn.id().to_owned();
789 if let Err(error) = self.send_actor_event(
790 "grace_disconnect_conn",
791 ActorEvent::DisconnectConn { conn_id, reply },
792 ) {
793 tracing::error!(?error, "failed to enqueue disconnect cleanup event");
794 }
795 }
796
797 self.ctx.begin_core_dispatched_hook();
798 let reply = self.core_dispatched_hook_reply("run_graceful_cleanup");
799 if let Err(error) = self.send_actor_event(
800 "grace_run_cleanup",
801 ActorEvent::RunGracefulCleanup { reason, reply },
802 ) {
803 tracing::error!(?error, "failed to enqueue run cleanup event");
804 }
805 self.ctx.reset_sleep_timer();
806 }
807
808 fn core_dispatched_hook_reply(&self, operation: &'static str) -> Reply<()> {
809 let (tx, rx) = oneshot::channel();
810 let ctx = self.ctx.clone();
811 let task = async move {
812 match rx.await {
813 Ok(Ok(())) => {}
814 Ok(Err(error)) => {
815 tracing::error!(?error, operation, "core dispatched hook failed");
816 }
817 Err(error) => {
818 tracing::error!(?error, operation, "core dispatched hook reply dropped");
819 }
820 }
821 ctx.mark_core_dispatched_hook_completed();
822 }
823 .in_current_span();
824 RuntimeSpawner::spawn(task);
825 tx.into()
826 }
827
828 async fn handle_event(&mut self, event: LifecycleEvent) {
829 tracing::debug!(
830 actor_id = %self.ctx.actor_id(),
831 event = event.kind(),
832 "actor lifecycle event drained"
833 );
834 match event {
835 LifecycleEvent::SaveRequested { immediate } => {
836 self.schedule_state_save(immediate);
837 self.sync_inspector_serialize_deadline();
838 }
839 LifecycleEvent::WorkflowFlushRequested { writes, reply } => {
840 reply.send(self.flush_workflow_state(writes).await);
841 }
842 LifecycleEvent::InspectorSerializeRequested
843 | LifecycleEvent::InspectorAttachmentsChanged => {
844 self.sync_inspector_serialize_deadline();
845 }
846 LifecycleEvent::SleepTick => {
847 self.on_sleep_tick().await;
848 }
849 }
850 }
851
852 async fn flush_workflow_state(&mut self, writes: Vec<WorkflowKvWrite>) -> Result<()> {
853 if !matches!(
854 self.lifecycle,
855 LifecycleState::Started | LifecycleState::SleepGrace
856 ) {
857 return Err(ActorLifecycleError::NotReady.build());
858 }
859 loop {
860 let save_request_revision = self.ctx.save_request_revision();
861 let state_transaction_epoch = self.ctx.state_transaction_epoch();
862 let (reply_tx, reply_rx) = oneshot::channel();
863 self.send_actor_event(
864 "workflow_flush_serialize_state",
865 ActorEvent::SerializeState {
866 reason: SerializeStateReason::Save,
867 reply: Reply::from(reply_tx),
868 },
869 )?;
870 let deltas = reply_rx
871 .await
872 .context("receive workflow flush serialize-state reply")??;
873 if self
874 .ctx
875 .save_state_and_workflow_batch_at_transaction_epoch(
876 deltas,
877 writes.clone(),
878 save_request_revision,
879 state_transaction_epoch,
880 )
881 .await?
882 {
883 return Ok(());
884 }
885 }
886 }
887
888 async fn handle_dispatch(&mut self, command: DispatchCommand) {
889 let command_kind = command.kind();
890 tracing::debug!(
891 actor_id = %self.ctx.actor_id(),
892 command = command_kind,
893 "actor dispatch command received"
894 );
895 if let Some(error) = self.dispatch_lifecycle_error() {
896 self.reply_dispatch_error(command, error);
897 self.log_dispatch_command_handled(command_kind, "rejected_lifecycle");
898 return;
899 }
900
901 match command {
902 DispatchCommand::Action {
903 name,
904 args,
905 conn,
906 reply,
907 } => {
908 tracing::info!(
909 actor_id = %self.ctx.actor_id(),
910 action_name = %name,
911 conn_id = ?conn.id(),
912 args_len = args.len(),
913 "actor task: handling DispatchCommand::Action"
914 );
915 let (tracked_reply_tx, tracked_reply_rx) = oneshot::channel();
916 let action_name_for_log = name.clone();
917 match self.send_actor_event(
918 "dispatch_action",
919 ActorEvent::Action {
920 name,
921 args,
922 conn: Some(conn),
923 scheduled_fire: None,
924 reply: Reply::from(tracked_reply_tx),
925 },
926 ) {
927 Ok(()) => {
928 tracing::info!(
929 actor_id = %self.ctx.actor_id(),
930 action_name = %action_name_for_log,
931 "actor task: ActorEvent::Action enqueued"
932 );
933 self.log_dispatch_command_handled(command_kind, "enqueued");
934 let actor_id = self.ctx.actor_id().to_owned();
935 let ctx = self.ctx.clone();
936 self.ctx.spawn_work(ActorWorkKind::Action, async move {
937 match tracked_reply_rx.await {
938 Ok(result) => {
939 let result =
940 result.map_err(|error| ctx.attach_actor_to_error(error));
941 tracing::info!(
942 actor_id = %actor_id,
943 action_name = %action_name_for_log,
944 ok = result.is_ok(),
945 "actor task: tracked reply received, forwarding"
946 );
947 let _ = reply.send(result);
948 }
949 Err(_) => {
950 tracing::warn!(
951 actor_id = %actor_id,
952 action_name = %action_name_for_log,
953 "actor task: tracked reply dropped before completion"
954 );
955 let error = ctx.attach_actor_to_error(
956 ActorLifecycleError::DroppedReply.build(),
957 );
958 let _ = reply.send(Err(error));
959 }
960 }
961 });
962 }
963 Err(error) => {
964 tracing::warn!(
965 actor_id = %self.ctx.actor_id(),
966 action_name = %action_name_for_log,
967 ?error,
968 "actor task: failed to enqueue ActorEvent::Action"
969 );
970 let _ = reply.send(Err(self.attach_actor_to_error(error)));
971 self.log_dispatch_command_handled(command_kind, "enqueue_failed");
972 }
973 }
974 }
975 DispatchCommand::QueueSend {
976 name,
977 body,
978 conn,
979 request,
980 wait,
981 timeout_ms,
982 reply,
983 } => match self.send_actor_event(
984 "dispatch_queue_send",
985 ActorEvent::QueueSend {
986 name,
987 body,
988 conn,
989 request,
990 wait,
991 timeout_ms,
992 reply: Reply::from(reply),
993 },
994 ) {
995 Ok(()) => {
996 self.log_dispatch_command_handled(command_kind, "enqueued");
997 }
998 Err(_error) => {
999 self.log_dispatch_command_handled(command_kind, "enqueue_failed");
1000 }
1001 },
1002 DispatchCommand::Http { request, reply } => {
1003 match self.send_actor_event(
1004 "dispatch_http",
1005 ActorEvent::HttpRequest {
1006 request,
1007 reply: Reply::from(reply),
1008 },
1009 ) {
1010 Ok(()) => {
1011 self.log_dispatch_command_handled(command_kind, "enqueued");
1012 }
1013 Err(_error) => {
1014 self.log_dispatch_command_handled(command_kind, "enqueue_failed");
1015 }
1016 }
1017 }
1018 DispatchCommand::OpenWebSocket {
1019 conn,
1020 ws,
1021 request,
1022 reply,
1023 } => {
1024 match self.send_actor_event(
1025 "dispatch_websocket_open",
1026 ActorEvent::WebSocketOpen {
1027 conn,
1028 ws,
1029 request,
1030 reply: Reply::from(reply),
1031 },
1032 ) {
1033 Ok(()) => {
1034 self.log_dispatch_command_handled(command_kind, "enqueued");
1035 }
1036 Err(_error) => {
1037 self.log_dispatch_command_handled(command_kind, "enqueue_failed");
1038 }
1039 }
1040 }
1041 DispatchCommand::WorkflowHistory { reply } => {
1042 match self.send_actor_event(
1043 "dispatch_workflow_history",
1044 ActorEvent::WorkflowHistoryRequested {
1045 reply: Reply::from(reply),
1046 },
1047 ) {
1048 Ok(()) => {
1049 self.log_dispatch_command_handled(command_kind, "enqueued");
1050 }
1051 Err(_error) => {
1052 self.log_dispatch_command_handled(command_kind, "enqueue_failed");
1053 }
1054 }
1055 }
1056 DispatchCommand::WorkflowReplay { entry_id, reply } => {
1057 match self.send_actor_event(
1058 "dispatch_workflow_replay",
1059 ActorEvent::WorkflowReplayRequested {
1060 entry_id,
1061 reply: Reply::from(reply),
1062 },
1063 ) {
1064 Ok(()) => {
1065 self.log_dispatch_command_handled(command_kind, "enqueued");
1066 }
1067 Err(_error) => {
1068 self.log_dispatch_command_handled(command_kind, "enqueue_failed");
1069 }
1070 }
1071 }
1072 }
1073 }
1074
1075 fn log_dispatch_command_handled(&self, command: &'static str, outcome: &'static str) {
1076 tracing::debug!(
1077 actor_id = %self.ctx.actor_id(),
1078 command,
1079 outcome,
1080 "actor dispatch command handled"
1081 );
1082 }
1083
1084 fn send_actor_event(&self, operation: &'static str, event: ActorEvent) -> Result<()> {
1085 let sender = self
1086 .actor_event_tx
1087 .as_ref()
1088 .ok_or_else(|| ActorLifecycleError::NotReady.build())?;
1089 tracing::debug!(
1090 actor_id = %self.ctx.actor_id(),
1091 operation,
1092 event = event.kind(),
1093 "actor event enqueued"
1094 );
1095 sender
1096 .send(event)
1097 .map_err(|_| ActorLifecycleError::NotReady.build())
1098 }
1099
1100 fn reply_dispatch_error(&self, command: DispatchCommand, error: anyhow::Error) {
1101 let error = self.ctx.attach_actor_to_error(error);
1102 match command {
1103 DispatchCommand::Action { reply, .. } => {
1104 let _ = reply.send(Err(error));
1105 }
1106 DispatchCommand::QueueSend { reply, .. } => {
1107 let _ = reply.send(Err(error));
1108 }
1109 DispatchCommand::Http { reply, .. } => {
1110 let _ = reply.send(Err(error));
1111 }
1112 DispatchCommand::OpenWebSocket { reply, .. } => {
1113 let _ = reply.send(Err(error));
1114 }
1115 DispatchCommand::WorkflowHistory { reply } => {
1116 let _ = reply.send(Err(error));
1117 }
1118 DispatchCommand::WorkflowReplay { reply, .. } => {
1119 let _ = reply.send(Err(error));
1120 }
1121 }
1122 }
1123
1124 fn attach_actor_to_error(&self, error: anyhow::Error) -> anyhow::Error {
1125 self.ctx.attach_actor_to_error(error)
1126 }
1127
1128 fn dispatch_lifecycle_error(&self) -> Option<anyhow::Error> {
1129 if self.ctx.destroy_requested() {
1131 self.ctx.warn_work_sent_to_stopping_instance("dispatch");
1132 return Some(ActorLifecycleError::Destroying.build());
1133 }
1134
1135 match self.lifecycle {
1136 LifecycleState::Started | LifecycleState::SleepGrace => None,
1137 LifecycleState::SleepFinalize | LifecycleState::DestroyGrace => {
1138 self.ctx.warn_work_sent_to_stopping_instance("dispatch");
1139 Some(ActorLifecycleError::Stopping.build())
1140 }
1141 LifecycleState::Destroying | LifecycleState::Terminated => {
1142 self.ctx.warn_work_sent_to_stopping_instance("dispatch");
1143 Some(ActorLifecycleError::Destroying.build())
1144 }
1145 LifecycleState::Loading => {
1146 self.ctx.warn_self_call_risk("dispatch");
1147 Some(ActorLifecycleError::NotReady.build())
1148 }
1149 }
1150 }
1151
1152 async fn start_actor(&mut self) -> Result<()> {
1153 let mut startup_timer = self.ctx.metrics().begin_startup_timer();
1154 let actor_id = self.ctx.actor_id().to_owned();
1155 if !self.ctx.started() {
1156 self.ctx.configure_sleep(self.factory.config().clone());
1157 self.ctx
1158 .configure_connection_runtime(self.factory.config().clone());
1159 }
1160 self.ensure_actor_event_channel();
1161 self.ctx.configure_actor_events(self.actor_event_tx.clone());
1162
1163 let schema_started_at = Instant::now();
1164 crate::actor::internal_storage::schema::ensure_internal_schema(self.ctx.sql())
1165 .await
1166 .context("initialize internal sqlite schema")?;
1167 tracing::debug!(
1168 actor_id = %actor_id,
1169 duration_ms = duration_ms_f64(schema_started_at.elapsed()),
1170 "perf internal: initInternalSqliteSchemaMs"
1171 );
1172
1173 let load_state_started_at = Instant::now();
1174 let load_state_result = self.load_persisted_startup().await;
1175 let persisted = self.ctx.metrics().observe_startup_phase_result(
1176 StartupPhase::LoadPersisted,
1177 None,
1178 load_state_started_at,
1179 load_state_result,
1180 )?;
1181 let is_new = !persisted.actor.has_initialized;
1182 startup_timer.set_is_new(is_new);
1183 tracing::debug!(
1184 actor_id = %actor_id,
1185 duration_ms = duration_ms_f64(load_state_started_at.elapsed()),
1186 "perf internal: loadStateMs"
1187 );
1188
1189 self.ctx.metrics().set_startup_phase(StartupPhase::CoreInit);
1190 let core_init_started_at = Instant::now();
1191 let core_init_result: Result<()> = async {
1192 self.ctx.load_persisted_actor(persisted.actor);
1193 self.ctx.load_last_pushed_alarm(persisted.last_pushed_alarm);
1194 self.ctx.load_run_wake_at(persisted.run_wake_at);
1195 if !is_new || !self.factory.requires_manual_startup_ready() {
1199 self.ctx.set_has_initialized(true);
1200 self.ctx
1201 .persist_state(SaveStateOpts { immediate: true })
1202 .await
1203 .context("persist actor initialization")?;
1204 }
1205 let init_inspector_token_started_at = Instant::now();
1206 crate::inspector::auth::init_inspector_token(&self.ctx)
1207 .await
1208 .context("initialize inspector token")?;
1209 tracing::debug!(
1210 actor_id = %actor_id,
1211 duration_ms = duration_ms_f64(init_inspector_token_started_at.elapsed()),
1212 "perf internal: initInspectorTokenMs"
1213 );
1214 self.ctx
1215 .restore_hibernatable_connections()
1216 .await
1217 .context("restore hibernatable connections")?;
1218 Self::settle_hibernated_connections(self.ctx.clone())
1219 .await
1220 .context("settle hibernated connections")?;
1221 self.ctx.init_alarms().await;
1222 Ok(())
1223 }
1224 .await;
1225 self.ctx.metrics().observe_startup_phase_result(
1226 StartupPhase::CoreInit,
1227 Some(is_new),
1228 core_init_started_at,
1229 core_init_result,
1230 )?;
1231
1232 self.transition_to(LifecycleState::Started);
1233 self.ctx
1234 .metrics()
1235 .set_startup_phase(StartupPhase::RuntimePreamble);
1236 let runtime_preamble_started_at = Instant::now();
1237 let runtime_preamble_result = self.spawn_run_handle(is_new).await;
1238 self.ctx.metrics().observe_startup_phase_result(
1239 StartupPhase::RuntimePreamble,
1240 Some(is_new),
1241 runtime_preamble_started_at,
1242 runtime_preamble_result,
1243 )?;
1244
1245 self.ctx
1246 .metrics()
1247 .set_startup_phase(StartupPhase::PostReady);
1248 let post_ready_started_at = Instant::now();
1249 let post_ready_result: Result<()> = async {
1250 if is_new {
1251 if !self.ctx.persisted_actor().has_initialized {
1255 self.ctx.set_has_initialized(true);
1256 }
1257 self.ctx
1258 .persist_state(SaveStateOpts { immediate: true })
1259 .await
1260 .context("persist actor startup state")?;
1261 }
1262 self.reset_sleep_deadline().await;
1263 self.ctx.drain_overdue_scheduled_events().await?;
1264 Ok(())
1265 }
1266 .await;
1267 self.ctx.metrics().observe_startup_phase_result(
1268 StartupPhase::PostReady,
1269 Some(is_new),
1270 post_ready_started_at,
1271 post_ready_result,
1272 )?;
1273 let startup_elapsed = startup_timer.finish_success();
1274 tracing::debug!(
1275 actor_id = %actor_id,
1276 duration_ms = duration_ms_f64(startup_elapsed),
1277 is_new,
1278 "perf internal: startupTotalMs"
1279 );
1280 Ok(())
1281 }
1282
1283 async fn load_persisted_startup(&mut self) -> Result<PersistedStartup> {
1284 crate::actor::migrate_kv_to_sqlite::import_core_state_if_needed(&self.ctx)
1285 .await
1286 .context("import legacy core actor storage to sqlite")?;
1287 if let Some(snapshot) = crate::actor::internal_storage::load_actor_snapshot(self.ctx.sql())
1288 .await
1289 .context("load persisted actor startup data from sqlite")?
1290 {
1291 return Ok(PersistedStartup {
1292 actor: snapshot.actor,
1293 last_pushed_alarm: snapshot.last_pushed_alarm,
1294 run_wake_at: snapshot.run_wake_at,
1295 });
1296 }
1297 Ok(PersistedStartup {
1298 actor: PersistedActor {
1299 input: self.start_input.clone(),
1300 ..PersistedActor::default()
1301 },
1302 last_pushed_alarm: None,
1303 run_wake_at: None,
1304 })
1305 }
1306
1307 fn ensure_actor_event_channel(&mut self) {
1308 if self.actor_event_tx.is_some() && self.actor_event_rx.is_some() {
1309 return;
1310 }
1311
1312 let (actor_event_tx, actor_event_rx) = mpsc::unbounded_channel();
1313 self.actor_event_tx = Some(actor_event_tx);
1314 self.actor_event_rx = Some(actor_event_rx);
1315 }
1316
1317 async fn spawn_run_handle(&mut self, is_new: bool) -> Result<()> {
1318 if self.run_handle.is_some() {
1319 return Ok(());
1320 }
1321
1322 let Some(actor_events) = self.actor_event_rx.take() else {
1323 return Ok(());
1324 };
1325 let requires_manual_startup_ready = self.factory.requires_manual_startup_ready();
1326 let (startup_ready_tx, startup_ready_rx) = if requires_manual_startup_ready {
1327 let (tx, rx) = oneshot::channel();
1328 (Some(tx), Some(rx))
1329 } else {
1330 (None, None)
1331 };
1332 let start = ActorStart {
1333 ctx: self.ctx.clone(),
1334 input: self.ctx.persisted_actor().input.clone(),
1335 is_new,
1336 snapshot: (!is_new).then(|| self.ctx.state()),
1337 hibernated: self
1338 .ctx
1339 .conns()
1340 .filter(|conn| conn.is_hibernatable())
1341 .map(|conn| {
1342 let bytes = conn.state();
1343 (conn, bytes)
1344 })
1345 .collect(),
1346 events: ActorEvents::new(self.ctx.actor_id().to_owned(), actor_events),
1347 startup_ready: startup_ready_tx,
1348 };
1349 let factory = self.factory.clone();
1350 let run_dispatch = tracing::dispatcher::get_default(Clone::clone);
1351 self.run_handle = Some(RuntimeSpawner::spawn(
1352 async move {
1353 match AssertUnwindSafe(factory.start(start)).catch_unwind().await {
1354 Ok(result) => result,
1355 Err(_) => Err(ActorRuntime::Panicked {
1356 operation: "run handler".to_owned(),
1357 }
1358 .build()),
1359 }
1360 }
1361 .in_current_span()
1362 .with_subscriber(run_dispatch),
1363 ));
1364 if let Some(startup_ready_rx) = startup_ready_rx {
1365 startup_ready_rx
1366 .await
1367 .context("receive runtime startup ready reply")?
1368 .context("runtime startup preamble")?;
1369 }
1370 Ok(())
1371 }
1372
1373 async fn settle_hibernated_connections(ctx: ActorContext) -> Result<()> {
1374 let actor_id = ctx.actor_id().to_owned();
1375 let mut dead_conn_ids = Vec::new();
1376 for conn in ctx.conns().filter(|conn| conn.is_hibernatable()) {
1377 let hibernation = conn.hibernation();
1378 let Some(hibernation) = hibernation else {
1379 tracing::debug!(
1380 actor_id = %actor_id,
1381 conn_id = conn.id(),
1382 outcome = "dead_missing_hibernation_metadata",
1383 "hibernated connection settled"
1384 );
1385 dead_conn_ids.push(conn.id().to_owned());
1386 continue;
1387 };
1388 let is_live = ctx
1389 .hibernated_connection_is_live(&hibernation.gateway_id, &hibernation.request_id)?;
1390 if is_live {
1391 tracing::debug!(
1392 actor_id = %actor_id,
1393 conn_id = conn.id(),
1394 outcome = "live",
1395 "hibernated connection settled"
1396 );
1397 continue;
1398 }
1399 tracing::debug!(
1400 actor_id = %actor_id,
1401 conn_id = conn.id(),
1402 outcome = "dead_not_live",
1403 "hibernated connection settled"
1404 );
1405 dead_conn_ids.push(conn.id().to_owned());
1406 }
1407
1408 for conn_id in dead_conn_ids {
1409 ctx.request_hibernation_transport_removal(conn_id.clone());
1410 ctx.remove_conn(&conn_id);
1411 tracing::debug!(
1412 actor_id = %actor_id,
1413 conn_id = %conn_id,
1414 "dead hibernated connection removed"
1415 );
1416 }
1417
1418 Ok(())
1419 }
1420
1421 async fn fire_due_alarms(&mut self) -> Result<()> {
1422 if !matches!(
1423 self.lifecycle,
1424 LifecycleState::Started | LifecycleState::SleepGrace | LifecycleState::DestroyGrace
1425 ) {
1426 return Ok(());
1427 }
1428
1429 let due_run_wake = self.ctx.consume_due_run_wake().await?;
1430 if let Err(error) = self.ctx.drain_overdue_scheduled_events().await {
1431 if self.lifecycle != LifecycleState::DestroyGrace
1432 && let Some((wake_at, wake_revision)) = due_run_wake
1433 && let Err(restore_error) = self
1434 .ctx
1435 .restore_run_wake_at_if_unchanged(wake_at, wake_revision)
1436 .await
1437 {
1438 tracing::error!(
1439 ?restore_error,
1440 wake_at,
1441 "failed to restore run wake after schedule alarm dispatch failed",
1442 );
1443 }
1444 return Err(error);
1445 }
1446 if self.lifecycle == LifecycleState::DestroyGrace {
1450 return Ok(());
1451 }
1452 if let Some((wake_at, wake_revision)) = due_run_wake {
1453 let (reply_tx, reply_rx) = oneshot::channel();
1454 if let Err(error) = self.ctx.try_send_actor_event(
1455 ActorEvent::RunWake {
1456 wake_at,
1457 wake_revision,
1458 reply: Reply::from(reply_tx),
1459 },
1460 "run_wake",
1461 ) {
1462 self.ctx
1463 .restore_run_wake_at_if_unchanged(wake_at, wake_revision)
1464 .await?;
1465 return Err(error).context("dispatch due run wake");
1466 }
1467 let restart_result = match reply_rx.await {
1468 Ok(result) => result,
1469 Err(error) => {
1470 self.ctx
1471 .restore_run_wake_at_if_unchanged(wake_at, wake_revision)
1472 .await?;
1473 return Err(error).context("receive due run wake reply");
1474 }
1475 };
1476 if let Err(error) = restart_result {
1477 self.ctx
1478 .restore_run_wake_at_if_unchanged(wake_at, wake_revision)
1479 .await?;
1480 return Err(error).context("ensure run handler active for due wake");
1481 }
1482 }
1483 Ok(())
1484 }
1485
1486 fn handle_run_handle_outcome(
1487 &mut self,
1488 outcome: std::result::Result<Result<()>, JoinError>,
1489 ) -> Option<LiveExit> {
1490 self.run_handle = None;
1491 let (clean_exit, crash_message) = match outcome {
1492 Ok(Ok(())) => (true, None),
1493 Ok(Err(error)) => {
1494 log_actor_error(&error, "actor run handler failed");
1495 (false, Some(format!("{error:#}")))
1496 }
1497 Err(error) => {
1498 tracing::error!(?error, "actor run handler join failed");
1499 let message =
1502 (!error.is_cancelled()).then(|| "actor run handler panicked".to_string());
1503 (false, message)
1504 }
1505 };
1506
1507 if clean_exit && self.lifecycle == LifecycleState::Started {
1508 tracing::debug!(
1509 actor_id = %self.ctx.actor_id(),
1510 "actor run handler exited cleanly while awaiting engine stop"
1511 );
1512 return None;
1513 }
1514
1515 if self.lifecycle == LifecycleState::Started {
1516 if let Some(message) = crash_message {
1523 match self.ctx.stop_with_error(message.clone()) {
1524 Ok(()) => return None,
1525 Err(error) => {
1526 if self.ctx.destroy_requested() {
1527 tracing::debug!(
1528 ?error,
1529 actor_id = %self.ctx.actor_id(),
1530 "run handler failed while a stop was already requested"
1531 );
1532 return None;
1533 }
1534 tracing::error!(
1535 ?error,
1536 dropped_message = %message,
1537 actor_id = %self.ctx.actor_id(),
1538 "failed to report run handler error to engine"
1539 );
1540 }
1541 }
1542 }
1543 self.transition_to(LifecycleState::Terminated);
1544 }
1545
1546 self.ctx.reset_sleep_timer();
1547 self.state_save_deadline = None;
1548 self.inspector_serialize_state_deadline = None;
1549 self.close_actor_event_channel();
1550
1551 None
1552 }
1553
1554 async fn wait_for_run_handle(
1555 run_handle: Option<&mut JoinHandle<Result<()>>>,
1556 ) -> std::result::Result<Result<()>, JoinError> {
1557 let Some(run_handle) = run_handle else {
1558 future::pending::<()>().await;
1559 unreachable!();
1560 };
1561 run_handle.await
1562 }
1563
1564 fn close_actor_event_channel(&mut self) {
1565 self.actor_event_tx = None;
1566 self.ctx.configure_actor_events(None);
1567 }
1568
1569 fn start_grace(&mut self, reason: ShutdownKind) {
1570 let grace_period = self.factory.config().effective_sleep_grace_period();
1571 self.sleep_deadline = None;
1572 self.ctx.cancel_sleep_timer();
1573 self.ctx.cancel_actor_abort_signal();
1574 self.sleep_grace = Some(SleepGraceState {
1575 deadline: Instant::now() + grace_period,
1576 reason,
1577 });
1578 self.ctx.reset_sleep_timer();
1579 }
1580
1581 async fn sleep_grace_tick(deadline: Option<Instant>) {
1582 let Some(deadline) = deadline else {
1583 future::pending::<()>().await;
1584 return;
1585 };
1586
1587 sleep_until(deadline).await;
1588 }
1589
1590 async fn on_activity_signal(&mut self) -> Option<LiveExit> {
1591 match self.lifecycle {
1592 LifecycleState::Started => {
1593 self.reset_sleep_deadline().await;
1594 None
1595 }
1596 LifecycleState::SleepGrace | LifecycleState::DestroyGrace => self.try_finish_grace(),
1597 LifecycleState::Loading
1601 | LifecycleState::SleepFinalize
1602 | LifecycleState::Destroying
1603 | LifecycleState::Terminated => None,
1604 }
1605 }
1606
1607 fn try_finish_grace(&mut self) -> Option<LiveExit> {
1608 let Some(grace) = self.sleep_grace.as_ref() else {
1609 return None;
1610 };
1611 if self.ctx.can_finalize_shutdown(grace.reason) {
1612 let reason = grace.reason;
1613 self.sleep_grace = None;
1614 return Some(LiveExit::Shutdown { reason });
1615 }
1616 None
1617 }
1618
1619 async fn on_sleep_grace_deadline(&mut self) -> Option<LiveExit> {
1620 let Some(grace) = self.sleep_grace.take() else {
1621 return None;
1622 };
1623 if let Some(run_handle) = self.run_handle.as_mut() {
1624 run_handle.abort();
1625 }
1626 self.ctx.cancel_shutdown_deadline();
1627 self.ctx.mark_shutdown_deadline_reached();
1631 self.ctx.record_shutdown_timeout(grace.reason);
1632 tracing::warn!(
1633 actor_id = %self.ctx.actor_id(),
1634 reason = shutdown_reason_label(grace.reason),
1635 deadline_missed_by_ms = Instant::now()
1636 .saturating_duration_since(grace.deadline)
1637 .as_millis() as u64,
1638 core_dispatched_hook_count = self.ctx.core_dispatched_hook_count(),
1639 shutdown_task_count = self.ctx.shutdown_task_count(),
1640 sleep_keep_awake_count = self.ctx.sleep_keep_awake_count(),
1641 sleep_internal_keep_awake_count = self.ctx.sleep_internal_keep_awake_count(),
1642 active_http_request_count = self.ctx.active_http_request_count(),
1643 websocket_callback_count = self.ctx.websocket_callback_count(),
1644 pending_disconnect_count = self.ctx.pending_disconnect_count(),
1645 connection_count = self.ctx.conns().len(),
1646 "actor shutdown reached the grace deadline"
1647 );
1648 Some(LiveExit::Shutdown {
1649 reason: grace.reason,
1650 })
1651 }
1652
1653 async fn join_aborted_run_handle(&mut self) {
1654 let Some(mut run_handle) = self.run_handle.take() else {
1655 return;
1656 };
1657 match (&mut run_handle).await {
1658 Ok(Ok(())) => {}
1659 Ok(Err(error)) => {
1660 log_actor_error(&error, "actor run handler failed during shutdown");
1661 }
1662 Err(error) => {
1663 if !error.is_cancelled() {
1664 tracing::error!(?error, "actor run handler join failed during shutdown");
1665 }
1666 }
1667 };
1668 }
1669
1670 #[cfg(test)]
1671 async fn drain_tracked_work(
1672 &mut self,
1673 reason: ShutdownKind,
1674 phase: &'static str,
1675 deadline: Instant,
1676 ) -> bool {
1677 Self::drain_tracked_work_with_ctx(self.ctx.clone(), reason, phase, deadline).await
1678 }
1679
1680 #[cfg(test)]
1681 async fn drain_tracked_work_with_ctx(
1682 ctx: ActorContext,
1683 reason: ShutdownKind,
1684 phase: &'static str,
1685 deadline: Instant,
1686 ) -> bool {
1687 let started_at = Instant::now();
1688 tokio::select! {
1689 result = ctx.wait_for_shutdown_tasks(deadline) => result,
1690 _ = sleep(LONG_SHUTDOWN_DRAIN_WARNING_THRESHOLD) => {
1691 if ctx.wait_for_shutdown_tasks(Instant::now()).await {
1692 true
1693 } else {
1694 tracing::warn!(
1695 actor_id = %ctx.actor_id(),
1696 reason = reason.as_metric_label(),
1697 phase,
1698 elapsed_ms = Instant::now().duration_since(started_at).as_millis() as u64,
1699 "actor shutdown drain is taking longer than expected"
1700 );
1701 ctx.wait_for_shutdown_tasks(deadline).await
1702 }
1703 }
1704 }
1705 }
1706
1707 fn log_lifecycle_command_received(&self, command: &'static str, reason: Option<&'static str>) {
1708 tracing::debug!(
1709 actor_id = %self.ctx.actor_id(),
1710 command,
1711 reason,
1712 "actor lifecycle command received"
1713 );
1714 }
1715
1716 fn reply_lifecycle_command(
1717 &self,
1718 command: &'static str,
1719 reason: Option<&'static str>,
1720 reply: oneshot::Sender<Result<()>>,
1721 result: Result<()>,
1722 ) {
1723 let result = result.map_err(|error| self.attach_actor_to_error(error));
1724 let outcome = result_outcome(&result);
1725 let delivered = reply.send(result).is_ok();
1726 tracing::debug!(
1727 actor_id = %self.ctx.actor_id(),
1728 command,
1729 reason,
1730 outcome,
1731 delivered,
1732 "actor lifecycle command replied"
1733 );
1734 }
1735
1736 fn register_shutdown_reply(
1737 &mut self,
1738 command: &'static str,
1739 reason: Option<&'static str>,
1740 reply: oneshot::Sender<Result<()>>,
1741 ) {
1742 if self.shutdown_reply.is_some() {
1743 debug_assert!(false, "engine actor2 sends one Stop per actor instance");
1744 tracing::warn!(
1745 actor_id = %self.ctx.actor_id(),
1746 command,
1747 reason,
1748 "duplicate Stop after shutdown reply was registered, dropping new reply"
1749 );
1750 return;
1751 }
1752 self.shutdown_reply = Some(PendingLifecycleReply {
1753 command,
1754 reason,
1755 reply,
1756 });
1757 }
1758
1759 fn deliver_shutdown_reply(&mut self, reason: ShutdownKind, result: &Result<()>) {
1760 #[cfg(test)]
1761 run_shutdown_reply_hook(&self.ctx, reason);
1762
1763 let Some(pending) = self.shutdown_reply.take() else {
1764 return;
1765 };
1766 let outcome = result_outcome(result);
1767 let delivered = pending.reply.send(clone_shutdown_result(result)).is_ok();
1768 tracing::debug!(
1769 actor_id = %self.ctx.actor_id(),
1770 command = pending.command,
1771 reason = pending.reason,
1772 shutdown_reason = shutdown_reason_label(reason),
1773 outcome,
1774 delivered,
1775 "actor lifecycle command replied"
1776 );
1777 }
1778
1779 async fn run_shutdown(&mut self, reason: ShutdownKind) -> Result<()> {
1780 self.sleep_grace = None;
1781 let started_at = Instant::now();
1782 self.state_save_deadline = None;
1783 self.inspector_serialize_state_deadline = None;
1784 self.sleep_deadline = None;
1785 self.transition_to(match reason {
1786 ShutdownKind::Sleep => LifecycleState::SleepFinalize,
1787 ShutdownKind::Destroy => LifecycleState::Destroying,
1788 });
1789 let result: Result<()> = async {
1790 self.save_final_state().await?;
1791 self.close_actor_event_channel();
1792 self.join_aborted_run_handle().await;
1793 Self::finish_shutdown_cleanup_with_ctx(self.ctx.clone(), reason).await
1794 }
1795 .await;
1796 if result.is_ok() && matches!(reason, ShutdownKind::Destroy) {
1797 self.ctx.mark_destroy_completed();
1798 }
1799 self.ctx.record_shutdown_wait(reason, started_at.elapsed());
1800 result
1801 }
1802
1803 async fn save_final_state(&mut self) -> Result<()> {
1804 let (reply_tx, reply_rx) = oneshot::channel();
1805 if let Err(error) = self.send_actor_event(
1806 "shutdown_serialize_state",
1807 ActorEvent::SerializeState {
1808 reason: SerializeStateReason::Save,
1809 reply: Reply::from(reply_tx),
1810 },
1811 ) {
1812 tracing::error!(?error, "shutdown serialize-state enqueue failed");
1813 return self.ctx.save_state(Vec::new()).await;
1814 }
1815
1816 let cap = SERIALIZE_STATE_SHUTDOWN_SANITY_CAP
1821 .max(self.factory.config().effective_sleep_grace_period());
1822 let deltas = match timeout(cap, reply_rx).await {
1823 Ok(Ok(Ok(deltas))) => deltas,
1824 Ok(Ok(Err(error))) => {
1825 tracing::error!(?error, "serializeState callback returned error");
1826 Vec::new()
1827 }
1828 Ok(Err(error)) => {
1829 tracing::error!(?error, "serializeState reply dropped");
1830 Vec::new()
1831 }
1832 Err(_) => {
1833 tracing::error!(
1834 actor_id = %self.ctx.actor_id(),
1835 cap_ms = cap.as_millis() as u64,
1836 "serializeState timed out; saving with empty deltas, prior persisted state retained"
1837 );
1838 Vec::new()
1839 }
1840 };
1841
1842 self.ctx.save_state(deltas).await
1843 }
1844
1845 async fn finish_shutdown_cleanup_with_ctx(
1846 ctx: ActorContext,
1847 reason: ShutdownKind,
1848 ) -> Result<()> {
1849 let reason_label = shutdown_reason_label(reason);
1850 let actor_id = ctx.actor_id().to_owned();
1851 ctx.teardown_sleep_state().await;
1852 tracing::debug!(
1853 actor_id = %actor_id,
1854 reason = reason_label,
1855 step = "teardown_sleep_state",
1856 "actor shutdown cleanup step completed"
1857 );
1858 #[cfg(test)]
1859 run_shutdown_cleanup_hook(&ctx, reason_label);
1860 ctx.wait_for_pending_state_writes().await;
1861 tracing::debug!(
1862 actor_id = %actor_id,
1863 reason = reason_label,
1864 step = "wait_for_pending_state_writes",
1865 "actor shutdown cleanup step completed"
1866 );
1867 ctx.sync_alarm_logged().await;
1868 tracing::debug!(
1869 actor_id = %actor_id,
1870 reason = reason_label,
1871 step = "sync_alarm",
1872 "actor shutdown cleanup step completed"
1873 );
1874 match reason {
1877 ShutdownKind::Destroy => {
1878 ctx.cancel_driver_alarm_logged();
1879 tracing::debug!(
1880 actor_id = %actor_id,
1881 reason = reason_label,
1882 step = "cancel_driver_alarm",
1883 "actor shutdown cleanup step completed"
1884 );
1885 }
1886 ShutdownKind::Sleep => {}
1887 }
1888 ctx.wait_for_pending_alarm_writes().await;
1889 tracing::debug!(
1890 actor_id = %actor_id,
1891 reason = reason_label,
1892 step = "wait_for_pending_alarm_writes",
1893 "actor shutdown cleanup step completed"
1894 );
1895 #[cfg(feature = "sqlite-local")]
1896 ctx.shutdown_actor_runtime_socket().await;
1897 ctx.sql()
1898 .cleanup_for_shutdown(reason == ShutdownKind::Sleep)
1899 .await
1900 .with_context(|| format!("cleanup sqlite during {reason_label} shutdown"))?;
1901 trim_native_allocator_after_shutdown(&actor_id, reason_label);
1902 tracing::debug!(
1903 actor_id = %actor_id,
1904 reason = reason_label,
1905 step = "cleanup_sqlite",
1906 "actor shutdown cleanup step completed"
1907 );
1908 match reason {
1909 ShutdownKind::Sleep => {
1913 ctx.cancel_local_alarm_timeouts();
1914 tracing::debug!(
1915 actor_id = %actor_id,
1916 reason = reason_label,
1917 step = "cancel_local_alarm_timeouts",
1918 "actor shutdown cleanup step completed"
1919 );
1920 }
1921 ShutdownKind::Destroy => {}
1922 }
1923 Ok(())
1924 }
1925
1926 fn record_inbox_depths(&self) {
1927 self.ctx
1928 .metrics()
1929 .set_lifecycle_inbox_depth(self.lifecycle_inbox.len());
1930 self.ctx
1931 .metrics()
1932 .set_dispatch_inbox_depth(self.dispatch_inbox.len());
1933 self.ctx
1934 .metrics()
1935 .set_lifecycle_event_inbox_depth(self.lifecycle_events.len());
1936 }
1937
1938 fn accepting_dispatch(&self) -> bool {
1939 matches!(
1940 self.lifecycle,
1941 LifecycleState::Started | LifecycleState::SleepGrace | LifecycleState::DestroyGrace
1942 )
1943 }
1944
1945 fn sleep_timer_active(&self) -> bool {
1946 self.sleep_deadline.is_some()
1947 }
1948
1949 fn state_save_timer_active(&self) -> bool {
1950 self.state_save_deadline.is_some()
1951 }
1952
1953 fn inspector_serialize_timer_active(&self) -> bool {
1954 self.inspector_serialize_state_deadline.is_some()
1955 }
1956
1957 fn schedule_state_save(&mut self, immediate: bool) {
1958 if !matches!(
1959 self.lifecycle,
1960 LifecycleState::Started | LifecycleState::SleepGrace
1961 ) || !self.ctx.save_requested()
1962 {
1963 self.state_save_deadline = None;
1964 return;
1965 }
1966
1967 let next_deadline = self.ctx.save_deadline(immediate);
1968 self.state_save_deadline = Some(match self.state_save_deadline {
1969 Some(existing) => existing.min(next_deadline),
1970 None => next_deadline,
1971 });
1972 }
1973
1974 async fn sleep_tick(deadline: Option<Instant>) {
1975 let Some(deadline) = deadline else {
1976 future::pending::<()>().await;
1977 return;
1978 };
1979
1980 sleep_until(deadline).await;
1981 }
1982
1983 async fn state_save_tick(deadline: Option<Instant>) {
1984 let Some(deadline) = deadline else {
1985 future::pending::<()>().await;
1986 return;
1987 };
1988
1989 sleep_until(deadline).await;
1990 }
1991
1992 async fn inspector_serialize_state_tick(deadline: Option<Instant>) {
1993 let Some(deadline) = deadline else {
1994 future::pending::<()>().await;
1995 return;
1996 };
1997
1998 sleep_until(deadline).await;
1999 }
2000
2001 async fn on_state_save_tick(&mut self) {
2002 self.state_save_deadline = None;
2003 self.inspector_serialize_state_deadline = None;
2004 if !matches!(
2005 self.lifecycle,
2006 LifecycleState::Started | LifecycleState::SleepGrace
2007 ) || !self.ctx.save_requested()
2008 {
2009 return;
2010 }
2011
2012 let save_request_revision = self.ctx.save_request_revision();
2013 let state_transaction_epoch = self.ctx.state_transaction_epoch();
2014 let (reply_tx, reply_rx) = oneshot::channel();
2015 match self.send_actor_event(
2016 "save_tick",
2017 ActorEvent::SerializeState {
2018 reason: SerializeStateReason::Save,
2019 reply: Reply::from(reply_tx),
2020 },
2021 ) {
2022 Ok(()) => {}
2023 Err(error) => {
2024 tracing::warn!(?error, "failed to enqueue save tick");
2025 self.schedule_state_save(true);
2026 return;
2027 }
2028 }
2029
2030 match reply_rx.await {
2031 Ok(Ok(deltas)) => {
2032 let serialized_bytes = state_delta_payload_bytes(&deltas);
2033 tracing::debug!(
2034 actor_id = %self.ctx.actor_id(),
2035 reason = SerializeStateReason::Save.label(),
2036 delta_count = deltas.len(),
2037 serialized_bytes,
2038 save_request_revision,
2039 "actor serializeState completed"
2040 );
2041 match self
2046 .ctx
2047 .save_state_with_revision_at_transaction_epoch(
2048 deltas,
2049 save_request_revision,
2050 state_transaction_epoch,
2051 )
2052 .await
2053 {
2054 Ok(true) => {
2055 if self.ctx.save_requested() {
2056 self.schedule_state_save(self.ctx.save_requested_immediate());
2057 self.sync_inspector_serialize_deadline();
2058 }
2059 }
2060 Ok(false) => {
2061 self.ctx.request_save(RequestSaveOpts {
2062 immediate: true,
2063 max_wait_ms: None,
2064 });
2065 self.schedule_state_save(true);
2066 self.sync_inspector_serialize_deadline();
2067 }
2068 Err(error) => {
2069 tracing::error!(?error, "failed to persist actor save tick");
2070 self.schedule_state_save(true);
2071 self.sync_inspector_serialize_deadline();
2072 }
2073 }
2074 }
2075 Ok(Err(error)) => {
2076 tracing::error!(?error, "actor save tick failed");
2077 self.schedule_state_save(true);
2078 self.sync_inspector_serialize_deadline();
2079 }
2080 Err(error) => {
2081 tracing::error!(?error, "actor save tick reply dropped");
2082 self.schedule_state_save(true);
2083 self.sync_inspector_serialize_deadline();
2084 }
2085 }
2086 }
2087
2088 async fn on_inspector_serialize_state_tick(&mut self) {
2089 self.inspector_serialize_state_deadline = None;
2090 if !matches!(
2091 self.lifecycle,
2092 LifecycleState::Started | LifecycleState::SleepGrace
2093 ) || self.inspector_attach_count.load(Ordering::SeqCst) == 0
2094 || !self.ctx.save_requested()
2095 {
2096 return;
2097 }
2098
2099 let (reply_tx, reply_rx) = oneshot::channel();
2100 match self.send_actor_event(
2101 "inspector_serialize_state",
2102 ActorEvent::SerializeState {
2103 reason: SerializeStateReason::Inspector,
2104 reply: Reply::from(reply_tx),
2105 },
2106 ) {
2107 Ok(()) => {}
2108 Err(error) => {
2109 tracing::warn!(?error, "failed to enqueue inspector serialize tick");
2110 self.sync_inspector_serialize_deadline();
2111 return;
2112 }
2113 }
2114
2115 match reply_rx.await {
2116 Ok(Ok(deltas)) => {
2117 tracing::debug!(
2118 actor_id = %self.ctx.actor_id(),
2119 reason = SerializeStateReason::Inspector.label(),
2120 delta_count = deltas.len(),
2121 serialized_bytes = state_delta_payload_bytes(&deltas),
2122 "actor serializeState completed"
2123 );
2124 self.broadcast_inspector_overlay(&deltas);
2125 }
2126 Ok(Err(error)) => {
2127 tracing::error!(?error, "actor inspector serialize tick failed");
2128 self.sync_inspector_serialize_deadline();
2129 }
2130 Err(error) => {
2131 tracing::error!(?error, "actor inspector serialize tick reply dropped");
2132 self.sync_inspector_serialize_deadline();
2133 }
2134 }
2135 }
2136
2137 async fn on_sleep_tick(&mut self) {
2138 self.sleep_deadline = None;
2139 if self.lifecycle != LifecycleState::Started {
2140 return;
2141 }
2142
2143 let can_sleep = self.ctx.can_sleep().await;
2144 if can_sleep == crate::actor::sleep::CanSleep::Yes {
2145 tracing::debug!(
2146 actor_id = %self.ctx.actor_id(),
2147 sleep_timeout_ms = self.factory.config().sleep_timeout.as_millis() as u64,
2148 "sleep idle deadline elapsed"
2149 );
2150 if let Err(err) = self.ctx.sleep() {
2151 tracing::debug!(
2152 actor_id = %self.ctx.actor_id(),
2153 ?err,
2154 "sleep idle deadline request suppressed"
2155 );
2156 }
2157 } else {
2158 tracing::warn!(
2159 actor_id = %self.ctx.actor_id(),
2160 reason = ?can_sleep,
2161 "sleep idle deadline elapsed but actor stayed awake"
2162 );
2163 self.reset_sleep_deadline().await;
2164 }
2165 }
2166
2167 async fn reset_sleep_deadline(&mut self) {
2168 if self.lifecycle != LifecycleState::Started {
2169 self.sleep_deadline = None;
2170 tracing::debug!(
2171 actor_id = %self.ctx.actor_id(),
2172 lifecycle = ?self.lifecycle,
2173 "sleep activity reset skipped outside started state"
2174 );
2175 return;
2176 }
2177
2178 let can_sleep = self.ctx.can_sleep().await;
2179 if can_sleep == crate::actor::sleep::CanSleep::Yes {
2180 let deadline = Instant::now() + self.factory.config().sleep_timeout;
2181 self.sleep_deadline = Some(deadline);
2182 tracing::debug!(
2183 actor_id = %self.ctx.actor_id(),
2184 sleep_timeout_ms = self.factory.config().sleep_timeout.as_millis() as u64,
2185 "sleep activity reset"
2186 );
2187 } else {
2188 self.sleep_deadline = None;
2189 tracing::debug!(
2190 actor_id = %self.ctx.actor_id(),
2191 reason = ?can_sleep,
2192 "sleep activity reset skipped"
2193 );
2194 }
2195 }
2196
2197 fn sync_inspector_serialize_deadline(&mut self) {
2198 if !matches!(
2199 self.lifecycle,
2200 LifecycleState::Started | LifecycleState::SleepGrace
2201 ) || self.inspector_attach_count.load(Ordering::SeqCst) == 0
2202 || !self.ctx.save_requested()
2203 {
2204 self.inspector_serialize_state_deadline = None;
2205 return;
2206 }
2207
2208 self.inspector_serialize_state_deadline
2209 .get_or_insert_with(|| Instant::now() + INSPECTOR_SERIALIZE_STATE_INTERVAL);
2210 }
2211
2212 fn broadcast_inspector_overlay(&self, deltas: &[StateDelta]) {
2213 if self.inspector_attach_count.load(Ordering::SeqCst) == 0 || deltas.is_empty() {
2214 return;
2215 }
2216
2217 let mut payload = Vec::new();
2218 if let Err(error) = ciborium::into_writer(deltas, &mut payload) {
2219 tracing::error!(?error, "failed to encode inspector overlay deltas");
2220 return;
2221 }
2222
2223 let payload = Arc::new(payload);
2224 let payload_bytes = payload.len();
2225 match self.inspector_overlay_tx.send(payload) {
2226 Ok(receiver_count) => {
2227 tracing::debug!(
2228 actor_id = %self.ctx.actor_id(),
2229 delta_count = deltas.len(),
2230 payload_bytes,
2231 receiver_count,
2232 "inspector overlay broadcast"
2233 );
2234 }
2235 Err(error) => {
2236 tracing::debug!(
2237 actor_id = %self.ctx.actor_id(),
2238 delta_count = deltas.len(),
2239 payload_bytes,
2240 error = ?error,
2241 "inspector overlay broadcast dropped"
2242 );
2243 }
2244 }
2245 }
2246
2247 fn should_terminate(&self) -> bool {
2248 matches!(self.lifecycle, LifecycleState::Terminated)
2249 }
2250
2251 fn log_closed_channel(&self, channel: &'static str, message: &'static str) {
2252 tracing::warn!(
2253 actor_id = %self.ctx.actor_id(),
2254 channel,
2255 reason = "all senders dropped",
2256 "{message}"
2257 );
2258 }
2259
2260 fn transition_to(&mut self, lifecycle: LifecycleState) {
2261 let old = self.lifecycle;
2262 tracing::info!(
2263 actor_id = %self.ctx.actor_id(),
2264 old = ?old,
2265 new = ?lifecycle,
2266 "actor lifecycle transition"
2267 );
2268 self.lifecycle = lifecycle;
2269 if matches!(lifecycle, LifecycleState::Started) {
2270 self.ctx.reset_abort_signal_for_start();
2273 self.ctx.clear_sleep_requested();
2274 }
2275 self.ctx.set_started(matches!(
2276 lifecycle,
2277 LifecycleState::Started | LifecycleState::SleepGrace
2278 ));
2279 }
2280}
2281
2282fn shutdown_reason_label(reason: ShutdownKind) -> &'static str {
2283 match reason {
2284 ShutdownKind::Sleep => "sleep",
2285 ShutdownKind::Destroy => "destroy",
2286 }
2287}
2288
2289#[cfg(all(unix, target_env = "gnu"))]
2290fn trim_native_allocator_after_shutdown(actor_id: &str, reason: &str) {
2291 unsafe extern "C" {
2292 fn malloc_trim(pad: usize) -> i32;
2293 }
2294
2295 let rc = unsafe { malloc_trim(0) };
2296 tracing::debug!(
2297 actor_id,
2298 reason,
2299 rc,
2300 "trimmed native allocator after actor shutdown"
2301 );
2302}
2303
2304#[cfg(not(all(unix, target_env = "gnu")))]
2305fn trim_native_allocator_after_shutdown(_actor_id: &str, _reason: &str) {}
2306
2307fn clone_shutdown_result(result: &Result<()>) -> Result<()> {
2308 match result {
2309 Ok(()) => Ok(()),
2310 Err(error) => {
2311 let error = rivet_error::RivetError::extract(error);
2312 Err(anyhow::Error::new(error))
2313 }
2314 }
2315}
2316
2317fn log_actor_error(error: &anyhow::Error, log_message: &'static str) {
2318 let structured = rivet_error::RivetError::extract(error);
2319 tracing::error!(
2320 ?error,
2321 group = structured.group(),
2322 code = structured.code(),
2323 message = %structured.message(),
2324 metadata = ?structured.metadata(),
2325 "{log_message}"
2326 );
2327}
2328
2329fn result_outcome<T>(result: &Result<T>) -> &'static str {
2330 match result {
2331 Ok(_) => "ok",
2332 Err(_) => "error",
2333 }
2334}
2335
2336fn state_delta_payload_bytes(deltas: &[StateDelta]) -> usize {
2337 deltas.iter().map(StateDelta::payload_len).sum()
2338}
2339
2340fn duration_ms_f64(duration: Duration) -> f64 {
2341 duration.as_secs_f64() * 1000.0
2342}