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 pub async fn started(&self) -> anyhow::Result<()> {
104 self.handle.started().await
105 }
106
107 pub async fn wait_actors_drained(&self) {
109 self.handle.wait_actors_drained().await
110 }
111
112 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
168enum TransitionResult {
171 Transitioned(ActiveActorInstance),
173 Stale,
176 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#[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 shutdown.cancelled().await;
677
678 let shutdown_envoy = async {
679 let stop_threshold = handle
682 .get_protocol_metadata()
683 .await
684 .map(|x| x.actor_stop_threshold)
685 .unwrap_or(30 * 60 * 1000);
686 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 #[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 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 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 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 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 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 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 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
1331fn map_envoy_stop_reason(reason: &protocol::StopActorReason) -> ShutdownKind {
1336 match reason {
1337 protocol::StopActorReason::SleepIntent => ShutdownKind::Sleep,
1339 protocol::StopActorReason::GoingAway => ShutdownKind::Sleep,
1342 protocol::StopActorReason::Lost => ShutdownKind::Sleep,
1345 protocol::StopActorReason::StopIntent => ShutdownKind::Destroy,
1347 protocol::StopActorReason::Destroy => ShutdownKind::Destroy,
1349 }
1350}
1351
1352#[cfg(test)]
1354#[path = "../../tests/registry.rs"]
1355pub(crate) mod tests;
1356
1357#[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 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 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 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}