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