Skip to main content

rivetkit_core/actor/
context.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::future::Future;
3use std::sync::Weak;
4use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};
5use std::sync::{Arc, OnceLock};
6use std::time::Duration;
7
8use crate::time::{Instant, SystemTime, UNIX_EPOCH};
9
10use anyhow::{Context as AnyhowContext, Result};
11use futures::future::BoxFuture;
12use parking_lot::{Mutex, RwLock};
13use rivet_envoy_client::handle::EnvoyHandle;
14use rivet_envoy_client::tunnel::HibernatingWebSocketMetadata;
15use rivet_error::ActorSpecifier;
16use scc::HashMap as SccHashMap;
17use tokio::runtime::Handle;
18use tokio::sync::{Mutex as AsyncMutex, Notify, OnceCell, broadcast, mpsc, oneshot};
19use tokio::task::JoinHandle;
20use tokio_util::sync::CancellationToken;
21
22use crate::ActorConfig;
23use crate::actor::connection::{
24	ConnHandle, ConnHandles, HibernatableConnectionMetadata, PendingHibernationChanges,
25	hibernatable_id_from_slice,
26};
27use crate::actor::diagnostics::ActorDiagnostics;
28use crate::actor::lifecycle_hooks::Reply;
29use crate::actor::messages::{ActorEvent, Request, StateDelta};
30use crate::actor::metrics::ActorMetrics;
31use crate::actor::preload::PreloadedKv;
32use crate::actor::queue::{QueueInspectorUpdateCallback, QueueMetadata, QueueWaitActivityCallback};
33use crate::actor::schedule::{InternalKeepAwakeCallback, LocalAlarmCallback};
34use crate::actor::sleep::{CanSleep, SleepState};
35use crate::actor::state::{PendingSave, PersistedActor, RequestSaveOpts};
36use crate::actor::task::LifecycleEvent;
37use crate::actor::task_types::UserTaskKind;
38use crate::actor::work_registry::{ActorWorkKind, CountGuard, RegionGuard};
39use crate::error::{ActorLifecycle as ActorLifecycleError, ActorRuntime};
40use crate::inspector::{Inspector, InspectorSnapshot};
41use crate::kv::Kv;
42use crate::sqlite::SqliteDb;
43use crate::types::{ActorKey, ConnId, ListOpts, format_actor_key};
44
45/// Shared actor runtime context.
46///
47/// This public surface is the foreign-runtime contract for `rivetkit-core`.
48/// Native Rust, NAPI-backed TypeScript, and future V8 runtimes should be able
49/// to drive actor behavior through `ActorFactory` plus the methods exposed here
50/// and on the returned runtime objects like `Kv`, `SqliteDb`, schedule APIs,
51/// queue APIs, `ConnHandle`, and `WebSocket`.
52#[derive(Clone)]
53pub struct ActorContext(pub(crate) Arc<ActorContextInner>);
54
55pub(crate) struct ActorContextInner {
56	pub(super) kv: Kv,
57	sql: SqliteDb,
58	// Forced-sync: actor state snapshots are exposed through synchronous
59	// accessors and are never held across `.await`.
60	pub(super) current_state: RwLock<Vec<u8>>,
61	pub(super) persisted: RwLock<PersistedActor>,
62	pub(super) last_pushed_alarm: RwLock<Option<i64>>,
63	pub(super) state_save_interval: Duration,
64	pub(super) state_dirty: AtomicBool,
65	pub(super) state_revision: AtomicU64,
66	pub(super) save_request_revision: AtomicU64,
67	pub(super) save_completed_revision: AtomicU64,
68	pub(super) save_completion: Notify,
69	pub(super) save_requested: AtomicBool,
70	pub(super) save_requested_immediate: AtomicBool,
71	// Forced-sync: debounce bookkeeping is updated from sync save-request paths.
72	pub(super) save_requested_within_deadline: Mutex<Option<crate::time::Instant>>,
73	pub(super) last_save_at: Mutex<Option<crate::time::Instant>>,
74	pub(super) pending_save: Mutex<Option<PendingSave>>,
75	pub(super) tracked_persist: Mutex<Option<JoinHandle<()>>>,
76	pub(super) save_guard: AsyncMutex<()>,
77	pub(super) in_flight_state_writes: AtomicUsize,
78	pub(super) state_write_completion: Notify,
79	pub(super) on_state_change_in_flight: AtomicUsize,
80	pub(super) on_state_change_idle: Notify,
81	// Forced-sync: hooks are registered and cloned from synchronous runtime
82	// wiring slots before use.
83	pub(super) request_save_hooks: RwLock<Vec<Arc<dyn Fn(RequestSaveOpts) + Send + Sync>>>,
84	// Forced-sync: schedule runtime handles and callbacks are synchronous
85	// wiring slots cloned before actor/envoy I/O.
86	pub(super) schedule_generation: Mutex<Option<u32>>,
87	pub(super) schedule_envoy_handle: Mutex<Option<EnvoyHandle>>,
88	pub(super) client_endpoint: OnceLock<String>,
89	pub(super) client_token: OnceLock<String>,
90	pub(super) client_namespace: OnceLock<String>,
91	pub(super) client_pool_name: OnceLock<String>,
92	pub(super) schedule_internal_keep_awake: Mutex<Option<InternalKeepAwakeCallback>>,
93	pub(super) schedule_local_alarm_callback: Mutex<Option<LocalAlarmCallback>>,
94	// Forced-sync: the local alarm timer is aborted from sync paths.
95	pub(super) schedule_local_alarm_task: Mutex<Option<JoinHandle<()>>>,
96	// Forced-sync: receivers are pushed/taken from sync paths and awaited after
97	// being moved out of the lock.
98	pub(super) schedule_pending_alarm_writes: Mutex<Vec<oneshot::Receiver<()>>>,
99	pub(super) schedule_local_alarm_epoch: AtomicU64,
100	pub(super) schedule_alarm_dispatch_enabled: AtomicBool,
101	pub(super) schedule_dirty_since_push: AtomicBool,
102	#[cfg(test)]
103	pub(super) schedule_driver_alarm_cancel_count: AtomicUsize,
104	// Forced-sync: queue config is read from sync public methods before blocking
105	// on async queue work.
106	pub(super) queue_config: Mutex<ActorConfig>,
107	pub(super) queue_abort_signal: Mutex<CancellationToken>,
108	pub(super) queue_initialize: OnceCell<()>,
109	// Forced-sync: startup installs preload before any queue method awaits init.
110	pub(super) queue_preloaded_kv: Mutex<Option<PreloadedKv>>,
111	pub(super) queue_preloaded_message_entries: Mutex<Option<Vec<(Vec<u8>, Vec<u8>)>>>,
112	pub(super) queue_metadata: AsyncMutex<QueueMetadata>,
113	pub(super) queue_receive_lock: AsyncMutex<()>,
114	pub(super) queue_completion_waiters: SccHashMap<u64, oneshot::Sender<Option<Vec<u8>>>>,
115	pub(super) queue_notify: Notify,
116	pub(super) active_queue_wait_count: AtomicU32,
117	// Forced-sync: callbacks are registered and cloned from synchronous hooks.
118	pub(super) queue_wait_activity_callback: Mutex<Option<QueueWaitActivityCallback>>,
119	pub(super) queue_inspector_update_callback: Mutex<Option<QueueInspectorUpdateCallback>>,
120	// Forced-sync: connection operations expose sync accessors or clone handles
121	// before awaiting; connection_disconnect_state serializes disconnect
122	// bookkeeping with pending hibernation snapshots.
123	pub(super) connection_config: RwLock<ActorConfig>,
124	pub(super) connections: RwLock<BTreeMap<ConnId, ConnHandle>>,
125	pub(super) pending_hibernation_updates: RwLock<BTreeSet<ConnId>>,
126	pub(super) pending_hibernation_removals: RwLock<BTreeSet<ConnId>>,
127	pub(super) connection_disconnect_state: Mutex<()>,
128	pub(super) sleep: SleepState,
129	activity: ActivityState,
130	sleep_requested: AtomicBool,
131	destroy_requested: AtomicBool,
132	destroy_completed: AtomicBool,
133	destroy_completion_notify: Notify,
134	abort_signal: Mutex<CancellationToken>,
135	shutdown_deadline: CancellationToken,
136	// Forced-sync: runtime wiring slots are configured through synchronous
137	// lifecycle setup and cloned before sending events.
138	inspector: RwLock<Option<Inspector>>,
139	inspector_attach_count: RwLock<Option<Arc<AtomicU32>>>,
140	inspector_overlay_tx: RwLock<Option<broadcast::Sender<Arc<Vec<u8>>>>>,
141	actor_events: RwLock<Option<mpsc::UnboundedSender<ActorEvent>>>,
142	pub(super) lifecycle_events: RwLock<Option<mpsc::UnboundedSender<LifecycleEvent>>>,
143	hibernated_connection_liveness_override: RwLock<Option<BTreeSet<(Vec<u8>, Vec<u8>)>>>,
144	pub(super) metrics: ActorMetrics,
145	diagnostics: ActorDiagnostics,
146	actor_id: String,
147	name: String,
148	key: ActorKey,
149	region: String,
150}
151
152#[derive(Debug, Default)]
153pub(crate) struct ActivityState {
154	dirty: AtomicBool,
155}
156
157impl ActivityState {
158	fn mark_dirty(&self) -> bool {
159		!self.dirty.swap(true, Ordering::AcqRel)
160	}
161
162	fn take_dirty(&self) -> bool {
163		self.dirty.swap(false, Ordering::AcqRel)
164	}
165}
166
167impl ActorContext {
168	pub fn new(
169		actor_id: impl Into<String>,
170		name: impl Into<String>,
171		key: ActorKey,
172		region: impl Into<String>,
173	) -> Self {
174		Self::build(
175			actor_id.into(),
176			name.into(),
177			key,
178			region.into(),
179			None,
180			String::new(),
181			ActorConfig::default(),
182			Kv::default(),
183			SqliteDb::default(),
184		)
185	}
186
187	pub fn new_with_kv(
188		actor_id: impl Into<String>,
189		name: impl Into<String>,
190		key: ActorKey,
191		region: impl Into<String>,
192		kv: Kv,
193	) -> Self {
194		Self::build(
195			actor_id.into(),
196			name.into(),
197			key,
198			region.into(),
199			None,
200			String::new(),
201			ActorConfig::default(),
202			kv,
203			SqliteDb::default(),
204		)
205	}
206
207	#[cfg(test)]
208	pub(crate) fn new_for_state_tests(kv: Kv, config: ActorConfig) -> Self {
209		Self::build(
210			"state-test".to_owned(),
211			"state-test".to_owned(),
212			Vec::new(),
213			"local".to_owned(),
214			None,
215			String::new(),
216			config,
217			kv,
218			SqliteDb::default(),
219		)
220	}
221
222	pub(crate) fn build(
223		actor_id: String,
224		name: String,
225		key: ActorKey,
226		region: String,
227		_generation: Option<u32>,
228		_envoy_key: String,
229		config: ActorConfig,
230		kv: Kv,
231		sql: SqliteDb,
232	) -> Self {
233		let metrics = ActorMetrics::new(name.clone());
234		#[cfg(feature = "sqlite-local")]
235		let mut sql = sql;
236		#[cfg(feature = "sqlite-local")]
237		sql.set_vfs_metrics(Arc::new(metrics.clone()));
238		let diagnostics = ActorDiagnostics::new(actor_id.clone());
239		let state_save_interval = config.state_save_interval;
240		let abort_signal = CancellationToken::new();
241		let shutdown_deadline = CancellationToken::new();
242		let sleep = SleepState::new(config.clone());
243		let ctx = Self(Arc::new(ActorContextInner {
244			kv,
245			sql,
246			current_state: RwLock::new(Vec::new()),
247			persisted: RwLock::new(PersistedActor::default()),
248			last_pushed_alarm: RwLock::new(None),
249			state_save_interval,
250			state_dirty: AtomicBool::new(false),
251			state_revision: AtomicU64::new(0),
252			save_request_revision: AtomicU64::new(0),
253			save_completed_revision: AtomicU64::new(0),
254			save_completion: Notify::new(),
255			save_requested: AtomicBool::new(false),
256			save_requested_immediate: AtomicBool::new(false),
257			save_requested_within_deadline: Mutex::new(None),
258			last_save_at: Mutex::new(None),
259			pending_save: Mutex::new(None),
260			tracked_persist: Mutex::new(None),
261			save_guard: AsyncMutex::new(()),
262			in_flight_state_writes: AtomicUsize::new(0),
263			state_write_completion: Notify::new(),
264			on_state_change_in_flight: AtomicUsize::new(0),
265			on_state_change_idle: Notify::new(),
266			request_save_hooks: RwLock::new(Vec::new()),
267			schedule_generation: Mutex::new(None),
268			schedule_envoy_handle: Mutex::new(None),
269			client_endpoint: OnceLock::new(),
270			client_token: OnceLock::new(),
271			client_namespace: OnceLock::new(),
272			client_pool_name: OnceLock::new(),
273			schedule_internal_keep_awake: Mutex::new(None),
274			schedule_local_alarm_callback: Mutex::new(None),
275			schedule_local_alarm_task: Mutex::new(None),
276			schedule_pending_alarm_writes: Mutex::new(Vec::new()),
277			schedule_local_alarm_epoch: AtomicU64::new(0),
278			schedule_alarm_dispatch_enabled: AtomicBool::new(true),
279			// A fresh actor context has no in-process record of a successful
280			// envoy alarm push yet, so the first sync must always push.
281			schedule_dirty_since_push: AtomicBool::new(true),
282			#[cfg(test)]
283			schedule_driver_alarm_cancel_count: AtomicUsize::new(0),
284			queue_config: Mutex::new(config.clone()),
285			queue_abort_signal: Mutex::new(abort_signal.clone()),
286			queue_initialize: OnceCell::new(),
287			queue_preloaded_kv: Mutex::new(None),
288			queue_preloaded_message_entries: Mutex::new(None),
289			queue_metadata: AsyncMutex::new(QueueMetadata::default()),
290			queue_receive_lock: AsyncMutex::new(()),
291			queue_completion_waiters: SccHashMap::new(),
292			queue_notify: Notify::new(),
293			active_queue_wait_count: AtomicU32::new(0),
294			queue_wait_activity_callback: Mutex::new(None),
295			queue_inspector_update_callback: Mutex::new(None),
296			connection_config: RwLock::new(config),
297			connections: RwLock::new(BTreeMap::new()),
298			pending_hibernation_updates: RwLock::new(BTreeSet::new()),
299			pending_hibernation_removals: RwLock::new(BTreeSet::new()),
300			connection_disconnect_state: Mutex::new(()),
301			sleep,
302			activity: ActivityState::default(),
303			sleep_requested: AtomicBool::new(false),
304			destroy_requested: AtomicBool::new(false),
305			destroy_completed: AtomicBool::new(false),
306			destroy_completion_notify: Notify::new(),
307			abort_signal: Mutex::new(abort_signal),
308			shutdown_deadline,
309			inspector: RwLock::new(None),
310			inspector_attach_count: RwLock::new(None),
311			inspector_overlay_tx: RwLock::new(None),
312			actor_events: RwLock::new(None),
313			lifecycle_events: RwLock::new(None),
314			hibernated_connection_liveness_override: RwLock::new(None),
315			metrics,
316			diagnostics,
317			actor_id,
318			name,
319			key,
320			region,
321		}));
322		ctx.configure_sleep_hooks();
323		ctx
324	}
325
326	#[deprecated(
327		note = "Actor KV is deprecated. Use embedded SQLite (`sql()`) or actor state instead."
328	)]
329	pub async fn kv_batch_get(&self, keys: &[&[u8]]) -> Result<Vec<Option<Vec<u8>>>> {
330		self.0.kv.batch_get(keys).await
331	}
332
333	#[deprecated(
334		note = "Actor KV is deprecated. Use embedded SQLite (`sql()`) or actor state instead."
335	)]
336	pub async fn kv_batch_put(&self, entries: &[(&[u8], &[u8])]) -> Result<()> {
337		self.0.kv.batch_put(entries).await
338	}
339
340	#[deprecated(
341		note = "Actor KV is deprecated. Use embedded SQLite (`sql()`) or actor state instead."
342	)]
343	pub async fn kv_batch_delete(&self, keys: &[&[u8]]) -> Result<()> {
344		self.0.kv.batch_delete(keys).await
345	}
346
347	#[deprecated(
348		note = "Actor KV is deprecated. Use embedded SQLite (`sql()`) or actor state instead."
349	)]
350	pub async fn kv_delete_range(&self, start: &[u8], end: &[u8]) -> Result<()> {
351		self.0.kv.delete_range(start, end).await
352	}
353
354	#[deprecated(
355		note = "Actor KV is deprecated. Use embedded SQLite (`sql()`) or actor state instead."
356	)]
357	pub async fn kv_list_prefix(
358		&self,
359		prefix: &[u8],
360		opts: ListOpts,
361	) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
362		self.0.kv.list_prefix(prefix, opts).await
363	}
364
365	#[deprecated(
366		note = "Actor KV is deprecated. Use embedded SQLite (`sql()`) or actor state instead."
367	)]
368	pub async fn kv_list_range(
369		&self,
370		start: &[u8],
371		end: &[u8],
372		opts: ListOpts,
373	) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
374		self.0.kv.list_range(start, end, opts).await
375	}
376
377	#[deprecated(
378		note = "Actor KV is deprecated. Use embedded SQLite (`sql()`) or actor state instead."
379	)]
380	pub fn kv(&self) -> &Kv {
381		&self.0.kv
382	}
383
384	/// Internal accessor for the actor KV store. Core uses KV as the backing
385	/// store for the inspector token and actor startup persistence. Unlike the
386	/// public `kv()` accessor, this is not deprecated because the storage
387	/// machinery itself is not going away, only the public actor-facing API.
388	pub(crate) fn kv_internal(&self) -> &Kv {
389		&self.0.kv
390	}
391
392	pub fn sql(&self) -> &SqliteDb {
393		&self.0.sql
394	}
395
396	pub async fn db_exec(&self, sql: &str) -> Result<Vec<u8>> {
397		self.0.sql.exec_rows_cbor(sql).await
398	}
399
400	pub async fn db_query(&self, sql: &str, params: Option<&[u8]>) -> Result<Vec<u8>> {
401		self.0.sql.query_rows_cbor(sql, params).await
402	}
403
404	pub async fn db_execute(&self, sql: &str, params: Option<&[u8]>) -> Result<Vec<u8>> {
405		self.0.sql.execute_rows_cbor(sql, params).await
406	}
407
408	pub async fn db_run(&self, sql: &str, params: Option<&[u8]>) -> Result<()> {
409		self.0.sql.run_cbor(sql, params).await?;
410		Ok(())
411	}
412
413	pub fn set_alarm(&self, timestamp_ms: Option<i64>) -> Result<()> {
414		self.set_schedule_alarm(timestamp_ms)
415	}
416
417	/// Resync persisted alarms with the runtime's alarm transport.
418	///
419	/// Foreign-runtime adapters should call this during startup after loading
420	/// any persisted schedule state and before accepting user callbacks that rely
421	/// on future alarms being armed.
422	pub fn init_alarms(&self) {
423		self.sync_future_alarm_logged();
424	}
425
426	pub fn queue(&self) -> &Self {
427		self
428	}
429
430	pub fn sleep(&self) -> Result<()> {
431		// `started` is cleared when the lifecycle state machine transitions
432		// into SleepGrace / DestroyGrace, so `started=false` covers both
433		// "never started" and "already shutting down". Distinguish with the
434		// request flags for an accurate diagnostic.
435		if !self.0.sleep.lifecycle_started.load(Ordering::SeqCst) {
436			let already_stopping = self.0.sleep_requested.load(Ordering::SeqCst)
437				|| self.0.destroy_requested.load(Ordering::SeqCst);
438			return if already_stopping {
439				Err(ActorLifecycleError::Stopping.build()).context("actor is already shutting down")
440			} else {
441				Err(ActorLifecycleError::Starting.build())
442					.context("cannot request sleep before actor startup completes")
443			};
444		}
445		if self.0.sleep_requested.swap(true, Ordering::SeqCst) {
446			return Ok(());
447		}
448		self.cancel_sleep_timer();
449		if Handle::try_current().is_ok() {
450			let ctx = self.clone();
451			let tracked = self.track_shutdown_task(async move {
452				ctx.record_user_task_started(UserTaskKind::SleepFinalize);
453				let started_at = Instant::now();
454				ctx.request_sleep_from_envoy();
455				ctx.record_user_task_finished(UserTaskKind::SleepFinalize, started_at.elapsed());
456			});
457			if tracked {
458				return Ok(());
459			}
460		}
461
462		self.request_sleep_from_envoy();
463		Ok(())
464	}
465
466	pub fn destroy(&self) -> Result<()> {
467		// See `sleep` for why the request flags disambiguate `started=false`.
468		// destroy() is allowed after sleep() has been requested because
469		// destroy is a stronger signal that escalates an in-flight sleep.
470		if !self.0.sleep.lifecycle_started.load(Ordering::SeqCst)
471			&& !self.0.sleep_requested.load(Ordering::SeqCst)
472			&& !self.0.destroy_requested.load(Ordering::SeqCst)
473		{
474			return Err(ActorLifecycleError::Starting.build())
475				.context("cannot request destroy before actor startup completes");
476		}
477		if self.0.destroy_requested.swap(true, Ordering::SeqCst) {
478			return Err(ActorLifecycleError::Stopping.build())
479				.context("destroy already requested for this generation");
480		}
481		// Reuse the shared teardown sequence used by the registry shutdown path
482		// so future changes to `mark_destroy_requested` cannot drift.
483		// `destroy_requested` is already true from the swap above. The redundant
484		// `store(true)` inside is harmless.
485		#[cfg(not(feature = "wasm-runtime"))]
486		self.mark_destroy_requested();
487		#[cfg(feature = "wasm-runtime")]
488		self.mark_destroy_requested_without_spawn();
489
490		let ctx = self.clone();
491		if Handle::try_current().is_ok() {
492			let tracked = self.track_shutdown_task(async move {
493				ctx.record_user_task_started(UserTaskKind::DestroyRequest);
494				let started_at = Instant::now();
495				ctx.request_destroy_from_envoy();
496				ctx.record_user_task_finished(UserTaskKind::DestroyRequest, started_at.elapsed());
497			});
498			if tracked {
499				return Ok(());
500			}
501		}
502
503		self.request_destroy_from_envoy();
504		Ok(())
505	}
506
507	pub fn mark_destroy_requested(&self) {
508		self.cancel_sleep_timer();
509		self.flush_on_shutdown();
510		self.0.destroy_requested.store(true, Ordering::SeqCst);
511		self.0.destroy_completed.store(false, Ordering::SeqCst);
512	}
513
514	#[cfg(feature = "wasm-runtime")]
515	fn mark_destroy_requested_without_spawn(&self) {
516		self.cancel_sleep_timer();
517		self.0.destroy_requested.store(true, Ordering::SeqCst);
518		self.0.destroy_completed.store(false, Ordering::SeqCst);
519	}
520
521	#[doc(hidden)]
522	pub fn cancel_actor_abort_signal(&self) {
523		self.0.abort_signal.lock().cancel();
524	}
525
526	pub(crate) fn reset_abort_signal_for_start(&self) {
527		let mut abort_signal = self.0.abort_signal.lock();
528		if !abort_signal.is_cancelled() {
529			return;
530		}
531
532		// Sleep or destroy cancels the generation abort signal to break actor
533		// scoped waits. A restarted actor needs a fresh signal so the next
534		// generation can wait normally.
535		let next_signal = CancellationToken::new();
536		*abort_signal = next_signal.clone();
537		*self.0.queue_abort_signal.lock() = next_signal;
538	}
539
540	#[doc(hidden)]
541	pub fn actor_abort_signal(&self) -> CancellationToken {
542		self.0.abort_signal.lock().clone()
543	}
544
545	#[doc(hidden)]
546	pub fn actor_aborted(&self) -> bool {
547		self.0.abort_signal.lock().is_cancelled()
548	}
549
550	/// Fires when the shutdown grace deadline has elapsed and core is forcing
551	/// cleanup. Foreign-runtime adapters should abort any in-flight shutdown
552	/// work (for example `onSleep` / `onDestroy`) when this token is cancelled
553	/// so resources like SQLite are not torn down mid-operation.
554	#[doc(hidden)]
555	pub fn shutdown_deadline_token(&self) -> CancellationToken {
556		self.0.shutdown_deadline.clone()
557	}
558
559	#[doc(hidden)]
560	pub fn cancel_shutdown_deadline(&self) {
561		self.0.shutdown_deadline.cancel();
562	}
563
564	/// Deprecated no-op. Use `keep_awake` to hold the actor awake for the
565	/// duration of a future, or `wait_until` to keep work alive across the
566	/// sleep grace period. Retained only for NAPI bridge compatibility.
567	#[deprecated(note = "no-op: use `keep_awake` or `wait_until` instead")]
568	pub fn set_prevent_sleep(&self, _enabled: bool) {}
569
570	#[deprecated(note = "no-op: always returns false")]
571	pub fn prevent_sleep(&self) -> bool {
572		false
573	}
574
575	#[cfg(not(feature = "wasm-runtime"))]
576	pub fn wait_until(&self, future: impl Future<Output = ()> + Send + 'static) {
577		self.spawn_work(ActorWorkKind::WaitUntil, future);
578	}
579
580	#[cfg(not(feature = "wasm-runtime"))]
581	pub fn register_task(&self, future: impl Future<Output = ()> + Send + 'static) {
582		self.spawn_work(ActorWorkKind::RegisteredTask, future);
583	}
584
585	#[cfg(feature = "wasm-runtime")]
586	pub fn wait_until(&self, future: impl Future<Output = ()> + 'static) {
587		self.spawn_work(ActorWorkKind::WaitUntil, future);
588	}
589
590	#[cfg(feature = "wasm-runtime")]
591	pub fn register_task(&self, future: impl Future<Output = ()> + 'static) {
592		self.spawn_work(ActorWorkKind::RegisteredTask, future);
593	}
594
595	pub async fn keep_awake<F>(&self, future: F) -> F::Output
596	where
597		F: Future,
598	{
599		self.track_work(ActorWorkKind::KeepAwake, future).await
600	}
601
602	pub fn keep_awake_region(&self) -> KeepAwakeRegion {
603		KeepAwakeRegion {
604			region: Some(self.begin_work_region(ActorWorkKind::KeepAwake)),
605		}
606	}
607
608	pub async fn internal_keep_awake<F>(&self, future: F) -> F::Output
609	where
610		F: Future,
611	{
612		self.track_work(ActorWorkKind::InternalKeepAwake, future)
613			.await
614	}
615
616	pub fn keep_awake_count(&self) -> usize {
617		self.sleep_keep_awake_count()
618	}
619
620	pub fn internal_keep_awake_count(&self) -> usize {
621		self.sleep_internal_keep_awake_count()
622	}
623
624	pub async fn track_work<F>(&self, kind: ActorWorkKind, future: F) -> F::Output
625	where
626		F: Future,
627	{
628		let _region = self.begin_work_region(kind);
629		future.await
630	}
631
632	#[cfg(not(feature = "wasm-runtime"))]
633	pub fn spawn_work<F>(&self, kind: ActorWorkKind, future: F)
634	where
635		F: Future<Output = ()> + Send + 'static,
636	{
637		self.spawn_work_inner(kind, future);
638	}
639
640	#[cfg(feature = "wasm-runtime")]
641	pub fn spawn_work<F>(&self, kind: ActorWorkKind, future: F)
642	where
643		F: Future<Output = ()> + 'static,
644	{
645		self.spawn_work_inner(kind, future);
646	}
647
648	pub fn begin_work_region(&self, kind: ActorWorkKind) -> ActorWorkRegion {
649		ActorWorkRegion {
650			guard: Some(ActorWorkGuard::new(self.clone(), kind)),
651		}
652	}
653
654	pub fn actor_id(&self) -> &str {
655		&self.0.actor_id
656	}
657
658	pub fn name(&self) -> &str {
659		&self.0.name
660	}
661
662	pub fn key(&self) -> &ActorKey {
663		&self.0.key
664	}
665
666	pub(crate) fn actor_specifier(&self) -> Option<ActorSpecifier> {
667		Some(
668			ActorSpecifier::new(self.actor_id().to_owned(), self.sleep_generation()? as u64)
669				.with_key(format_actor_key(self.key())),
670		)
671	}
672
673	pub(crate) fn attach_actor_to_error(&self, error: anyhow::Error) -> anyhow::Error {
674		match self.actor_specifier() {
675			Some(actor) => error.context(actor),
676			None => error,
677		}
678	}
679
680	pub fn region(&self) -> &str {
681		&self.0.region
682	}
683
684	pub fn has_state(&self) -> bool {
685		self.0.connection_config.read().has_state
686	}
687
688	#[doc(hidden)]
689	pub fn record_startup_create_state(&self, duration: Duration) {
690		self.0.metrics.observe_create_state(duration);
691	}
692
693	#[doc(hidden)]
694	pub fn record_startup_create_vars(&self, duration: Duration) {
695		self.0.metrics.observe_create_vars(duration);
696	}
697
698	pub fn broadcast(&self, name: &str, args: &[u8]) {
699		for connection in self.conns() {
700			if connection.is_subscribed(name) {
701				connection.send(name, args);
702			}
703		}
704	}
705
706	/// Returns a lock-backed iterator over live connections.
707	///
708	/// Do not hold the returned iterator across `.await`. It keeps a read lock
709	/// on the connection map until dropped, which blocks connection writers.
710	#[must_use]
711	pub fn conns(&self) -> ConnHandles<'_> {
712		self.iter_connections()
713	}
714
715	pub fn client_endpoint(&self) -> Option<&str> {
716		self.0.client_endpoint.get().map(String::as_str)
717	}
718
719	pub fn client_token(&self) -> Option<&str> {
720		self.0.client_token.get().map(String::as_str)
721	}
722
723	pub fn client_namespace(&self) -> Option<&str> {
724		self.0.client_namespace.get().map(String::as_str)
725	}
726
727	pub fn client_pool_name(&self) -> Option<&str> {
728		self.0.client_pool_name.get().map(String::as_str)
729	}
730
731	pub fn ack_hibernatable_websocket_message(
732		&self,
733		gateway_id: &[u8],
734		request_id: &[u8],
735		server_message_index: u16,
736	) -> Result<()> {
737		let gateway_id = hibernatable_id_from_slice("gateway_id", gateway_id)?;
738		let request_id = hibernatable_id_from_slice("request_id", request_id)?;
739		let envoy_handle = self.sleep_envoy_handle().ok_or_else(|| {
740			ActorRuntime::NotConfigured {
741				component: "hibernatable websocket ack".to_owned(),
742			}
743			.build()
744		})?;
745		envoy_handle.send_hibernatable_ws_message_ack(gateway_id, request_id, server_message_index);
746		Ok(())
747	}
748
749	pub(crate) fn load_persisted_actor(&self, persisted: PersistedActor) {
750		self.load_persisted(persisted);
751	}
752
753	pub(crate) fn persisted_actor(&self) -> PersistedActor {
754		self.persisted()
755	}
756
757	/// Dispatches any scheduled actions whose deadline has already passed.
758	///
759	/// Foreign-runtime adapters should call this after startup callbacks complete
760	/// so overdue scheduled work enters the normal actor event loop.
761	pub async fn drain_overdue_scheduled_events(&self) -> Result<()> {
762		for event in self.due_scheduled_events(now_timestamp_ms()) {
763			self.dispatch_scheduled_action(
764				&event.event_id,
765				event.action,
766				event.args.unwrap_or_default(),
767			)
768			.await;
769		}
770
771		self.sync_alarm_logged();
772		Ok(())
773	}
774
775	pub(crate) fn metrics(&self) -> &ActorMetrics {
776		&self.0.metrics
777	}
778
779	pub(crate) fn record_user_task_started(&self, kind: UserTaskKind) {
780		self.0.metrics.begin_user_task(kind);
781	}
782
783	pub(crate) fn record_user_task_finished(&self, kind: UserTaskKind, duration: Duration) {
784		self.0.metrics.end_user_task(kind, duration);
785	}
786
787	pub(crate) fn record_shutdown_wait(
788		&self,
789		reason: crate::actor::task_types::ShutdownKind,
790		duration: Duration,
791	) {
792		self.0.metrics.observe_shutdown_wait(reason, duration);
793	}
794
795	pub(crate) fn record_shutdown_timeout(&self, reason: crate::actor::task_types::ShutdownKind) {
796		self.0.metrics.inc_shutdown_timeout(reason);
797	}
798
799	pub(crate) fn record_direct_subsystem_shutdown_warning(
800		&self,
801		subsystem: &str,
802		operation: &str,
803	) {
804		self.0
805			.metrics
806			.inc_direct_subsystem_shutdown_warning(subsystem, operation);
807	}
808
809	pub(crate) fn warn_work_sent_to_stopping_instance(&self, operation: &'static str) {
810		if let Some(suppression) = self.0.diagnostics.record("work_sent_to_stopping_instance") {
811			tracing::warn!(
812				actor_id = %suppression.actor_id,
813				operation,
814				per_actor_suppressed = suppression.per_actor_suppressed,
815				global_suppressed = suppression.global_suppressed,
816				"work sent to stopping actor instance"
817			);
818		}
819	}
820
821	pub(crate) fn warn_self_call_risk(&self, operation: &'static str) {
822		if let Some(suppression) = self.0.diagnostics.record("self_call_risk") {
823			tracing::warn!(
824				actor_id = %suppression.actor_id,
825				operation,
826				per_actor_suppressed = suppression.per_actor_suppressed,
827				global_suppressed = suppression.global_suppressed,
828				"actor dispatch may be parked behind the current instance"
829			);
830		}
831	}
832
833	#[cfg(test)]
834	pub(crate) fn add_conn(&self, conn: ConnHandle) {
835		self.insert_existing(conn);
836		self.record_connections_updated();
837		self.reset_sleep_timer();
838	}
839
840	pub(crate) fn remove_conn(&self, conn_id: &str) -> Option<ConnHandle> {
841		let removed = self.remove_existing(conn_id);
842		if removed.is_some() {
843			self.record_connections_updated();
844			self.reset_sleep_timer();
845		}
846		removed
847	}
848
849	pub(crate) fn configure_connection_runtime(&self, config: ActorConfig) {
850		self.configure_sleep_state(config.clone());
851		self.configure_connection_storage(config);
852	}
853
854	pub(crate) fn configure_queue_preload(&self, preloaded_kv: Option<PreloadedKv>) {
855		self.configure_preload(preloaded_kv);
856	}
857
858	pub(crate) fn configure_actor_events(&self, sender: Option<mpsc::UnboundedSender<ActorEvent>>) {
859		*self.0.actor_events.write() = sender;
860	}
861
862	pub(crate) fn try_send_actor_event(
863		&self,
864		event: ActorEvent,
865		operation: &'static str,
866	) -> Result<()> {
867		let sender = self.0.actor_events.read().clone().ok_or_else(|| {
868			ActorRuntime::NotConfigured {
869				component: "actor event inbox".to_owned(),
870			}
871			.build()
872		})?;
873		tracing::debug!(
874			actor_id = %self.actor_id(),
875			operation,
876			event = event.kind(),
877			"actor event enqueued"
878		);
879		sender.send(event).map_err(|_| {
880			ActorRuntime::NotConfigured {
881				component: "actor event inbox".to_owned(),
882			}
883			.build()
884		})
885	}
886
887	#[doc(hidden)]
888	pub fn configure_envoy(&self, envoy_handle: EnvoyHandle, generation: Option<u32>) {
889		let _ = self
890			.0
891			.client_endpoint
892			.set(envoy_handle.endpoint().to_owned());
893		if let Some(token) = envoy_handle.token() {
894			let _ = self.0.client_token.set(token.to_owned());
895		}
896		let _ = self
897			.0
898			.client_namespace
899			.set(envoy_handle.namespace().to_owned());
900		let _ = self
901			.0
902			.client_pool_name
903			.set(envoy_handle.pool_name().to_owned());
904		self.configure_sleep_envoy(envoy_handle.clone(), generation);
905		self.configure_schedule_envoy(envoy_handle, generation);
906	}
907
908	pub(crate) async fn connect_conn<F>(
909		&self,
910		params: Vec<u8>,
911		is_hibernatable: bool,
912		hibernation: Option<HibernatableConnectionMetadata>,
913		request: Option<Request>,
914		create_state: F,
915	) -> Result<ConnHandle>
916	where
917		F: Future<Output = Result<Vec<u8>>> + Send,
918	{
919		let conn = self
920			.connect_with_state(params, is_hibernatable, hibernation, request, create_state)
921			.await?;
922		self.record_connections_updated();
923		self.reset_sleep_timer();
924		Ok(conn)
925	}
926
927	pub(crate) async fn connect_conn_with_prepare<F, P>(
928		&self,
929		params: Vec<u8>,
930		is_hibernatable: bool,
931		hibernation: Option<HibernatableConnectionMetadata>,
932		request: Option<Request>,
933		create_state: F,
934		prepare_connection: P,
935	) -> Result<ConnHandle>
936	where
937		F: Future<Output = Result<Vec<u8>>> + Send,
938		P: FnOnce(&ConnHandle) -> Result<()>,
939	{
940		let conn = self
941			.connect_with_state_and_prepare(
942				params,
943				is_hibernatable,
944				hibernation,
945				request,
946				create_state,
947				prepare_connection,
948			)
949			.await?;
950		self.record_connections_updated();
951		self.reset_sleep_timer();
952		Ok(conn)
953	}
954
955	pub async fn connect_conn_with_request<F>(
956		&self,
957		params: Vec<u8>,
958		request: Option<Request>,
959		create_state: F,
960	) -> Result<ConnHandle>
961	where
962		F: Future<Output = Result<Vec<u8>>> + Send,
963	{
964		self.connect_conn(params, false, None, request, create_state)
965			.await
966	}
967
968	pub(crate) fn reconnect_hibernatable_conn(
969		&self,
970		gateway_id: &[u8],
971		request_id: &[u8],
972	) -> Result<ConnHandle> {
973		self.reconnect_hibernatable(gateway_id, request_id)
974	}
975
976	pub async fn disconnect_conn(&self, id: ConnId) -> Result<()> {
977		self.disconnect_transport_only(|conn| conn.id() == id).await
978	}
979
980	pub async fn disconnect_conns<F>(&self, predicate: F) -> Result<()>
981	where
982		F: FnMut(&ConnHandle) -> bool,
983	{
984		self.disconnect_transport_only(predicate).await
985	}
986
987	pub(crate) fn request_hibernation_transport_save(&self, conn_id: &str) {
988		self.queue_hibernation_update(conn_id.to_owned());
989		self.request_save(RequestSaveOpts::default());
990	}
991
992	pub(crate) fn request_hibernation_transport_removal(&self, conn_id: impl Into<String>) {
993		self.queue_hibernation_removal_inner(conn_id.into());
994		self.request_save(RequestSaveOpts::default());
995	}
996
997	pub fn queue_hibernation_removal(&self, conn_id: impl Into<String>) {
998		self.request_hibernation_transport_removal(conn_id);
999	}
1000
1001	pub fn has_pending_hibernation_changes(&self) -> bool {
1002		self.has_pending_hibernation_changes_inner()
1003	}
1004
1005	pub fn take_pending_hibernation_changes(&self) -> Vec<ConnId> {
1006		self.pending_hibernation_removals()
1007	}
1008
1009	pub fn dirty_hibernatable_conns(&self) -> Vec<ConnHandle> {
1010		self.dirty_hibernatable_conns_inner()
1011	}
1012
1013	pub(crate) fn hibernated_connection_is_live(
1014		&self,
1015		gateway_id: &[u8],
1016		request_id: &[u8],
1017	) -> Result<bool> {
1018		let gateway_id = hibernatable_id_from_slice("gateway_id", gateway_id)?;
1019		let request_id = hibernatable_id_from_slice("request_id", request_id)?;
1020
1021		if let Some(override_pairs) = self
1022			.0
1023			.hibernated_connection_liveness_override
1024			.read()
1025			.as_ref()
1026		{
1027			return Ok(override_pairs.contains(&(gateway_id.to_vec(), request_id.to_vec())));
1028		}
1029
1030		let Some(envoy_handle) = self.sleep_envoy_handle() else {
1031			return Ok(false);
1032		};
1033		let is_live = envoy_handle.hibernatable_connection_is_live(
1034			self.actor_id(),
1035			self.sleep_generation(),
1036			gateway_id,
1037			request_id,
1038		);
1039		Ok(is_live)
1040	}
1041
1042	#[cfg(test)]
1043	pub(crate) fn set_hibernated_connection_liveness_override<I>(&self, pairs: I)
1044	where
1045		I: IntoIterator<Item = (Vec<u8>, Vec<u8>)>,
1046	{
1047		*self.0.hibernated_connection_liveness_override.write() = Some(pairs.into_iter().collect());
1048	}
1049
1050	fn prepare_state_deltas(
1051		&self,
1052		deltas: Vec<StateDelta>,
1053	) -> Result<(Vec<StateDelta>, PendingHibernationChanges)> {
1054		fn finish_with_error(
1055			ctx: &ActorContext,
1056			pending: PendingHibernationChanges,
1057			error: anyhow::Error,
1058		) -> Result<(Vec<StateDelta>, PendingHibernationChanges)> {
1059			ctx.restore_pending_hibernation_changes(pending);
1060			Err(error)
1061		}
1062
1063		let mut next_deltas = Vec::new();
1064		let mut explicit_updates = std::collections::BTreeMap::new();
1065		let mut explicit_removals = std::collections::BTreeSet::new();
1066
1067		for delta in deltas {
1068			match delta {
1069				StateDelta::ConnHibernation { conn, bytes } => {
1070					if let Some(handle) = self.connection(&conn) {
1071						handle.set_state_initial(bytes.clone());
1072					}
1073					explicit_updates.insert(conn, bytes);
1074				}
1075				StateDelta::ConnHibernationRemoved(conn) => {
1076					explicit_removals.insert(conn);
1077				}
1078				other => next_deltas.push(other),
1079			}
1080		}
1081
1082		let pending = self.take_pending_hibernation_changes_inner();
1083		let mut removal_ids = pending.removed.clone();
1084		removal_ids.extend(explicit_removals.iter().cloned());
1085
1086		let explicit_update_ids: std::collections::BTreeSet<_> =
1087			explicit_updates.keys().cloned().collect();
1088
1089		for (conn, bytes) in explicit_updates {
1090			if removal_ids.contains(&conn) {
1091				continue;
1092			}
1093			let encoded = match self.encode_hibernation_delta(&conn, bytes) {
1094				Ok(encoded) => encoded,
1095				Err(error) => {
1096					return finish_with_error(self, pending, error);
1097				}
1098			};
1099			next_deltas.push(StateDelta::ConnHibernation {
1100				conn,
1101				bytes: encoded,
1102			});
1103		}
1104
1105		for conn in &pending.updated {
1106			if removal_ids.contains(conn)
1107				|| explicit_removals.contains(conn)
1108				|| explicit_update_ids.contains(conn)
1109			{
1110				continue;
1111			}
1112			let Some(handle) = self.connection(conn) else {
1113				continue;
1114			};
1115			if !handle.is_hibernatable() || handle.hibernation().is_none() {
1116				continue;
1117			}
1118			let encoded = match self.encode_hibernation_delta(conn, handle.state()) {
1119				Ok(encoded) => encoded,
1120				Err(error) => {
1121					return finish_with_error(self, pending, error);
1122				}
1123			};
1124			next_deltas.push(StateDelta::ConnHibernation {
1125				conn: conn.clone(),
1126				bytes: encoded,
1127			});
1128		}
1129
1130		for conn in removal_ids {
1131			next_deltas.push(StateDelta::ConnHibernationRemoved(conn));
1132		}
1133
1134		Ok((next_deltas, pending))
1135	}
1136
1137	#[cfg(test)]
1138	pub(crate) async fn restore_hibernatable_connections(&self) -> Result<Vec<ConnHandle>> {
1139		self.restore_hibernatable_connections_with_preload(None)
1140			.await
1141	}
1142
1143	pub(crate) async fn restore_hibernatable_connections_with_preload(
1144		&self,
1145		preloaded_kv: Option<&PreloadedKv>,
1146	) -> Result<Vec<ConnHandle>> {
1147		let restored = self.restore_persisted(preloaded_kv).await?;
1148		if !restored.is_empty() {
1149			if let Some(envoy_handle) = self.sleep_envoy_handle() {
1150				let meta_entries: Vec<_> = restored
1151					.iter()
1152					.filter_map(|conn| {
1153						let hibernation = conn.hibernation()?;
1154						Some(HibernatingWebSocketMetadata {
1155							gateway_id: hibernation.gateway_id,
1156							request_id: hibernation.request_id,
1157							envoy_message_index: hibernation.client_message_index,
1158							rivet_message_index: hibernation.server_message_index,
1159							path: hibernation.request_path,
1160							headers: hibernation.request_headers.into_iter().collect(),
1161						})
1162					})
1163					.collect();
1164				envoy_handle.restore_hibernating_requests(self.actor_id().to_owned(), meta_entries);
1165			}
1166			self.record_connections_updated();
1167			self.reset_sleep_timer();
1168		}
1169		Ok(restored)
1170	}
1171
1172	pub(crate) fn configure_inspector(&self, inspector: Option<Inspector>) {
1173		*self.0.inspector.write() = inspector;
1174	}
1175
1176	pub(crate) fn inspector(&self) -> Option<Inspector> {
1177		self.0.inspector.read().clone()
1178	}
1179
1180	pub fn inspector_snapshot(&self) -> InspectorSnapshot {
1181		self.inspector()
1182			.map(|inspector| inspector.snapshot())
1183			.unwrap_or_default()
1184	}
1185
1186	pub(crate) fn configure_inspector_runtime(
1187		&self,
1188		attach_count: Arc<AtomicU32>,
1189		overlay_tx: broadcast::Sender<Arc<Vec<u8>>>,
1190	) {
1191		*self.0.inspector_attach_count.write() = Some(attach_count);
1192		*self.0.inspector_overlay_tx.write() = Some(overlay_tx);
1193	}
1194
1195	pub(crate) fn inspector_attach(&self) -> Option<InspectorAttachGuard> {
1196		InspectorAttachGuard::new(self.clone())
1197	}
1198
1199	#[cfg(test)]
1200	pub(crate) fn inspector_attach_count(&self) -> u32 {
1201		self.inspector_attach_count_arc()
1202			.map(|attach_count| attach_count.load(Ordering::SeqCst))
1203			.unwrap_or(0)
1204	}
1205
1206	pub(crate) fn subscribe_inspector(&self) -> Option<broadcast::Receiver<Arc<Vec<u8>>>> {
1207		self.0
1208			.inspector_overlay_tx
1209			.read()
1210			.clone()
1211			.map(|overlay_tx| overlay_tx.subscribe())
1212	}
1213
1214	pub(crate) fn downgrade(&self) -> Weak<ActorContextInner> {
1215		Arc::downgrade(&self.0)
1216	}
1217
1218	pub(crate) fn from_weak(weak: &Weak<ActorContextInner>) -> Option<Self> {
1219		weak.upgrade().map(Self)
1220	}
1221
1222	#[doc(hidden)]
1223	pub fn set_started(&self, started: bool) {
1224		self.set_lifecycle_started(started);
1225		self.reset_sleep_timer();
1226	}
1227
1228	#[doc(hidden)]
1229	pub fn started(&self) -> bool {
1230		self.lifecycle_started()
1231	}
1232
1233	pub(crate) fn destroy_requested(&self) -> bool {
1234		self.0.destroy_requested.load(Ordering::SeqCst)
1235	}
1236
1237	pub fn is_destroy_requested(&self) -> bool {
1238		self.destroy_requested()
1239	}
1240
1241	pub(crate) async fn wait_for_destroy_completion(&self) {
1242		if self.0.destroy_completed.load(Ordering::SeqCst) {
1243			return;
1244		}
1245
1246		loop {
1247			let notified = self.0.destroy_completion_notify.notified();
1248			if self.0.destroy_completed.load(Ordering::SeqCst) {
1249				return;
1250			}
1251			notified.await;
1252			if self.0.destroy_completed.load(Ordering::SeqCst) {
1253				return;
1254			}
1255		}
1256	}
1257
1258	pub async fn wait_for_destroy_completion_public(&self) {
1259		self.wait_for_destroy_completion().await;
1260	}
1261
1262	pub(crate) fn mark_destroy_completed(&self) {
1263		self.0.destroy_completed.store(true, Ordering::SeqCst);
1264		self.0.destroy_completion_notify.notify_waiters();
1265	}
1266
1267	pub(crate) async fn can_sleep(&self) -> CanSleep {
1268		self.can_arm_sleep_timer().await
1269	}
1270
1271	pub(crate) fn pending_disconnect_count(&self) -> usize {
1272		self.0.sleep.work.disconnect_callback.load()
1273	}
1274
1275	pub async fn with_disconnect_callback<F, Fut, T>(&self, run: F) -> T
1276	where
1277		F: FnOnce() -> Fut,
1278		Fut: Future<Output = T>,
1279	{
1280		self.track_work(ActorWorkKind::DisconnectCallback, run())
1281			.await
1282	}
1283
1284	pub(crate) fn configure_lifecycle_events(
1285		&self,
1286		sender: Option<mpsc::UnboundedSender<LifecycleEvent>>,
1287	) {
1288		*self.0.lifecycle_events.write() = sender;
1289	}
1290
1291	pub(crate) fn notify_inspector_serialize_requested(&self) {
1292		self.try_send_lifecycle_event(
1293			LifecycleEvent::InspectorSerializeRequested,
1294			"inspector_serialize_requested",
1295		);
1296	}
1297
1298	pub(crate) fn notify_activity_dirty(&self) -> bool {
1299		if self.0.lifecycle_events.read().is_none() {
1300			return false;
1301		}
1302		if self.0.activity.mark_dirty() {
1303			self.sleep_activity_notify().notify_one();
1304		}
1305		true
1306	}
1307
1308	pub(crate) fn acknowledge_activity_dirty(&self) -> bool {
1309		self.0.activity.take_dirty()
1310	}
1311
1312	/// Notify the ActorTask that a `can_sleep` input has changed so the sleep
1313	/// deadline gets re-evaluated. Falls back to the detached compat timer
1314	/// when the actor has no wired `ActorTask` (test-only contexts).
1315	pub(crate) fn reset_sleep_timer(&self) {
1316		if self.notify_activity_dirty() {
1317			return;
1318		}
1319
1320		#[cfg(feature = "wasm-runtime")]
1321		return;
1322
1323		#[cfg(not(feature = "wasm-runtime"))]
1324		self.reset_sleep_timer_state();
1325	}
1326
1327	fn notify_inspector_attachments_changed(&self) {
1328		self.try_send_lifecycle_event(
1329			LifecycleEvent::InspectorAttachmentsChanged,
1330			"inspector_attachments_changed",
1331		);
1332	}
1333
1334	pub(crate) fn configure_sleep(&self, config: ActorConfig) {
1335		self.configure_sleep_state(config.clone());
1336		self.configure_queue(config);
1337		self.reset_sleep_timer();
1338	}
1339
1340	pub(crate) fn sleep_config(&self) -> ActorConfig {
1341		self.sleep_state_config()
1342	}
1343
1344	pub(crate) fn sleep_requested(&self) -> bool {
1345		self.0.sleep_requested.load(Ordering::SeqCst)
1346	}
1347
1348	pub(crate) fn clear_sleep_requested(&self) {
1349		self.0.sleep_requested.store(false, Ordering::SeqCst);
1350	}
1351
1352	pub(crate) async fn internal_keep_awake_task(
1353		&self,
1354		future: BoxFuture<'static, Result<()>>,
1355	) -> Result<()> {
1356		self.internal_keep_awake(future).await
1357	}
1358
1359	pub fn websocket_callback_region(&self) -> WebSocketCallbackRegion {
1360		WebSocketCallbackRegion {
1361			region: Some(self.begin_work_region(ActorWorkKind::WebSocketCallback)),
1362		}
1363	}
1364
1365	pub(crate) async fn with_websocket_callback<F, Fut, T>(&self, run: F) -> T
1366	where
1367		F: FnOnce() -> Fut,
1368		Fut: Future<Output = T>,
1369	{
1370		let _guard = self.websocket_callback_region();
1371		run().await
1372	}
1373
1374	fn idle_work_region(&self, kind: ActorWorkKind) -> Option<RegionGuard> {
1375		if !kind.policy().blocks_idle_sleep {
1376			return None;
1377		}
1378		let region = match kind {
1379			ActorWorkKind::Action => self.internal_keep_awake_region(),
1380			ActorWorkKind::KeepAwake => self.keep_awake_region_state(),
1381			ActorWorkKind::InternalKeepAwake => self.internal_keep_awake_region(),
1382			ActorWorkKind::WaitUntil => return None,
1383			ActorWorkKind::RegisteredTask => return None,
1384			ActorWorkKind::WebSocketCallback => self.websocket_callback_region_state(),
1385			ActorWorkKind::DisconnectCallback => self.disconnect_callback_region_state(),
1386		};
1387		Some(region.with_log_fields(kind.label(), Some(self.actor_id().to_owned())))
1388	}
1389
1390	fn shutdown_work_region(&self) -> CountGuard {
1391		let counter = self.0.sleep.work.shutdown_counter.clone();
1392		counter.increment();
1393		CountGuard::from_incremented(counter)
1394	}
1395
1396	fn configure_sleep_hooks(&self) {
1397		let keep_awake_ctx = self.downgrade();
1398		self.0
1399			.sleep
1400			.work
1401			.keep_awake
1402			.register_change_callback(Arc::new(move || {
1403				if let Some(ctx) = ActorContext::from_weak(&keep_awake_ctx) {
1404					ctx.0
1405						.metrics
1406						.set_keep_awake_active(ctx.sleep_keep_awake_count());
1407				}
1408			}));
1409
1410		let internal_keep_awake_metric_ctx = self.downgrade();
1411		self.0
1412			.sleep
1413			.work
1414			.internal_keep_awake
1415			.register_change_callback(Arc::new(move || {
1416				if let Some(ctx) = ActorContext::from_weak(&internal_keep_awake_metric_ctx) {
1417					ctx.0
1418						.metrics
1419						.set_internal_keep_awake_active(ctx.sleep_internal_keep_awake_count());
1420				}
1421			}));
1422
1423		let shutdown_tasks_ctx = self.downgrade();
1424		self.0
1425			.sleep
1426			.work
1427			.shutdown_counter
1428			.register_change_callback(Arc::new(move || {
1429				if let Some(ctx) = ActorContext::from_weak(&shutdown_tasks_ctx) {
1430					ctx.0
1431						.metrics
1432						.set_shutdown_tasks_active(ctx.shutdown_task_count());
1433				}
1434			}));
1435
1436		let internal_keep_awake_ctx = self.downgrade();
1437		self.set_internal_keep_awake(Some(Arc::new(move |future| {
1438			let ctx = ActorContext::from_weak(&internal_keep_awake_ctx);
1439			Box::pin(async move {
1440				let Some(ctx) = ctx else {
1441					return Err(ActorRuntime::NotConfigured {
1442						component: "actor context".to_owned(),
1443					}
1444					.build());
1445				};
1446				ctx.internal_keep_awake_task(future).await
1447			})
1448		})));
1449
1450		let queue_ctx = self.downgrade();
1451		self.set_wait_activity_callback(Some(Arc::new(move || {
1452			if let Some(ctx) = ActorContext::from_weak(&queue_ctx) {
1453				ctx.reset_sleep_timer();
1454			}
1455		})));
1456
1457		let queue_ctx = self.downgrade();
1458		self.set_inspector_update_callback(Some(Arc::new(move |queue_size| {
1459			if let Some(ctx) = ActorContext::from_weak(&queue_ctx) {
1460				ctx.record_queue_updated(queue_size);
1461			}
1462		})));
1463	}
1464
1465	pub(crate) fn record_state_updated(&self) {
1466		if let Some(inspector) = self.inspector() {
1467			inspector.record_state_updated();
1468		}
1469	}
1470
1471	pub(crate) fn record_connections_updated(&self) {
1472		let Some(inspector) = self.inspector() else {
1473			return;
1474		};
1475		let active_connections = self.active_connection_count();
1476		inspector.record_connections_updated(active_connections);
1477	}
1478
1479	fn record_queue_updated(&self, queue_size: u32) {
1480		if let Some(inspector) = self.inspector() {
1481			inspector.record_queue_updated(queue_size);
1482		}
1483	}
1484
1485	pub(crate) async fn save_state_with_revision(
1486		&self,
1487		deltas: Vec<StateDelta>,
1488		save_request_revision: u64,
1489	) -> Result<()> {
1490		let (deltas, pending_hibernation_changes) = match self.prepare_state_deltas(deltas) {
1491			Ok(prepared) => prepared,
1492			Err(error) => return Err(error),
1493		};
1494		if let Err(error) = self.apply_state_deltas(deltas, save_request_revision).await {
1495			self.restore_pending_hibernation_changes(pending_hibernation_changes);
1496			return Err(error);
1497		}
1498		self.record_state_updated();
1499		Ok(())
1500	}
1501
1502	async fn dispatch_scheduled_action(&self, event_id: &str, action: String, args: Vec<u8>) {
1503		self.cancel_scheduled_event(event_id);
1504		let ctx = self.clone();
1505		let event_id = event_id.to_owned();
1506		let internal_keep_awake_region = self.begin_work_region(ActorWorkKind::InternalKeepAwake);
1507
1508		self.track_shutdown_task(async move {
1509			let _internal_keep_awake_region = internal_keep_awake_region;
1510			ctx.record_user_task_started(UserTaskKind::ScheduledAction);
1511			let started_at = Instant::now();
1512			let action_name = action.clone();
1513			let (reply_tx, reply_rx) = oneshot::channel();
1514
1515			match ctx.try_send_actor_event(
1516				ActorEvent::Action {
1517					name: action.clone(),
1518					args,
1519					conn: None,
1520					reply: Reply::from(reply_tx),
1521				},
1522				"scheduled_action",
1523			) {
1524				Ok(()) => match reply_rx.await {
1525					Ok(Ok(_)) => {}
1526					Ok(Err(error)) => {
1527						tracing::error!(
1528							?error,
1529							event_id,
1530							action_name,
1531							"scheduled event execution failed"
1532						);
1533					}
1534					Err(error) => {
1535						tracing::error!(
1536							?error,
1537							event_id,
1538							action_name,
1539							"scheduled event reply dropped"
1540						);
1541					}
1542				},
1543				Err(error) => {
1544					tracing::error!(
1545						?error,
1546						event_id,
1547						action_name,
1548						"failed to enqueue scheduled event"
1549					);
1550				}
1551			}
1552
1553			ctx.record_user_task_finished(UserTaskKind::ScheduledAction, started_at.elapsed());
1554		});
1555	}
1556
1557	fn inspector_attach_count_arc(&self) -> Option<Arc<AtomicU32>> {
1558		self.0.inspector_attach_count.read().clone()
1559	}
1560
1561	fn try_send_lifecycle_event(&self, event: LifecycleEvent, operation: &'static str) {
1562		let Some(sender) = self.0.lifecycle_events.read().clone() else {
1563			return;
1564		};
1565
1566		if sender.send(event).is_err() {
1567			tracing::warn!(operation, "failed to enqueue actor lifecycle event");
1568		}
1569	}
1570}
1571
1572fn now_timestamp_ms() -> i64 {
1573	let duration = SystemTime::now()
1574		.duration_since(UNIX_EPOCH)
1575		.unwrap_or_default();
1576	i64::try_from(duration.as_millis()).unwrap_or(i64::MAX)
1577}
1578
1579struct ActorWorkGuard {
1580	ctx: ActorContext,
1581	kind: ActorWorkKind,
1582	started_at: Option<Instant>,
1583	idle_region: Option<RegionGuard>,
1584	shutdown_region: Option<CountGuard>,
1585}
1586
1587#[must_use]
1588pub struct ActorWorkRegion {
1589	guard: Option<ActorWorkGuard>,
1590}
1591
1592impl ActorWorkGuard {
1593	fn new(ctx: ActorContext, kind: ActorWorkKind) -> Self {
1594		let policy = kind.policy();
1595		let idle_region = ctx.idle_work_region(kind);
1596		let shutdown_region = if policy.drains_shutdown_grace {
1597			Some(ctx.shutdown_work_region())
1598		} else {
1599			None
1600		};
1601		let started_at = if let Some(user_task_kind) = policy.user_task_kind {
1602			ctx.record_user_task_started(user_task_kind);
1603			Some(Instant::now())
1604		} else {
1605			None
1606		};
1607		ctx.reset_sleep_timer();
1608		Self {
1609			ctx,
1610			kind,
1611			started_at,
1612			idle_region,
1613			shutdown_region,
1614		}
1615	}
1616}
1617
1618impl Drop for ActorWorkGuard {
1619	fn drop(&mut self) {
1620		if let Some(started_at) = self.started_at.take()
1621			&& let Some(user_task_kind) = self.kind.policy().user_task_kind
1622		{
1623			self.ctx
1624				.record_user_task_finished(user_task_kind, started_at.elapsed());
1625		}
1626		self.idle_region.take();
1627		self.shutdown_region.take();
1628		self.ctx.reset_sleep_timer();
1629	}
1630}
1631
1632impl Drop for ActorWorkRegion {
1633	fn drop(&mut self) {
1634		self.guard.take();
1635	}
1636}
1637
1638#[must_use]
1639#[derive(Debug)]
1640pub(crate) struct InspectorAttachGuard {
1641	ctx: ActorContext,
1642}
1643
1644impl InspectorAttachGuard {
1645	fn new(ctx: ActorContext) -> Option<Self> {
1646		let attach_count = ctx.inspector_attach_count_arc()?;
1647		let previous = attach_count.fetch_add(1, Ordering::SeqCst);
1648		let current = previous.saturating_add(1);
1649		tracing::debug!(
1650			actor_id = %ctx.actor_id(),
1651			previous_count = previous,
1652			current_count = current,
1653			"inspector attached"
1654		);
1655		if previous == 0 {
1656			ctx.notify_inspector_attachments_changed();
1657		}
1658		Some(Self { ctx })
1659	}
1660}
1661
1662impl Drop for InspectorAttachGuard {
1663	fn drop(&mut self) {
1664		let Some(attach_count) = self.ctx.inspector_attach_count_arc() else {
1665			return;
1666		};
1667		let Ok(previous) =
1668			attach_count.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current| {
1669				current.checked_sub(1)
1670			})
1671		else {
1672			return;
1673		};
1674		let current = previous.saturating_sub(1);
1675		tracing::debug!(
1676			actor_id = %self.ctx.actor_id(),
1677			previous_count = previous,
1678			current_count = current,
1679			"inspector detached"
1680		);
1681		if previous == 1 {
1682			self.ctx.notify_inspector_attachments_changed();
1683		}
1684	}
1685}
1686
1687pub struct WebSocketCallbackRegion {
1688	region: Option<ActorWorkRegion>,
1689}
1690
1691pub struct KeepAwakeRegion {
1692	region: Option<ActorWorkRegion>,
1693}
1694
1695impl Drop for WebSocketCallbackRegion {
1696	fn drop(&mut self) {
1697		self.region.take();
1698	}
1699}
1700
1701impl Drop for KeepAwakeRegion {
1702	fn drop(&mut self) {
1703		// Take the region explicitly to mirror WebSocketCallbackRegion.
1704		self.region.take();
1705	}
1706}
1707
1708impl std::fmt::Debug for ActorContext {
1709	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1710		f.debug_struct("ActorContext")
1711			.field("actor_id", &self.0.actor_id)
1712			.field("name", &self.0.name)
1713			.field("key", &self.0.key)
1714			.field("region", &self.0.region)
1715			.finish()
1716	}
1717}
1718
1719// Test shim keeps moved tests in crate-root tests/ with private-module access.
1720#[cfg(test)]
1721#[path = "../../tests/context.rs"]
1722pub(crate) mod tests;