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