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