Skip to main content

rivetkit_core/registry/
mod.rs

1use std::collections::HashMap;
2use std::env;
3use std::io::Cursor;
4use std::path::PathBuf;
5use std::sync::Arc;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::time::Duration;
8
9use crate::time::{Instant, timeout};
10
11use ::http::StatusCode;
12use anyhow::{Context, Result};
13use parking_lot::Mutex;
14use rivet_envoy_client::config::{
15	ActorStopHandle, BoxFuture as EnvoyBoxFuture, EnvoyCallbacks, HttpRequest, HttpResponse,
16	WebSocketHandler, WebSocketMessage, WebSocketSender,
17};
18use rivet_envoy_client::envoy::start_envoy;
19use rivet_envoy_client::handle::EnvoyHandle;
20use rivet_envoy_client::protocol;
21use rivet_error::{ActorSpecifier, RivetError};
22use rivetkit_client_protocol as client_protocol;
23use rivetkit_shared_types::serverless_metadata::{
24	ActorName, ServerlessMetadataEnvoy, ServerlessMetadataEnvoyKind, ServerlessMetadataPayload,
25};
26use scc::{HashMap as SccHashMap, hash_map::Entry as SccEntry};
27use serde::{Deserialize, Serialize};
28use serde_bytes::ByteBuf;
29use serde_json::{Value as JsonValue, json};
30use tokio::sync::{Mutex as TokioMutex, Notify, broadcast, mpsc, oneshot};
31use tokio::task::JoinHandle;
32use tokio_util::sync::CancellationToken;
33use url::Url;
34use vbare::OwnedVersionedData;
35
36use crate::actor::action::ActionDispatchError;
37use crate::actor::config::CanHibernateWebSocket;
38use crate::actor::connection::{ConnHandle, HibernatableConnectionMetadata};
39use crate::actor::context::{ActorContext, InspectorAttachGuard};
40use crate::actor::factory::ActorFactory;
41use crate::actor::kv::LegacyActorKv;
42use crate::actor::lifecycle_hooks::Reply;
43use crate::actor::messages::{ActorEvent, QueueSendResult, Request, Response, StateDelta};
44use crate::actor::task::{
45	ActorTask, DispatchCommand, LifecycleCommand, try_send_dispatch_command,
46	try_send_lifecycle_command,
47};
48use crate::actor::task_types::ShutdownKind;
49#[cfg(feature = "native-runtime")]
50use crate::engine_process::{EngineProcessManager, EngineResolverConfig};
51use crate::error::{ActorLifecycle as ActorLifecycleError, ActorRuntime};
52use crate::inspector::protocol::{
53	self as inspector_protocol, ServerMessage as InspectorServerMessage,
54};
55use crate::inspector::{Inspector, InspectorAuth, InspectorSignal, InspectorSubscription};
56use crate::runtime::RuntimeSpawner;
57use crate::sqlite::SqliteDb;
58use crate::types::{ActorKey, ActorKeySegment, WsMessage, format_actor_key};
59use crate::websocket::WebSocket;
60
61mod actor_connect;
62mod dispatch;
63mod envoy_callbacks;
64mod http;
65mod inspector;
66mod inspector_ws;
67#[cfg(feature = "native-runtime")]
68mod runner_config;
69mod websocket;
70
71use inspector::build_actor_inspector;
72use websocket::is_actor_connect_path;
73
74#[derive(Default)]
75pub struct CoreRegistry {
76	factories: HashMap<String, Arc<ActorFactory>>,
77}
78
79#[derive(Clone)]
80pub struct CoreEnvoyHandle {
81	handle: EnvoyHandle,
82}
83
84#[derive(Clone, Debug)]
85pub struct CoreEnvoyStatus {
86	pub active_actor_count: usize,
87	pub ping_healthy: bool,
88}
89
90impl CoreEnvoyHandle {
91	pub(crate) fn new(handle: EnvoyHandle) -> Self {
92		Self { handle }
93	}
94
95	pub fn status(&self) -> CoreEnvoyStatus {
96		CoreEnvoyStatus {
97			active_actor_count: self.handle.active_actor_count(),
98			ping_healthy: self.handle.is_ping_healthy(),
99		}
100	}
101
102	/// Engine-reported drain threshold in milliseconds. `None` until the
103	/// envoy has completed its first protocol-metadata exchange with the
104	/// engine.
105	pub async fn actor_stop_threshold_ms(&self) -> Option<i64> {
106		self.handle
107			.get_protocol_metadata()
108			.await
109			.map(|metadata| metadata.actor_stop_threshold)
110	}
111}
112
113#[derive(Clone)]
114struct ActorTaskHandle {
115	actor_id: String,
116	actor_name: String,
117	generation: u32,
118	ctx: ActorContext,
119	factory: Arc<ActorFactory>,
120	inspector: Inspector,
121	lifecycle: mpsc::UnboundedSender<LifecycleCommand>,
122	dispatch: mpsc::UnboundedSender<DispatchCommand>,
123	join: Arc<TokioMutex<Option<JoinHandle<Result<()>>>>>,
124}
125
126type ActiveActorInstance = Arc<ActorTaskHandle>;
127
128enum ActorInstanceState {
129	Active(ActiveActorInstance),
130	Stopping {
131		instance: ActiveActorInstance,
132		reason: ShutdownKind,
133	},
134}
135
136impl ActorInstanceState {
137	fn instance(&self) -> ActiveActorInstance {
138		match self {
139			Self::Active(instance) | Self::Stopping { instance, .. } => instance.clone(),
140		}
141	}
142
143	fn active_instance(&self) -> Option<ActiveActorInstance> {
144		match self {
145			Self::Active(instance) => Some(instance.clone()),
146			Self::Stopping { .. } => None,
147		}
148	}
149}
150
151#[derive(Clone)]
152struct PendingStop {
153	reason: protocol::StopActorReason,
154	stop_handle: ActorStopHandle,
155}
156
157pub(crate) struct RegistryDispatcher {
158	pub(crate) factories: HashMap<String, Arc<ActorFactory>>,
159	actor_instances: SccHashMap<String, ActorInstanceState>,
160	starting_instances: SccHashMap<String, Arc<Notify>>,
161	pending_stops: SccHashMap<String, PendingStop>,
162	region: String,
163	handle_inspector_http_in_runtime: bool,
164}
165
166pub(crate) struct RegistryCallbacks {
167	pub(crate) dispatcher: Arc<RegistryDispatcher>,
168}
169
170#[derive(Clone, Debug)]
171struct StartActorRequest {
172	actor_id: String,
173	generation: u32,
174	actor_name: String,
175	input: Option<Vec<u8>>,
176	ctx: ActorContext,
177}
178
179#[derive(Clone, Debug)]
180struct ServeSettings {
181	version: u32,
182	endpoint: String,
183	token: Option<String>,
184	namespace: String,
185	pool_name: String,
186	engine_binary_path: Option<PathBuf>,
187	engine_host: Option<String>,
188	engine_port: Option<u16>,
189	engine_spawn: EngineSpawnMode,
190	engine_auto_download: bool,
191	handle_inspector_http_in_runtime: bool,
192	serverless_base_path: Option<String>,
193	serverless_package_version: String,
194	serverless_client_endpoint: Option<String>,
195	serverless_client_namespace: Option<String>,
196	serverless_client_token: Option<String>,
197	serverless_validate_endpoint: bool,
198	serverless_max_start_payload_bytes: usize,
199}
200
201#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
202pub enum EngineSpawnMode {
203	#[default]
204	Auto,
205	Always,
206	Never,
207}
208
209impl EngineSpawnMode {
210	pub(crate) fn from_env() -> Self {
211		match env::var("RIVETKIT_ENGINE_SPAWN") {
212			Ok(value) if value.eq_ignore_ascii_case("always") => Self::Always,
213			Ok(value) if value.eq_ignore_ascii_case("never") => Self::Never,
214			_ => Self::Auto,
215		}
216	}
217}
218
219#[derive(Clone, Debug, Default)]
220pub struct ServeConfig {
221	pub version: u32,
222	pub endpoint: String,
223	pub token: Option<String>,
224	pub namespace: String,
225	pub pool_name: String,
226	pub engine_binary_path: Option<PathBuf>,
227	pub engine_host: Option<String>,
228	pub engine_port: Option<u16>,
229	pub engine_spawn: EngineSpawnMode,
230	pub engine_auto_download: bool,
231	pub handle_inspector_http_in_runtime: bool,
232	pub serverless_base_path: Option<String>,
233	pub serverless_package_version: String,
234	pub serverless_client_endpoint: Option<String>,
235	pub serverless_client_namespace: Option<String>,
236	pub serverless_client_token: Option<String>,
237	pub serverless_validate_endpoint: bool,
238	pub serverless_max_start_payload_bytes: usize,
239	pub serverless_cache_envoy: bool,
240}
241
242#[derive(Debug, Default, Deserialize)]
243#[serde(default)]
244struct InspectorPatchStateBody {
245	state: JsonValue,
246}
247
248#[derive(Debug, Default, Deserialize)]
249#[serde(default)]
250struct InspectorActionBody {
251	args: Vec<JsonValue>,
252	properties: Option<JsonValue>,
253}
254
255#[derive(Debug, Default, Deserialize)]
256#[serde(default)]
257struct InspectorDatabaseExecuteBody {
258	sql: String,
259	args: Vec<JsonValue>,
260	properties: Option<JsonValue>,
261}
262
263#[derive(Debug, Default, Deserialize)]
264#[serde(default, rename_all = "camelCase")]
265struct InspectorWorkflowReplayBody {
266	entry_id: Option<String>,
267}
268
269#[derive(Debug, Default, Deserialize)]
270#[serde(default)]
271struct InspectorEnqueueBody {
272	name: String,
273	body: Option<JsonValue>,
274}
275
276#[derive(Debug, Serialize)]
277#[serde(rename_all = "camelCase")]
278struct InspectorQueueMessageJson {
279	id: u64,
280	name: String,
281	created_at_ms: i64,
282}
283
284#[derive(Debug, Serialize)]
285#[serde(rename_all = "camelCase")]
286struct InspectorQueueResponseJson {
287	size: u32,
288	max_size: u32,
289	truncated: bool,
290	messages: Vec<InspectorQueueMessageJson>,
291}
292
293#[derive(Debug, Deserialize)]
294#[serde(default)]
295struct HttpActionRequestJson {
296	args: JsonValue,
297}
298
299impl Default for HttpActionRequestJson {
300	fn default() -> Self {
301		Self {
302			args: JsonValue::Array(Vec::new()),
303		}
304	}
305}
306
307pub(crate) fn should_manage_engine(endpoint: &str, spawn_mode: EngineSpawnMode) -> Result<bool> {
308	match spawn_mode {
309		EngineSpawnMode::Always => Ok(true),
310		EngineSpawnMode::Never => Ok(false),
311		EngineSpawnMode::Auto => is_loopback_endpoint(endpoint),
312	}
313}
314
315fn is_loopback_endpoint(endpoint: &str) -> Result<bool> {
316	let url =
317		Url::parse(endpoint).with_context(|| format!("parse engine endpoint `{endpoint}`"))?;
318	let Some(host) = url.host_str() else {
319		anyhow::bail!("engine endpoint `{endpoint}` is invalid: missing host");
320	};
321
322	if host == "localhost" || host.ends_with(".localhost") {
323		return Ok(true);
324	}
325
326	let ip_host = host
327		.strip_prefix('[')
328		.and_then(|value| value.strip_suffix(']'))
329		.unwrap_or(host);
330
331	Ok(ip_host
332		.parse::<std::net::IpAddr>()
333		.map(|ip| ip.is_loopback() || ip.is_unspecified())
334		.unwrap_or(false))
335}
336
337#[cfg(test)]
338mod engine_spawn_tests {
339	use super::{EngineSpawnMode, should_manage_engine};
340
341	#[test]
342	fn auto_manages_loopback_endpoints() {
343		assert!(should_manage_engine("http://127.0.0.1:6420", EngineSpawnMode::Auto).unwrap());
344		assert!(should_manage_engine("http://localhost:6420", EngineSpawnMode::Auto).unwrap());
345		assert!(should_manage_engine("http://dev.localhost:6420", EngineSpawnMode::Auto).unwrap());
346		assert!(should_manage_engine("http://[::1]:6420", EngineSpawnMode::Auto).unwrap());
347	}
348
349	#[test]
350	fn auto_leaves_remote_endpoints_connect_only() {
351		assert!(!should_manage_engine("https://api.rivet.dev", EngineSpawnMode::Auto).unwrap());
352		assert!(!should_manage_engine("http://192.0.2.10:6420", EngineSpawnMode::Auto).unwrap());
353	}
354
355	#[test]
356	fn explicit_spawn_mode_overrides_endpoint_shape() {
357		assert!(should_manage_engine("https://api.rivet.dev", EngineSpawnMode::Always).unwrap());
358		assert!(!should_manage_engine("http://127.0.0.1:6420", EngineSpawnMode::Never).unwrap());
359	}
360}
361
362#[derive(Debug, Deserialize)]
363#[serde(default, rename_all = "camelCase")]
364struct HttpQueueSendRequestJson {
365	body: JsonValue,
366	wait: Option<bool>,
367	timeout: Option<u64>,
368}
369
370impl Default for HttpQueueSendRequestJson {
371	fn default() -> Self {
372		Self {
373			body: JsonValue::Null,
374			wait: None,
375			timeout: None,
376		}
377	}
378}
379
380#[derive(RivetError)]
381#[error("message", "incoming_too_long", "Incoming message too long")]
382struct IncomingMessageTooLong;
383
384#[derive(RivetError)]
385#[error("message", "outgoing_too_long", "Outgoing message too long")]
386struct OutgoingMessageTooLong;
387
388#[derive(RivetError)]
389#[error("actor", "action_timed_out", "Action timed out")]
390struct ActionTimedOut;
391
392#[derive(RivetError, Serialize)]
393#[error("actor", "method_not_allowed", "Method not allowed")]
394struct MethodNotAllowed {
395	method: String,
396	path: String,
397}
398
399#[derive(Debug, Serialize)]
400#[serde(rename_all = "camelCase")]
401struct InspectorConnectionJson {
402	#[serde(rename = "type")]
403	connection_type: Option<String>,
404	id: String,
405	details: InspectorConnectionDetailsJson,
406}
407
408#[derive(Debug, Serialize)]
409#[serde(rename_all = "camelCase")]
410struct InspectorConnectionDetailsJson {
411	#[serde(rename = "type")]
412	connection_type: Option<String>,
413	params: JsonValue,
414	state_enabled: bool,
415	state: JsonValue,
416	subscriptions: usize,
417	is_hibernatable: bool,
418}
419
420#[derive(Debug, Serialize)]
421#[serde(rename_all = "camelCase")]
422struct InspectorSummaryJson {
423	state: JsonValue,
424	is_state_enabled: bool,
425	connections: Vec<InspectorConnectionJson>,
426	rpcs: Vec<String>,
427	queue_size: u32,
428	is_database_enabled: bool,
429	#[serde(rename = "isWorkflowEnabled")]
430	workflow_supported: bool,
431	workflow_history: Option<JsonValue>,
432}
433
434const WS_PROTOCOL_ENCODING: &str = "rivet_encoding.";
435const WS_PROTOCOL_CONN_PARAMS: &str = "rivet_conn_params.";
436
437#[derive(Debug)]
438struct ActorConnectInit {
439	actor_id: String,
440	connection_id: String,
441}
442
443#[derive(Debug)]
444struct ActorConnectError {
445	group: String,
446	code: String,
447	message: String,
448	metadata: Option<ByteBuf>,
449	action_id: Option<u64>,
450	actor: Option<ActorSpecifier>,
451}
452
453#[derive(Debug)]
454struct ActorConnectActionResponse {
455	id: u64,
456	output: ByteBuf,
457}
458
459#[derive(Debug)]
460struct ActorConnectEvent {
461	name: String,
462	args: ByteBuf,
463}
464
465#[derive(Clone, Copy, Debug, PartialEq, Eq)]
466enum ActorConnectEncoding {
467	Json,
468	Cbor,
469	Bare,
470}
471
472#[derive(Debug)]
473enum ActorConnectToClient {
474	Init(ActorConnectInit),
475	Error(ActorConnectError),
476	ActionResponse(ActorConnectActionResponse),
477	Event(ActorConnectEvent),
478}
479
480#[derive(Debug)]
481struct ActorConnectActionRequest {
482	id: u64,
483	name: String,
484	args: ByteBuf,
485}
486
487#[derive(Debug)]
488enum ActorConnectSendError {
489	OutgoingTooLong,
490	Encode(anyhow::Error),
491}
492
493#[derive(Debug, Deserialize)]
494struct ActorConnectSubscriptionRequest {
495	#[serde(rename = "eventName")]
496	event_name: String,
497	subscribe: bool,
498}
499
500#[derive(Debug)]
501enum ActorConnectToServer {
502	ActionRequest(ActorConnectActionRequest),
503	SubscriptionRequest(ActorConnectSubscriptionRequest),
504}
505
506#[derive(Debug, Deserialize)]
507struct ActorConnectActionRequestJson {
508	id: u64,
509	name: String,
510	args: JsonValue,
511}
512
513#[derive(Debug, Deserialize)]
514#[serde(tag = "tag", content = "val")]
515enum ActorConnectToServerJsonBody {
516	ActionRequest(ActorConnectActionRequestJson),
517	SubscriptionRequest(ActorConnectSubscriptionRequest),
518}
519
520#[derive(Debug, Deserialize)]
521struct ActorConnectToServerJsonEnvelope {
522	body: ActorConnectToServerJsonBody,
523}
524
525impl CoreRegistry {
526	pub fn new() -> Self {
527		Self::default()
528	}
529
530	pub fn register(&mut self, name: &str, factory: ActorFactory) {
531		self.factories.insert(name.to_owned(), Arc::new(factory));
532	}
533
534	pub fn register_shared(&mut self, name: &str, factory: Arc<ActorFactory>) {
535		self.factories.insert(name.to_owned(), factory);
536	}
537
538	pub fn normal_metadata_payload(&self, config: &ServeConfig) -> ServerlessMetadataPayload {
539		serverless_metadata_payload(
540			build_actor_metadata_map_from_factories(&self.factories),
541			config,
542			ServerlessMetadataEnvoyKind::Normal {},
543		)
544	}
545
546	pub fn serverless_metadata_payload(&self, config: &ServeConfig) -> ServerlessMetadataPayload {
547		serverless_metadata_payload(
548			build_actor_metadata_map_from_factories(&self.factories),
549			config,
550			ServerlessMetadataEnvoyKind::Serverless {},
551		)
552	}
553
554	pub async fn serve(self, shutdown: CancellationToken) -> Result<()> {
555		self.serve_with_config(ServeConfig::from_env(), shutdown)
556			.await
557	}
558
559	pub async fn serve_with_config(
560		self,
561		config: ServeConfig,
562		shutdown: CancellationToken,
563	) -> Result<()> {
564		self.serve_with_config_and_handle_observer(config, shutdown, |_| {})
565			.await
566	}
567
568	pub async fn serve_with_config_and_handle_observer(
569		self,
570		config: ServeConfig,
571		shutdown: CancellationToken,
572		on_handle: impl FnOnce(CoreEnvoyHandle) + Send + 'static,
573	) -> Result<()> {
574		crate::metrics_endpoint::record_rivetkit_info(
575			config.serverless_package_version.clone(),
576			config.version,
577			"serverful",
578			config.pool_name.clone(),
579		);
580
581		let dispatcher = self.into_dispatcher(&config);
582		#[cfg(feature = "native-runtime")]
583		let _engine_process = if should_manage_engine(&config.endpoint, config.engine_spawn)? {
584			Some(
585				EngineProcessManager::start_or_reuse(EngineResolverConfig::from_parts(
586					&config.endpoint,
587					config.engine_binary_path.clone(),
588					config.engine_host.clone(),
589					config.engine_port,
590					config.engine_auto_download,
591				))
592				.await?,
593			)
594		} else {
595			None
596		};
597		#[cfg(not(feature = "native-runtime"))]
598		if should_manage_engine(&config.endpoint, config.engine_spawn)? {
599			anyhow::bail!("engine process spawning requires the `native-runtime` feature");
600		}
601
602		#[cfg(feature = "native-runtime")]
603		runner_config::ensure_local_normal_runner_config(&config).await?;
604		let callbacks = Arc::new(RegistryCallbacks {
605			dispatcher: dispatcher.clone(),
606		});
607
608		let prepopulate_actor_names = dispatcher
609			.build_actor_metadata_map()
610			.into_iter()
611			.map(|(name, metadata)| (name, rivet_envoy_client::config::ActorName { metadata }))
612			.collect();
613		let handle = start_envoy(rivet_envoy_client::config::EnvoyConfig {
614			version: config.version,
615			endpoint: config.endpoint,
616			token: config.token,
617			namespace: config.namespace,
618			pool_name: config.pool_name,
619			prepopulate_actor_names,
620			metadata: Some(json!({
621				"rivetkit": { "version": config.serverless_package_version },
622			})),
623			not_global: false,
624			debug_latency_ms: None,
625			callbacks,
626		})
627		.await;
628		on_handle(CoreEnvoyHandle::new(handle.clone()));
629
630		// Do not install `tokio::signal::ctrl_c()` here. It calls
631		// `sigaction(SIGINT, ...)` at the POSIX level, which overrides the
632		// host's default SIGINT handling when rivetkit-core is embedded in
633		// Node via NAPI and leaves the host process unable to exit. Callers
634		// trip the `shutdown` token instead.
635		shutdown.cancelled().await;
636
637		// TODO: Move into envoy-client since timing out has to do with protocol compliance
638		// Read threshold from protocol metadata, fall back to 30 min
639		let stop_threshold = handle
640			.get_protocol_metadata()
641			.await
642			.map(|x| x.actor_stop_threshold)
643			.unwrap_or(30 * 60 * 1000);
644		// Bounded drain. If envoy cannot reach the engine (reconnect loop stuck),
645		// we fall back to immediate `Stop` rather than hanging indefinitely.
646		// The outer host (TS signal handler / Rust binary) is the backstop.
647		match timeout(
648			Duration::from_millis(stop_threshold as u64),
649			handle.shutdown_and_wait(false),
650		)
651		.await
652		{
653			Ok(()) => {}
654			Err(_) => {
655				tracing::warn!("envoy shutdown drain exceeded timeout; forcing immediate stop");
656				handle.shutdown(true);
657				handle.wait_stopped().await;
658			}
659		}
660
661		Ok(())
662	}
663
664	fn into_dispatcher(self, config: &ServeConfig) -> Arc<RegistryDispatcher> {
665		Arc::new(RegistryDispatcher::new(
666			self.factories,
667			config.handle_inspector_http_in_runtime,
668		))
669	}
670
671	pub async fn into_serverless_runtime(
672		self,
673		config: ServeConfig,
674	) -> Result<crate::serverless::CoreServerlessRuntime> {
675		crate::serverless::CoreServerlessRuntime::new(self.factories, config).await
676	}
677}
678
679impl RegistryDispatcher {
680	pub(crate) fn new(
681		factories: HashMap<String, Arc<ActorFactory>>,
682		handle_inspector_http_in_runtime: bool,
683	) -> Self {
684		Self {
685			factories,
686			actor_instances: SccHashMap::new(),
687			starting_instances: SccHashMap::new(),
688			pending_stops: SccHashMap::new(),
689			region: env::var("RIVET_REGION").unwrap_or_default(),
690			handle_inspector_http_in_runtime,
691		}
692	}
693
694	pub(crate) fn build_actor_metadata_map(&self) -> HashMap<String, JsonValue> {
695		build_actor_metadata_map_from_factories(&self.factories)
696	}
697}
698
699pub(crate) fn serverless_metadata_payload(
700	actor_metadata: HashMap<String, JsonValue>,
701	config: &ServeConfig,
702	envoy_kind: ServerlessMetadataEnvoyKind,
703) -> ServerlessMetadataPayload {
704	let actor_names = actor_metadata
705		.into_iter()
706		.map(|(name, metadata)| {
707			(
708				name,
709				ActorName {
710					metadata: Some(metadata),
711				},
712			)
713		})
714		.collect::<HashMap<_, _>>();
715
716	ServerlessMetadataPayload {
717		runtime: "rivetkit".to_owned(),
718		version: config.serverless_package_version.clone(),
719		envoy_protocol_version: Some(protocol::PROTOCOL_VERSION),
720		actor_names,
721		envoy: Some(ServerlessMetadataEnvoy {
722			kind: Some(envoy_kind),
723			version: Some(config.version),
724		}),
725		runner: None,
726		client_endpoint: config.serverless_client_endpoint.clone(),
727		client_namespace: config.serverless_client_namespace.clone(),
728		client_token: config.serverless_client_token.clone(),
729	}
730}
731
732fn build_actor_metadata_map_from_factories(
733	factories: &HashMap<String, Arc<ActorFactory>>,
734) -> HashMap<String, JsonValue> {
735	factories
736		.iter()
737		.map(|(actor_name, factory)| {
738			let config = factory.config();
739			let mut metadata = serde_json::Map::new();
740			if let Some(icon) = &config.icon {
741				metadata.insert("icon".to_owned(), json!(icon));
742			}
743			if let Some(name) = &config.name {
744				metadata.insert("name".to_owned(), json!(name));
745			}
746			(actor_name.clone(), JsonValue::Object(metadata))
747		})
748		.collect()
749}
750
751impl RegistryDispatcher {
752	async fn start_actor(self: &Arc<Self>, request: StartActorRequest) -> Result<()> {
753		let startup_notify = Arc::new(Notify::new());
754		let _ = self
755			.starting_instances
756			.insert_async(request.actor_id.clone(), startup_notify.clone())
757			.await;
758		let factory = self
759			.factories
760			.get(&request.actor_name)
761			.cloned()
762			.ok_or_else(|| {
763				ActorRuntime::NotRegistered {
764					actor_name: request.actor_name.clone(),
765				}
766				.build()
767			})?;
768		let (lifecycle_tx, lifecycle_rx) = mpsc::unbounded_channel();
769		let (dispatch_tx, dispatch_rx) = mpsc::unbounded_channel();
770		let (lifecycle_events_tx, lifecycle_events_rx) = mpsc::unbounded_channel();
771		request
772			.ctx
773			.configure_lifecycle_events(Some(lifecycle_events_tx));
774		request.ctx.cancel_sleep_timer();
775		request.ctx.set_local_alarm_callback(Some(Arc::new({
776			let lifecycle_tx = lifecycle_tx.clone();
777			move || {
778				let lifecycle_tx = lifecycle_tx.clone();
779				Box::pin(async move {
780					let (reply_tx, reply_rx) = oneshot::channel();
781					if let Err(error) = try_send_lifecycle_command(
782						&lifecycle_tx,
783						LifecycleCommand::FireAlarm { reply: reply_tx },
784					) {
785						tracing::warn!(?error, "failed to enqueue actor alarm");
786						return;
787					}
788					let _ = reply_rx.await;
789				})
790			}
791		})));
792		let task = ActorTask::new(
793			request.actor_id.clone(),
794			request.generation,
795			lifecycle_rx,
796			dispatch_rx,
797			lifecycle_events_rx,
798			factory.clone(),
799			request.ctx.clone(),
800			request.input,
801		);
802		let join = RuntimeSpawner::spawn(task.run());
803
804		let (start_tx, start_rx) = oneshot::channel();
805		let result: Result<Arc<ActorTaskHandle>> = async {
806			try_send_lifecycle_command(&lifecycle_tx, LifecycleCommand::Start { reply: start_tx })
807				.context("send actor task start command")?;
808			start_rx
809				.await
810				.context("receive actor task start reply")?
811				.context("actor task start")?;
812			let inspector = build_actor_inspector();
813			request.ctx.configure_inspector(Some(inspector.clone()));
814			Ok::<Arc<ActorTaskHandle>, anyhow::Error>(Arc::new(ActorTaskHandle {
815				actor_id: request.actor_id.clone(),
816				actor_name: request.actor_name.clone(),
817				generation: request.generation,
818				ctx: request.ctx.clone(),
819				factory,
820				inspector,
821				lifecycle: lifecycle_tx,
822				dispatch: dispatch_tx,
823				join: Arc::new(TokioMutex::new(Some(join))),
824			}))
825		}
826		.await
827		.with_context(|| format!("start actor `{}`", request.actor_id));
828
829		match result {
830			Ok(instance) => {
831				let pending_stop = self
832					.pending_stops
833					.remove_async(&request.actor_id.clone())
834					.await
835					.map(|(_, pending_stop)| pending_stop);
836				if let Some(pending_stop) = pending_stop {
837					let actor_id = request.actor_id.clone();
838					let stop_reason = map_envoy_stop_reason(&pending_stop.reason);
839					if matches!(stop_reason, ShutdownKind::Destroy) {
840						instance.ctx.mark_destroy_requested();
841					}
842					self.set_actor_instance_state(
843						actor_id.clone(),
844						ActorInstanceState::Stopping {
845							instance: instance.clone(),
846							reason: stop_reason,
847						},
848					)
849					.await;
850					let _ = self
851						.starting_instances
852						.remove_async(&request.actor_id.clone())
853						.await;
854
855					let dispatcher = self.clone();
856					RuntimeSpawner::spawn(async move {
857						if let Err(error) = dispatcher
858							.shutdown_started_instance(
859								&actor_id,
860								instance.clone(),
861								pending_stop.reason,
862								pending_stop.stop_handle,
863							)
864							.await
865						{
866							tracing::error!(
867								actor_id,
868								?error,
869								"failed to stop actor queued during startup"
870							);
871						}
872						dispatcher
873							.remove_stopping_actor_instance(&actor_id, &instance)
874							.await;
875					});
876					startup_notify.notify_waiters();
877
878					Ok(())
879				} else {
880					self.set_actor_instance_state(
881						request.actor_id.clone(),
882						ActorInstanceState::Active(instance),
883					)
884					.await;
885					let _ = self
886						.starting_instances
887						.remove_async(&request.actor_id.clone())
888						.await;
889					startup_notify.notify_waiters();
890					Ok(())
891				}
892			}
893			Err(error) => {
894				let _ = self
895					.starting_instances
896					.remove_async(&request.actor_id.clone())
897					.await;
898				startup_notify.notify_waiters();
899				Err(error)
900			}
901		}
902	}
903
904	async fn set_actor_instance_state(&self, actor_id: String, state: ActorInstanceState) {
905		match self.actor_instances.entry_async(actor_id).await {
906			SccEntry::Occupied(mut entry) => {
907				entry.insert(state);
908			}
909			SccEntry::Vacant(entry) => {
910				entry.insert_entry(state);
911			}
912		}
913	}
914
915	async fn transition_actor_to_stopping(
916		&self,
917		actor_id: &str,
918		reason: ShutdownKind,
919	) -> Option<ActiveActorInstance> {
920		match self.actor_instances.entry_async(actor_id.to_owned()).await {
921			SccEntry::Occupied(mut entry) => {
922				let instance = entry.get().instance();
923				if matches!(entry.get(), ActorInstanceState::Active(_)) {
924					entry.insert(ActorInstanceState::Stopping {
925						instance: instance.clone(),
926						reason,
927					});
928				} else {
929					instance
930						.ctx
931						.warn_work_sent_to_stopping_instance("stop_actor");
932				}
933				Some(instance)
934			}
935			SccEntry::Vacant(entry) => {
936				drop(entry);
937				None
938			}
939		}
940	}
941
942	async fn remove_stopping_actor_instance(&self, actor_id: &str, expected: &ActiveActorInstance) {
943		match self.actor_instances.entry_async(actor_id.to_owned()).await {
944			SccEntry::Occupied(entry) => {
945				let should_remove = match entry.get() {
946					ActorInstanceState::Stopping { instance, .. } => {
947						Arc::ptr_eq(instance, expected)
948					}
949					ActorInstanceState::Active(_) => false,
950				};
951				if should_remove {
952					let _ = entry.remove_entry();
953				}
954			}
955			SccEntry::Vacant(entry) => {
956				drop(entry);
957			}
958		}
959	}
960
961	async fn active_actor(&self, actor_id: &str) -> Result<Arc<ActorTaskHandle>> {
962		if let Some(instance) = self.actor_instances.get_async(&actor_id.to_owned()).await {
963			match instance.get() {
964				ActorInstanceState::Active(instance) => {
965					let instance = instance.clone();
966					// TODO: Share admission policy with ActorTask::dispatch_lifecycle_error.
967					if instance.ctx.started() {
968						if instance.ctx.destroy_requested() {
969							instance
970								.ctx
971								.warn_work_sent_to_stopping_instance("active_actor");
972							return Err(ActorLifecycleError::Destroying.build());
973						}
974						return Ok(instance);
975					}
976
977					instance
978						.ctx
979						.warn_work_sent_to_stopping_instance("active_actor");
980					return Err(if instance.ctx.destroy_requested() {
981						ActorLifecycleError::Destroying.build()
982					} else if instance.ctx.sleep_requested() {
983						ActorLifecycleError::Stopping.build()
984					} else {
985						ActorLifecycleError::Starting.build()
986					});
987				}
988				ActorInstanceState::Stopping { instance, reason } => {
989					let instance = instance.clone();
990					match reason {
991						ShutdownKind::Sleep if instance.ctx.started() => return Ok(instance),
992						ShutdownKind::Sleep => {
993							instance
994								.ctx
995								.warn_work_sent_to_stopping_instance("active_actor");
996							return Err(ActorLifecycleError::Stopping.build());
997						}
998						ShutdownKind::Destroy => {
999							instance
1000								.ctx
1001								.warn_work_sent_to_stopping_instance("active_actor");
1002							return Err(ActorLifecycleError::Destroying.build());
1003						}
1004					}
1005				}
1006			}
1007		}
1008
1009		tracing::warn!(actor_id, "actor instance not found");
1010		Err(ActorRuntime::NotFound {
1011			resource: "instance".to_owned(),
1012			id: actor_id.to_owned(),
1013		}
1014		.build())
1015	}
1016
1017	async fn stop_actor(
1018		&self,
1019		actor_id: &str,
1020		reason: protocol::StopActorReason,
1021		stop_handle: ActorStopHandle,
1022	) -> Result<()> {
1023		if self
1024			.starting_instances
1025			.get_async(&actor_id.to_owned())
1026			.await
1027			.is_some()
1028		{
1029			let _ = self
1030				.pending_stops
1031				.insert_async(
1032					actor_id.to_owned(),
1033					PendingStop {
1034						reason,
1035						stop_handle,
1036					},
1037				)
1038				.await;
1039			return Ok(());
1040		}
1041
1042		let task_stop_reason = map_envoy_stop_reason(&reason);
1043		let instance = match self
1044			.transition_actor_to_stopping(actor_id, task_stop_reason)
1045			.await
1046		{
1047			Some(instance) => instance,
1048			None => {
1049				let _ = self
1050					.pending_stops
1051					.insert_async(
1052						actor_id.to_owned(),
1053						PendingStop {
1054							reason,
1055							stop_handle,
1056						},
1057					)
1058					.await;
1059				return Ok(());
1060			}
1061		};
1062		let result = self
1063			.shutdown_started_instance(actor_id, instance.clone(), reason, stop_handle)
1064			.await;
1065		self.remove_stopping_actor_instance(actor_id, &instance)
1066			.await;
1067		result
1068	}
1069
1070	async fn shutdown_started_instance(
1071		&self,
1072		actor_id: &str,
1073		instance: Arc<ActorTaskHandle>,
1074		reason: protocol::StopActorReason,
1075		stop_handle: ActorStopHandle,
1076	) -> Result<()> {
1077		let task_stop_reason = map_envoy_stop_reason(&reason);
1078
1079		if matches!(task_stop_reason, ShutdownKind::Destroy) {
1080			instance.ctx.mark_destroy_requested();
1081		}
1082
1083		tracing::debug!(
1084			actor_id,
1085			handle_actor_id = %instance.actor_id,
1086			actor_name = %instance.actor_name,
1087			generation = instance.generation,
1088			?reason,
1089			?task_stop_reason,
1090			"stopping actor instance"
1091		);
1092
1093		let (reply_tx, reply_rx) = oneshot::channel();
1094		let shutdown_result = match try_send_lifecycle_command(
1095			&instance.lifecycle,
1096			LifecycleCommand::Stop {
1097				reason: task_stop_reason,
1098				reply: reply_tx,
1099			},
1100		) {
1101			Ok(()) => reply_rx
1102				.await
1103				.context("receive actor task stop reply")
1104				.and_then(|result| result),
1105			Err(error) => Err(error),
1106		};
1107
1108		if matches!(task_stop_reason, ShutdownKind::Destroy) {
1109			let shutdown_deadline =
1110				Instant::now() + instance.factory.config().effective_sleep_grace_period();
1111			if !instance
1112				.ctx
1113				.wait_for_internal_keep_awake_idle(shutdown_deadline)
1114				.await
1115			{
1116				instance.ctx.record_direct_subsystem_shutdown_warning(
1117					"internal_keep_awake",
1118					"destroy_drain",
1119				);
1120				tracing::warn!(
1121					actor_id,
1122					"destroy shutdown timed out waiting for in-flight actions"
1123				);
1124			}
1125			if !instance
1126				.ctx
1127				.wait_for_http_requests_drained(shutdown_deadline)
1128				.await
1129			{
1130				instance
1131					.ctx
1132					.record_direct_subsystem_shutdown_warning("http_requests", "destroy_drain");
1133				tracing::warn!(
1134					actor_id,
1135					"destroy shutdown timed out waiting for in-flight http requests"
1136				);
1137			}
1138		}
1139
1140		let mut join_guard = instance.join.lock().await;
1141		if let Some(join) = join_guard.take() {
1142			join.await
1143				.context("join actor task")?
1144				.context("actor task failed")?;
1145		}
1146		instance.ctx.configure_lifecycle_events(None);
1147
1148		match shutdown_result {
1149			Ok(_) => {
1150				let _ = stop_handle.complete();
1151				Ok(())
1152			}
1153			Err(error) => {
1154				let _ = stop_handle.fail(anyhow::Error::new(RivetError::extract(&error)));
1155				Err(error).with_context(|| format!("stop actor `{actor_id}`"))
1156			}
1157		}
1158	}
1159}
1160
1161impl RegistryDispatcher {
1162	fn can_hibernate(&self, actor_id: &str, request: &HttpRequest) -> bool {
1163		if matches!(is_actor_connect_path(&request.path), Ok(true)) {
1164			return true;
1165		}
1166
1167		let Some(instance) = self
1168			.actor_instances
1169			.read_sync(actor_id, |_, state| state.active_instance())
1170			.flatten()
1171		else {
1172			return false;
1173		};
1174
1175		match &instance.factory.config().can_hibernate_websocket {
1176			CanHibernateWebSocket::Bool(value) => *value,
1177			CanHibernateWebSocket::Callback(callback) => callback(request),
1178		}
1179	}
1180
1181	#[allow(clippy::too_many_arguments)]
1182	fn build_actor_context(
1183		&self,
1184		handle: EnvoyHandle,
1185		actor_id: &str,
1186		generation: u32,
1187		actor_name: &str,
1188		key: ActorKey,
1189		factory: &ActorFactory,
1190	) -> Result<ActorContext> {
1191		let formatted_key = format_actor_key(&key);
1192		let ctx = ActorContext::build(
1193			actor_id.to_owned(),
1194			actor_name.to_owned(),
1195			key,
1196			self.region.clone(),
1197			Some(generation),
1198			handle.get_envoy_key().to_owned(),
1199			factory.config().clone(),
1200			LegacyActorKv::new(handle.clone(), actor_id.to_owned()),
1201			SqliteDb::new_with_remote_sqlite(
1202				handle.clone(),
1203				actor_id.to_owned(),
1204				Some(formatted_key),
1205				Some(generation as u64),
1206				factory.config().has_database,
1207				factory.config().remote_sqlite,
1208			)?,
1209		);
1210		ctx.configure_envoy(handle, Some(generation));
1211		Ok(ctx)
1212	}
1213}
1214
1215/// Maps an envoy-protocol stop reason to the lifecycle `ShutdownKind` used by
1216/// `ActorTask`. Reallocation paths (the actor will resurrect on a new envoy)
1217/// are routed through `Sleep` so user `onSleep` runs and durable state is
1218/// preserved without firing a permanent destroy.
1219fn map_envoy_stop_reason(reason: &protocol::StopActorReason) -> ShutdownKind {
1220	match reason {
1221		// Idle sleep requested by the actor itself.
1222		protocol::StopActorReason::SleepIntent => ShutdownKind::Sleep,
1223		// Runner is being drained; engine will reallocate the actor on a new
1224		// envoy. Treat as sleep so persistent state and onSleep semantics hold.
1225		protocol::StopActorReason::GoingAway => ShutdownKind::Sleep,
1226		// Runner connection lost; once reconnected (or another runner is
1227		// allocated) the actor resurrects with the same id.
1228		protocol::StopActorReason::Lost => ShutdownKind::Sleep,
1229		// User-initiated stop intent (`ctx.destroy()` and equivalents).
1230		protocol::StopActorReason::StopIntent => ShutdownKind::Destroy,
1231		// Engine-initiated permanent destroy.
1232		protocol::StopActorReason::Destroy => ShutdownKind::Destroy,
1233	}
1234}
1235
1236// Test shim keeps moved tests in crate-root tests/ with private-module access.
1237#[cfg(test)]
1238#[path = "../../tests/registry.rs"]
1239pub(crate) mod tests;