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