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