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_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 shutdown.cancelled().await;
675
676 let shutdown_envoy = async {
677 let stop_threshold = handle
680 .get_protocol_metadata()
681 .await
682 .map(|x| x.actor_stop_threshold)
683 .unwrap_or(30 * 60 * 1000);
684 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 #[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 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 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 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 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 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 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 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
1329fn map_envoy_stop_reason(reason: &protocol::StopActorReason) -> ShutdownKind {
1334 match reason {
1335 protocol::StopActorReason::SleepIntent => ShutdownKind::Sleep,
1337 protocol::StopActorReason::GoingAway => ShutdownKind::Sleep,
1340 protocol::StopActorReason::Lost => ShutdownKind::Sleep,
1343 protocol::StopActorReason::StopIntent => ShutdownKind::Destroy,
1345 protocol::StopActorReason::Destroy => ShutdownKind::Destroy,
1347 }
1348}
1349
1350#[cfg(test)]
1352#[path = "../../tests/registry.rs"]
1353pub(crate) mod tests;
1354
1355#[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 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 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 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}