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