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_endpoint::record_rivetkit_info(
622			config.serverless_package_version.clone(),
623			config.version,
624			"serverful",
625			config.pool_name.clone(),
626		);
627
628		let dispatcher = self.into_dispatcher(&config);
629		let manage_engine = should_manage_engine(&config.endpoint, config.engine_spawn)?;
630		#[cfg(feature = "native-runtime")]
631		let development_processes = if manage_engine {
632			Some(DevelopmentProcessManager::start(&config).await?)
633		} else {
634			None
635		};
636		#[cfg(not(feature = "native-runtime"))]
637		if manage_engine {
638			anyhow::bail!("engine process spawning requires the `native-runtime` feature");
639		}
640
641		#[cfg(feature = "native-runtime")]
642		runner_config::ensure_local_normal_runner_config(&config).await?;
643		let callbacks = Arc::new(RegistryCallbacks {
644			dispatcher: dispatcher.clone(),
645		});
646
647		let prepopulate_actor_names = dispatcher
648			.build_actor_metadata_map()
649			.into_iter()
650			.map(|(name, metadata)| (name, rivet_envoy_client::config::ActorName { metadata }))
651			.collect();
652		let handle = start_envoy(rivet_envoy_client::config::EnvoyConfig {
653			version: config.version,
654			endpoint: config.endpoint,
655			token: config.token,
656			namespace: config.namespace,
657			pool_name: config.pool_name,
658			prepopulate_actor_names,
659			metadata: Some(json!({
660				"rivetkit": { "version": config.serverless_package_version },
661			})),
662			not_global: false,
663			debug_latency_ms: None,
664			callbacks,
665		})
666		.await;
667		on_handle(CoreEnvoyHandle::new(handle.clone()));
668
669		// Do not install `tokio::signal::ctrl_c()` here. It calls
670		// `sigaction(SIGINT, ...)` at the POSIX level, which overrides the
671		// host's default SIGINT handling when rivetkit-core is embedded in
672		// Node via NAPI and leaves the host process unable to exit. Callers
673		// trip the `shutdown` token instead.
674		shutdown.cancelled().await;
675
676		let shutdown_envoy = async {
677			// TODO: Move into envoy-client since timing out has to do with protocol compliance
678			// Read threshold from protocol metadata, fall back to 30 min
679			let stop_threshold = handle
680				.get_protocol_metadata()
681				.await
682				.map(|x| x.actor_stop_threshold)
683				.unwrap_or(30 * 60 * 1000);
684			// Bounded drain. If envoy cannot reach the engine (reconnect loop stuck),
685			// we fall back to immediate `Stop` rather than hanging indefinitely.
686			// The outer host (TS signal handler / Rust binary) is the backstop.
687			match timeout(
688				Duration::from_millis(stop_threshold as u64),
689				handle.shutdown_and_wait(false),
690			)
691			.await
692			{
693				Ok(()) => {}
694				Err(_) => {
695					tracing::warn!("envoy shutdown drain exceeded timeout; forcing immediate stop");
696					handle.shutdown(true);
697					handle.wait_stopped().await;
698				}
699			}
700		};
701		#[cfg(feature = "native-runtime")]
702		let shutdown_development_processes = async move {
703			if let Some(development_processes) = development_processes {
704				development_processes.shutdown().await;
705			}
706		};
707		#[cfg(feature = "native-runtime")]
708		tokio::join!(shutdown_envoy, shutdown_development_processes);
709		#[cfg(not(feature = "native-runtime"))]
710		shutdown_envoy.await;
711
712		Ok(())
713	}
714
715	fn into_dispatcher(self, config: &ServeConfig) -> Arc<RegistryDispatcher> {
716		Arc::new(RegistryDispatcher::new(
717			self.factories,
718			config.handle_inspector_http_in_runtime,
719		))
720	}
721
722	pub async fn into_serverless_runtime(
723		self,
724		config: ServeConfig,
725	) -> Result<crate::serverless::CoreServerlessRuntime> {
726		crate::serverless::CoreServerlessRuntime::new(self.factories, config).await
727	}
728}
729
730impl RegistryDispatcher {
731	pub(crate) fn new(
732		factories: HashMap<String, Arc<ActorFactory>>,
733		handle_inspector_http_in_runtime: bool,
734	) -> Self {
735		Self {
736			factories,
737			actor_instances: SccHashMap::new(),
738			starting_instances: SccHashMap::new(),
739			pending_stops: SccHashMap::new(),
740			region: env::var("RIVET_REGION").unwrap_or_default(),
741			handle_inspector_http_in_runtime,
742		}
743	}
744
745	pub(crate) fn build_actor_metadata_map(&self) -> HashMap<String, JsonValue> {
746		build_actor_metadata_map_from_factories(&self.factories)
747	}
748}
749
750pub(crate) fn serverless_metadata_payload(
751	actor_metadata: HashMap<String, JsonValue>,
752	config: &ServeConfig,
753	envoy_kind: ServerlessMetadataEnvoyKind,
754) -> ServerlessMetadataPayload {
755	let actor_names = actor_metadata
756		.into_iter()
757		.map(|(name, metadata)| {
758			(
759				name,
760				ActorName {
761					metadata: Some(metadata),
762				},
763			)
764		})
765		.collect::<HashMap<_, _>>();
766
767	ServerlessMetadataPayload {
768		runtime: "rivetkit".to_owned(),
769		version: config.serverless_package_version.clone(),
770		envoy_protocol_version: Some(protocol::PROTOCOL_VERSION),
771		actor_names,
772		envoy: Some(ServerlessMetadataEnvoy {
773			kind: Some(envoy_kind),
774			version: Some(config.version),
775		}),
776		runner: None,
777		client_endpoint: config.serverless_client_endpoint.clone(),
778		client_namespace: config.serverless_client_namespace.clone(),
779		client_token: config.serverless_client_token.clone(),
780	}
781}
782
783fn build_actor_metadata_map_from_factories(
784	factories: &HashMap<String, Arc<ActorFactory>>,
785) -> HashMap<String, JsonValue> {
786	factories
787		.iter()
788		.map(|(actor_name, factory)| {
789			let config = factory.config();
790			let mut metadata = serde_json::Map::new();
791			if let Some(icon) = &config.icon {
792				metadata.insert("icon".to_owned(), json!(icon));
793			}
794			if let Some(name) = &config.name {
795				metadata.insert("name".to_owned(), json!(name));
796			}
797			(actor_name.clone(), JsonValue::Object(metadata))
798		})
799		.collect()
800}
801
802impl RegistryDispatcher {
803	async fn start_actor(self: &Arc<Self>, request: StartActorRequest) -> Result<()> {
804		let startup_notify = Arc::new(Notify::new());
805		let _ = self
806			.starting_instances
807			.insert_async(request.actor_id.clone(), startup_notify.clone())
808			.await;
809		// Test-only seam: lets a test hold a generation in the "starting" window so it
810		// can deterministically deliver a stop for a previous generation that parks
811		// under the actor id and gets consumed by this startup.
812		#[cfg(test)]
813		test_hooks::wait_for_startup_gate(&request.actor_id).await;
814		let factory = self
815			.factories
816			.get(&request.actor_name)
817			.cloned()
818			.ok_or_else(|| {
819				ActorRuntime::NotRegistered {
820					actor_name: request.actor_name.clone(),
821				}
822				.build()
823			})?;
824		let (lifecycle_tx, lifecycle_rx) = mpsc::unbounded_channel();
825		let (dispatch_tx, dispatch_rx) = mpsc::unbounded_channel();
826		let (lifecycle_events_tx, lifecycle_events_rx) = mpsc::unbounded_channel();
827		request
828			.ctx
829			.configure_lifecycle_events(Some(lifecycle_events_tx));
830		request.ctx.cancel_sleep_timer();
831		request.ctx.set_local_alarm_callback(Some(Arc::new({
832			let lifecycle_tx = lifecycle_tx.clone();
833			move || {
834				let lifecycle_tx = lifecycle_tx.clone();
835				Box::pin(async move {
836					let (reply_tx, reply_rx) = oneshot::channel();
837					if let Err(error) = try_send_lifecycle_command(
838						&lifecycle_tx,
839						LifecycleCommand::FireAlarm { reply: reply_tx },
840					) {
841						tracing::warn!(?error, "failed to enqueue actor alarm");
842						return;
843					}
844					let _ = reply_rx.await;
845				})
846			}
847		})));
848		let task = ActorTask::new(
849			request.actor_id.clone(),
850			request.generation,
851			lifecycle_rx,
852			dispatch_rx,
853			lifecycle_events_rx,
854			factory.clone(),
855			request.ctx.clone(),
856			request.input,
857		);
858		let join = RuntimeSpawner::spawn(task.run());
859
860		let (start_tx, start_rx) = oneshot::channel();
861		let result: Result<Arc<ActorTaskHandle>> = async {
862			try_send_lifecycle_command(&lifecycle_tx, LifecycleCommand::Start { reply: start_tx })
863				.context("send actor task start command")?;
864			start_rx
865				.await
866				.context("receive actor task start reply")?
867				.context("actor task start")?;
868			let inspector = build_actor_inspector();
869			request.ctx.configure_inspector(Some(inspector.clone()));
870			Ok::<Arc<ActorTaskHandle>, anyhow::Error>(Arc::new(ActorTaskHandle {
871				actor_id: request.actor_id.clone(),
872				actor_name: request.actor_name.clone(),
873				generation: request.generation,
874				ctx: request.ctx.clone(),
875				factory,
876				inspector,
877				lifecycle: lifecycle_tx,
878				dispatch: dispatch_tx,
879				join: Arc::new(TokioMutex::new(Some(join))),
880			}))
881		}
882		.await
883		.with_context(|| format!("start actor `{}`", request.actor_id));
884
885		match result {
886			Ok(instance) => {
887				let pending_stop = self
888					.pending_stops
889					.remove_async(&request.actor_id.clone())
890					.await
891					.map(|(_, pending_stop)| pending_stop);
892				// Only apply a parked stop if it targets the generation we just started.
893				// A stop parked for a previous generation is stale: complete its handle
894				// so teardown finalizes, but leave the new generation running.
895				let pending_stop = match pending_stop {
896					Some(pending_stop) if pending_stop.generation == request.generation => {
897						Some(pending_stop)
898					}
899					Some(stale_stop) => {
900						let _ = stale_stop.stop_handle.complete();
901						None
902					}
903					None => None,
904				};
905				if let Some(pending_stop) = pending_stop {
906					let actor_id = request.actor_id.clone();
907					let stop_reason = map_envoy_stop_reason(&pending_stop.reason);
908					if matches!(stop_reason, ShutdownKind::Destroy) {
909						instance.ctx.mark_destroy_requested();
910					}
911					self.set_actor_instance_state(
912						actor_id.clone(),
913						ActorInstanceState::Stopping {
914							instance: instance.clone(),
915							reason: stop_reason,
916						},
917					)
918					.await;
919					let _ = self
920						.starting_instances
921						.remove_async(&request.actor_id.clone())
922						.await;
923
924					let dispatcher = self.clone();
925					RuntimeSpawner::spawn(async move {
926						if let Err(error) = dispatcher
927							.shutdown_started_instance(
928								&actor_id,
929								instance.clone(),
930								pending_stop.reason,
931								pending_stop.stop_handle,
932							)
933							.await
934						{
935							tracing::error!(
936								actor_id,
937								?error,
938								"failed to stop actor queued during startup"
939							);
940						}
941						dispatcher
942							.remove_stopping_actor_instance(&actor_id, &instance)
943							.await;
944					});
945					startup_notify.notify_waiters();
946
947					Ok(())
948				} else {
949					self.set_actor_instance_state(
950						request.actor_id.clone(),
951						ActorInstanceState::Active(instance),
952					)
953					.await;
954					let _ = self
955						.starting_instances
956						.remove_async(&request.actor_id.clone())
957						.await;
958					startup_notify.notify_waiters();
959					Ok(())
960				}
961			}
962			Err(error) => {
963				let _ = self
964					.starting_instances
965					.remove_async(&request.actor_id.clone())
966					.await;
967				// A stop parked while this start was in flight would otherwise leak its
968				// ActorStopHandle in the map (the map holds the sender alive, hanging the
969				// caller). Drain and complete it since there is no instance to stop.
970				if let Some((_, pending_stop)) = self
971					.pending_stops
972					.remove_async(&request.actor_id.clone())
973					.await
974				{
975					let _ = pending_stop.stop_handle.complete();
976				}
977				startup_notify.notify_waiters();
978				Err(error)
979			}
980		}
981	}
982
983	async fn set_actor_instance_state(&self, actor_id: String, state: ActorInstanceState) {
984		match self.actor_instances.entry_async(actor_id).await {
985			SccEntry::Occupied(mut entry) => {
986				entry.insert(state);
987			}
988			SccEntry::Vacant(entry) => {
989				entry.insert_entry(state);
990			}
991		}
992	}
993
994	async fn transition_actor_to_stopping(
995		&self,
996		actor_id: &str,
997		generation: u32,
998		reason: ShutdownKind,
999	) -> TransitionResult {
1000		match self.actor_instances.entry_async(actor_id.to_owned()).await {
1001			SccEntry::Occupied(mut entry) => {
1002				let instance = entry.get().instance();
1003				// A stop is scoped to the generation it was issued for. If the currently
1004				// registered instance is a different generation (e.g. a lost previous
1005				// generation whose replacement is already running), the stop is stale and
1006				// must not tear down the newer generation.
1007				if instance.generation != generation {
1008					return TransitionResult::Stale;
1009				}
1010				if matches!(entry.get(), ActorInstanceState::Active(_)) {
1011					entry.insert(ActorInstanceState::Stopping {
1012						instance: instance.clone(),
1013						reason,
1014					});
1015				} else {
1016					instance
1017						.ctx
1018						.warn_work_sent_to_stopping_instance("stop_actor");
1019				}
1020				TransitionResult::Transitioned(instance)
1021			}
1022			SccEntry::Vacant(entry) => {
1023				drop(entry);
1024				TransitionResult::Vacant
1025			}
1026		}
1027	}
1028
1029	async fn remove_stopping_actor_instance(&self, actor_id: &str, expected: &ActiveActorInstance) {
1030		match self.actor_instances.entry_async(actor_id.to_owned()).await {
1031			SccEntry::Occupied(entry) => {
1032				let should_remove = match entry.get() {
1033					ActorInstanceState::Stopping { instance, .. } => {
1034						Arc::ptr_eq(instance, expected)
1035					}
1036					ActorInstanceState::Active(_) => false,
1037				};
1038				if should_remove {
1039					let _ = entry.remove_entry();
1040				}
1041			}
1042			SccEntry::Vacant(entry) => {
1043				drop(entry);
1044			}
1045		}
1046	}
1047
1048	async fn active_actor(&self, actor_id: &str) -> Result<Arc<ActorTaskHandle>> {
1049		if let Some(instance) = self.actor_instances.get_async(&actor_id.to_owned()).await {
1050			match instance.get() {
1051				ActorInstanceState::Active(instance) => {
1052					let instance = instance.clone();
1053					// TODO: Share admission policy with ActorTask::dispatch_lifecycle_error.
1054					if instance.ctx.started() {
1055						if instance.ctx.destroy_requested() {
1056							instance
1057								.ctx
1058								.warn_work_sent_to_stopping_instance("active_actor");
1059							return Err(ActorLifecycleError::Destroying.build());
1060						}
1061						return Ok(instance);
1062					}
1063
1064					instance
1065						.ctx
1066						.warn_work_sent_to_stopping_instance("active_actor");
1067					return Err(if instance.ctx.destroy_requested() {
1068						ActorLifecycleError::Destroying.build()
1069					} else if instance.ctx.sleep_requested() {
1070						ActorLifecycleError::Stopping.build()
1071					} else {
1072						ActorLifecycleError::Starting.build()
1073					});
1074				}
1075				ActorInstanceState::Stopping { instance, reason } => {
1076					let instance = instance.clone();
1077					match reason {
1078						ShutdownKind::Sleep if instance.ctx.started() => return Ok(instance),
1079						ShutdownKind::Sleep => {
1080							instance
1081								.ctx
1082								.warn_work_sent_to_stopping_instance("active_actor");
1083							return Err(ActorLifecycleError::Stopping.build());
1084						}
1085						ShutdownKind::Destroy => {
1086							instance
1087								.ctx
1088								.warn_work_sent_to_stopping_instance("active_actor");
1089							return Err(ActorLifecycleError::Destroying.build());
1090						}
1091					}
1092				}
1093			}
1094		}
1095
1096		tracing::warn!(actor_id, "actor instance not found");
1097		Err(ActorRuntime::NotFound {
1098			resource: "instance".to_owned(),
1099			id: actor_id.to_owned(),
1100		}
1101		.build())
1102	}
1103
1104	async fn stop_actor(
1105		&self,
1106		actor_id: &str,
1107		generation: u32,
1108		reason: protocol::StopActorReason,
1109		stop_handle: ActorStopHandle,
1110	) -> Result<()> {
1111		if self
1112			.starting_instances
1113			.get_async(&actor_id.to_owned())
1114			.await
1115			.is_some()
1116		{
1117			// The target generation is still starting. Park the stop with its generation
1118			// so startup can decide whether it belongs to the generation being started.
1119			let _ = self
1120				.pending_stops
1121				.insert_async(
1122					actor_id.to_owned(),
1123					PendingStop {
1124						generation,
1125						reason,
1126						stop_handle,
1127					},
1128				)
1129				.await;
1130			return Ok(());
1131		}
1132
1133		let task_stop_reason = map_envoy_stop_reason(&reason);
1134		match self
1135			.transition_actor_to_stopping(actor_id, generation, task_stop_reason)
1136			.await
1137		{
1138			TransitionResult::Transitioned(instance) => {
1139				let result = self
1140					.shutdown_started_instance(actor_id, instance.clone(), reason, stop_handle)
1141					.await;
1142				self.remove_stopping_actor_instance(actor_id, &instance)
1143					.await;
1144				result
1145			}
1146			TransitionResult::Stale => {
1147				// The running instance is a different generation; this stop targets a
1148				// generation that is already gone. Complete the handle so envoy-client
1149				// finalizes teardown cleanly instead of warning about a dropped handle.
1150				let _ = stop_handle.complete();
1151				Ok(())
1152			}
1153			TransitionResult::Vacant => {
1154				let _ = self
1155					.pending_stops
1156					.insert_async(
1157						actor_id.to_owned(),
1158						PendingStop {
1159							generation,
1160							reason,
1161							stop_handle,
1162						},
1163					)
1164					.await;
1165				Ok(())
1166			}
1167		}
1168	}
1169
1170	async fn shutdown_started_instance(
1171		&self,
1172		actor_id: &str,
1173		instance: Arc<ActorTaskHandle>,
1174		reason: protocol::StopActorReason,
1175		stop_handle: ActorStopHandle,
1176	) -> Result<()> {
1177		let task_stop_reason = map_envoy_stop_reason(&reason);
1178
1179		if matches!(task_stop_reason, ShutdownKind::Destroy) {
1180			instance.ctx.mark_destroy_requested();
1181		}
1182
1183		tracing::debug!(
1184			actor_id,
1185			handle_actor_id = %instance.actor_id,
1186			actor_name = %instance.actor_name,
1187			generation = instance.generation,
1188			?reason,
1189			?task_stop_reason,
1190			"stopping actor instance"
1191		);
1192
1193		let (reply_tx, reply_rx) = oneshot::channel();
1194		let shutdown_result = match try_send_lifecycle_command(
1195			&instance.lifecycle,
1196			LifecycleCommand::Stop {
1197				reason: task_stop_reason,
1198				reply: reply_tx,
1199			},
1200		) {
1201			Ok(()) => reply_rx
1202				.await
1203				.context("receive actor task stop reply")
1204				.and_then(|result| result),
1205			Err(error) => Err(error),
1206		};
1207
1208		if matches!(task_stop_reason, ShutdownKind::Destroy) {
1209			let shutdown_deadline =
1210				Instant::now() + instance.factory.config().effective_sleep_grace_period();
1211			if !instance
1212				.ctx
1213				.wait_for_internal_keep_awake_idle(shutdown_deadline)
1214				.await
1215			{
1216				instance.ctx.record_direct_subsystem_shutdown_warning(
1217					"internal_keep_awake",
1218					"destroy_drain",
1219				);
1220				tracing::warn!(
1221					actor_id,
1222					"destroy shutdown timed out waiting for in-flight actions"
1223				);
1224			}
1225			if !instance
1226				.ctx
1227				.wait_for_http_requests_drained(shutdown_deadline)
1228				.await
1229			{
1230				instance
1231					.ctx
1232					.record_direct_subsystem_shutdown_warning("http_requests", "destroy_drain");
1233				tracing::warn!(
1234					actor_id,
1235					"destroy shutdown timed out waiting for in-flight http requests"
1236				);
1237			}
1238		}
1239
1240		let mut join_guard = instance.join.lock().await;
1241		// Fold the join outcome into the result instead of propagating with `?`,
1242		// so the stop handle is always signaled and never dropped unsignaled.
1243		let join_result = if let Some(join) = join_guard.take() {
1244			join.await
1245				.context("join actor task")
1246				.and_then(|result| result.context("actor task failed"))
1247		} else {
1248			Ok(())
1249		};
1250		instance.ctx.configure_lifecycle_events(None);
1251
1252		if let (Err(shutdown_error), Err(join_error)) = (&shutdown_result, &join_result) {
1253			tracing::warn!(
1254				actor_id,
1255				%shutdown_error,
1256				discarded_join_error = %join_error,
1257				"actor stop had both shutdown and join failures; only the shutdown error is returned"
1258			);
1259		}
1260
1261		let final_result = shutdown_result.and(join_result);
1262		match &final_result {
1263			Ok(_) => {
1264				let _ = stop_handle.complete();
1265			}
1266			Err(error) => {
1267				let _ = stop_handle.fail(anyhow::Error::new(RivetError::extract(error)));
1268			}
1269		}
1270
1271		final_result.with_context(|| format!("stop actor `{actor_id}`"))
1272	}
1273}
1274
1275impl RegistryDispatcher {
1276	fn can_hibernate(&self, actor_id: &str, request: &HttpRequest) -> bool {
1277		if matches!(is_actor_connect_path(&request.path), Ok(true)) {
1278			return true;
1279		}
1280
1281		let Some(instance) = self
1282			.actor_instances
1283			.read_sync(actor_id, |_, state| state.active_instance())
1284			.flatten()
1285		else {
1286			return false;
1287		};
1288
1289		match &instance.factory.config().can_hibernate_websocket {
1290			CanHibernateWebSocket::Bool(value) => *value,
1291			CanHibernateWebSocket::Callback(callback) => callback(request),
1292		}
1293	}
1294
1295	#[allow(clippy::too_many_arguments)]
1296	fn build_actor_context(
1297		&self,
1298		handle: EnvoyHandle,
1299		actor_id: &str,
1300		generation: u32,
1301		actor_name: &str,
1302		key: ActorKey,
1303		factory: &ActorFactory,
1304	) -> Result<ActorContext> {
1305		let formatted_key = format_actor_key(&key);
1306		let ctx = ActorContext::build(
1307			actor_id.to_owned(),
1308			actor_name.to_owned(),
1309			key,
1310			self.region.clone(),
1311			Some(generation),
1312			handle.get_envoy_key().to_owned(),
1313			factory.config().clone(),
1314			LegacyActorKv::new(handle.clone(), actor_id.to_owned()),
1315			SqliteDb::new_with_remote_sqlite(
1316				handle.clone(),
1317				actor_id.to_owned(),
1318				Some(formatted_key),
1319				Some(generation as u64),
1320				factory.config().has_database,
1321				factory.config().remote_sqlite,
1322			)?,
1323		);
1324		ctx.configure_envoy(handle, Some(generation));
1325		Ok(ctx)
1326	}
1327}
1328
1329/// Maps an envoy-protocol stop reason to the lifecycle `ShutdownKind` used by
1330/// `ActorTask`. Reallocation paths (the actor will resurrect on a new envoy)
1331/// are routed through `Sleep` so user `onSleep` runs and durable state is
1332/// preserved without firing a permanent destroy.
1333fn map_envoy_stop_reason(reason: &protocol::StopActorReason) -> ShutdownKind {
1334	match reason {
1335		// Idle sleep requested by the actor itself.
1336		protocol::StopActorReason::SleepIntent => ShutdownKind::Sleep,
1337		// Runner is being drained; engine will reallocate the actor on a new
1338		// envoy. Treat as sleep so persistent state and onSleep semantics hold.
1339		protocol::StopActorReason::GoingAway => ShutdownKind::Sleep,
1340		// Runner connection lost; once reconnected (or another runner is
1341		// allocated) the actor resurrects with the same id.
1342		protocol::StopActorReason::Lost => ShutdownKind::Sleep,
1343		// User-initiated stop intent (`ctx.destroy()` and equivalents).
1344		protocol::StopActorReason::StopIntent => ShutdownKind::Destroy,
1345		// Engine-initiated permanent destroy.
1346		protocol::StopActorReason::Destroy => ShutdownKind::Destroy,
1347	}
1348}
1349
1350// Test shim keeps moved tests in crate-root tests/ with private-module access.
1351#[cfg(test)]
1352#[path = "../../tests/registry.rs"]
1353pub(crate) mod tests;
1354
1355// Test-only hooks used by the moved registry tests to deterministically drive the
1356// generation-stop race. Gated behind `cfg(test)` so there is no production impact.
1357#[cfg(test)]
1358pub(crate) mod test_hooks {
1359	use std::sync::{Arc, OnceLock};
1360
1361	use scc::HashMap as SccHashMap;
1362	use tokio::sync::Semaphore;
1363
1364	static STARTUP_GATES: OnceLock<SccHashMap<String, Arc<Semaphore>>> = OnceLock::new();
1365
1366	fn gates() -> &'static SccHashMap<String, Arc<Semaphore>> {
1367		STARTUP_GATES.get_or_init(SccHashMap::new)
1368	}
1369
1370	/// Arms a gate so `start_actor` pauses for `actor_id` after registering as
1371	/// starting, until `release_startup_gate` is called.
1372	pub(crate) fn arm_startup_gate(actor_id: &str) {
1373		let _ = gates().insert_sync(actor_id.to_owned(), Arc::new(Semaphore::new(0)));
1374	}
1375
1376	/// Releases a previously armed gate, letting the paused `start_actor` continue.
1377	pub(crate) fn release_startup_gate(actor_id: &str) {
1378		if let Some(sem) = gates().read_sync(actor_id, |_, sem| sem.clone()) {
1379			sem.add_permits(1);
1380		}
1381	}
1382
1383	/// Called from inside `start_actor`. Blocks only if a gate is armed for the
1384	/// actor. Order-independent: a release before this runs still lets it through.
1385	pub(crate) async fn wait_for_startup_gate(actor_id: &str) {
1386		let sem = gates().read_sync(actor_id, |_, sem| sem.clone());
1387		if let Some(sem) = sem {
1388			let permit = sem.acquire().await.expect("startup gate semaphore closed");
1389			permit.forget();
1390			let _ = gates().remove_sync(actor_id);
1391		}
1392	}
1393}