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