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