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, ActorHttpResponse, QueueSendResult, Request, 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::development_process::DevelopmentProcessManager;
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	/// Resolves after the Engine sends the envoy initialization message.
103	pub async fn started(&self) -> anyhow::Result<()> {
104		self.handle.started().await
105	}
106
107	/// Resolves once the envoy has no active actors (or has stopped).
108	pub async fn wait_actors_drained(&self) {
109		self.handle.wait_actors_drained().await
110	}
111
112	/// Engine-reported drain threshold in milliseconds. `None` until the
113	/// envoy has completed its first protocol-metadata exchange with the
114	/// engine.
115	pub async fn actor_stop_threshold_ms(&self) -> Option<i64> {
116		self.handle
117			.get_protocol_metadata()
118			.await
119			.map(|metadata| metadata.actor_stop_threshold)
120	}
121}
122
123#[derive(Clone)]
124struct ActorTaskHandle {
125	actor_id: String,
126	actor_name: String,
127	generation: u32,
128	ctx: ActorContext,
129	factory: Arc<ActorFactory>,
130	inspector: Inspector,
131	lifecycle: mpsc::UnboundedSender<LifecycleCommand>,
132	dispatch: mpsc::UnboundedSender<DispatchCommand>,
133	join: Arc<TokioMutex<Option<JoinHandle<Result<()>>>>>,
134}
135
136type ActiveActorInstance = Arc<ActorTaskHandle>;
137
138enum ActorInstanceState {
139	Active(ActiveActorInstance),
140	Stopping {
141		instance: ActiveActorInstance,
142		reason: ShutdownKind,
143	},
144}
145
146impl ActorInstanceState {
147	fn instance(&self) -> ActiveActorInstance {
148		match self {
149			Self::Active(instance) | Self::Stopping { instance, .. } => instance.clone(),
150		}
151	}
152
153	fn active_instance(&self) -> Option<ActiveActorInstance> {
154		match self {
155			Self::Active(instance) => Some(instance.clone()),
156			Self::Stopping { .. } => None,
157		}
158	}
159}
160
161#[derive(Clone)]
162struct PendingStop {
163	generation: u32,
164	reason: protocol::StopActorReason,
165	stop_handle: ActorStopHandle,
166}
167
168/// Outcome of attempting to transition the actor instance under an id to stopping
169/// for a specific generation.
170enum TransitionResult {
171	/// The current instance matches the requested generation and was moved to stopping.
172	Transitioned(ActiveActorInstance),
173	/// An instance exists but for a different generation than the stop targets, so it
174	/// was left untouched. The stop must not be applied to it.
175	Stale,
176	/// No instance is registered for the actor id.
177	Vacant,
178}
179
180pub(crate) struct RegistryDispatcher {
181	pub(crate) factories: HashMap<String, Arc<ActorFactory>>,
182	actor_instances: SccHashMap<String, ActorInstanceState>,
183	starting_instances: SccHashMap<String, Arc<Notify>>,
184	pending_stops: SccHashMap<String, PendingStop>,
185	region: String,
186	handle_inspector_http_in_runtime: bool,
187}
188
189pub(crate) struct RegistryCallbacks {
190	pub(crate) dispatcher: Arc<RegistryDispatcher>,
191}
192
193#[derive(Clone, Debug)]
194struct StartActorRequest {
195	actor_id: String,
196	generation: u32,
197	actor_name: String,
198	input: Option<Vec<u8>>,
199	ctx: ActorContext,
200}
201
202#[derive(Clone, Debug)]
203struct ServeSettings {
204	version: u32,
205	endpoint: String,
206	token: Option<String>,
207	namespace: String,
208	pool_name: String,
209	engine_binary_path: Option<PathBuf>,
210	start_services: bool,
211	services_binary_path: Option<PathBuf>,
212	engine_host: Option<String>,
213	engine_port: Option<u16>,
214	engine_spawn: EngineSpawnMode,
215	engine_auto_download: bool,
216	handle_inspector_http_in_runtime: bool,
217	serverless_base_path: Option<String>,
218	serverless_package_version: String,
219	serverless_client_endpoint: Option<String>,
220	serverless_client_namespace: Option<String>,
221	serverless_client_token: Option<String>,
222	serverless_validate_endpoint: bool,
223	serverless_max_start_payload_bytes: usize,
224}
225
226#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
227pub enum EngineSpawnMode {
228	#[default]
229	Auto,
230	Always,
231	Never,
232}
233
234impl EngineSpawnMode {
235	pub(crate) fn from_env() -> Self {
236		match env::var("RIVETKIT_ENGINE_SPAWN") {
237			Ok(value) if value.eq_ignore_ascii_case("always") => Self::Always,
238			Ok(value) if value.eq_ignore_ascii_case("never") => Self::Never,
239			_ => Self::Auto,
240		}
241	}
242}
243
244/// Selects how `Registry::start` runs, mirroring the TypeScript
245/// `RIVETKIT_RUNTIME_MODE` env var. `Envoy` holds one long-lived outbound
246/// envoy for the process lifetime; `Serverless` runs an HTTP listener that
247/// lazily starts and caches an envoy on the first request.
248#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
249pub enum RuntimeMode {
250	#[default]
251	Envoy,
252	Serverless,
253}
254
255impl RuntimeMode {
256	pub fn from_env() -> Self {
257		match env::var("RIVETKIT_RUNTIME_MODE") {
258			Ok(value) if value.eq_ignore_ascii_case("serverless") => Self::Serverless,
259			_ => Self::Envoy,
260		}
261	}
262}
263
264#[derive(Clone, Debug, Default)]
265pub struct ServeConfig {
266	pub version: u32,
267	pub endpoint: String,
268	pub token: Option<String>,
269	pub namespace: String,
270	pub pool_name: String,
271	pub engine_binary_path: Option<PathBuf>,
272	pub start_services: bool,
273	pub services_binary_path: Option<PathBuf>,
274	pub engine_host: Option<String>,
275	pub engine_port: Option<u16>,
276	pub engine_spawn: EngineSpawnMode,
277	pub engine_auto_download: bool,
278	pub handle_inspector_http_in_runtime: bool,
279	pub serverless_base_path: Option<String>,
280	pub serverless_package_version: String,
281	pub serverless_client_endpoint: Option<String>,
282	pub serverless_client_namespace: Option<String>,
283	pub serverless_client_token: Option<String>,
284	pub serverless_validate_endpoint: bool,
285	pub serverless_max_start_payload_bytes: usize,
286	pub serverless_cache_envoy: bool,
287}
288
289#[derive(Debug, Default, Deserialize)]
290#[serde(default)]
291struct InspectorPatchStateBody {
292	state: JsonValue,
293}
294
295#[derive(Debug, Default, Deserialize)]
296#[serde(default)]
297struct InspectorActionBody {
298	args: Vec<JsonValue>,
299	properties: Option<JsonValue>,
300}
301
302#[derive(Debug, Default, Deserialize)]
303#[serde(default)]
304struct InspectorDatabaseExecuteBody {
305	sql: String,
306	args: Vec<JsonValue>,
307	properties: Option<JsonValue>,
308}
309
310#[derive(Debug, Default, Deserialize)]
311#[serde(default, rename_all = "camelCase")]
312struct InspectorWorkflowReplayBody {
313	entry_id: Option<String>,
314}
315
316#[derive(Debug, Default, Deserialize)]
317#[serde(default)]
318struct InspectorEnqueueBody {
319	name: String,
320	body: Option<JsonValue>,
321}
322
323#[derive(Debug, Serialize)]
324#[serde(rename_all = "camelCase")]
325struct InspectorQueueMessageJson {
326	id: u64,
327	name: String,
328	created_at_ms: i64,
329}
330
331#[derive(Debug, Serialize)]
332#[serde(rename_all = "camelCase")]
333struct InspectorQueueResponseJson {
334	size: u32,
335	max_size: u32,
336	truncated: bool,
337	messages: Vec<InspectorQueueMessageJson>,
338}
339
340#[derive(Debug, Deserialize)]
341#[serde(default)]
342struct HttpActionRequestJson {
343	args: JsonValue,
344}
345
346impl Default for HttpActionRequestJson {
347	fn default() -> Self {
348		Self {
349			args: JsonValue::Array(Vec::new()),
350		}
351	}
352}
353
354pub(crate) fn should_manage_engine(endpoint: &str, spawn_mode: EngineSpawnMode) -> Result<bool> {
355	match spawn_mode {
356		EngineSpawnMode::Always => Ok(true),
357		EngineSpawnMode::Never => Ok(false),
358		EngineSpawnMode::Auto => is_loopback_endpoint(endpoint),
359	}
360}
361
362fn is_loopback_endpoint(endpoint: &str) -> Result<bool> {
363	let url =
364		Url::parse(endpoint).with_context(|| format!("parse engine endpoint `{endpoint}`"))?;
365	let Some(host) = url.host_str() else {
366		anyhow::bail!("engine endpoint `{endpoint}` is invalid: missing host");
367	};
368
369	if host == "localhost" || host.ends_with(".localhost") {
370		return Ok(true);
371	}
372
373	let ip_host = host
374		.strip_prefix('[')
375		.and_then(|value| value.strip_suffix(']'))
376		.unwrap_or(host);
377
378	Ok(ip_host
379		.parse::<std::net::IpAddr>()
380		.map(|ip| ip.is_loopback() || ip.is_unspecified())
381		.unwrap_or(false))
382}
383
384#[cfg(test)]
385mod engine_spawn_tests {
386	use super::{EngineSpawnMode, should_manage_engine};
387
388	#[test]
389	fn auto_manages_loopback_endpoints() {
390		assert!(should_manage_engine("http://127.0.0.1:6420", EngineSpawnMode::Auto).unwrap());
391		assert!(should_manage_engine("http://localhost:6420", EngineSpawnMode::Auto).unwrap());
392		assert!(should_manage_engine("http://dev.localhost:6420", EngineSpawnMode::Auto).unwrap());
393		assert!(should_manage_engine("http://[::1]:6420", EngineSpawnMode::Auto).unwrap());
394	}
395
396	#[test]
397	fn auto_leaves_remote_endpoints_connect_only() {
398		assert!(!should_manage_engine("https://api.rivet.dev", EngineSpawnMode::Auto).unwrap());
399		assert!(!should_manage_engine("http://192.0.2.10:6420", EngineSpawnMode::Auto).unwrap());
400	}
401
402	#[test]
403	fn explicit_spawn_mode_overrides_endpoint_shape() {
404		assert!(should_manage_engine("https://api.rivet.dev", EngineSpawnMode::Always).unwrap());
405		assert!(!should_manage_engine("http://127.0.0.1:6420", EngineSpawnMode::Never).unwrap());
406	}
407}
408
409#[derive(Debug, Deserialize)]
410#[serde(default, rename_all = "camelCase")]
411struct HttpQueueSendRequestJson {
412	body: JsonValue,
413	wait: Option<bool>,
414	timeout: Option<u64>,
415}
416
417impl Default for HttpQueueSendRequestJson {
418	fn default() -> Self {
419		Self {
420			body: JsonValue::Null,
421			wait: None,
422			timeout: None,
423		}
424	}
425}
426
427#[derive(RivetError)]
428#[error("message", "incoming_too_long", "Incoming message too long")]
429struct IncomingMessageTooLong;
430
431#[derive(RivetError)]
432#[error("message", "outgoing_too_long", "Outgoing message too long")]
433struct OutgoingMessageTooLong;
434
435#[derive(RivetError)]
436#[error("actor", "action_timed_out", "Action timed out")]
437struct ActionTimedOut;
438
439#[derive(RivetError, Serialize)]
440#[error("actor", "method_not_allowed", "Method not allowed")]
441struct MethodNotAllowed {
442	method: String,
443	path: String,
444}
445
446#[derive(Debug, Serialize)]
447#[serde(rename_all = "camelCase")]
448struct InspectorConnectionJson {
449	#[serde(rename = "type")]
450	connection_type: Option<String>,
451	id: String,
452	details: InspectorConnectionDetailsJson,
453}
454
455#[derive(Debug, Serialize)]
456#[serde(rename_all = "camelCase")]
457struct InspectorConnectionDetailsJson {
458	#[serde(rename = "type")]
459	connection_type: Option<String>,
460	params: JsonValue,
461	state_enabled: bool,
462	state: JsonValue,
463	subscriptions: usize,
464	is_hibernatable: bool,
465}
466
467#[derive(Debug, Serialize)]
468#[serde(rename_all = "camelCase")]
469struct InspectorSummaryJson {
470	state: JsonValue,
471	is_state_enabled: bool,
472	connections: Vec<InspectorConnectionJson>,
473	rpcs: Vec<String>,
474	queue_size: u32,
475	is_database_enabled: bool,
476	#[serde(rename = "isWorkflowEnabled")]
477	workflow_supported: bool,
478	workflow_history: Option<JsonValue>,
479}
480
481const WS_PROTOCOL_ENCODING: &str = "rivet_encoding.";
482const WS_PROTOCOL_CONN_PARAMS: &str = "rivet_conn_params.";
483
484#[derive(Debug)]
485struct ActorConnectInit {
486	actor_id: String,
487	connection_id: String,
488}
489
490#[derive(Debug)]
491struct ActorConnectError {
492	group: String,
493	code: String,
494	message: String,
495	metadata: Option<ByteBuf>,
496	action_id: Option<u64>,
497	actor: Option<ActorSpecifier>,
498}
499
500#[derive(Debug)]
501struct ActorConnectActionResponse {
502	id: u64,
503	output: ByteBuf,
504}
505
506#[derive(Debug)]
507struct ActorConnectEvent {
508	name: String,
509	args: ByteBuf,
510}
511
512#[derive(Clone, Copy, Debug, PartialEq, Eq)]
513enum ActorConnectEncoding {
514	Json,
515	Cbor,
516	Bare,
517}
518
519#[derive(Debug)]
520enum ActorConnectToClient {
521	Init(ActorConnectInit),
522	Error(ActorConnectError),
523	ActionResponse(ActorConnectActionResponse),
524	Event(ActorConnectEvent),
525}
526
527#[derive(Debug)]
528struct ActorConnectActionRequest {
529	id: u64,
530	name: String,
531	args: ByteBuf,
532}
533
534#[derive(Debug)]
535enum ActorConnectSendError {
536	OutgoingTooLong,
537	Encode(anyhow::Error),
538}
539
540#[derive(Debug, Deserialize)]
541struct ActorConnectSubscriptionRequest {
542	#[serde(rename = "eventName")]
543	event_name: String,
544	subscribe: bool,
545}
546
547#[derive(Debug)]
548enum ActorConnectToServer {
549	ActionRequest(ActorConnectActionRequest),
550	SubscriptionRequest(ActorConnectSubscriptionRequest),
551}
552
553#[derive(Debug, Deserialize)]
554struct ActorConnectActionRequestJson {
555	id: u64,
556	name: String,
557	args: JsonValue,
558}
559
560#[derive(Debug, Deserialize)]
561#[serde(tag = "tag", content = "val")]
562enum ActorConnectToServerJsonBody {
563	ActionRequest(ActorConnectActionRequestJson),
564	SubscriptionRequest(ActorConnectSubscriptionRequest),
565}
566
567#[derive(Debug, Deserialize)]
568struct ActorConnectToServerJsonEnvelope {
569	body: ActorConnectToServerJsonBody,
570}
571
572impl CoreRegistry {
573	pub fn new() -> Self {
574		Self::default()
575	}
576
577	pub fn register(&mut self, name: &str, factory: ActorFactory) {
578		self.factories.insert(name.to_owned(), Arc::new(factory));
579	}
580
581	pub fn register_shared(&mut self, name: &str, factory: Arc<ActorFactory>) {
582		self.factories.insert(name.to_owned(), factory);
583	}
584
585	pub fn normal_metadata_payload(&self, config: &ServeConfig) -> ServerlessMetadataPayload {
586		serverless_metadata_payload(
587			build_actor_metadata_map_from_factories(&self.factories),
588			config,
589			ServerlessMetadataEnvoyKind::Normal {},
590		)
591	}
592
593	pub fn serverless_metadata_payload(&self, config: &ServeConfig) -> ServerlessMetadataPayload {
594		serverless_metadata_payload(
595			build_actor_metadata_map_from_factories(&self.factories),
596			config,
597			ServerlessMetadataEnvoyKind::Serverless {},
598		)
599	}
600
601	pub async fn serve(self, shutdown: CancellationToken) -> Result<()> {
602		self.serve_with_config(ServeConfig::from_env()?, shutdown)
603			.await
604	}
605
606	pub async fn serve_with_config(
607		self,
608		config: ServeConfig,
609		shutdown: CancellationToken,
610	) -> Result<()> {
611		self.serve_with_config_and_handle_observer(config, shutdown, |_| {})
612			.await
613	}
614
615	pub async fn serve_with_config_and_handle_observer(
616		self,
617		config: ServeConfig,
618		shutdown: CancellationToken,
619		on_handle: impl FnOnce(CoreEnvoyHandle) + Send + 'static,
620	) -> Result<()> {
621		crate::metrics::record_rivetkit_info(
622			config.serverless_package_version.clone(),
623			config.version,
624			"serverful",
625			config.pool_name.clone(),
626		);
627		#[cfg(not(target_arch = "wasm32"))]
628		crate::tokio_runtime_metrics::ensure_sampler_started();
629
630		let dispatcher = self.into_dispatcher(&config);
631		let manage_engine = should_manage_engine(&config.endpoint, config.engine_spawn)?;
632		#[cfg(feature = "native-runtime")]
633		let development_processes = if manage_engine {
634			Some(DevelopmentProcessManager::start(&config).await?)
635		} else {
636			None
637		};
638		#[cfg(not(feature = "native-runtime"))]
639		if manage_engine {
640			anyhow::bail!("engine process spawning requires the `native-runtime` feature");
641		}
642
643		#[cfg(feature = "native-runtime")]
644		runner_config::ensure_local_normal_runner_config(&config).await?;
645		let callbacks = Arc::new(RegistryCallbacks {
646			dispatcher: dispatcher.clone(),
647		});
648
649		let prepopulate_actor_names = dispatcher
650			.build_actor_metadata_map()
651			.into_iter()
652			.map(|(name, metadata)| (name, rivet_envoy_client::config::ActorName { metadata }))
653			.collect();
654		let handle = start_envoy(rivet_envoy_client::config::EnvoyConfig {
655			version: config.version,
656			endpoint: config.endpoint,
657			token: config.token,
658			namespace: config.namespace,
659			pool_name: config.pool_name,
660			prepopulate_actor_names,
661			metadata: Some(json!({
662				"rivetkit": { "version": config.serverless_package_version },
663			})),
664			not_global: false,
665			debug_latency_ms: None,
666			callbacks,
667		})
668		.await;
669		on_handle(CoreEnvoyHandle::new(handle.clone()));
670
671		// Do not install `tokio::signal::ctrl_c()` here. It calls
672		// `sigaction(SIGINT, ...)` at the POSIX level, which overrides the
673		// host's default SIGINT handling when rivetkit-core is embedded in
674		// Node via NAPI and leaves the host process unable to exit. Callers
675		// trip the `shutdown` token instead.
676		shutdown.cancelled().await;
677
678		let shutdown_envoy = async {
679			// TODO: Move into envoy-client since timing out has to do with protocol compliance
680			// Read threshold from protocol metadata, fall back to 30 min
681			let stop_threshold = handle
682				.get_protocol_metadata()
683				.await
684				.map(|x| x.actor_stop_threshold)
685				.unwrap_or(30 * 60 * 1000);
686			// Bounded drain. If envoy cannot reach the engine (reconnect loop stuck),
687			// we fall back to immediate `Stop` rather than hanging indefinitely.
688			// The outer host (TS signal handler / Rust binary) is the backstop.
689			match timeout(
690				Duration::from_millis(stop_threshold as u64),
691				handle.shutdown_and_wait(false),
692			)
693			.await
694			{
695				Ok(()) => {}
696				Err(_) => {
697					tracing::warn!("envoy shutdown drain exceeded timeout; forcing immediate stop");
698					handle.shutdown(true);
699					handle.wait_stopped().await;
700				}
701			}
702		};
703		#[cfg(feature = "native-runtime")]
704		let shutdown_development_processes = async move {
705			if let Some(development_processes) = development_processes {
706				development_processes.shutdown().await;
707			}
708		};
709		#[cfg(feature = "native-runtime")]
710		tokio::join!(shutdown_envoy, shutdown_development_processes);
711		#[cfg(not(feature = "native-runtime"))]
712		shutdown_envoy.await;
713
714		Ok(())
715	}
716
717	fn into_dispatcher(self, config: &ServeConfig) -> Arc<RegistryDispatcher> {
718		Arc::new(RegistryDispatcher::new(
719			self.factories,
720			config.handle_inspector_http_in_runtime,
721		))
722	}
723
724	pub async fn into_serverless_runtime(
725		self,
726		config: ServeConfig,
727	) -> Result<crate::serverless::CoreServerlessRuntime> {
728		crate::serverless::CoreServerlessRuntime::new(self.factories, config).await
729	}
730}
731
732impl RegistryDispatcher {
733	pub(crate) fn new(
734		factories: HashMap<String, Arc<ActorFactory>>,
735		handle_inspector_http_in_runtime: bool,
736	) -> Self {
737		Self {
738			factories,
739			actor_instances: SccHashMap::new(),
740			starting_instances: SccHashMap::new(),
741			pending_stops: SccHashMap::new(),
742			region: env::var("RIVET_REGION").unwrap_or_default(),
743			handle_inspector_http_in_runtime,
744		}
745	}
746
747	pub(crate) fn build_actor_metadata_map(&self) -> HashMap<String, JsonValue> {
748		build_actor_metadata_map_from_factories(&self.factories)
749	}
750}
751
752pub(crate) fn serverless_metadata_payload(
753	actor_metadata: HashMap<String, JsonValue>,
754	config: &ServeConfig,
755	envoy_kind: ServerlessMetadataEnvoyKind,
756) -> ServerlessMetadataPayload {
757	let actor_names = actor_metadata
758		.into_iter()
759		.map(|(name, metadata)| {
760			(
761				name,
762				ActorName {
763					metadata: Some(metadata),
764				},
765			)
766		})
767		.collect::<HashMap<_, _>>();
768
769	ServerlessMetadataPayload {
770		runtime: "rivetkit".to_owned(),
771		version: config.serverless_package_version.clone(),
772		envoy_protocol_version: Some(protocol::PROTOCOL_VERSION),
773		actor_names,
774		envoy: Some(ServerlessMetadataEnvoy {
775			kind: Some(envoy_kind),
776			version: Some(config.version),
777		}),
778		runner: None,
779		client_endpoint: config.serverless_client_endpoint.clone(),
780		client_namespace: config.serverless_client_namespace.clone(),
781		client_token: config.serverless_client_token.clone(),
782	}
783}
784
785fn build_actor_metadata_map_from_factories(
786	factories: &HashMap<String, Arc<ActorFactory>>,
787) -> HashMap<String, JsonValue> {
788	factories
789		.iter()
790		.map(|(actor_name, factory)| {
791			let config = factory.config();
792			let mut metadata = serde_json::Map::new();
793			if let Some(icon) = &config.icon {
794				metadata.insert("icon".to_owned(), json!(icon));
795			}
796			if let Some(name) = &config.name {
797				metadata.insert("name".to_owned(), json!(name));
798			}
799			(actor_name.clone(), JsonValue::Object(metadata))
800		})
801		.collect()
802}
803
804impl RegistryDispatcher {
805	async fn start_actor(self: &Arc<Self>, request: StartActorRequest) -> Result<()> {
806		let startup_notify = Arc::new(Notify::new());
807		let _ = self
808			.starting_instances
809			.insert_async(request.actor_id.clone(), startup_notify.clone())
810			.await;
811		// Test-only seam: lets a test hold a generation in the "starting" window so it
812		// can deterministically deliver a stop for a previous generation that parks
813		// under the actor id and gets consumed by this startup.
814		#[cfg(test)]
815		test_hooks::wait_for_startup_gate(&request.actor_id).await;
816		let factory = self
817			.factories
818			.get(&request.actor_name)
819			.cloned()
820			.ok_or_else(|| {
821				ActorRuntime::NotRegistered {
822					actor_name: request.actor_name.clone(),
823				}
824				.build()
825			})?;
826		let (lifecycle_tx, lifecycle_rx) = mpsc::unbounded_channel();
827		let (dispatch_tx, dispatch_rx) = mpsc::unbounded_channel();
828		let (lifecycle_events_tx, lifecycle_events_rx) = mpsc::unbounded_channel();
829		request
830			.ctx
831			.configure_lifecycle_events(Some(lifecycle_events_tx));
832		request.ctx.cancel_sleep_timer();
833		request.ctx.set_local_alarm_callback(Some(Arc::new({
834			let lifecycle_tx = lifecycle_tx.clone();
835			move || {
836				let lifecycle_tx = lifecycle_tx.clone();
837				Box::pin(async move {
838					let (reply_tx, reply_rx) = oneshot::channel();
839					if let Err(error) = try_send_lifecycle_command(
840						&lifecycle_tx,
841						LifecycleCommand::FireAlarm { reply: reply_tx },
842					) {
843						tracing::warn!(?error, "failed to enqueue actor alarm");
844						return;
845					}
846					let _ = reply_rx.await;
847				})
848			}
849		})));
850		let task = ActorTask::new(
851			request.actor_id.clone(),
852			request.generation,
853			lifecycle_rx,
854			dispatch_rx,
855			lifecycle_events_rx,
856			factory.clone(),
857			request.ctx.clone(),
858			request.input,
859		);
860		let join = RuntimeSpawner::spawn(task.run());
861
862		let (start_tx, start_rx) = oneshot::channel();
863		let result: Result<Arc<ActorTaskHandle>> = async {
864			try_send_lifecycle_command(&lifecycle_tx, LifecycleCommand::Start { reply: start_tx })
865				.context("send actor task start command")?;
866			start_rx
867				.await
868				.context("receive actor task start reply")?
869				.context("actor task start")?;
870			let inspector = build_actor_inspector();
871			request.ctx.configure_inspector(Some(inspector.clone()));
872			Ok::<Arc<ActorTaskHandle>, anyhow::Error>(Arc::new(ActorTaskHandle {
873				actor_id: request.actor_id.clone(),
874				actor_name: request.actor_name.clone(),
875				generation: request.generation,
876				ctx: request.ctx.clone(),
877				factory,
878				inspector,
879				lifecycle: lifecycle_tx,
880				dispatch: dispatch_tx,
881				join: Arc::new(TokioMutex::new(Some(join))),
882			}))
883		}
884		.await
885		.with_context(|| format!("start actor `{}`", request.actor_id));
886
887		match result {
888			Ok(instance) => {
889				let pending_stop = self
890					.pending_stops
891					.remove_async(&request.actor_id.clone())
892					.await
893					.map(|(_, pending_stop)| pending_stop);
894				// Only apply a parked stop if it targets the generation we just started.
895				// A stop parked for a previous generation is stale: complete its handle
896				// so teardown finalizes, but leave the new generation running.
897				let pending_stop = match pending_stop {
898					Some(pending_stop) if pending_stop.generation == request.generation => {
899						Some(pending_stop)
900					}
901					Some(stale_stop) => {
902						let _ = stale_stop.stop_handle.complete();
903						None
904					}
905					None => None,
906				};
907				if let Some(pending_stop) = pending_stop {
908					let actor_id = request.actor_id.clone();
909					let stop_reason = map_envoy_stop_reason(&pending_stop.reason);
910					if matches!(stop_reason, ShutdownKind::Destroy) {
911						instance.ctx.mark_destroy_requested();
912					}
913					self.set_actor_instance_state(
914						actor_id.clone(),
915						ActorInstanceState::Stopping {
916							instance: instance.clone(),
917							reason: stop_reason,
918						},
919					)
920					.await;
921					let _ = self
922						.starting_instances
923						.remove_async(&request.actor_id.clone())
924						.await;
925
926					let dispatcher = self.clone();
927					RuntimeSpawner::spawn(async move {
928						if let Err(error) = dispatcher
929							.shutdown_started_instance(
930								&actor_id,
931								instance.clone(),
932								pending_stop.reason,
933								pending_stop.stop_handle,
934							)
935							.await
936						{
937							tracing::error!(
938								actor_id,
939								?error,
940								"failed to stop actor queued during startup"
941							);
942						}
943						dispatcher
944							.remove_stopping_actor_instance(&actor_id, &instance)
945							.await;
946					});
947					startup_notify.notify_waiters();
948
949					Ok(())
950				} else {
951					self.set_actor_instance_state(
952						request.actor_id.clone(),
953						ActorInstanceState::Active(instance),
954					)
955					.await;
956					let _ = self
957						.starting_instances
958						.remove_async(&request.actor_id.clone())
959						.await;
960					startup_notify.notify_waiters();
961					Ok(())
962				}
963			}
964			Err(error) => {
965				let _ = self
966					.starting_instances
967					.remove_async(&request.actor_id.clone())
968					.await;
969				// A stop parked while this start was in flight would otherwise leak its
970				// ActorStopHandle in the map (the map holds the sender alive, hanging the
971				// caller). Drain and complete it since there is no instance to stop.
972				if let Some((_, pending_stop)) = self
973					.pending_stops
974					.remove_async(&request.actor_id.clone())
975					.await
976				{
977					let _ = pending_stop.stop_handle.complete();
978				}
979				startup_notify.notify_waiters();
980				Err(error)
981			}
982		}
983	}
984
985	async fn set_actor_instance_state(&self, actor_id: String, state: ActorInstanceState) {
986		match self.actor_instances.entry_async(actor_id).await {
987			SccEntry::Occupied(mut entry) => {
988				entry.insert(state);
989			}
990			SccEntry::Vacant(entry) => {
991				entry.insert_entry(state);
992			}
993		}
994	}
995
996	async fn transition_actor_to_stopping(
997		&self,
998		actor_id: &str,
999		generation: u32,
1000		reason: ShutdownKind,
1001	) -> TransitionResult {
1002		match self.actor_instances.entry_async(actor_id.to_owned()).await {
1003			SccEntry::Occupied(mut entry) => {
1004				let instance = entry.get().instance();
1005				// A stop is scoped to the generation it was issued for. If the currently
1006				// registered instance is a different generation (e.g. a lost previous
1007				// generation whose replacement is already running), the stop is stale and
1008				// must not tear down the newer generation.
1009				if instance.generation != generation {
1010					return TransitionResult::Stale;
1011				}
1012				if matches!(entry.get(), ActorInstanceState::Active(_)) {
1013					entry.insert(ActorInstanceState::Stopping {
1014						instance: instance.clone(),
1015						reason,
1016					});
1017				} else {
1018					instance
1019						.ctx
1020						.warn_work_sent_to_stopping_instance("stop_actor");
1021				}
1022				TransitionResult::Transitioned(instance)
1023			}
1024			SccEntry::Vacant(entry) => {
1025				drop(entry);
1026				TransitionResult::Vacant
1027			}
1028		}
1029	}
1030
1031	async fn remove_stopping_actor_instance(&self, actor_id: &str, expected: &ActiveActorInstance) {
1032		match self.actor_instances.entry_async(actor_id.to_owned()).await {
1033			SccEntry::Occupied(entry) => {
1034				let should_remove = match entry.get() {
1035					ActorInstanceState::Stopping { instance, .. } => {
1036						Arc::ptr_eq(instance, expected)
1037					}
1038					ActorInstanceState::Active(_) => false,
1039				};
1040				if should_remove {
1041					let _ = entry.remove_entry();
1042				}
1043			}
1044			SccEntry::Vacant(entry) => {
1045				drop(entry);
1046			}
1047		}
1048	}
1049
1050	async fn active_actor(&self, actor_id: &str) -> Result<Arc<ActorTaskHandle>> {
1051		if let Some(instance) = self.actor_instances.get_async(&actor_id.to_owned()).await {
1052			match instance.get() {
1053				ActorInstanceState::Active(instance) => {
1054					let instance = instance.clone();
1055					// TODO: Share admission policy with ActorTask::dispatch_lifecycle_error.
1056					if instance.ctx.started() {
1057						if instance.ctx.destroy_requested() {
1058							instance
1059								.ctx
1060								.warn_work_sent_to_stopping_instance("active_actor");
1061							return Err(ActorLifecycleError::Destroying.build());
1062						}
1063						return Ok(instance);
1064					}
1065
1066					instance
1067						.ctx
1068						.warn_work_sent_to_stopping_instance("active_actor");
1069					return Err(if instance.ctx.destroy_requested() {
1070						ActorLifecycleError::Destroying.build()
1071					} else if instance.ctx.sleep_requested() {
1072						ActorLifecycleError::Stopping.build()
1073					} else {
1074						ActorLifecycleError::Starting.build()
1075					});
1076				}
1077				ActorInstanceState::Stopping { instance, reason } => {
1078					let instance = instance.clone();
1079					match reason {
1080						ShutdownKind::Sleep if instance.ctx.started() => return Ok(instance),
1081						ShutdownKind::Sleep => {
1082							instance
1083								.ctx
1084								.warn_work_sent_to_stopping_instance("active_actor");
1085							return Err(ActorLifecycleError::Stopping.build());
1086						}
1087						ShutdownKind::Destroy => {
1088							instance
1089								.ctx
1090								.warn_work_sent_to_stopping_instance("active_actor");
1091							return Err(ActorLifecycleError::Destroying.build());
1092						}
1093					}
1094				}
1095			}
1096		}
1097
1098		tracing::warn!(actor_id, "actor instance not found");
1099		Err(ActorRuntime::NotFound {
1100			resource: "instance".to_owned(),
1101			id: actor_id.to_owned(),
1102		}
1103		.build())
1104	}
1105
1106	async fn stop_actor(
1107		&self,
1108		actor_id: &str,
1109		generation: u32,
1110		reason: protocol::StopActorReason,
1111		stop_handle: ActorStopHandle,
1112	) -> Result<()> {
1113		if self
1114			.starting_instances
1115			.get_async(&actor_id.to_owned())
1116			.await
1117			.is_some()
1118		{
1119			// The target generation is still starting. Park the stop with its generation
1120			// so startup can decide whether it belongs to the generation being started.
1121			let _ = self
1122				.pending_stops
1123				.insert_async(
1124					actor_id.to_owned(),
1125					PendingStop {
1126						generation,
1127						reason,
1128						stop_handle,
1129					},
1130				)
1131				.await;
1132			return Ok(());
1133		}
1134
1135		let task_stop_reason = map_envoy_stop_reason(&reason);
1136		match self
1137			.transition_actor_to_stopping(actor_id, generation, task_stop_reason)
1138			.await
1139		{
1140			TransitionResult::Transitioned(instance) => {
1141				let result = self
1142					.shutdown_started_instance(actor_id, instance.clone(), reason, stop_handle)
1143					.await;
1144				self.remove_stopping_actor_instance(actor_id, &instance)
1145					.await;
1146				result
1147			}
1148			TransitionResult::Stale => {
1149				// The running instance is a different generation; this stop targets a
1150				// generation that is already gone. Complete the handle so envoy-client
1151				// finalizes teardown cleanly instead of warning about a dropped handle.
1152				let _ = stop_handle.complete();
1153				Ok(())
1154			}
1155			TransitionResult::Vacant => {
1156				let _ = self
1157					.pending_stops
1158					.insert_async(
1159						actor_id.to_owned(),
1160						PendingStop {
1161							generation,
1162							reason,
1163							stop_handle,
1164						},
1165					)
1166					.await;
1167				Ok(())
1168			}
1169		}
1170	}
1171
1172	async fn shutdown_started_instance(
1173		&self,
1174		actor_id: &str,
1175		instance: Arc<ActorTaskHandle>,
1176		reason: protocol::StopActorReason,
1177		stop_handle: ActorStopHandle,
1178	) -> Result<()> {
1179		let task_stop_reason = map_envoy_stop_reason(&reason);
1180
1181		if matches!(task_stop_reason, ShutdownKind::Destroy) {
1182			instance.ctx.mark_destroy_requested();
1183		}
1184
1185		tracing::debug!(
1186			actor_id,
1187			handle_actor_id = %instance.actor_id,
1188			actor_name = %instance.actor_name,
1189			generation = instance.generation,
1190			?reason,
1191			?task_stop_reason,
1192			"stopping actor instance"
1193		);
1194
1195		let (reply_tx, reply_rx) = oneshot::channel();
1196		let shutdown_result = match try_send_lifecycle_command(
1197			&instance.lifecycle,
1198			LifecycleCommand::Stop {
1199				reason: task_stop_reason,
1200				reply: reply_tx,
1201			},
1202		) {
1203			Ok(()) => reply_rx
1204				.await
1205				.context("receive actor task stop reply")
1206				.and_then(|result| result),
1207			Err(error) => Err(error),
1208		};
1209
1210		if matches!(task_stop_reason, ShutdownKind::Destroy) {
1211			let shutdown_deadline =
1212				Instant::now() + instance.factory.config().effective_sleep_grace_period();
1213			if !instance
1214				.ctx
1215				.wait_for_internal_keep_awake_idle(shutdown_deadline)
1216				.await
1217			{
1218				instance.ctx.record_direct_subsystem_shutdown_warning(
1219					"internal_keep_awake",
1220					"destroy_drain",
1221				);
1222				tracing::warn!(
1223					actor_id,
1224					"destroy shutdown timed out waiting for in-flight actions"
1225				);
1226			}
1227			if !instance
1228				.ctx
1229				.wait_for_http_requests_drained(shutdown_deadline)
1230				.await
1231			{
1232				instance
1233					.ctx
1234					.record_direct_subsystem_shutdown_warning("http_requests", "destroy_drain");
1235				tracing::warn!(
1236					actor_id,
1237					"destroy shutdown timed out waiting for in-flight http requests"
1238				);
1239			}
1240		}
1241
1242		let mut join_guard = instance.join.lock().await;
1243		// Fold the join outcome into the result instead of propagating with `?`,
1244		// so the stop handle is always signaled and never dropped unsignaled.
1245		let join_result = if let Some(join) = join_guard.take() {
1246			join.await
1247				.context("join actor task")
1248				.and_then(|result| result.context("actor task failed"))
1249		} else {
1250			Ok(())
1251		};
1252		instance.ctx.configure_lifecycle_events(None);
1253
1254		if let (Err(shutdown_error), Err(join_error)) = (&shutdown_result, &join_result) {
1255			tracing::warn!(
1256				actor_id,
1257				%shutdown_error,
1258				discarded_join_error = %join_error,
1259				"actor stop had both shutdown and join failures; only the shutdown error is returned"
1260			);
1261		}
1262
1263		let final_result = shutdown_result.and(join_result);
1264		match &final_result {
1265			Ok(_) => {
1266				let _ = stop_handle.complete();
1267			}
1268			Err(error) => {
1269				let _ = stop_handle.fail(anyhow::Error::new(RivetError::extract(error)));
1270			}
1271		}
1272
1273		final_result.with_context(|| format!("stop actor `{actor_id}`"))
1274	}
1275}
1276
1277impl RegistryDispatcher {
1278	fn can_hibernate(&self, actor_id: &str, request: &HttpRequest) -> bool {
1279		if matches!(is_actor_connect_path(&request.path), Ok(true)) {
1280			return true;
1281		}
1282
1283		let Some(instance) = self
1284			.actor_instances
1285			.read_sync(actor_id, |_, state| state.active_instance())
1286			.flatten()
1287		else {
1288			return false;
1289		};
1290
1291		match &instance.factory.config().can_hibernate_websocket {
1292			CanHibernateWebSocket::Bool(value) => *value,
1293			CanHibernateWebSocket::Callback(callback) => callback(request),
1294		}
1295	}
1296
1297	#[allow(clippy::too_many_arguments)]
1298	fn build_actor_context(
1299		&self,
1300		handle: EnvoyHandle,
1301		actor_id: &str,
1302		generation: u32,
1303		actor_name: &str,
1304		key: ActorKey,
1305		factory: &ActorFactory,
1306	) -> Result<ActorContext> {
1307		let formatted_key = format_actor_key(&key);
1308		let ctx = ActorContext::build(
1309			actor_id.to_owned(),
1310			actor_name.to_owned(),
1311			key,
1312			self.region.clone(),
1313			Some(generation),
1314			handle.get_envoy_key().to_owned(),
1315			factory.config().clone(),
1316			LegacyActorKv::new(handle.clone(), actor_id.to_owned()),
1317			SqliteDb::new_with_remote_sqlite(
1318				handle.clone(),
1319				actor_id.to_owned(),
1320				Some(formatted_key),
1321				Some(generation as u64),
1322				factory.config().has_database,
1323				factory.config().remote_sqlite,
1324			)?,
1325		);
1326		ctx.configure_envoy(handle, Some(generation));
1327		Ok(ctx)
1328	}
1329}
1330
1331/// Maps an envoy-protocol stop reason to the lifecycle `ShutdownKind` used by
1332/// `ActorTask`. Reallocation paths (the actor will resurrect on a new envoy)
1333/// are routed through `Sleep` so user `onSleep` runs and durable state is
1334/// preserved without firing a permanent destroy.
1335fn map_envoy_stop_reason(reason: &protocol::StopActorReason) -> ShutdownKind {
1336	match reason {
1337		// Idle sleep requested by the actor itself.
1338		protocol::StopActorReason::SleepIntent => ShutdownKind::Sleep,
1339		// Runner is being drained; engine will reallocate the actor on a new
1340		// envoy. Treat as sleep so persistent state and onSleep semantics hold.
1341		protocol::StopActorReason::GoingAway => ShutdownKind::Sleep,
1342		// Runner connection lost; once reconnected (or another runner is
1343		// allocated) the actor resurrects with the same id.
1344		protocol::StopActorReason::Lost => ShutdownKind::Sleep,
1345		// User-initiated stop intent (`ctx.destroy()` and equivalents).
1346		protocol::StopActorReason::StopIntent => ShutdownKind::Destroy,
1347		// Engine-initiated permanent destroy.
1348		protocol::StopActorReason::Destroy => ShutdownKind::Destroy,
1349	}
1350}
1351
1352// Test shim keeps moved tests in crate-root tests/ with private-module access.
1353#[cfg(test)]
1354#[path = "../../tests/registry.rs"]
1355pub(crate) mod tests;
1356
1357// Test-only hooks used by the moved registry tests to deterministically drive the
1358// generation-stop race. Gated behind `cfg(test)` so there is no production impact.
1359#[cfg(test)]
1360pub(crate) mod test_hooks {
1361	use std::sync::{Arc, OnceLock};
1362
1363	use scc::HashMap as SccHashMap;
1364	use tokio::sync::Semaphore;
1365
1366	static STARTUP_GATES: OnceLock<SccHashMap<String, Arc<Semaphore>>> = OnceLock::new();
1367
1368	fn gates() -> &'static SccHashMap<String, Arc<Semaphore>> {
1369		STARTUP_GATES.get_or_init(SccHashMap::new)
1370	}
1371
1372	/// Arms a gate so `start_actor` pauses for `actor_id` after registering as
1373	/// starting, until `release_startup_gate` is called.
1374	pub(crate) fn arm_startup_gate(actor_id: &str) {
1375		let _ = gates().insert_sync(actor_id.to_owned(), Arc::new(Semaphore::new(0)));
1376	}
1377
1378	/// Releases a previously armed gate, letting the paused `start_actor` continue.
1379	pub(crate) fn release_startup_gate(actor_id: &str) {
1380		if let Some(sem) = gates().read_sync(actor_id, |_, sem| sem.clone()) {
1381			sem.add_permits(1);
1382		}
1383	}
1384
1385	/// Called from inside `start_actor`. Blocks only if a gate is armed for the
1386	/// actor. Order-independent: a release before this runs still lets it through.
1387	pub(crate) async fn wait_for_startup_gate(actor_id: &str) {
1388		let sem = gates().read_sync(actor_id, |_, sem| sem.clone());
1389		if let Some(sem) = sem {
1390			let permit = sem.acquire().await.expect("startup gate semaphore closed");
1391			permit.forget();
1392			let _ = gates().remove_sync(actor_id);
1393		}
1394	}
1395}