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