Skip to main content

rivetkit_core/actor/
task.rs

1//! Actor lifecycle task orchestration.
2//!
3//! `ActorTask` deliberately uses four separate unbounded `mpsc` receivers instead
4//! of one tagged command queue:
5//!
6//! - `lifecycle_inbox` carries trusted registry/envoy lifecycle commands:
7//!   start, stop, destroy, and driver-alarm wakeups.
8//! - `dispatch_inbox` carries client-facing actor work such as actions, raw
9//!   HTTP, raw WebSockets, and inspector workflow requests.
10//! - `lifecycle_events` carries internal subsystem signals from
11//!   `ActorContext`: save requests, activity changes, inspector attach changes,
12//!   and sleep ticks.
13//! - `actor_event_rx` feeds the user runtime adapter with actor events after
14//!   `ActorTask` accepts dispatch work.
15//!
16//! Keeping these queues split gives the task loop explicit priority boundaries.
17//! Client dispatch does not compete directly with lifecycle stop/destroy
18//! commands, and internal save/sleep/inspector events do not compete with
19//! untrusted client traffic. The main `tokio::select!` is biased so lifecycle
20//! commands are observed first, then internal lifecycle events, then dispatch
21//! and timers. During sleep grace, the same priority keeps lifecycle handling
22//! live while still draining accepted dispatch replies before final teardown.
23//!
24//! The sender topology follows the trust boundary: registry/envoy owns lifecycle
25//! and dispatch senders, core subsystems enqueue lifecycle events through
26//! `ActorContext`, and only `ActorTask` forwards accepted work into the
27//! actor-event stream consumed by user code.
28
29use 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// Test shim keeps moved tests in crate-root tests/ with private-module access.
78#[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)]
90// Forced-sync: test hooks are installed and cleared from synchronous guard APIs.
91static 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)]
100// Forced-sync: test hooks are installed and cleared from synchronous guard APIs.
101static 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	// === IDENTITY ===
330	pub actor_id: String,
331	pub generation: u32,
332
333	// === INBOX CHANNELS ===
334	/// Lifecycle commands (Start / Stop / FireAlarm) sent by the registry
335	/// in response to engine-driven `EnvoyCallbacks` from the envoy client.
336	pub lifecycle_inbox: mpsc::UnboundedReceiver<LifecycleCommand>,
337	/// Client-originated work sent by `RegistryDispatcher` in
338	/// `registry/dispatch.rs` (Action, OpenWebSocket, Workflow*) and
339	/// `registry/http.rs` (Http, QueueSend).
340	pub dispatch_inbox: mpsc::UnboundedReceiver<DispatchCommand>,
341	/// Internal self-events the actor enqueues onto itself via `ActorContext`
342	/// hooks (save/inspector/activity notifications from
343	/// `actor/state.rs`, `actor/connection.rs`, `actor/context.rs`).
344	pub lifecycle_events: mpsc::UnboundedReceiver<LifecycleEvent>,
345
346	// === RUNTIME STATE ===
347	pub lifecycle: LifecycleState,
348	pub factory: Arc<ActorFactory>,
349	pub ctx: ActorContext,
350
351	// === STARTUP ===
352	pub start_input: Option<Vec<u8>>,
353
354	// === USER RUNTIME BRIDGE ===
355	/// Sends `ActorEvent`s from core subsystems and `ActorTask` to the
356	/// user runtime adapter.
357	actor_event_tx: Option<mpsc::UnboundedSender<ActorEvent>>,
358	/// Receiver half. Not consumed by `ActorTask`. `spawn_run_handle`
359	/// `take()`s it and hands it to the user `run` handler via `ActorStart`
360	/// so the runtime adapter (e.g. NAPI receive loop) drains events there.
361	actor_event_rx: Option<mpsc::UnboundedReceiver<ActorEvent>>,
362	/// Join handle for the user `run` task spawned by `spawn_run_handle`.
363	/// Awaited as a `select!` arm; cleared on shutdown abort/await.
364	run_handle: Option<JoinHandle<Result<()>>>,
365
366	// === INSPECTOR ===
367	/// Live count of attached inspector websockets. Read from request-save
368	/// hooks to decide whether to debounce a `SerializeState { Inspector }`.
369	inspector_attach_count: Arc<AtomicU32>,
370	/// Live `StateDelta` stream broadcast to attached inspector WebSockets
371	/// so their snapshot stays in sync without re-fetching.
372	inspector_overlay_tx: broadcast::Sender<Arc<Vec<u8>>>,
373
374	// === TIMERS ===
375	/// Next deadline at which `on_state_save_tick` should flush a deferred
376	/// state save. Cleared while no save is requested.
377	pub state_save_deadline: Option<Instant>,
378	/// Next deadline at which an inspector-driven `SerializeState` should
379	/// fire. Debounces inspector overlay refreshes.
380	pub inspector_serialize_state_deadline: Option<Instant>,
381	/// Next deadline at which the actor becomes eligible for sleep if it
382	/// stays idle. Cleared on activity and during sleep grace.
383	pub sleep_deadline: Option<Instant>,
384
385	// === SHUTDOWN ===
386	/// The single lifecycle reply for shutdown. Engine actor2 sends at most
387	/// one Stop command per actor instance; duplicates are a protocol bug.
388	shutdown_reply: Option<PendingLifecycleReply>,
389	/// Active sleep-grace idle wait. Polled by the main loop so grace keeps the
390	/// same inbox/timer handling as the started actor.
391	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			// TODO: Sample inbox depths periodically instead of on every loop iteration.
484			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		// TODO: Share admission policy with RegistryDispatcher::active_actor.
1117		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			// New manual-startup runtimes must not persist initialization until the
1182			// runtime startup_ready handshake completes. The runtime preamble owns
1183			// initial state creation.
1184			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				// Manual-startup runtimes usually mark initialization during their
1238				// preamble. This is the fallback for runtimes that completed startup
1239				// without doing so.
1240				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				// Deliberate cancellations are not crashes and must not be
1430				// reported to the engine as one.
1431				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			// A failed run handler while `Started` must reach the engine as an
1447			// errored stop instead of dying silently until the lost timeout.
1448			// Keep the generation alive so the engine's answering `Stop`
1449			// command still drives the destroy grace hooks; transitioning to
1450			// `Terminated` here would drop the lifecycle inbox before that
1451			// command arrives.
1452			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			// Pre-startup, post-finalize, and tear-down states intentionally
1528			// drop activity signals: there is no sleep deadline to reset and no
1529			// grace window left to advance.
1530			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		// The deadline changes teardown from graceful draining to cancellation.
1558		// Without this marker, final cleanup would wait on work that already
1559		// exhausted its grace budget.
1560		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		// Cap at the larger of the default sanity bound or the user-configured
1747		// sleep grace period. Without this, an actor with `sleepGracePeriod`
1748		// raised above the default would silently truncate large state writes
1749		// to empty deltas inside the user's own grace budget.
1750		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			// Match the reference TS runtime: keep the persisted engine alarm armed
1826			// across sleep so the next instance still has a wake trigger, but abort
1827			// the local Tokio timer owned by the shutting-down instance.
1828			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				// Skip the overlay broadcast on the save path. save_state_with_revision
1965				// triggers record_state_updated after persist, which the inspector
1966				// websocket signal subscriber forwards as StateUpdated. Broadcasting
1967				// the overlay here too would deliver a duplicate message.
1968				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			// A restarted actor is a new generation. Clear shutdown state that was
2177			// only meant to stop the previous generation.
2178			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}