Skip to main content

rivetkit_core/actor/
context.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::future::Future;
3use std::sync::Weak;
4#[cfg(any(test, feature = "test-support"))]
5use std::sync::atomic::AtomicI64;
6use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};
7use std::sync::{Arc, OnceLock};
8use std::time::Duration;
9
10use crate::time::Instant;
11
12use anyhow::{Context as AnyhowContext, Result};
13use futures::future::BoxFuture;
14use parking_lot::{Mutex, RwLock};
15use rivet_envoy_client::handle::EnvoyHandle;
16use rivet_envoy_client::tunnel::HibernatingWebSocketMetadata;
17use rivet_error::ActorSpecifier;
18use scc::HashMap as SccHashMap;
19use scc::HashSet as SccHashSet;
20use tokio::runtime::Handle;
21use tokio::sync::{Mutex as AsyncMutex, Notify, OnceCell, broadcast, mpsc, oneshot};
22use tokio::task::JoinHandle;
23use tokio_util::sync::CancellationToken;
24
25#[cfg(not(feature = "wasm-runtime"))]
26type LocalAlarmTask = JoinHandle<()>;
27#[cfg(feature = "wasm-runtime")]
28type LocalAlarmTask = futures::future::AbortHandle;
29
30#[cfg(feature = "sqlite-local")]
31use crate::actor::actor_runtime_socket::{
32	ActorRuntimeSocketEndpoint, ActorRuntimeSocketEndpointInfo,
33};
34use crate::actor::connection::{
35	ConnHandle, ConnHandles, HibernatableConnectionMetadata, PendingHibernationChanges,
36	hibernatable_id_from_slice,
37};
38use crate::actor::diagnostics::ActorDiagnostics;
39use crate::actor::internal_storage;
40use crate::actor::kv::LegacyActorKv;
41use crate::actor::lifecycle_hooks::Reply;
42use crate::actor::messages::{ActorEvent, Request, StateDelta, WorkflowKvWrite};
43use crate::actor::metrics::ActorMetrics;
44use crate::actor::queue::{QueueInspectorUpdateCallback, QueueMetadata, QueueWaitActivityCallback};
45use crate::actor::schedule::{InternalKeepAwakeCallback, LocalAlarmCallback};
46use crate::actor::sleep::{CanSleep, SleepState};
47use crate::actor::state::{PendingSave, PersistedActor, RequestSaveOpts, StateTransactionSlot};
48use crate::actor::task::LifecycleEvent;
49use crate::actor::task_types::UserTaskKind;
50use crate::actor::work_registry::{ActorWorkKind, CountGuard, RegionGuard};
51use crate::error::{ActorLifecycle as ActorLifecycleError, ActorRuntime};
52use crate::inspector::{Inspector, InspectorSnapshot};
53use crate::sqlite::SqliteDb;
54use crate::telemetry::{
55	ActorInvocationTelemetry, ActorInvocationTraceContext, ActorTelemetryIdentity,
56	IncomingTraceContext, OutboundCallInvocation, WorkflowRunInvocation, WorkflowRunOutcome,
57	WorkflowStepSpan,
58};
59use crate::types::{ActorKey, ConnId, ListOpts, format_actor_key};
60use crate::{ActorConfig, ActorTracingConfig};
61
62/// Shared actor runtime context.
63///
64/// This public surface is the foreign-runtime contract for `rivetkit-core`.
65/// Native Rust, NAPI-backed TypeScript, and future V8 runtimes should be able
66/// to drive actor behavior through `ActorFactory` plus the methods exposed here
67/// and on the returned runtime objects like `SqliteDb`, schedule APIs,
68/// queue APIs, `ConnHandle`, and `WebSocket`.
69#[derive(Clone)]
70pub struct ActorContext(
71	pub(crate) Arc<ActorContextInner>,
72	/// Telemetry of the invocation this handle serves, `None` outside an invocation.
73	pub(crate) Option<ActorInvocationTelemetry>,
74);
75
76#[derive(Clone)]
77pub struct ActorKv {
78	sql: SqliteDb,
79}
80
81/// The startup commit that runs while the actor serves its first requests.
82#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
83pub(crate) struct StartupCommit {
84	/// Startup commits still in flight: one for a new local actor, otherwise none.
85	pub(crate) pending: usize,
86	/// Whether one of them failed. The actor is then stopping with an error.
87	pub(crate) failed: bool,
88	/// Whether startup has returned. A commit that fails before then fails the startup itself;
89	/// one that fails after stops the running actor with an error.
90	pub(crate) returned: bool,
91}
92
93pub(crate) struct ActorContextInner {
94	pub(super) legacy_kv: LegacyActorKv,
95	user_kv: ActorKv,
96	sql: SqliteDb,
97	/// Set when the start command reported an empty actor keyspace, which the engine sends only
98	/// on an actor's first generation. Startup consumes it once.
99	new_actor_storage: AtomicBool,
100	/// When the runtime received this generation's start, for the `before_storage` startup
101	/// phase. Startup consumes it once.
102	// Forced-sync: set once before the actor task starts and taken by its startup.
103	start_received_at: parking_lot::Mutex<Option<crate::time::Instant>>,
104	#[cfg(feature = "sqlite-local")]
105	actor_runtime_socket: ActorRuntimeSocketEndpoint,
106	// Forced-sync: actor state snapshots are exposed through synchronous
107	// accessors and are never held across `.await`.
108	pub(super) current_state: RwLock<Vec<u8>>,
109	pub(super) persisted: RwLock<PersistedActor>,
110	pub(super) last_pushed_alarm: RwLock<Option<i64>>,
111	pub(super) run_wake_at: RwLock<Option<i64>>,
112	pub(super) run_wake_revision: AtomicU64,
113	pub(super) state_save_interval: Duration,
114	pub(super) state_dirty: AtomicBool,
115	pub(super) state_revision: AtomicU64,
116	pub(super) save_request_revision: AtomicU64,
117	pub(super) save_completed_revision: AtomicU64,
118	/// Every save request revision at or below this fails unless written; shutdown sets it
119	/// to `u64::MAX` once no later save can run.
120	pub(super) save_failed_revision: AtomicU64,
121	pub(super) save_completion: Notify,
122	pub(super) save_requested: AtomicBool,
123	pub(super) save_requested_immediate: AtomicBool,
124	// Forced-sync: debounce bookkeeping is updated from sync save-request paths.
125	pub(super) save_requested_within_deadline: Mutex<Option<crate::time::Instant>>,
126	pub(super) last_save_at: Mutex<Option<crate::time::Instant>>,
127	pub(super) pending_save: Mutex<Option<PendingSave>>,
128	pub(super) tracked_persist: Mutex<Option<JoinHandle<()>>>,
129	pub(super) save_guard: Arc<AsyncMutex<()>>,
130	// Forced-sync: the slot is also cleared from the transaction owner's `Drop`.
131	pub(super) state_transaction: Mutex<StateTransactionSlot>,
132	pub(super) state_transaction_epoch: AtomicU64,
133	pub(super) in_flight_state_writes: AtomicUsize,
134	/// The startup state commit that runs while the actor serves its first requests. Responses
135	/// wait for it, so none reports state that is not yet durable.
136	pub(super) startup_commit: tokio::sync::watch::Sender<StartupCommit>,
137	pub(super) state_write_completion: Notify,
138	pub(super) on_state_change_in_flight: AtomicUsize,
139	pub(super) on_state_change_idle: Notify,
140	// Forced-sync: hooks are registered and cloned from synchronous runtime
141	// wiring slots before use.
142	pub(super) request_save_hooks: RwLock<Vec<Arc<dyn Fn(RequestSaveOpts) + Send + Sync>>>,
143	// Forced-sync: schedule runtime handles and callbacks are synchronous
144	// wiring slots cloned before actor/envoy I/O.
145	pub(super) schedule_generation: Mutex<Option<u32>>,
146	pub(super) schedule_envoy_handle: Mutex<Option<EnvoyHandle>>,
147	pub(super) client_endpoint: OnceLock<String>,
148	pub(super) client_token: OnceLock<String>,
149	pub(super) client_namespace: OnceLock<String>,
150	pub(super) client_pool_name: OnceLock<String>,
151	pub(super) schedule_internal_keep_awake: Mutex<Option<InternalKeepAwakeCallback>>,
152	pub(super) schedule_local_alarm_callback: Mutex<Option<LocalAlarmCallback>>,
153	// Forced-sync: the local alarm timer is aborted from sync paths.
154	pub(super) schedule_local_alarm_task: Mutex<Option<LocalAlarmTask>>,
155	// Forced-sync: receivers are pushed/taken from sync paths and awaited after
156	// being moved out of the lock.
157	pub(super) schedule_pending_alarm_writes: Mutex<Vec<oneshot::Receiver<()>>>,
158	pub(super) schedule_local_alarm_epoch: AtomicU64,
159	pub(super) schedule_alarm_push_epoch: AtomicU64,
160	/// Serializes the persists of acknowledged alarm pushes.
161	pub(super) schedule_alarm_persist_lock: AsyncMutex<()>,
162	pub(super) schedule_alarm_dispatch_enabled: AtomicBool,
163	pub(super) schedule_dirty_since_push: AtomicBool,
164	pub(super) schedule_mutation_lock: AsyncMutex<()>,
165	pub(super) schedule_running: SccHashSet<String>,
166	pub(super) schedule_history_insert_count: AtomicUsize,
167	pub(super) max_schedules: u32,
168	#[cfg(any(test, feature = "test-support"))]
169	pub(super) schedule_now_override: AtomicI64,
170	
171	
172	// Forced-sync: queue config is read from sync public methods before blocking
173	// on async queue work.
174	pub(super) queue_config: Mutex<ActorConfig>,
175	pub(super) queue_abort_signal: Mutex<CancellationToken>,
176	pub(super) queue_initialize: OnceCell<()>,
177	pub(super) queue_metadata: AsyncMutex<QueueMetadata>,
178	pub(super) queue_receive_lock: AsyncMutex<()>,
179	pub(super) queue_completion_waiters: SccHashMap<u64, oneshot::Sender<Option<Vec<u8>>>>,
180	pub(super) queue_notify: Notify,
181	pub(super) active_queue_wait_count: AtomicU32,
182	// Forced-sync: callbacks are registered and cloned from synchronous hooks.
183	pub(super) queue_wait_activity_callback: Mutex<Option<QueueWaitActivityCallback>>,
184	pub(super) queue_inspector_update_callback: Mutex<Option<QueueInspectorUpdateCallback>>,
185	// Forced-sync: connection operations expose sync accessors or clone handles
186	// before awaiting; connection_disconnect_state serializes disconnect
187	// bookkeeping with pending hibernation snapshots.
188	pub(super) connection_config: RwLock<ActorConfig>,
189	pub(super) connections: RwLock<BTreeMap<ConnId, ConnHandle>>,
190	pub(super) pending_hibernation_updates: RwLock<BTreeSet<ConnId>>,
191	pub(super) pending_hibernation_removals: RwLock<BTreeSet<ConnId>>,
192	pub(super) connection_disconnect_state: Mutex<()>,
193	pub(super) sleep: SleepState,
194	activity: ActivityState,
195	sleep_requested: AtomicBool,
196	destroy_requested: AtomicBool,
197	destroy_completed: AtomicBool,
198	destroy_completion_notify: Notify,
199	abort_signal: Mutex<CancellationToken>,
200	shutdown_deadline: CancellationToken,
201	// Forced-sync: runtime wiring slots are configured through synchronous
202	// lifecycle setup and cloned before sending events.
203	inspector: RwLock<Option<Inspector>>,
204	inspector_attach_count: RwLock<Option<Arc<AtomicU32>>>,
205	inspector_overlay_tx: RwLock<Option<broadcast::Sender<Arc<Vec<u8>>>>>,
206	actor_events: RwLock<Option<mpsc::UnboundedSender<ActorEvent>>>,
207	pub(super) lifecycle_events: RwLock<Option<mpsc::UnboundedSender<LifecycleEvent>>>,
208	hibernated_connection_liveness_override: RwLock<Option<BTreeSet<(Vec<u8>, Vec<u8>)>>>,
209	pub(super) metrics: ActorMetrics,
210	diagnostics: ActorDiagnostics,
211	telemetry_identity: Arc<ActorTelemetryIdentity>,
212	tracing_config: Arc<ActorTracingConfig>,
213	actor_id: String,
214	name: String,
215	key: ActorKey,
216	region: String,
217}
218
219#[derive(Debug, Default)]
220pub(crate) struct ActivityState {
221	dirty: AtomicBool,
222}
223
224impl ActivityState {
225	fn mark_dirty(&self) -> bool {
226		!self.dirty.swap(true, Ordering::AcqRel)
227	}
228
229	fn take_dirty(&self) -> bool {
230		self.dirty.swap(false, Ordering::AcqRel)
231	}
232}
233
234impl ActorKv {
235	pub async fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
236		let mut values = self.batch_get(&[key]).await?;
237		Ok(values.pop().flatten())
238	}
239
240	pub async fn put(&self, key: &[u8], value: &[u8]) -> Result<()> {
241		self.batch_put(&[(key, value)]).await
242	}
243
244	pub async fn delete(&self, key: &[u8]) -> Result<()> {
245		self.batch_delete(&[key]).await
246	}
247
248	pub async fn batch_get(&self, keys: &[&[u8]]) -> Result<Vec<Option<Vec<u8>>>> {
249		internal_storage::user_kv_batch_get(&self.sql, keys).await
250	}
251
252	pub async fn batch_put(&self, entries: &[(&[u8], &[u8])]) -> Result<()> {
253		internal_storage::user_kv_batch_put(&self.sql, entries).await
254	}
255
256	pub async fn batch_delete(&self, keys: &[&[u8]]) -> Result<()> {
257		internal_storage::user_kv_batch_delete(&self.sql, keys).await
258	}
259
260	pub async fn delete_range(&self, start: &[u8], end: &[u8]) -> Result<()> {
261		internal_storage::user_kv_delete_range(&self.sql, start, end).await
262	}
263
264	pub async fn list_prefix(
265		&self,
266		prefix: &[u8],
267		opts: ListOpts,
268	) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
269		internal_storage::user_kv_list_prefix(&self.sql, prefix, opts).await
270	}
271
272	pub async fn list_range(
273		&self,
274		start: &[u8],
275		end: &[u8],
276		opts: ListOpts,
277	) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
278		internal_storage::user_kv_list_range(&self.sql, start, end, opts).await
279	}
280}
281
282impl ActorContext {
283	/// Returns a handle bound to `telemetry`, so schedules and SQLite work done
284	/// through it are attributed to that invocation.
285	pub fn with_invocation_telemetry(
286		mut self,
287		telemetry: Option<ActorInvocationTelemetry>,
288	) -> Self {
289		self.1 = telemetry;
290		self
291	}
292
293	/// Returns a handle for the same invocation whose spans parent to the
294	/// application span the host runtime has active, given as W3C
295	/// `traceparent` and `tracestate`. A handle that serves no invocation is
296	/// returned unchanged, because it opens no spans.
297	#[doc(hidden)]
298	pub fn with_application_span(
299		&self,
300		traceparent: Option<&str>,
301		tracestate: Option<&str>,
302	) -> Self {
303		Self(
304			self.0.clone(),
305			self.1
306				.as_ref()
307				.map(|telemetry| telemetry.with_application_span(traceparent, tracestate)),
308		)
309	}
310
311	pub(crate) fn invocation_telemetry(&self) -> Option<&ActorInvocationTelemetry> {
312		self.1.as_ref()
313	}
314
315	/// Returns the SQLite handle bound to this handle's invocation.
316	pub fn invocation_sql(&self) -> SqliteDb {
317		self.0.sql.clone().with_invocation_telemetry(self.1.clone())
318	}
319
320	/// Opens the span covering one call out to another actor, or nothing when
321	/// this handle serves no invocation or tracing is disabled.
322	///
323	/// `actor_name` and `action_name` name the callee. Both come from the
324	/// caller's own registry rather than from a remote peer, so neither is a
325	/// cardinality surface.
326	#[doc(hidden)]
327	pub fn start_call_span(
328		&self,
329		actor_name: &str,
330		action_name: &str,
331	) -> Option<OutboundCallInvocation> {
332		self.1
333			.as_ref()?
334			.start_outbound_call(actor_name, action_name)
335	}
336
337	/// Opens one workflow run as its own invocation, linked to the span the
338	/// previous run persisted.
339	#[doc(hidden)]
340	pub async fn start_workflow_span(&self) -> WorkflowRunInvocation {
341		let previous = if tracing::enabled!(target: "rivetkit::telemetry", tracing::Level::INFO) {
342			internal_storage::load_workflow_trace(&self.0.sql)
343				.await
344				.unwrap_or_else(|error| {
345					tracing::warn!(
346						actor_id = %self.actor_id(),
347						?error,
348						"failed to load the previous workflow run span, so this run will not link to it"
349					);
350					IncomingTraceContext::default()
351				})
352		} else {
353			IncomingTraceContext::default()
354		};
355		WorkflowRunInvocation::start(self, previous)
356	}
357
358	/// Closes a workflow run and persists its span and ray for the next run.
359	#[doc(hidden)]
360	pub async fn finish_workflow_span(
361		&self,
362		run: WorkflowRunInvocation,
363		outcome: WorkflowRunOutcome,
364	) {
365		let Some(trace_context) = run.finish(outcome) else {
366			return;
367		};
368		if let Err(error) =
369			internal_storage::persist_workflow_trace(&self.0.sql, trace_context).await
370		{
371			tracing::warn!(
372				actor_id = %self.actor_id(),
373				?error,
374				"failed to persist the workflow run span, so the next run will not link to it"
375			);
376		}
377	}
378
379	/// Opens the span for one step attempt, or nothing when this handle serves
380	/// no invocation or tracing is off.
381	#[doc(hidden)]
382	pub fn start_workflow_step_span(
383		&self,
384		step_name: &str,
385		attempt: u32,
386	) -> Option<WorkflowStepSpan> {
387		WorkflowStepSpan::start(self, step_name, attempt)
388	}
389
390	/// Returns correlation for the invocation this handle serves, absent when
391	/// the handle is not bound to one or tracing is disabled.
392	pub fn invocation_trace_context(&self) -> Option<ActorInvocationTraceContext> {
393		self.1.as_ref()?.trace_context()
394	}
395
396	/// Returns whether two handles belong to the same running actor generation.
397	#[doc(hidden)]
398	pub fn is_same_instance(&self, other: &Self) -> bool {
399		Arc::ptr_eq(&self.0, &other.0)
400	}
401
402	/// Identifies the running actor generation this handle belongs to. Every handle of one
403	/// generation returns the same value, and no two live generations share one, even when they
404	/// have the same actor id. A value may be reused only after every handle of its generation
405	/// has dropped.
406	#[doc(hidden)]
407	pub fn instance_key(&self) -> usize {
408		Arc::as_ptr(&self.0) as usize
409	}
410
411	
412
413	
414
415	
416
417	pub(crate) fn build(
418		actor_id: String,
419		name: String,
420		key: ActorKey,
421		region: String,
422		_generation: Option<u32>,
423		_envoy_key: String,
424		config: ActorConfig,
425		legacy_kv: LegacyActorKv,
426		sql: SqliteDb,
427	) -> Self {
428		#[cfg(feature = "sqlite-local")]
429		let mut sql = sql;
430		#[cfg(feature = "sqlite-local")]
431		sql.set_profiling_config(config.sqlite_profiling.clone());
432		let metrics =
433			ActorMetrics::new_with_sqlite_profiling(name.clone(), config.sqlite_profiling.clone());
434		#[cfg(feature = "sqlite-local")]
435		sql.set_vfs_metrics(Arc::new(metrics.clone()));
436		let diagnostics = ActorDiagnostics::new(actor_id.clone());
437		#[cfg(feature = "sqlite-local")]
438		let actor_runtime_socket =
439			ActorRuntimeSocketEndpoint::new(config.enable_actor_runtime_socket, sql.clone());
440		let state_save_interval = config.state_save_interval;
441		let tracing_config = Arc::clone(&config.tracing);
442		let max_schedules = config.max_schedules;
443		let abort_signal = CancellationToken::new();
444		let shutdown_deadline = CancellationToken::new();
445		let sleep = SleepState::new(config.clone());
446		let user_kv = ActorKv { sql: sql.clone() };
447		let inner = Arc::new(ActorContextInner {
448			legacy_kv,
449			user_kv,
450			sql,
451			new_actor_storage: AtomicBool::new(false),
452			start_received_at: parking_lot::Mutex::new(None),
453			#[cfg(feature = "sqlite-local")]
454			actor_runtime_socket,
455			current_state: RwLock::new(Vec::new()),
456			persisted: RwLock::new(PersistedActor::default()),
457			last_pushed_alarm: RwLock::new(None),
458			run_wake_at: RwLock::new(None),
459			run_wake_revision: AtomicU64::new(0),
460			state_save_interval,
461			state_dirty: AtomicBool::new(false),
462			state_revision: AtomicU64::new(0),
463			save_request_revision: AtomicU64::new(0),
464			save_completed_revision: AtomicU64::new(0),
465			save_failed_revision: AtomicU64::new(0),
466			save_completion: Notify::new(),
467			save_requested: AtomicBool::new(false),
468			save_requested_immediate: AtomicBool::new(false),
469			save_requested_within_deadline: Mutex::new(None),
470			last_save_at: Mutex::new(None),
471			pending_save: Mutex::new(None),
472			tracked_persist: Mutex::new(None),
473			save_guard: Arc::new(AsyncMutex::new(())),
474			state_transaction: Mutex::new(StateTransactionSlot::Idle),
475			state_transaction_epoch: AtomicU64::new(0),
476			in_flight_state_writes: AtomicUsize::new(0),
477			startup_commit: tokio::sync::watch::Sender::new(StartupCommit::default()),
478			state_write_completion: Notify::new(),
479			on_state_change_in_flight: AtomicUsize::new(0),
480			on_state_change_idle: Notify::new(),
481			request_save_hooks: RwLock::new(Vec::new()),
482			schedule_generation: Mutex::new(None),
483			schedule_envoy_handle: Mutex::new(None),
484			client_endpoint: OnceLock::new(),
485			client_token: OnceLock::new(),
486			client_namespace: OnceLock::new(),
487			client_pool_name: OnceLock::new(),
488			schedule_internal_keep_awake: Mutex::new(None),
489			schedule_local_alarm_callback: Mutex::new(None),
490			schedule_local_alarm_task: Mutex::new(None),
491			schedule_pending_alarm_writes: Mutex::new(Vec::new()),
492			schedule_local_alarm_epoch: AtomicU64::new(0),
493			schedule_alarm_push_epoch: AtomicU64::new(0),
494			schedule_alarm_persist_lock: AsyncMutex::new(()),
495			schedule_alarm_dispatch_enabled: AtomicBool::new(true),
496			// A fresh actor context has no in-process record of a successful
497			// envoy alarm push yet, so the first sync must always push.
498			schedule_dirty_since_push: AtomicBool::new(true),
499			schedule_mutation_lock: AsyncMutex::new(()),
500			schedule_running: SccHashSet::new(),
501			schedule_history_insert_count: AtomicUsize::new(0),
502			max_schedules,
503			#[cfg(any(test, feature = "test-support"))]
504			schedule_now_override: AtomicI64::new(i64::MIN),
505			
506			
507			queue_config: Mutex::new(config.clone()),
508			queue_abort_signal: Mutex::new(abort_signal.clone()),
509			queue_initialize: OnceCell::new(),
510			queue_metadata: AsyncMutex::new(QueueMetadata::default()),
511			queue_receive_lock: AsyncMutex::new(()),
512			queue_completion_waiters: SccHashMap::new(),
513			queue_notify: Notify::new(),
514			active_queue_wait_count: AtomicU32::new(0),
515			queue_wait_activity_callback: Mutex::new(None),
516			queue_inspector_update_callback: Mutex::new(None),
517			connection_config: RwLock::new(config),
518			connections: RwLock::new(BTreeMap::new()),
519			pending_hibernation_updates: RwLock::new(BTreeSet::new()),
520			pending_hibernation_removals: RwLock::new(BTreeSet::new()),
521			connection_disconnect_state: Mutex::new(()),
522			sleep,
523			activity: ActivityState::default(),
524			sleep_requested: AtomicBool::new(false),
525			destroy_requested: AtomicBool::new(false),
526			destroy_completed: AtomicBool::new(false),
527			destroy_completion_notify: Notify::new(),
528			abort_signal: Mutex::new(abort_signal),
529			shutdown_deadline,
530			inspector: RwLock::new(None),
531			inspector_attach_count: RwLock::new(None),
532			inspector_overlay_tx: RwLock::new(None),
533			actor_events: RwLock::new(None),
534			lifecycle_events: RwLock::new(None),
535			hibernated_connection_liveness_override: RwLock::new(None),
536			metrics,
537			diagnostics,
538			tracing_config,
539			telemetry_identity: Arc::new(ActorTelemetryIdentity {
540				actor_id: actor_id.clone(),
541				actor_name: name.clone(),
542				actor_key: format_actor_key(&key),
543			}),
544			actor_id,
545			name,
546			key,
547			region,
548		});
549		let ctx = Self(inner, None);
550		ctx.configure_sleep_hooks();
551		ctx
552	}
553
554	#[deprecated(
555		note = "Actor KV is deprecated. Use embedded SQLite (`sql()`) or actor state instead."
556	)]
557	pub async fn kv_batch_get(&self, keys: &[&[u8]]) -> Result<Vec<Option<Vec<u8>>>> {
558		self.0.user_kv.batch_get(keys).await
559	}
560
561	#[deprecated(
562		note = "Actor KV is deprecated. Use embedded SQLite (`sql()`) or actor state instead."
563	)]
564	pub async fn kv_batch_put(&self, entries: &[(&[u8], &[u8])]) -> Result<()> {
565		self.0.user_kv.batch_put(entries).await
566	}
567
568	#[deprecated(
569		note = "Actor KV is deprecated. Use embedded SQLite (`sql()`) or actor state instead."
570	)]
571	pub async fn kv_batch_delete(&self, keys: &[&[u8]]) -> Result<()> {
572		self.0.user_kv.batch_delete(keys).await
573	}
574
575	#[deprecated(
576		note = "Actor KV is deprecated. Use embedded SQLite (`sql()`) or actor state instead."
577	)]
578	pub async fn kv_delete_range(&self, start: &[u8], end: &[u8]) -> Result<()> {
579		self.0.user_kv.delete_range(start, end).await
580	}
581
582	#[deprecated(
583		note = "Actor KV is deprecated. Use embedded SQLite (`sql()`) or actor state instead."
584	)]
585	pub async fn kv_list_prefix(
586		&self,
587		prefix: &[u8],
588		opts: ListOpts,
589	) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
590		self.0.user_kv.list_prefix(prefix, opts).await
591	}
592
593	#[deprecated(
594		note = "Actor KV is deprecated. Use embedded SQLite (`sql()`) or actor state instead."
595	)]
596	pub async fn kv_list_range(
597		&self,
598		start: &[u8],
599		end: &[u8],
600		opts: ListOpts,
601	) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
602		self.0.user_kv.list_range(start, end, opts).await
603	}
604
605	#[deprecated(
606		note = "Actor KV is deprecated. Use embedded SQLite (`sql()`) or actor state instead."
607	)]
608	pub fn kv(&self) -> &ActorKv {
609		&self.0.user_kv
610	}
611
612	/// Legacy actor KV access used only by the one-time KV-to-SQLite importer and
613	/// the temporary inspector-token mirror consumed by the dashboard.
614	pub(crate) fn legacy_kv(&self) -> &LegacyActorKv {
615		&self.0.legacy_kv
616	}
617
618	pub fn sql(&self) -> &SqliteDb {
619		&self.0.sql
620	}
621
622	/// Records whether the start command showed this actor's storage was never written.
623	pub(crate) fn set_new_actor_storage(&self, new_actor_storage: bool) {
624		self.0
625			.new_actor_storage
626			.store(new_actor_storage, Ordering::Release);
627	}
628
629	/// Records when the runtime received this generation's start.
630	pub(crate) fn set_start_received_at(&self, received_at: crate::time::Instant) {
631		*self.0.start_received_at.lock() = Some(received_at);
632	}
633
634	/// When the runtime received this generation's start, at most once.
635	pub(crate) fn take_start_received_at(&self) -> Option<crate::time::Instant> {
636		self.0.start_received_at.lock().take()
637	}
638
639	/// Whether the start command showed this actor's storage was never written. Returns true
640	/// at most once, so a restart within this context probes storage again.
641	pub(crate) fn take_new_actor_storage(&self) -> bool {
642		self.0.new_actor_storage.swap(false, Ordering::AcqRel)
643	}
644
645	#[cfg(feature = "sqlite-local")]
646	pub async fn provision_actor_runtime_socket(&self) -> Result<ActorRuntimeSocketEndpointInfo> {
647		self.0.actor_runtime_socket.provision().await
648	}
649
650	#[cfg(feature = "sqlite-local")]
651	pub(crate) async fn shutdown_actor_runtime_socket(&self) {
652		self.0.actor_runtime_socket.shutdown().await;
653	}
654
655	pub async fn db_exec(&self, sql: &str) -> Result<Vec<u8>> {
656		self.0.sql.exec_rows_cbor(sql).await
657	}
658
659	pub async fn db_query(&self, sql: &str, params: Option<&[u8]>) -> Result<Vec<u8>> {
660		self.0.sql.query_rows_cbor(sql, params).await
661	}
662
663	pub async fn db_execute(&self, sql: &str, params: Option<&[u8]>) -> Result<Vec<u8>> {
664		self.0.sql.execute_rows_cbor(sql, params).await
665	}
666
667	pub async fn db_run(&self, sql: &str, params: Option<&[u8]>) -> Result<()> {
668		self.0.sql.run_cbor(sql, params).await?;
669		Ok(())
670	}
671
672	pub fn set_alarm(&self, timestamp_ms: Option<i64>) -> Result<()> {
673		self.set_schedule_alarm(timestamp_ms)
674	}
675
676	/// Resync persisted alarms with the runtime's alarm transport.
677	///
678	/// Foreign-runtime adapters should call this during startup after loading
679	/// any persisted schedule state and before accepting user callbacks that rely
680	/// on future alarms being armed.
681	pub async fn init_alarms(&self) {
682		if let Err(error) = self.recover_interrupted_schedule_history().await {
683			tracing::error!(?error, "failed to recover interrupted schedule history");
684		}
685		self.sync_future_alarm_logged().await;
686	}
687
688	pub fn queue(&self) -> &Self {
689		self
690	}
691
692	pub fn sleep(&self) -> Result<()> {
693		// `started` is cleared when the lifecycle state machine transitions
694		// into SleepGrace / DestroyGrace, so `started=false` covers both
695		// "never started" and "already shutting down". Distinguish with the
696		// request flags for an accurate diagnostic.
697		if !self.0.sleep.lifecycle_started.load(Ordering::SeqCst) {
698			let already_stopping = self.0.sleep_requested.load(Ordering::SeqCst)
699				|| self.0.destroy_requested.load(Ordering::SeqCst);
700			return if already_stopping {
701				Err(ActorLifecycleError::Stopping.build()).context("actor is already shutting down")
702			} else {
703				Err(ActorLifecycleError::Starting.build())
704					.context("cannot request sleep before actor startup completes")
705			};
706		}
707		if self.0.sleep_requested.swap(true, Ordering::SeqCst) {
708			return Ok(());
709		}
710		self.cancel_sleep_timer();
711		if Handle::try_current().is_ok() {
712			let ctx = self.clone();
713			let tracked = self.track_shutdown_task(async move {
714				ctx.record_user_task_started(UserTaskKind::SleepFinalize);
715				let started_at = Instant::now();
716				ctx.request_sleep_from_envoy();
717				ctx.record_user_task_finished(UserTaskKind::SleepFinalize, started_at.elapsed());
718			});
719			if tracked {
720				return Ok(());
721			}
722		}
723
724		self.request_sleep_from_envoy();
725		Ok(())
726	}
727
728	pub fn destroy(&self) -> Result<()> {
729		self.request_stop(None)
730	}
731
732	/// Request a stop with an error attached. The envoy reports the stop to the
733	/// engine with `StopCode::Error` and the message, and the engine answers a
734	/// crash by putting the actor back to sleep rather than destroying it. The
735	/// local teardown therefore takes the sleep path too, so `onSleep` runs,
736	/// hibernatable connections are preserved, the persisted alarm stays armed
737	/// for the next generation, and incoming work is not rejected with
738	/// `Destroying` for an actor that is about to resume.
739	pub fn stop_with_error(&self, message: impl Into<String>) -> Result<()> {
740		self.request_stop(Some(truncate_stop_error_message(message.into())))
741	}
742
743	fn request_stop(&self, error: Option<String>) -> Result<()> {
744		// See `sleep` for why the request flags disambiguate `started=false`.
745		// destroy() is allowed after sleep() has been requested because
746		// destroy is a stronger signal that escalates an in-flight sleep.
747		if !self.0.sleep.lifecycle_started.load(Ordering::SeqCst)
748			&& !self.0.sleep_requested.load(Ordering::SeqCst)
749			&& !self.0.destroy_requested.load(Ordering::SeqCst)
750		{
751			return Err(ActorLifecycleError::Starting.build())
752				.context("cannot request stop before actor startup completes");
753		}
754
755		if let Some(error) = error {
756			// An errored stop is a crash report, not a destroy. A destroy that
757			// already won owns the teardown, so leave it alone.
758			if self.0.destroy_requested.load(Ordering::SeqCst) {
759				return Err(ActorLifecycleError::Stopping.build())
760					.context("destroy already requested for this generation");
761			}
762			// Record the error even when a sleep is already in flight. The
763			// envoy attaches it to the actor regardless of whether the intent
764			// itself is a duplicate, so the eventual `Stopped` still carries
765			// `StopCode::Error` and the engine still records the crash.
766			let queued_error = self.0.sleep.stop_error.lock().replace(error);
767			if !self.0.sleep_requested.swap(true, Ordering::SeqCst) {
768				self.mark_errored_stop_requested();
769			}
770			// An errored stop is already queued and has not reached the envoy
771			// yet. It picks up the error recorded above, so sending a second
772			// intent would only race an error-less `stop_actor` against it:
773			// `request_stop_from_envoy` consumes the single error slot with
774			// `take`, and the loser reports the crash as a deliberate destroy.
775			// A sleep that is already in flight leaves the slot empty, so the
776			// `sleep()` -> `stop_with_error()` upgrade still reports here.
777			if queued_error.is_some() {
778				return Ok(());
779			}
780		} else {
781			if self.0.destroy_requested.swap(true, Ordering::SeqCst) {
782				return Err(ActorLifecycleError::Stopping.build())
783					.context("destroy already requested for this generation");
784			}
785			// A destroy supersedes an errored stop that has not reached the
786			// envoy yet. Without clearing the slot, whichever request runs
787			// first consumes the error and reports this destroy as a sleep
788			// intent, leaving the actor alive.
789			*self.0.sleep.stop_error.lock() = None;
790			// Reuse the shared teardown sequence used by the registry shutdown
791			// path so future changes to `mark_destroy_requested` cannot drift.
792			// `destroy_requested` is already true from the swap above. The
793			// redundant `store(true)` inside is harmless.
794			#[cfg(not(feature = "wasm-runtime"))]
795			self.mark_destroy_requested();
796			#[cfg(feature = "wasm-runtime")]
797			self.mark_destroy_requested_without_spawn();
798		}
799
800		let ctx = self.clone();
801		if Handle::try_current().is_ok() {
802			let tracked = self.track_shutdown_task(async move {
803				ctx.record_user_task_started(UserTaskKind::DestroyRequest);
804				let started_at = Instant::now();
805				ctx.request_stop_from_envoy();
806				ctx.record_user_task_finished(UserTaskKind::DestroyRequest, started_at.elapsed());
807			});
808			if tracked {
809				return Ok(());
810			}
811		}
812
813		self.request_stop_from_envoy();
814		Ok(())
815	}
816
817	pub fn mark_destroy_requested(&self) {
818		self.cancel_sleep_timer();
819		self.flush_on_shutdown();
820		self.0.destroy_requested.store(true, Ordering::SeqCst);
821		self.0.destroy_completed.store(false, Ordering::SeqCst);
822	}
823
824	/// Teardown bookkeeping for an errored stop. Mirrors
825	/// `mark_destroy_requested` minus the destroy flags, since the actor is
826	/// heading for the sleep path. The state flush still runs so a crash
827	/// persists whatever the actor had before its teardown.
828	fn mark_errored_stop_requested(&self) {
829		self.cancel_sleep_timer();
830		#[cfg(not(feature = "wasm-runtime"))]
831		self.flush_on_shutdown();
832	}
833
834	#[cfg(feature = "wasm-runtime")]
835	fn mark_destroy_requested_without_spawn(&self) {
836		self.cancel_sleep_timer();
837		self.0.destroy_requested.store(true, Ordering::SeqCst);
838		self.0.destroy_completed.store(false, Ordering::SeqCst);
839	}
840
841	#[doc(hidden)]
842	pub fn cancel_actor_abort_signal(&self) {
843		self.0.abort_signal.lock().cancel();
844	}
845
846	pub(crate) fn reset_abort_signal_for_start(&self) {
847		let mut abort_signal = self.0.abort_signal.lock();
848		if !abort_signal.is_cancelled() {
849			return;
850		}
851
852		// Sleep or destroy cancels the generation abort signal to break actor
853		// scoped waits. A restarted actor needs a fresh signal so the next
854		// generation can wait normally.
855		let next_signal = CancellationToken::new();
856		*abort_signal = next_signal.clone();
857		*self.0.queue_abort_signal.lock() = next_signal;
858	}
859
860	#[doc(hidden)]
861	pub fn actor_abort_signal(&self) -> CancellationToken {
862		self.0.abort_signal.lock().clone()
863	}
864
865	#[doc(hidden)]
866	pub fn actor_aborted(&self) -> bool {
867		self.0.abort_signal.lock().is_cancelled()
868	}
869
870	/// Fires when the shutdown grace deadline has elapsed and core is forcing
871	/// cleanup. Foreign-runtime adapters should abort any in-flight shutdown
872	/// work (for example `onSleep` / `onDestroy`) when this token is cancelled
873	/// so resources like SQLite are not torn down mid-operation.
874	#[doc(hidden)]
875	pub fn shutdown_deadline_token(&self) -> CancellationToken {
876		self.0.shutdown_deadline.clone()
877	}
878
879	#[doc(hidden)]
880	pub fn cancel_shutdown_deadline(&self) {
881		self.0.shutdown_deadline.cancel();
882	}
883
884	/// Deprecated no-op. Use `keep_awake` to hold the actor awake for the
885	/// duration of a future, or `wait_until` to keep work alive across the
886	/// sleep grace period. Retained only for NAPI bridge compatibility.
887	#[deprecated(note = "no-op: use `keep_awake` or `wait_until` instead")]
888	pub fn set_prevent_sleep(&self, _enabled: bool) {}
889
890	#[deprecated(note = "no-op: always returns false")]
891	pub fn prevent_sleep(&self) -> bool {
892		false
893	}
894
895	/// Runs `future` to completion after the current reply, without blocking
896	/// it. Work started from an invocation keeps that invocation's span open
897	/// until it settles, so its SQLite operations and logs stay attributed to
898	/// the request that started them.
899	#[cfg(not(feature = "wasm-runtime"))]
900	pub fn wait_until(&self, future: impl Future<Output = ()> + Send + 'static) {
901		let invocation = self
902			.1
903			.as_ref()
904			.and_then(crate::ActorInvocationTelemetry::hold_open);
905		self.spawn_work(ActorWorkKind::WaitUntil, async move {
906			future.await;
907			drop(invocation);
908		});
909	}
910
911	#[cfg(not(feature = "wasm-runtime"))]
912	pub fn register_task(&self, future: impl Future<Output = ()> + Send + 'static) {
913		self.spawn_work(ActorWorkKind::RegisteredTask, future);
914	}
915
916	#[cfg(feature = "wasm-runtime")]
917	pub fn wait_until(&self, future: impl Future<Output = ()> + 'static) {
918		let invocation = self
919			.1
920			.as_ref()
921			.and_then(crate::ActorInvocationTelemetry::hold_open);
922		self.spawn_work(ActorWorkKind::WaitUntil, async move {
923			future.await;
924			drop(invocation);
925		});
926	}
927
928	#[cfg(feature = "wasm-runtime")]
929	pub fn register_task(&self, future: impl Future<Output = ()> + 'static) {
930		self.spawn_work(ActorWorkKind::RegisteredTask, future);
931	}
932
933	pub async fn keep_awake<F>(&self, future: F) -> F::Output
934	where
935		F: Future,
936	{
937		self.track_work(ActorWorkKind::KeepAwake, future).await
938	}
939
940	pub fn keep_awake_region(&self) -> KeepAwakeRegion {
941		KeepAwakeRegion {
942			region: Some(self.begin_work_region(ActorWorkKind::KeepAwake)),
943		}
944	}
945
946	pub async fn internal_keep_awake<F>(&self, future: F) -> F::Output
947	where
948		F: Future,
949	{
950		self.track_work(ActorWorkKind::InternalKeepAwake, future)
951			.await
952	}
953
954	pub fn keep_awake_count(&self) -> usize {
955		self.sleep_keep_awake_count()
956	}
957
958	pub fn internal_keep_awake_count(&self) -> usize {
959		self.sleep_internal_keep_awake_count()
960	}
961
962	pub async fn track_work<F>(&self, kind: ActorWorkKind, future: F) -> F::Output
963	where
964		F: Future,
965	{
966		let _region = self.begin_work_region(kind);
967		future.await
968	}
969
970	#[cfg(not(feature = "wasm-runtime"))]
971	pub fn spawn_work<F>(&self, kind: ActorWorkKind, future: F)
972	where
973		F: Future<Output = ()> + Send + 'static,
974	{
975		self.spawn_work_inner(kind, future);
976	}
977
978	#[cfg(feature = "wasm-runtime")]
979	pub fn spawn_work<F>(&self, kind: ActorWorkKind, future: F)
980	where
981		F: Future<Output = ()> + 'static,
982	{
983		self.spawn_work_inner(kind, future);
984	}
985
986	pub fn begin_work_region(&self, kind: ActorWorkKind) -> ActorWorkRegion {
987		ActorWorkRegion {
988			guard: Some(ActorWorkGuard::new(self.clone(), kind)),
989		}
990	}
991
992	pub fn actor_id(&self) -> &str {
993		&self.0.actor_id
994	}
995
996	pub fn name(&self) -> &str {
997		&self.0.name
998	}
999
1000	pub fn key(&self) -> &ActorKey {
1001		&self.0.key
1002	}
1003
1004	pub(crate) fn actor_specifier(&self) -> Option<ActorSpecifier> {
1005		Some(
1006			ActorSpecifier::new(self.actor_id().to_owned(), self.sleep_generation()? as u64)
1007				.with_key(format_actor_key(self.key())),
1008		)
1009	}
1010
1011	pub(crate) fn attach_actor_to_error(&self, error: anyhow::Error) -> anyhow::Error {
1012		match self.actor_specifier() {
1013			Some(actor) => error.context(actor),
1014			None => error,
1015		}
1016	}
1017
1018	pub fn region(&self) -> &str {
1019		&self.0.region
1020	}
1021
1022	pub fn has_state(&self) -> bool {
1023		self.0.connection_config.read().has_state
1024	}
1025
1026	#[doc(hidden)]
1027	pub fn record_startup_create_state(&self, duration: Duration) {
1028		self.0.metrics.observe_create_state(duration);
1029	}
1030
1031	/// Runs a foreign runtime's startup preamble callback and records how long it took, so a
1032	/// slow start can be attributed to the callback that held it.
1033	#[doc(hidden)]
1034	pub async fn time_runtime_callback<T>(
1035		&self,
1036		callback: &'static str,
1037		future: impl std::future::Future<Output = T>,
1038	) -> T {
1039		let started_at = Instant::now();
1040		let result = future.await;
1041		self.0
1042			.metrics
1043			.observe_runtime_callback(callback, started_at.elapsed());
1044		result
1045	}
1046
1047	#[doc(hidden)]
1048	pub fn record_startup_create_vars(&self, duration: Duration) {
1049		self.0.metrics.observe_create_vars(duration);
1050	}
1051
1052	pub fn broadcast(&self, name: &str, args: &[u8]) {
1053		for connection in self.conns() {
1054			if connection.is_subscribed(name) {
1055				connection.send(name, args);
1056			}
1057		}
1058	}
1059
1060	/// Returns a lock-backed iterator over live connections.
1061	///
1062	/// Do not hold the returned iterator across `.await`. It keeps a read lock
1063	/// on the connection map until dropped, which blocks connection writers.
1064	#[must_use]
1065	pub fn conns(&self) -> ConnHandles<'_> {
1066		self.iter_connections()
1067	}
1068
1069	pub fn client_endpoint(&self) -> Option<&str> {
1070		self.0.client_endpoint.get().map(String::as_str)
1071	}
1072
1073	pub fn client_token(&self) -> Option<&str> {
1074		self.0.client_token.get().map(String::as_str)
1075	}
1076
1077	pub fn client_namespace(&self) -> Option<&str> {
1078		self.0.client_namespace.get().map(String::as_str)
1079	}
1080
1081	pub fn client_pool_name(&self) -> Option<&str> {
1082		self.0.client_pool_name.get().map(String::as_str)
1083	}
1084
1085	pub fn ack_hibernatable_websocket_message(
1086		&self,
1087		gateway_id: &[u8],
1088		request_id: &[u8],
1089		server_message_index: u16,
1090	) -> Result<()> {
1091		let gateway_id = hibernatable_id_from_slice("gateway_id", gateway_id)?;
1092		let request_id = hibernatable_id_from_slice("request_id", request_id)?;
1093		let envoy_handle = self.sleep_envoy_handle().ok_or_else(|| {
1094			ActorRuntime::NotConfigured {
1095				component: "hibernatable websocket ack".to_owned(),
1096			}
1097			.build()
1098		})?;
1099		envoy_handle.send_hibernatable_ws_message_ack(gateway_id, request_id, server_message_index);
1100		Ok(())
1101	}
1102
1103	pub(crate) fn load_persisted_actor(&self, persisted: PersistedActor) {
1104		self.load_persisted(persisted);
1105	}
1106
1107	pub(crate) fn persisted_actor(&self) -> PersistedActor {
1108		self.persisted()
1109	}
1110
1111	/// Dispatches any scheduled actions whose deadline has already passed.
1112	///
1113	/// Foreign-runtime adapters should call this after startup callbacks complete
1114	/// so overdue scheduled work enters the normal actor event loop.
1115	pub async fn drain_overdue_scheduled_events(&self) -> Result<()> {
1116		for dispatch in self.take_due_schedule_dispatches().await? {
1117			self.dispatch_scheduled_action(dispatch).await;
1118		}
1119		Ok(())
1120	}
1121
1122	pub(crate) fn metrics(&self) -> &ActorMetrics {
1123		&self.0.metrics
1124	}
1125
1126	/// Identity fields shared by every invocation on this actor. Built once so a
1127	/// span does not re-allocate them per action.
1128	pub(crate) fn telemetry_identity(&self) -> Arc<ActorTelemetryIdentity> {
1129		self.0.telemetry_identity.clone()
1130	}
1131
1132	pub(crate) fn tracing_config(&self) -> &ActorTracingConfig {
1133		&self.0.tracing_config
1134	}
1135
1136	pub(crate) fn record_user_task_started(&self, kind: UserTaskKind) {
1137		self.0.metrics.begin_user_task(kind);
1138	}
1139
1140	pub(crate) fn record_user_task_finished(&self, kind: UserTaskKind, duration: Duration) {
1141		self.0.metrics.end_user_task(kind, duration);
1142	}
1143
1144	pub(crate) fn record_shutdown_wait(
1145		&self,
1146		reason: crate::actor::task_types::ShutdownKind,
1147		duration: Duration,
1148	) {
1149		self.0.metrics.observe_shutdown_wait(reason, duration);
1150	}
1151
1152	pub(crate) fn record_shutdown_timeout(&self, reason: crate::actor::task_types::ShutdownKind) {
1153		self.0.metrics.inc_shutdown_timeout(reason);
1154	}
1155
1156	pub(crate) fn record_direct_subsystem_shutdown_warning(
1157		&self,
1158		subsystem: &str,
1159		operation: &str,
1160	) {
1161		self.0
1162			.metrics
1163			.inc_direct_subsystem_shutdown_warning(subsystem, operation);
1164	}
1165
1166	pub(crate) fn warn_work_sent_to_stopping_instance(&self, operation: &'static str) {
1167		if let Some(suppression) = self.0.diagnostics.record("work_sent_to_stopping_instance") {
1168			tracing::warn!(
1169				actor_id = %suppression.actor_id,
1170				operation,
1171				per_actor_suppressed = suppression.per_actor_suppressed,
1172				global_suppressed = suppression.global_suppressed,
1173				"work sent to stopping actor instance"
1174			);
1175		}
1176	}
1177
1178	pub(crate) fn warn_self_call_risk(&self, operation: &'static str) {
1179		if let Some(suppression) = self.0.diagnostics.record("self_call_risk") {
1180			tracing::warn!(
1181				actor_id = %suppression.actor_id,
1182				operation,
1183				per_actor_suppressed = suppression.per_actor_suppressed,
1184				global_suppressed = suppression.global_suppressed,
1185				"actor dispatch may be parked behind the current instance"
1186			);
1187		}
1188	}
1189
1190	
1191
1192	pub(crate) fn remove_conn(&self, conn_id: &str) -> Option<ConnHandle> {
1193		let removed = self.remove_existing(conn_id);
1194		if removed.is_some() {
1195			self.record_connections_updated();
1196			self.reset_sleep_timer();
1197		}
1198		removed
1199	}
1200
1201	pub(crate) fn configure_connection_runtime(&self, config: ActorConfig) {
1202		self.configure_sleep_state(config.clone());
1203		self.configure_connection_storage(config);
1204	}
1205
1206	pub(crate) fn configure_actor_events(&self, sender: Option<mpsc::UnboundedSender<ActorEvent>>) {
1207		*self.0.actor_events.write() = sender;
1208	}
1209
1210	pub(crate) fn try_send_actor_event(
1211		&self,
1212		event: ActorEvent,
1213		operation: &'static str,
1214	) -> Result<()> {
1215		let sender = self.0.actor_events.read().clone().ok_or_else(|| {
1216			ActorRuntime::NotConfigured {
1217				component: "actor event inbox".to_owned(),
1218			}
1219			.build()
1220		})?;
1221		tracing::debug!(
1222			actor_id = %self.actor_id(),
1223			operation,
1224			event = event.kind(),
1225			"actor event enqueued"
1226		);
1227		sender.send(event).map_err(|_| {
1228			ActorRuntime::NotConfigured {
1229				component: "actor event inbox".to_owned(),
1230			}
1231			.build()
1232		})
1233	}
1234
1235	#[doc(hidden)]
1236	pub fn configure_envoy(&self, envoy_handle: EnvoyHandle, generation: Option<u32>) {
1237		let _ = self
1238			.0
1239			.client_endpoint
1240			.set(envoy_handle.endpoint().to_owned());
1241		if let Some(token) = envoy_handle.token() {
1242			let _ = self.0.client_token.set(token.to_owned());
1243		}
1244		let _ = self
1245			.0
1246			.client_namespace
1247			.set(envoy_handle.namespace().to_owned());
1248		let _ = self
1249			.0
1250			.client_pool_name
1251			.set(envoy_handle.pool_name().to_owned());
1252		self.configure_sleep_envoy(envoy_handle.clone(), generation);
1253		self.configure_schedule_envoy(envoy_handle, generation);
1254	}
1255
1256	pub(crate) async fn connect_conn<F>(
1257		&self,
1258		params: Vec<u8>,
1259		is_hibernatable: bool,
1260		hibernation: Option<HibernatableConnectionMetadata>,
1261		request: Option<Request>,
1262		create_state: F,
1263	) -> Result<ConnHandle>
1264	where
1265		F: Future<Output = Result<Vec<u8>>> + Send,
1266	{
1267		let conn = self
1268			.connect_with_state(params, is_hibernatable, hibernation, request, create_state)
1269			.await?;
1270		self.record_connections_updated();
1271		self.reset_sleep_timer();
1272		Ok(conn)
1273	}
1274
1275	pub(crate) async fn connect_conn_with_prepare<F, P>(
1276		&self,
1277		params: Vec<u8>,
1278		is_hibernatable: bool,
1279		hibernation: Option<HibernatableConnectionMetadata>,
1280		request: Option<Request>,
1281		create_state: F,
1282		prepare_connection: P,
1283	) -> Result<ConnHandle>
1284	where
1285		F: Future<Output = Result<Vec<u8>>> + Send,
1286		P: FnOnce(&ConnHandle) -> Result<()>,
1287	{
1288		let conn = self
1289			.connect_with_state_and_prepare(
1290				params,
1291				is_hibernatable,
1292				hibernation,
1293				request,
1294				create_state,
1295				prepare_connection,
1296			)
1297			.await?;
1298		self.record_connections_updated();
1299		self.reset_sleep_timer();
1300		Ok(conn)
1301	}
1302
1303	pub async fn connect_conn_with_request<F>(
1304		&self,
1305		params: Vec<u8>,
1306		request: Option<Request>,
1307		create_state: F,
1308	) -> Result<ConnHandle>
1309	where
1310		F: Future<Output = Result<Vec<u8>>> + Send,
1311	{
1312		self.connect_conn(params, false, None, request, create_state)
1313			.await
1314	}
1315
1316	pub(crate) fn reconnect_hibernatable_conn(
1317		&self,
1318		gateway_id: &[u8],
1319		request_id: &[u8],
1320	) -> Result<ConnHandle> {
1321		self.reconnect_hibernatable(gateway_id, request_id)
1322	}
1323
1324	pub async fn disconnect_conn(&self, id: ConnId) -> Result<()> {
1325		self.disconnect_transport_only(|conn| conn.id() == id).await
1326	}
1327
1328	pub async fn disconnect_conns<F>(&self, predicate: F) -> Result<()>
1329	where
1330		F: FnMut(&ConnHandle) -> bool,
1331	{
1332		self.disconnect_transport_only(predicate).await
1333	}
1334
1335	pub(crate) fn request_hibernation_transport_save(&self, conn_id: &str) {
1336		self.queue_hibernation_update(conn_id.to_owned());
1337		self.request_save(RequestSaveOpts::default());
1338	}
1339
1340	pub(crate) fn request_hibernation_transport_removal(&self, conn_id: impl Into<String>) {
1341		self.queue_hibernation_removal_inner(conn_id.into());
1342		self.request_save(RequestSaveOpts::default());
1343	}
1344
1345	pub fn queue_hibernation_removal(&self, conn_id: impl Into<String>) {
1346		self.request_hibernation_transport_removal(conn_id);
1347	}
1348
1349	pub fn has_pending_hibernation_changes(&self) -> bool {
1350		self.has_pending_hibernation_changes_inner()
1351	}
1352
1353	pub fn take_pending_hibernation_changes(&self) -> Vec<ConnId> {
1354		self.pending_hibernation_removals()
1355	}
1356
1357	pub fn dirty_hibernatable_conns(&self) -> Vec<ConnHandle> {
1358		self.dirty_hibernatable_conns_inner()
1359	}
1360
1361	pub(crate) fn hibernated_connection_is_live(
1362		&self,
1363		gateway_id: &[u8],
1364		request_id: &[u8],
1365	) -> Result<bool> {
1366		let gateway_id = hibernatable_id_from_slice("gateway_id", gateway_id)?;
1367		let request_id = hibernatable_id_from_slice("request_id", request_id)?;
1368
1369		if let Some(override_pairs) = self
1370			.0
1371			.hibernated_connection_liveness_override
1372			.read()
1373			.as_ref()
1374		{
1375			return Ok(override_pairs.contains(&(gateway_id.to_vec(), request_id.to_vec())));
1376		}
1377
1378		let Some(envoy_handle) = self.sleep_envoy_handle() else {
1379			return Ok(false);
1380		};
1381		let is_live = envoy_handle.hibernatable_connection_is_live(
1382			self.actor_id(),
1383			self.sleep_generation(),
1384			gateway_id,
1385			request_id,
1386		);
1387		Ok(is_live)
1388	}
1389
1390	
1391
1392	pub(super) fn prepare_state_deltas(
1393		&self,
1394		deltas: Vec<StateDelta>,
1395	) -> Result<(Vec<StateDelta>, PendingHibernationChanges)> {
1396		fn finish_with_error(
1397			ctx: &ActorContext,
1398			pending: PendingHibernationChanges,
1399			error: anyhow::Error,
1400		) -> Result<(Vec<StateDelta>, PendingHibernationChanges)> {
1401			ctx.restore_pending_hibernation_changes(pending);
1402			Err(error)
1403		}
1404
1405		let mut next_deltas = Vec::new();
1406		let mut explicit_updates = std::collections::BTreeMap::new();
1407		let mut explicit_removals = std::collections::BTreeSet::new();
1408
1409		for delta in deltas {
1410			match delta {
1411				StateDelta::ConnHibernation { conn, bytes } => {
1412					explicit_updates.insert(conn, bytes);
1413				}
1414				StateDelta::ConnHibernationRemoved(conn) => {
1415					explicit_removals.insert(conn);
1416				}
1417				other => next_deltas.push(other),
1418			}
1419		}
1420
1421		let mut pending = self.take_pending_hibernation_changes_inner();
1422		let mut removal_ids = pending.removed.clone();
1423		removal_ids.extend(explicit_removals.iter().cloned());
1424
1425		let explicit_update_ids: std::collections::BTreeSet<_> =
1426			explicit_updates.keys().cloned().collect();
1427		pending.updated.extend(explicit_update_ids.iter().cloned());
1428		pending.removed.extend(explicit_removals.iter().cloned());
1429
1430		for (conn, bytes) in explicit_updates {
1431			if removal_ids.contains(&conn) {
1432				continue;
1433			}
1434			let encoded = match self.encode_hibernation_delta(&conn, bytes) {
1435				Ok(encoded) => encoded,
1436				Err(error) => {
1437					return finish_with_error(self, pending, error);
1438				}
1439			};
1440			next_deltas.push(StateDelta::ConnHibernation {
1441				conn,
1442				bytes: encoded,
1443			});
1444		}
1445
1446		for conn in &pending.updated {
1447			if removal_ids.contains(conn)
1448				|| explicit_removals.contains(conn)
1449				|| explicit_update_ids.contains(conn)
1450			{
1451				continue;
1452			}
1453			let Some(handle) = self.connection(conn) else {
1454				continue;
1455			};
1456			if !handle.is_hibernatable() || handle.hibernation().is_none() {
1457				continue;
1458			}
1459			let encoded = match self.encode_hibernation_delta(conn, handle.state()) {
1460				Ok(encoded) => encoded,
1461				Err(error) => {
1462					return finish_with_error(self, pending, error);
1463				}
1464			};
1465			next_deltas.push(StateDelta::ConnHibernation {
1466				conn: conn.clone(),
1467				bytes: encoded,
1468			});
1469		}
1470
1471		for conn in removal_ids {
1472			next_deltas.push(StateDelta::ConnHibernationRemoved(conn));
1473		}
1474
1475		Ok((next_deltas, pending))
1476	}
1477
1478	pub(crate) async fn restore_hibernatable_connections(&self) -> Result<Vec<ConnHandle>> {
1479		let restored = self.restore_persisted().await?;
1480		if !restored.is_empty() {
1481			if let Some(envoy_handle) = self.sleep_envoy_handle() {
1482				let meta_entries: Vec<_> = restored
1483					.iter()
1484					.filter_map(|conn| {
1485						let hibernation = conn.hibernation()?;
1486						Some(HibernatingWebSocketMetadata {
1487							gateway_id: hibernation.gateway_id,
1488							request_id: hibernation.request_id,
1489							envoy_message_index: hibernation.client_message_index,
1490							rivet_message_index: hibernation.server_message_index,
1491							path: hibernation.request_path,
1492							headers: hibernation.request_headers.into_iter().collect(),
1493						})
1494					})
1495					.collect();
1496				envoy_handle.restore_hibernating_requests(self.actor_id().to_owned(), meta_entries);
1497			}
1498			self.record_connections_updated();
1499			self.reset_sleep_timer();
1500		}
1501		Ok(restored)
1502	}
1503
1504	pub(crate) fn configure_inspector(&self, inspector: Option<Inspector>) {
1505		*self.0.inspector.write() = inspector;
1506	}
1507
1508	pub(crate) fn inspector(&self) -> Option<Inspector> {
1509		self.0.inspector.read().clone()
1510	}
1511
1512	pub fn inspector_snapshot(&self) -> InspectorSnapshot {
1513		self.inspector()
1514			.map(|inspector| inspector.snapshot())
1515			.unwrap_or_default()
1516	}
1517
1518	pub(crate) fn configure_inspector_runtime(
1519		&self,
1520		attach_count: Arc<AtomicU32>,
1521		overlay_tx: broadcast::Sender<Arc<Vec<u8>>>,
1522	) {
1523		*self.0.inspector_attach_count.write() = Some(attach_count);
1524		*self.0.inspector_overlay_tx.write() = Some(overlay_tx);
1525	}
1526
1527	pub(crate) fn inspector_attach(&self) -> Option<InspectorAttachGuard> {
1528		InspectorAttachGuard::new(self.clone())
1529	}
1530
1531	
1532
1533	pub(crate) fn subscribe_inspector(&self) -> Option<broadcast::Receiver<Arc<Vec<u8>>>> {
1534		self.0
1535			.inspector_overlay_tx
1536			.read()
1537			.clone()
1538			.map(|overlay_tx| overlay_tx.subscribe())
1539	}
1540
1541	pub(crate) fn downgrade(&self) -> Weak<ActorContextInner> {
1542		Arc::downgrade(&self.0)
1543	}
1544
1545	pub(crate) fn from_weak(weak: &Weak<ActorContextInner>) -> Option<Self> {
1546		weak.upgrade().map(|inner| Self(inner, None))
1547	}
1548
1549	#[doc(hidden)]
1550	pub fn set_started(&self, started: bool) {
1551		self.set_lifecycle_started(started);
1552		self.reset_sleep_timer();
1553	}
1554
1555	#[doc(hidden)]
1556	pub fn started(&self) -> bool {
1557		self.lifecycle_started()
1558	}
1559
1560	pub(crate) fn destroy_requested(&self) -> bool {
1561		self.0.destroy_requested.load(Ordering::SeqCst)
1562	}
1563
1564	pub fn is_destroy_requested(&self) -> bool {
1565		self.destroy_requested()
1566	}
1567
1568	pub(crate) async fn wait_for_destroy_completion(&self) {
1569		if self.0.destroy_completed.load(Ordering::SeqCst) {
1570			return;
1571		}
1572
1573		loop {
1574			let notified = self.0.destroy_completion_notify.notified();
1575			if self.0.destroy_completed.load(Ordering::SeqCst) {
1576				return;
1577			}
1578			notified.await;
1579			if self.0.destroy_completed.load(Ordering::SeqCst) {
1580				return;
1581			}
1582		}
1583	}
1584
1585	pub async fn wait_for_destroy_completion_public(&self) {
1586		self.wait_for_destroy_completion().await;
1587	}
1588
1589	pub(crate) fn mark_destroy_completed(&self) {
1590		self.0.destroy_completed.store(true, Ordering::SeqCst);
1591		self.0.destroy_completion_notify.notify_waiters();
1592	}
1593
1594	pub(crate) async fn can_sleep(&self) -> CanSleep {
1595		self.can_arm_sleep_timer().await
1596	}
1597
1598	pub(crate) fn pending_disconnect_count(&self) -> usize {
1599		self.0.sleep.work.disconnect_callback.load()
1600	}
1601
1602	pub async fn with_disconnect_callback<F, Fut, T>(&self, run: F) -> T
1603	where
1604		F: FnOnce() -> Fut,
1605		Fut: Future<Output = T>,
1606	{
1607		self.track_work(ActorWorkKind::DisconnectCallback, run())
1608			.await
1609	}
1610
1611	pub(crate) fn configure_lifecycle_events(
1612		&self,
1613		sender: Option<mpsc::UnboundedSender<LifecycleEvent>>,
1614	) {
1615		*self.0.lifecycle_events.write() = sender;
1616	}
1617
1618	pub(crate) fn notify_inspector_serialize_requested(&self) {
1619		self.try_send_lifecycle_event(
1620			LifecycleEvent::InspectorSerializeRequested,
1621			"inspector_serialize_requested",
1622		);
1623	}
1624
1625	pub(crate) fn notify_activity_dirty(&self) -> bool {
1626		if self.0.lifecycle_events.read().is_none() {
1627			return false;
1628		}
1629		if self.0.activity.mark_dirty() {
1630			self.sleep_activity_notify().notify_one();
1631		}
1632		true
1633	}
1634
1635	pub(crate) fn acknowledge_activity_dirty(&self) -> bool {
1636		self.0.activity.take_dirty()
1637	}
1638
1639	/// Notify the ActorTask that a `can_sleep` input has changed so the sleep
1640	/// deadline gets re-evaluated. Falls back to the detached compat timer
1641	/// when the actor has no wired `ActorTask` (test-only contexts).
1642	pub(crate) fn reset_sleep_timer(&self) {
1643		if self.notify_activity_dirty() {
1644			return;
1645		}
1646
1647		#[cfg(feature = "wasm-runtime")]
1648		return;
1649
1650		#[cfg(not(feature = "wasm-runtime"))]
1651		self.reset_sleep_timer_state();
1652	}
1653
1654	fn notify_inspector_attachments_changed(&self) {
1655		self.try_send_lifecycle_event(
1656			LifecycleEvent::InspectorAttachmentsChanged,
1657			"inspector_attachments_changed",
1658		);
1659	}
1660
1661	pub(crate) fn configure_sleep(&self, config: ActorConfig) {
1662		self.configure_sleep_state(config.clone());
1663		self.configure_queue(config);
1664		self.reset_sleep_timer();
1665	}
1666
1667	pub(crate) fn sleep_config(&self) -> ActorConfig {
1668		self.sleep_state_config()
1669	}
1670
1671	pub(crate) fn sleep_requested(&self) -> bool {
1672		self.0.sleep_requested.load(Ordering::SeqCst)
1673	}
1674
1675	pub(crate) fn clear_sleep_requested(&self) {
1676		self.0.sleep_requested.store(false, Ordering::SeqCst);
1677	}
1678
1679	pub(crate) async fn internal_keep_awake_task(
1680		&self,
1681		future: BoxFuture<'static, Result<()>>,
1682	) -> Result<()> {
1683		self.internal_keep_awake(future).await
1684	}
1685
1686	pub fn websocket_callback_region(&self) -> WebSocketCallbackRegion {
1687		WebSocketCallbackRegion {
1688			region: Some(self.begin_work_region(ActorWorkKind::WebSocketCallback)),
1689		}
1690	}
1691
1692	pub(crate) async fn with_websocket_callback<F, Fut, T>(&self, run: F) -> T
1693	where
1694		F: FnOnce() -> Fut,
1695		Fut: Future<Output = T>,
1696	{
1697		let _guard = self.websocket_callback_region();
1698		run().await
1699	}
1700
1701	fn idle_work_region(&self, kind: ActorWorkKind) -> Option<RegionGuard> {
1702		if !kind.policy().blocks_idle_sleep {
1703			return None;
1704		}
1705		let region = match kind {
1706			ActorWorkKind::Action => self.internal_keep_awake_region(),
1707			ActorWorkKind::KeepAwake => self.keep_awake_region_state(),
1708			ActorWorkKind::InternalKeepAwake => self.internal_keep_awake_region(),
1709			ActorWorkKind::WaitUntil => return None,
1710			ActorWorkKind::RegisteredTask => return None,
1711			ActorWorkKind::WebSocketCallback => self.websocket_callback_region_state(),
1712			ActorWorkKind::DisconnectCallback => self.disconnect_callback_region_state(),
1713		};
1714		Some(region.with_log_fields(kind.label(), Some(self.actor_id().to_owned())))
1715	}
1716
1717	fn shutdown_work_region(&self) -> CountGuard {
1718		let counter = self.0.sleep.work.shutdown_counter.clone();
1719		counter.increment();
1720		CountGuard::from_incremented(counter)
1721	}
1722
1723	fn configure_sleep_hooks(&self) {
1724		let keep_awake_ctx = self.downgrade();
1725		self.0
1726			.sleep
1727			.work
1728			.keep_awake
1729			.register_change_callback(Arc::new(move || {
1730				if let Some(ctx) = ActorContext::from_weak(&keep_awake_ctx) {
1731					ctx.0
1732						.metrics
1733						.set_keep_awake_active(ctx.sleep_keep_awake_count());
1734				}
1735			}));
1736
1737		let internal_keep_awake_metric_ctx = self.downgrade();
1738		self.0
1739			.sleep
1740			.work
1741			.internal_keep_awake
1742			.register_change_callback(Arc::new(move || {
1743				if let Some(ctx) = ActorContext::from_weak(&internal_keep_awake_metric_ctx) {
1744					ctx.0
1745						.metrics
1746						.set_internal_keep_awake_active(ctx.sleep_internal_keep_awake_count());
1747				}
1748			}));
1749
1750		let shutdown_tasks_ctx = self.downgrade();
1751		self.0
1752			.sleep
1753			.work
1754			.shutdown_counter
1755			.register_change_callback(Arc::new(move || {
1756				if let Some(ctx) = ActorContext::from_weak(&shutdown_tasks_ctx) {
1757					ctx.0
1758						.metrics
1759						.set_shutdown_tasks_active(ctx.shutdown_task_count());
1760				}
1761			}));
1762
1763		let internal_keep_awake_ctx = self.downgrade();
1764		self.set_internal_keep_awake(Some(Arc::new(move |future| {
1765			let ctx = ActorContext::from_weak(&internal_keep_awake_ctx);
1766			Box::pin(async move {
1767				let Some(ctx) = ctx else {
1768					return Err(ActorRuntime::NotConfigured {
1769						component: "actor context".to_owned(),
1770					}
1771					.build());
1772				};
1773				ctx.internal_keep_awake_task(future).await
1774			})
1775		})));
1776
1777		let queue_ctx = self.downgrade();
1778		self.set_wait_activity_callback(Some(Arc::new(move || {
1779			if let Some(ctx) = ActorContext::from_weak(&queue_ctx) {
1780				ctx.reset_sleep_timer();
1781			}
1782		})));
1783
1784		let queue_ctx = self.downgrade();
1785		self.set_inspector_update_callback(Some(Arc::new(move |queue_size| {
1786			if let Some(ctx) = ActorContext::from_weak(&queue_ctx) {
1787				ctx.record_queue_updated(queue_size);
1788			}
1789		})));
1790	}
1791
1792	pub(crate) fn record_state_updated(&self) {
1793		if let Some(inspector) = self.inspector() {
1794			inspector.record_state_updated();
1795		}
1796	}
1797
1798	pub(crate) fn record_connections_updated(&self) {
1799		let Some(inspector) = self.inspector() else {
1800			return;
1801		};
1802		let active_connections = self.active_connection_count();
1803		inspector.record_connections_updated(active_connections);
1804	}
1805
1806	fn record_queue_updated(&self, queue_size: u32) {
1807		if let Some(inspector) = self.inspector() {
1808			inspector.record_queue_updated(queue_size);
1809		}
1810	}
1811
1812	pub(crate) fn record_schedules_updated(&self) {
1813		if let Some(inspector) = self.inspector() {
1814			inspector.record_schedules_updated();
1815		}
1816	}
1817
1818	pub(crate) async fn save_state_with_revision(
1819		&self,
1820		deltas: Vec<StateDelta>,
1821		save_request_revision: u64,
1822	) -> Result<()> {
1823		let (deltas, pending_hibernation_changes) = match self.prepare_state_deltas(deltas) {
1824			Ok(prepared) => prepared,
1825			Err(error) => return Err(error),
1826		};
1827		if let Err(error) = self.apply_state_deltas(deltas, save_request_revision).await {
1828			self.restore_pending_hibernation_changes(pending_hibernation_changes);
1829			return Err(error);
1830		}
1831		self.record_state_updated();
1832		Ok(())
1833	}
1834
1835	pub(crate) fn state_transaction_epoch(&self) -> u64 {
1836		self.0.state_transaction_epoch.load(Ordering::SeqCst)
1837	}
1838
1839	pub(crate) async fn save_state_with_revision_at_transaction_epoch(
1840		&self,
1841		deltas: Vec<StateDelta>,
1842		save_request_revision: u64,
1843		state_transaction_epoch: u64,
1844	) -> Result<bool> {
1845		let (deltas, pending_hibernation_changes) = match self.prepare_state_deltas(deltas) {
1846			Ok(prepared) => prepared,
1847			Err(error) => return Err(error),
1848		};
1849		match self
1850			.apply_state_deltas_inner(
1851				deltas,
1852				None,
1853				save_request_revision,
1854				Some(state_transaction_epoch),
1855			)
1856			.await
1857		{
1858			Ok(true) => {
1859				self.record_state_updated();
1860				Ok(true)
1861			}
1862			Ok(false) => {
1863				self.restore_pending_hibernation_changes(pending_hibernation_changes);
1864				Ok(false)
1865			}
1866			Err(error) => {
1867				self.restore_pending_hibernation_changes(pending_hibernation_changes);
1868				Err(error)
1869			}
1870		}
1871	}
1872
1873	
1874
1875	pub(crate) async fn save_state_and_workflow_batch_at_transaction_epoch(
1876		&self,
1877		deltas: Vec<StateDelta>,
1878		workflow_writes: Vec<WorkflowKvWrite>,
1879		save_request_revision: u64,
1880		state_transaction_epoch: u64,
1881	) -> Result<bool> {
1882		self.save_state_and_workflow_batch_with_revision_inner(
1883			deltas,
1884			workflow_writes,
1885			save_request_revision,
1886			Some(state_transaction_epoch),
1887		)
1888		.await
1889	}
1890
1891	async fn save_state_and_workflow_batch_with_revision_inner(
1892		&self,
1893		deltas: Vec<StateDelta>,
1894		workflow_writes: Vec<WorkflowKvWrite>,
1895		save_request_revision: u64,
1896		expected_state_transaction_epoch: Option<u64>,
1897	) -> Result<bool> {
1898		let (deltas, pending_hibernation_changes) = match self.prepare_state_deltas(deltas) {
1899			Ok(prepared) => prepared,
1900			Err(error) => return Err(error),
1901		};
1902		match self
1903			.apply_state_deltas_inner(
1904				deltas,
1905				Some(workflow_writes),
1906				save_request_revision,
1907				expected_state_transaction_epoch,
1908			)
1909			.await
1910		{
1911			Ok(true) => {
1912				self.record_state_updated();
1913				Ok(true)
1914			}
1915			Ok(false) => {
1916				self.restore_pending_hibernation_changes(pending_hibernation_changes);
1917				Ok(false)
1918			}
1919			Err(error) => {
1920				self.restore_pending_hibernation_changes(pending_hibernation_changes);
1921				Err(error)
1922			}
1923		}
1924	}
1925
1926	async fn dispatch_scheduled_action(
1927		&self,
1928		dispatch: crate::actor::schedule::DueScheduleDispatch,
1929	) {
1930		let ctx = self.clone();
1931		let event_id = dispatch.event_id;
1932		let action = dispatch.action;
1933		let args = dispatch.args;
1934		let scheduled_fire = dispatch.fire;
1935		let recurring_name = scheduled_fire.name.clone();
1936		let history_id = dispatch.history_id;
1937		let internal_keep_awake_region = self.begin_work_region(ActorWorkKind::InternalKeepAwake);
1938
1939		self.track_shutdown_task(async move {
1940			let _internal_keep_awake_region = internal_keep_awake_region;
1941			ctx.record_user_task_started(UserTaskKind::ScheduledAction);
1942			let user_task_started_at = Instant::now();
1943			let action_name = action.clone();
1944			let invocation = crate::telemetry::ActorInvocation::start_scheduled(
1945				&ctx,
1946				&action_name,
1947				dispatch.trace_context,
1948			);
1949			let invocation_telemetry = invocation.telemetry();
1950			let (reply_tx, reply_rx) = oneshot::channel();
1951
1952			let mut dispatch_error = None;
1953			match ctx.try_send_actor_event(
1954				ActorEvent::Action {
1955					name: action.clone(),
1956					args,
1957					conn: None,
1958					scheduled_fire: Some(scheduled_fire),
1959					invocation_telemetry: Some(invocation_telemetry),
1960					reply: Reply::from(reply_tx),
1961				},
1962				"scheduled_action",
1963			) {
1964				Ok(()) => match reply_rx.await {
1965					Ok(Ok(_)) => {}
1966					Ok(Err(error)) => {
1967						dispatch_error = Some(error);
1968						tracing::error!(
1969							error = ?dispatch_error.as_ref().expect("just assigned"),
1970							event_id,
1971							action_name,
1972							"scheduled event execution failed"
1973						);
1974					}
1975					Err(_) => {
1976						dispatch_error = Some(ActorLifecycleError::DroppedReply.build());
1977						tracing::error!(
1978							error = ?dispatch_error.as_ref().expect("just assigned"),
1979							event_id,
1980							action_name,
1981							"scheduled event reply dropped"
1982						);
1983					}
1984				},
1985				Err(error) => {
1986					dispatch_error = Some(error);
1987					tracing::error!(
1988						error = ?dispatch_error.as_ref().expect("just assigned"),
1989						event_id,
1990						action_name,
1991						"failed to enqueue scheduled event"
1992					);
1993				}
1994			}
1995			invocation.finish(dispatch_error.as_ref());
1996
1997			ctx.finish_schedule_dispatch(&event_id, history_id, dispatch_error.as_ref())
1998				.await;
1999			if let (Some(name), Some(error)) = (recurring_name, dispatch_error.as_ref()) {
2000				let structured = rivet_error::RivetError::extract(error);
2001				if structured.group() == "actor" && structured.code() == "action_not_found" {
2002					if let Err(delete_error) = ctx.cron_delete_if_action(&name, &action_name).await
2003					{
2004						tracing::error!(
2005							?delete_error,
2006							%name,
2007							"failed to delete recurring schedule for missing action"
2008						);
2009					}
2010				}
2011			}
2012
2013			ctx.record_user_task_finished(
2014				UserTaskKind::ScheduledAction,
2015				user_task_started_at.elapsed(),
2016			);
2017		});
2018	}
2019
2020	fn inspector_attach_count_arc(&self) -> Option<Arc<AtomicU32>> {
2021		self.0.inspector_attach_count.read().clone()
2022	}
2023
2024	fn try_send_lifecycle_event(&self, event: LifecycleEvent, operation: &'static str) {
2025		let Some(sender) = self.0.lifecycle_events.read().clone() else {
2026			return;
2027		};
2028
2029		if sender.send(event).is_err() {
2030			tracing::warn!(operation, "failed to enqueue actor lifecycle event");
2031		}
2032	}
2033}
2034
2035/// Cap on the stop error message forwarded to the engine. The message is
2036/// persisted in engine workflow state and rendered in the dashboard, so
2037/// unbounded anyhow chains must be truncated at this boundary.
2038const MAX_STOP_ERROR_MESSAGE_LEN: usize = 2048;
2039
2040fn truncate_stop_error_message(mut message: String) -> String {
2041	if message.len() <= MAX_STOP_ERROR_MESSAGE_LEN {
2042		return message;
2043	}
2044	let mut end = MAX_STOP_ERROR_MESSAGE_LEN;
2045	while !message.is_char_boundary(end) {
2046		end -= 1;
2047	}
2048	message.truncate(end);
2049	message.push_str("... (truncated)");
2050	message
2051}
2052
2053struct ActorWorkGuard {
2054	ctx: ActorContext,
2055	kind: ActorWorkKind,
2056	started_at: Option<Instant>,
2057	idle_region: Option<RegionGuard>,
2058	shutdown_region: Option<CountGuard>,
2059}
2060
2061#[must_use]
2062pub struct ActorWorkRegion {
2063	guard: Option<ActorWorkGuard>,
2064}
2065
2066impl ActorWorkGuard {
2067	fn new(ctx: ActorContext, kind: ActorWorkKind) -> Self {
2068		let policy = kind.policy();
2069		let idle_region = ctx.idle_work_region(kind);
2070		let shutdown_region = if policy.drains_shutdown_grace {
2071			Some(ctx.shutdown_work_region())
2072		} else {
2073			None
2074		};
2075		let started_at = if let Some(user_task_kind) = policy.user_task_kind {
2076			ctx.record_user_task_started(user_task_kind);
2077			Some(Instant::now())
2078		} else {
2079			None
2080		};
2081		ctx.reset_sleep_timer();
2082		Self {
2083			ctx,
2084			kind,
2085			started_at,
2086			idle_region,
2087			shutdown_region,
2088		}
2089	}
2090}
2091
2092impl Drop for ActorWorkGuard {
2093	fn drop(&mut self) {
2094		if let Some(started_at) = self.started_at.take()
2095			&& let Some(user_task_kind) = self.kind.policy().user_task_kind
2096		{
2097			self.ctx
2098				.record_user_task_finished(user_task_kind, started_at.elapsed());
2099		}
2100		self.idle_region.take();
2101		self.shutdown_region.take();
2102		self.ctx.reset_sleep_timer();
2103	}
2104}
2105
2106impl Drop for ActorWorkRegion {
2107	fn drop(&mut self) {
2108		self.guard.take();
2109	}
2110}
2111
2112#[must_use]
2113#[derive(Debug)]
2114pub(crate) struct InspectorAttachGuard {
2115	ctx: ActorContext,
2116}
2117
2118impl InspectorAttachGuard {
2119	fn new(ctx: ActorContext) -> Option<Self> {
2120		let attach_count = ctx.inspector_attach_count_arc()?;
2121		let previous = attach_count.fetch_add(1, Ordering::SeqCst);
2122		let current = previous.saturating_add(1);
2123		tracing::debug!(
2124			actor_id = %ctx.actor_id(),
2125			previous_count = previous,
2126			current_count = current,
2127			"inspector attached"
2128		);
2129		if previous == 0 {
2130			ctx.notify_inspector_attachments_changed();
2131		}
2132		Some(Self { ctx })
2133	}
2134}
2135
2136impl Drop for InspectorAttachGuard {
2137	fn drop(&mut self) {
2138		let Some(attach_count) = self.ctx.inspector_attach_count_arc() else {
2139			return;
2140		};
2141		let Ok(previous) = attach_count.try_update(Ordering::SeqCst, Ordering::SeqCst, |current| {
2142			current.checked_sub(1)
2143		}) else {
2144			return;
2145		};
2146		let current = previous.saturating_sub(1);
2147		tracing::debug!(
2148			actor_id = %self.ctx.actor_id(),
2149			previous_count = previous,
2150			current_count = current,
2151			"inspector detached"
2152		);
2153		if previous == 1 {
2154			self.ctx.notify_inspector_attachments_changed();
2155		}
2156	}
2157}
2158
2159pub struct WebSocketCallbackRegion {
2160	region: Option<ActorWorkRegion>,
2161}
2162
2163pub struct KeepAwakeRegion {
2164	region: Option<ActorWorkRegion>,
2165}
2166
2167impl Drop for WebSocketCallbackRegion {
2168	fn drop(&mut self) {
2169		self.region.take();
2170	}
2171}
2172
2173impl Drop for KeepAwakeRegion {
2174	fn drop(&mut self) {
2175		// Take the region explicitly to mirror WebSocketCallbackRegion.
2176		self.region.take();
2177	}
2178}
2179
2180impl std::fmt::Debug for ActorContext {
2181	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2182		f.debug_struct("ActorContext")
2183			.field("actor_id", &self.0.actor_id)
2184			.field("name", &self.0.name)
2185			.field("key", &self.0.key)
2186			.field("region", &self.0.region)
2187			.finish()
2188	}
2189}
2190
2191// Test shim keeps moved tests in crate-root tests/ with private-module access.
2192