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