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_sync;
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, broadcast, mpsc, oneshot};
31use tokio::task::JoinHandle;
32use tokio_util::sync::CancellationToken;
33use url::Url;
34use vbare::Versioned;
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
73const ENVOY_START_TIMEOUT: Duration = Duration::from_secs(30);
74
75#[derive(Default)]
76pub struct CoreRegistry {
77 factories: HashMap<String, Arc<ActorFactory>>,
78}
79
80#[derive(Clone)]
81pub struct CoreEnvoyHandle {
82 handle: EnvoyHandle,
83}
84
85#[derive(Clone, Debug)]
86pub struct CoreEnvoyStatus {
87 pub active_actor_count: usize,
88 pub ping_healthy: bool,
89}
90
91impl CoreEnvoyHandle {
92 pub(crate) fn new(handle: EnvoyHandle) -> Self {
93 Self { handle }
94 }
95
96 pub fn status(&self) -> CoreEnvoyStatus {
97 CoreEnvoyStatus {
98 active_actor_count: self.handle.active_actor_count(),
99 ping_healthy: self.handle.is_ping_healthy(),
100 }
101 }
102
103 pub async fn started(&self) -> anyhow::Result<()> {
105 self.handle.started().await
106 }
107
108 pub async fn wait_actors_drained(&self) {
110 self.handle.wait_actors_drained().await
111 }
112
113 pub async fn actor_stop_threshold_ms(&self) -> Option<i64> {
117 self.handle
118 .get_protocol_metadata()
119 .await
120 .map(|metadata| metadata.actor_stop_threshold)
121 }
122}
123
124#[derive(Clone)]
125struct ActorTaskHandle {
126 actor_id: String,
127 actor_name: String,
128 generation: u32,
129 ctx: ActorContext,
130 factory: Arc<ActorFactory>,
131 inspector: Inspector,
132 lifecycle: mpsc::UnboundedSender<LifecycleCommand>,
133 dispatch: mpsc::UnboundedSender<DispatchCommand>,
134 join: Arc<TokioMutex<Option<JoinHandle<Result<()>>>>>,
135}
136
137type ActiveActorInstance = Arc<ActorTaskHandle>;
138
139enum ActorInstanceState {
140 Active(ActiveActorInstance),
141 Stopping {
142 instance: ActiveActorInstance,
143 reason: ShutdownKind,
144 },
145}
146
147impl ActorInstanceState {
148 fn instance(&self) -> ActiveActorInstance {
149 match self {
150 Self::Active(instance) | Self::Stopping { instance, .. } => instance.clone(),
151 }
152 }
153
154 fn active_instance(&self) -> Option<ActiveActorInstance> {
155 match self {
156 Self::Active(instance) => Some(instance.clone()),
157 Self::Stopping { .. } => None,
158 }
159 }
160}
161
162#[derive(Clone)]
163struct PendingStop {
164 generation: u32,
165 reason: protocol::StopActorReason,
166 stop_handle: ActorStopHandle,
167}
168
169struct StartingInstance {
177 generation: u32,
178 pending_stop: Option<PendingStop>,
179}
180
181enum TransitionResult {
184 Transitioned(ActiveActorInstance),
186 Stale,
189 Vacant,
191}
192
193pub(crate) struct RegistryDispatcher {
194 pub(crate) factories: HashMap<String, Arc<ActorFactory>>,
195 actor_instances: SccHashMap<String, ActorInstanceState>,
196 starting_instances: SccHashMap<String, StartingInstance>,
197 pending_stops: SccHashMap<String, PendingStop>,
198 region: String,
199 handle_inspector_http_in_runtime: bool,
200 #[cfg(feature = "sqlite-local")]
203 sqlite_pool: sqlite_embedded::worker::SqlitePool,
204}
205
206pub(crate) struct RegistryCallbacks {
207 pub(crate) dispatcher: Arc<RegistryDispatcher>,
208}
209
210#[derive(Clone, Debug)]
211struct StartActorRequest {
212 actor_id: String,
213 generation: u32,
214 actor_name: String,
215 input: Option<Vec<u8>>,
216 ctx: ActorContext,
217}
218
219#[derive(Clone, Debug)]
220struct ServeSettings {
221 version: u32,
222 endpoint: String,
223 token: Option<String>,
224 namespace: String,
225 pool_name: String,
226 engine_binary_path: Option<PathBuf>,
227 start_services: bool,
228 services_binary_path: Option<PathBuf>,
229 engine_host: Option<String>,
230 engine_port: Option<u16>,
231 engine_spawn: EngineSpawnMode,
232 engine_auto_download: bool,
233 handle_inspector_http_in_runtime: bool,
234 serverless_base_path: Option<String>,
235 serverless_package_version: String,
236 serverless_client_endpoint: Option<String>,
237 serverless_client_namespace: Option<String>,
238 serverless_client_token: Option<String>,
239 serverless_validate_endpoint: bool,
240 serverless_max_start_payload_bytes: usize,
241}
242
243#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
244pub enum EngineSpawnMode {
245 #[default]
246 Auto,
247 Always,
248 Never,
249}
250
251impl EngineSpawnMode {
252 pub(crate) fn from_env() -> Self {
253 match env::var("RIVETKIT_ENGINE_SPAWN") {
254 Ok(value) if value.eq_ignore_ascii_case("always") => Self::Always,
255 Ok(value) if value.eq_ignore_ascii_case("never") => Self::Never,
256 _ => Self::Auto,
257 }
258 }
259}
260
261#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
266pub enum RuntimeMode {
267 #[default]
268 Envoy,
269 Serverless,
270}
271
272impl RuntimeMode {
273 pub fn from_env() -> Self {
274 match env::var("RIVETKIT_RUNTIME_MODE") {
275 Ok(value) if value.eq_ignore_ascii_case("serverless") => Self::Serverless,
276 _ => Self::Envoy,
277 }
278 }
279}
280
281#[derive(Clone, Debug, Default)]
282pub struct ServeConfig {
283 pub version: u32,
284 pub endpoint: String,
285 pub token: Option<String>,
286 pub namespace: String,
287 pub pool_name: String,
288 pub engine_binary_path: Option<PathBuf>,
289 pub start_services: bool,
290 pub services_binary_path: Option<PathBuf>,
291 pub engine_host: Option<String>,
292 pub engine_port: Option<u16>,
293 pub engine_spawn: EngineSpawnMode,
294 pub engine_auto_download: bool,
295 pub handle_inspector_http_in_runtime: bool,
296 pub serverless_base_path: Option<String>,
297 pub serverless_package_version: String,
298 pub serverless_client_endpoint: Option<String>,
299 pub serverless_client_namespace: Option<String>,
300 pub serverless_client_token: Option<String>,
301 pub serverless_validate_endpoint: bool,
302 pub serverless_max_start_payload_bytes: usize,
303 pub serverless_cache_envoy: bool,
304}
305
306#[derive(Debug, Default, Deserialize)]
307#[serde(default)]
308struct InspectorPatchStateBody {
309 state: JsonValue,
310}
311
312#[derive(Debug, Default, Deserialize)]
313#[serde(default)]
314struct InspectorActionBody {
315 args: Vec<JsonValue>,
316 properties: Option<JsonValue>,
317}
318
319#[derive(Debug, Default, Deserialize)]
320#[serde(default)]
321struct InspectorDatabaseExecuteBody {
322 sql: String,
323 args: Vec<JsonValue>,
324 properties: Option<JsonValue>,
325}
326
327#[derive(Debug, Default, Deserialize)]
328#[serde(default, rename_all = "camelCase")]
329struct InspectorWorkflowReplayBody {
330 entry_id: Option<String>,
331}
332
333#[derive(Debug, Default, Deserialize)]
334#[serde(default)]
335struct InspectorEnqueueBody {
336 name: String,
337 body: Option<JsonValue>,
338}
339
340#[derive(Debug, Serialize)]
341#[serde(rename_all = "camelCase")]
342struct InspectorQueueMessageJson {
343 id: u64,
344 name: String,
345 created_at_ms: i64,
346}
347
348#[derive(Debug, Serialize)]
349#[serde(rename_all = "camelCase")]
350struct InspectorQueueResponseJson {
351 size: u32,
352 max_size: u32,
353 truncated: bool,
354 messages: Vec<InspectorQueueMessageJson>,
355}
356
357#[derive(Debug, Deserialize)]
358#[serde(default)]
359struct HttpActionRequestJson {
360 args: JsonValue,
361}
362
363impl Default for HttpActionRequestJson {
364 fn default() -> Self {
365 Self {
366 args: JsonValue::Array(Vec::new()),
367 }
368 }
369}
370
371pub(crate) fn should_manage_engine(spawn_mode: EngineSpawnMode) -> bool {
374 match spawn_mode {
375 EngineSpawnMode::Always => true,
376 EngineSpawnMode::Auto | EngineSpawnMode::Never => false,
377 }
378}
379
380fn resolve_engine_spawn(
382 has_explicit_endpoint: bool,
383 spawn_mode: EngineSpawnMode,
384) -> Result<EngineSpawnMode> {
385 if has_explicit_endpoint {
386 if spawn_mode == EngineSpawnMode::Always {
387 return Err(EngineSpawnEndpointConflict.build());
388 }
389 return Ok(EngineSpawnMode::Never);
390 }
391 Ok(match spawn_mode {
392 EngineSpawnMode::Auto | EngineSpawnMode::Always => EngineSpawnMode::Always,
393 EngineSpawnMode::Never => EngineSpawnMode::Never,
394 })
395}
396
397#[derive(RivetError)]
398#[error(
399 "engine",
400 "spawn_endpoint_conflict",
401 "Engine spawn conflicts with the configured endpoint",
402 "RIVETKIT_ENGINE_SPAWN=always cannot be used with RIVET_ENDPOINT. Remove one setting."
403)]
404struct EngineSpawnEndpointConflict;
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 #[cfg(feature = "sqlite-local")]
621 sqlite_embedded::vfs::verify_batch_atomic_writes_at_process_startup().await?;
622
623 crate::metrics::record_rivetkit_info(
624 config.serverless_package_version.clone(),
625 config.version,
626 "serverful",
627 config.pool_name.clone(),
628 );
629 #[cfg(not(target_arch = "wasm32"))]
630 crate::tokio_runtime_metrics::ensure_sampler_started();
631
632 let dispatcher = self.into_dispatcher(&config);
633 let manage_engine = should_manage_engine(config.engine_spawn);
634 #[cfg(feature = "native-runtime")]
635 let development_processes = if manage_engine {
636 Some(DevelopmentProcessManager::start(&config).await?)
637 } else {
638 None
639 };
640 #[cfg(not(feature = "native-runtime"))]
641 if manage_engine {
642 anyhow::bail!("engine process spawning requires the `native-runtime` feature");
643 }
644
645 #[cfg(feature = "native-runtime")]
646 {
647 let pool_ready: Result<bool> = tokio::select! {
648 biased;
649 _ = shutdown.cancelled() => Ok(false),
650 result = timeout(ENVOY_START_TIMEOUT, runner_config::ensure_local_normal_runner_config(&config)) => {
651 result.context("local node pool configuration timed out").and_then(|result| result).map(|_| true)
652 }
653 };
654 if !matches!(&pool_ready, Ok(true)) {
655 if let Some(development_processes) = development_processes {
656 development_processes.shutdown().await;
657 }
658 return pool_ready.map(|_| ());
659 }
660 }
661 let callbacks = Arc::new(RegistryCallbacks {
662 dispatcher: dispatcher.clone(),
663 });
664
665 let prepopulate_actor_names = dispatcher
666 .build_actor_metadata_map()
667 .into_iter()
668 .map(|(name, metadata)| (name, rivet_envoy_client::config::ActorName { metadata }))
669 .collect();
670 let handle = start_envoy_sync(rivet_envoy_client::config::EnvoyConfig {
671 version: config.version,
672 endpoint: config.endpoint,
673 token: config.token,
674 namespace: config.namespace,
675 pool_name: config.pool_name,
676 prepopulate_actor_names,
677 metadata: Some(json!({
678 "rivetkit": { "version": config.serverless_package_version },
679 })),
680 not_global: true,
682 debug_latency_ms: None,
683 max_queued_requests_per_actor:
684 rivet_envoy_client::config::DEFAULT_MAX_QUEUED_REQUESTS_PER_ACTOR,
685 stopped_generation_floor_ttl:
686 rivet_envoy_client::config::DEFAULT_STOPPED_GENERATION_FLOOR_TTL,
687 stopped_generation_floor_capacity:
688 rivet_envoy_client::config::DEFAULT_STOPPED_GENERATION_FLOOR_CAPACITY,
689 callbacks,
690 connector: None,
691 });
692 let envoy_ready: Result<bool> = tokio::select! {
693 biased;
694 _ = shutdown.cancelled() => Ok(false),
695 result = timeout(ENVOY_START_TIMEOUT, handle.started()) => {
696 result.context("serverful envoy startup timed out").and_then(|result| result).map(|_| true)
697 }
698 };
699 if !matches!(&envoy_ready, Ok(true)) {
700 handle.shutdown_and_wait(true).await;
701 #[cfg(feature = "native-runtime")]
702 if let Some(development_processes) = development_processes {
703 development_processes.shutdown().await;
704 }
705 return envoy_ready.map(|_| ());
706 }
707 on_handle(CoreEnvoyHandle::new(handle.clone()));
708
709 tokio::select! {
715 _ = shutdown.cancelled() => {},
716 _ = async {
717 handle.wait_stopped().await;
718 if handle.terminal_error().is_none() {
719 shutdown.cancelled().await;
721 }
722 } => {},
723 }
724
725 let shutdown_envoy = async {
726 if handle.terminal_error().is_some() {
727 handle.wait_stopped().await;
728 return;
729 }
730 if manage_engine {
731 if !handle.mark_stopping().await {
736 tracing::warn!("could not mark local development envoy as stopping");
737 }
738 let mut actors = Vec::new();
742 dispatcher
743 .actor_instances
744 .iter_async(|actor_id, state| {
745 if let Some(instance) = state.active_instance() {
746 actors.push((actor_id.clone(), instance.generation));
747 }
748 true
749 })
750 .await;
751 let actor_count = actors.len();
752 tracing::info!(
753 actors = actor_count,
754 "sleeping local development actors for shutdown"
755 );
756 for (actor_id, generation) in actors {
757 handle.sleep_actor(actor_id, Some(generation), None);
758 }
759 if actor_count > 0 {
760 let acknowledged = timeout(Duration::from_secs(3), async {
763 handle.wait_actors_drained().await;
764 handle.wait_for_events_acked().await
765 })
766 .await;
767 if !matches!(acknowledged, Ok(true)) {
768 tracing::warn!(
769 "local development actor stops were not acknowledged before shutdown"
770 );
771 }
772 }
773 }
774
775 let stop_threshold = handle
778 .get_protocol_metadata()
779 .await
780 .map(|x| x.actor_stop_threshold)
781 .unwrap_or(30 * 60 * 1000);
782 match timeout(
786 Duration::from_millis(stop_threshold as u64),
787 handle.shutdown_and_wait(false),
788 )
789 .await
790 {
791 Ok(()) => {}
792 Err(_) => {
793 tracing::warn!("envoy shutdown drain exceeded timeout; forcing immediate stop");
794 handle.shutdown(true);
795 handle.wait_stopped().await;
796 }
797 }
798 };
799 #[cfg(feature = "native-runtime")]
800 let shutdown_development_processes = async move {
801 if let Some(development_processes) = development_processes {
802 development_processes.shutdown().await;
803 }
804 };
805 #[cfg(feature = "native-runtime")]
806 {
807 shutdown_envoy.await;
808 shutdown_development_processes.await;
809 }
810 #[cfg(not(feature = "native-runtime"))]
811 shutdown_envoy.await;
812
813 match handle.terminal_error() {
814 Some(error) => Err(anyhow::anyhow!(error)),
815 None => Ok(()),
816 }
817 }
818
819 fn into_dispatcher(self, config: &ServeConfig) -> Arc<RegistryDispatcher> {
820 Arc::new(RegistryDispatcher::new(
821 self.factories,
822 config.handle_inspector_http_in_runtime,
823 ))
824 }
825
826 pub async fn into_serverless_runtime(
827 self,
828 config: ServeConfig,
829 ) -> Result<crate::serverless::CoreServerlessRuntime> {
830 crate::serverless::CoreServerlessRuntime::new(self.factories, config).await
831 }
832}
833
834impl RegistryDispatcher {
835 pub(crate) fn new(
836 factories: HashMap<String, Arc<ActorFactory>>,
837 handle_inspector_http_in_runtime: bool,
838 ) -> Self {
839 Self {
840 factories,
841 actor_instances: SccHashMap::new(),
842 starting_instances: SccHashMap::new(),
843 pending_stops: SccHashMap::new(),
844 region: env::var("RIVET_REGION").unwrap_or_default(),
845 handle_inspector_http_in_runtime,
846 #[cfg(feature = "sqlite-local")]
847 sqlite_pool: sqlite_embedded::worker::SqlitePool::from_env(Some(Arc::new(
848 crate::actor::sqlite::pool_metrics::PoolMetrics,
849 ))),
850 }
851 }
852
853 pub(crate) fn build_actor_metadata_map(&self) -> HashMap<String, JsonValue> {
854 build_actor_metadata_map_from_factories(&self.factories)
855 }
856}
857
858pub(crate) fn serverless_metadata_payload(
859 actor_metadata: HashMap<String, JsonValue>,
860 config: &ServeConfig,
861 envoy_kind: ServerlessMetadataEnvoyKind,
862) -> ServerlessMetadataPayload {
863 let actor_names = actor_metadata
864 .into_iter()
865 .map(|(name, metadata)| {
866 (
867 name,
868 ActorName {
869 metadata: Some(metadata),
870 },
871 )
872 })
873 .collect::<HashMap<_, _>>();
874
875 ServerlessMetadataPayload {
876 runtime: "rivetkit".to_owned(),
877 version: config.serverless_package_version.clone(),
878 envoy_protocol_version: Some(protocol::PROTOCOL_VERSION),
879 actor_names,
880 envoy: Some(ServerlessMetadataEnvoy {
881 kind: Some(envoy_kind),
882 version: Some(config.version),
883 }),
884 runner: None,
885 client_endpoint: config.serverless_client_endpoint.clone(),
886 client_namespace: config.serverless_client_namespace.clone(),
887 client_token: config.serverless_client_token.clone(),
888 }
889}
890
891fn build_actor_metadata_map_from_factories(
892 factories: &HashMap<String, Arc<ActorFactory>>,
893) -> HashMap<String, JsonValue> {
894 factories
895 .iter()
896 .map(|(actor_name, factory)| {
897 let config = factory.config();
898 let mut metadata = serde_json::Map::new();
899 if let Some(icon) = &config.icon {
900 metadata.insert("icon".to_owned(), json!(icon));
901 }
902 if let Some(name) = &config.name {
903 metadata.insert("name".to_owned(), json!(name));
904 }
905 (actor_name.clone(), JsonValue::Object(metadata))
906 })
907 .collect()
908}
909
910impl RegistryDispatcher {
911 async fn start_actor(self: &Arc<Self>, request: StartActorRequest) -> Result<()> {
912 let factory = self
913 .factories
914 .get(&request.actor_name)
915 .cloned()
916 .ok_or_else(|| {
917 ActorRuntime::NotRegistered {
918 actor_name: request.actor_name.clone(),
919 }
920 .build()
921 })?;
922 let _ = self
923 .starting_instances
924 .insert_async(
925 request.actor_id.clone(),
926 StartingInstance {
927 generation: request.generation,
928 pending_stop: None,
929 },
930 )
931 .await;
932 let (lifecycle_tx, lifecycle_rx) = mpsc::unbounded_channel();
937 let (dispatch_tx, dispatch_rx) = mpsc::unbounded_channel();
938 let (lifecycle_events_tx, lifecycle_events_rx) = mpsc::unbounded_channel();
939 request
940 .ctx
941 .configure_lifecycle_events(Some(lifecycle_events_tx));
942 request.ctx.cancel_sleep_timer();
943 request.ctx.set_local_alarm_callback(Some(Arc::new({
944 let lifecycle_tx = lifecycle_tx.clone();
945 move || {
946 let lifecycle_tx = lifecycle_tx.clone();
947 Box::pin(async move {
948 let (reply_tx, reply_rx) = oneshot::channel();
949 if let Err(error) = try_send_lifecycle_command(
950 &lifecycle_tx,
951 LifecycleCommand::FireAlarm { reply: reply_tx },
952 ) {
953 tracing::warn!(?error, "failed to enqueue actor alarm");
954 return;
955 }
956 let _ = reply_rx.await;
957 })
958 }
959 })));
960 let task = ActorTask::new(
961 request.actor_id.clone(),
962 request.generation,
963 lifecycle_rx,
964 dispatch_rx,
965 lifecycle_events_rx,
966 factory.clone(),
967 request.ctx.clone(),
968 request.input,
969 );
970 let join = RuntimeSpawner::spawn(task.run());
971
972 let (start_tx, start_rx) = oneshot::channel();
973 let result: Result<Arc<ActorTaskHandle>> = async {
974 try_send_lifecycle_command(&lifecycle_tx, LifecycleCommand::Start { reply: start_tx })
975 .context("send actor task start command")?;
976 start_rx
977 .await
978 .context("receive actor task start reply")?
979 .context("actor task start")?;
980 let inspector = build_actor_inspector();
981 request.ctx.configure_inspector(Some(inspector.clone()));
982 Ok::<Arc<ActorTaskHandle>, anyhow::Error>(Arc::new(ActorTaskHandle {
983 actor_id: request.actor_id.clone(),
984 actor_name: request.actor_name.clone(),
985 generation: request.generation,
986 ctx: request.ctx.clone(),
987 factory,
988 inspector,
989 lifecycle: lifecycle_tx,
990 dispatch: dispatch_tx,
991 join: Arc::new(TokioMutex::new(Some(join))),
992 }))
993 }
994 .await
995 .with_context(|| format!("start actor `{}`", request.actor_id));
996
997 match result {
998 Ok(instance) => {
999 self.set_actor_instance_state(
1002 request.actor_id.clone(),
1003 ActorInstanceState::Active(instance),
1004 )
1005 .await;
1006 let parked_during_start = self
1007 .starting_instances
1008 .remove_async(&request.actor_id)
1009 .await
1010 .and_then(|(_, starting)| starting.pending_stop);
1011 let parked_before_start = self
1012 .pending_stops
1013 .remove_async(&request.actor_id)
1014 .await
1015 .map(|(_, pending_stop)| pending_stop);
1016
1017 let mut current_generation_stop = None;
1021 for pending_stop in [parked_before_start, parked_during_start]
1022 .into_iter()
1023 .flatten()
1024 {
1025 if pending_stop.generation == request.generation
1026 && current_generation_stop.is_none()
1027 {
1028 current_generation_stop = Some(pending_stop);
1029 } else {
1030 let _ = pending_stop.stop_handle.complete();
1031 }
1032 }
1033
1034 if let Some(pending_stop) = current_generation_stop {
1035 self.apply_stop_queued_during_startup(&request.actor_id, pending_stop)
1036 .await;
1037 }
1038 Ok(())
1039 }
1040 Err(error) => {
1041 if let Some((_, starting)) = self
1045 .starting_instances
1046 .remove_async(&request.actor_id)
1047 .await && let Some(pending_stop) = starting.pending_stop
1048 {
1049 let _ = pending_stop.stop_handle.complete();
1050 }
1051 if let Some((_, pending_stop)) =
1052 self.pending_stops.remove_async(&request.actor_id).await
1053 {
1054 let _ = pending_stop.stop_handle.complete();
1055 }
1056 Err(error)
1057 }
1058 }
1059 }
1060
1061 async fn apply_stop_queued_during_startup(
1064 self: &Arc<Self>,
1065 actor_id: &str,
1066 pending_stop: PendingStop,
1067 ) {
1068 let stop_reason = map_envoy_stop_reason(&pending_stop.reason);
1069 let instance = match self
1070 .transition_actor_to_stopping(actor_id, pending_stop.generation, stop_reason)
1071 .await
1072 {
1073 TransitionResult::Transitioned(instance) => instance,
1074 TransitionResult::Stale | TransitionResult::Vacant => {
1075 let _ = pending_stop.stop_handle.complete();
1078 return;
1079 }
1080 };
1081
1082 let dispatcher = self.clone();
1083 let actor_id = actor_id.to_owned();
1084 RuntimeSpawner::spawn(async move {
1085 if let Err(error) = dispatcher
1086 .shutdown_started_instance(
1087 &actor_id,
1088 instance.clone(),
1089 pending_stop.reason,
1090 pending_stop.stop_handle,
1091 )
1092 .await
1093 {
1094 tracing::error!(
1095 actor_id,
1096 ?error,
1097 "failed to stop actor queued during startup"
1098 );
1099 }
1100 dispatcher
1101 .remove_stopping_actor_instance(&actor_id, &instance)
1102 .await;
1103 });
1104 }
1105
1106 async fn set_actor_instance_state(&self, actor_id: String, state: ActorInstanceState) {
1107 match self.actor_instances.entry_async(actor_id).await {
1108 SccEntry::Occupied(mut entry) => {
1109 entry.insert(state);
1110 }
1111 SccEntry::Vacant(entry) => {
1112 entry.insert_entry(state);
1113 }
1114 }
1115 }
1116
1117 async fn transition_actor_to_stopping(
1118 &self,
1119 actor_id: &str,
1120 generation: u32,
1121 reason: ShutdownKind,
1122 ) -> TransitionResult {
1123 match self.actor_instances.entry_async(actor_id.to_owned()).await {
1124 SccEntry::Occupied(mut entry) => {
1125 let instance = entry.get().instance();
1126 if instance.generation != generation {
1131 return TransitionResult::Stale;
1132 }
1133 if matches!(entry.get(), ActorInstanceState::Active(_)) {
1134 entry.insert(ActorInstanceState::Stopping {
1135 instance: instance.clone(),
1136 reason,
1137 });
1138 } else {
1139 instance
1140 .ctx
1141 .warn_work_sent_to_stopping_instance("stop_actor");
1142 }
1143 TransitionResult::Transitioned(instance)
1144 }
1145 SccEntry::Vacant(entry) => {
1146 drop(entry);
1147 TransitionResult::Vacant
1148 }
1149 }
1150 }
1151
1152 async fn remove_stopping_actor_instance(&self, actor_id: &str, expected: &ActiveActorInstance) {
1153 match self.actor_instances.entry_async(actor_id.to_owned()).await {
1154 SccEntry::Occupied(entry) => {
1155 let should_remove = match entry.get() {
1156 ActorInstanceState::Stopping { instance, .. } => {
1157 Arc::ptr_eq(instance, expected)
1158 }
1159 ActorInstanceState::Active(_) => false,
1160 };
1161 if should_remove {
1162 let _ = entry.remove_entry();
1163 }
1164 }
1165 SccEntry::Vacant(entry) => {
1166 drop(entry);
1167 }
1168 }
1169 }
1170
1171 async fn active_actor(&self, actor_id: &str) -> Result<Arc<ActorTaskHandle>> {
1172 if let Some(instance) = self.actor_instances.get_async(&actor_id.to_owned()).await {
1173 match instance.get() {
1174 ActorInstanceState::Active(instance) => {
1175 let instance = instance.clone();
1176 if instance.ctx.started() {
1178 if instance.ctx.destroy_requested() {
1179 instance
1180 .ctx
1181 .warn_work_sent_to_stopping_instance("active_actor");
1182 return Err(ActorLifecycleError::Destroying.build());
1183 }
1184 return Ok(instance);
1185 }
1186
1187 instance
1188 .ctx
1189 .warn_work_sent_to_stopping_instance("active_actor");
1190 return Err(if instance.ctx.destroy_requested() {
1191 ActorLifecycleError::Destroying.build()
1192 } else if instance.ctx.sleep_requested() {
1193 ActorLifecycleError::Stopping.build()
1194 } else {
1195 ActorLifecycleError::Starting.build()
1196 });
1197 }
1198 ActorInstanceState::Stopping { instance, reason } => {
1199 let instance = instance.clone();
1200 match reason {
1201 ShutdownKind::Sleep if instance.ctx.started() => return Ok(instance),
1202 ShutdownKind::Sleep => {
1203 instance
1204 .ctx
1205 .warn_work_sent_to_stopping_instance("active_actor");
1206 return Err(ActorLifecycleError::Stopping.build());
1207 }
1208 ShutdownKind::Destroy => {
1209 instance
1210 .ctx
1211 .warn_work_sent_to_stopping_instance("active_actor");
1212 return Err(ActorLifecycleError::Destroying.build());
1213 }
1214 }
1215 }
1216 }
1217 }
1218
1219 tracing::warn!(actor_id, "actor instance not found");
1220 Err(ActorRuntime::NotFound {
1221 resource: "instance".to_owned(),
1222 id: actor_id.to_owned(),
1223 }
1224 .build())
1225 }
1226
1227 async fn stop_actor(
1228 &self,
1229 actor_id: &str,
1230 generation: u32,
1231 reason: protocol::StopActorReason,
1232 stop_handle: ActorStopHandle,
1233 ) -> Result<()> {
1234 let mut pending_stop = PendingStop {
1235 generation,
1236 reason,
1237 stop_handle,
1238 };
1239 loop {
1240 match self
1243 .starting_instances
1244 .entry_async(actor_id.to_owned())
1245 .await
1246 {
1247 SccEntry::Occupied(mut entry) => {
1248 let starting = entry.get_mut();
1249 let rejected = if starting.generation != pending_stop.generation {
1250 Some(pending_stop)
1254 } else if starting.pending_stop.is_some() {
1255 Some(pending_stop)
1258 } else {
1259 starting.pending_stop = Some(pending_stop);
1260 None
1261 };
1262 drop(entry);
1263
1264 if let Some(rejected) = rejected {
1265 let _ = rejected.stop_handle.complete();
1266 }
1267 return Ok(());
1268 }
1269 SccEntry::Vacant(entry) => {
1270 drop(entry);
1271 }
1272 }
1273
1274 let task_stop_reason = map_envoy_stop_reason(&pending_stop.reason);
1275 match self
1276 .transition_actor_to_stopping(actor_id, pending_stop.generation, task_stop_reason)
1277 .await
1278 {
1279 TransitionResult::Transitioned(instance) => {
1280 let result = self
1281 .shutdown_started_instance(
1282 actor_id,
1283 instance.clone(),
1284 pending_stop.reason,
1285 pending_stop.stop_handle,
1286 )
1287 .await;
1288 self.remove_stopping_actor_instance(actor_id, &instance)
1289 .await;
1290 return result;
1291 }
1292 TransitionResult::Stale => {
1293 let _ = pending_stop.stop_handle.complete();
1297 return Ok(());
1298 }
1299 TransitionResult::Vacant => {}
1300 }
1301
1302
1303
1304 if self
1309 .pending_stops
1310 .insert_async(actor_id.to_owned(), pending_stop)
1311 .await
1312 .is_err()
1313 {
1314 return Ok(());
1315 }
1316 if !self
1317 .actor_instances
1318 .contains_async(&actor_id.to_owned())
1319 .await
1320 {
1321 return Ok(());
1322 }
1323 let Some((_, reclaimed)) = self
1324 .pending_stops
1325 .remove_if_async(&actor_id.to_owned(), |parked| {
1326 parked.generation == generation
1327 })
1328 .await
1329 else {
1330 return Ok(());
1332 };
1333 pending_stop = reclaimed;
1334 }
1335 }
1336
1337 async fn shutdown_started_instance(
1338 &self,
1339 actor_id: &str,
1340 instance: Arc<ActorTaskHandle>,
1341 reason: protocol::StopActorReason,
1342 stop_handle: ActorStopHandle,
1343 ) -> Result<()> {
1344 let task_stop_reason = map_envoy_stop_reason(&reason);
1345
1346 if matches!(task_stop_reason, ShutdownKind::Destroy) {
1347 instance.ctx.mark_destroy_requested();
1348 }
1349
1350 tracing::debug!(
1351 actor_id,
1352 handle_actor_id = %instance.actor_id,
1353 actor_name = %instance.actor_name,
1354 generation = instance.generation,
1355 ?reason,
1356 ?task_stop_reason,
1357 "stopping actor instance"
1358 );
1359
1360 let (reply_tx, reply_rx) = oneshot::channel();
1361 let shutdown_result = match try_send_lifecycle_command(
1362 &instance.lifecycle,
1363 LifecycleCommand::Stop {
1364 reason: task_stop_reason,
1365 reply: reply_tx,
1366 },
1367 ) {
1368 Ok(()) => reply_rx
1369 .await
1370 .context("receive actor task stop reply")
1371 .and_then(|result| result),
1372 Err(error) => Err(error),
1373 };
1374
1375 if matches!(task_stop_reason, ShutdownKind::Destroy) {
1376 let shutdown_deadline =
1377 Instant::now() + instance.factory.config().effective_sleep_grace_period();
1378 if !instance
1379 .ctx
1380 .wait_for_internal_keep_awake_idle(shutdown_deadline)
1381 .await
1382 {
1383 instance.ctx.record_direct_subsystem_shutdown_warning(
1384 "internal_keep_awake",
1385 "destroy_drain",
1386 );
1387 tracing::warn!(
1388 actor_id,
1389 "destroy shutdown timed out waiting for in-flight actions"
1390 );
1391 }
1392 if !instance
1393 .ctx
1394 .wait_for_http_requests_drained(shutdown_deadline)
1395 .await
1396 {
1397 instance
1398 .ctx
1399 .record_direct_subsystem_shutdown_warning("http_requests", "destroy_drain");
1400 tracing::warn!(
1401 actor_id,
1402 "destroy shutdown timed out waiting for in-flight http requests"
1403 );
1404 }
1405 }
1406
1407 let mut join_guard = instance.join.lock().await;
1408 let join_result = if let Some(join) = join_guard.take() {
1411 join.await
1412 .context("join actor task")
1413 .and_then(|result| result.context("actor task failed"))
1414 } else {
1415 Ok(())
1416 };
1417 instance.ctx.configure_lifecycle_events(None);
1418
1419 if let (Err(shutdown_error), Err(join_error)) = (&shutdown_result, &join_result) {
1420 tracing::warn!(
1421 actor_id,
1422 %shutdown_error,
1423 discarded_join_error = %join_error,
1424 "actor stop had both shutdown and join failures; only the shutdown error is returned"
1425 );
1426 }
1427
1428 let final_result = shutdown_result.and(join_result);
1429 match &final_result {
1430 Ok(_) => {
1431 let _ = stop_handle.complete();
1432 }
1433 Err(error) => {
1434 let _ = stop_handle.fail(anyhow::Error::new(RivetError::extract(error)));
1435 }
1436 }
1437
1438 final_result.with_context(|| format!("stop actor `{actor_id}`"))
1439 }
1440}
1441
1442impl RegistryDispatcher {
1443 fn can_hibernate(&self, actor_id: &str, request: &HttpRequest) -> bool {
1444 let Some(instance) = self
1445 .actor_instances
1446 .read_sync(actor_id, |_, state| state.active_instance())
1447 .flatten()
1448 else {
1449 return false;
1450 };
1451
1452 match &instance.factory.config().can_hibernate_websocket {
1453 CanHibernateWebSocket::Bool(value) => *value,
1454 CanHibernateWebSocket::Callback(callback) => callback(request),
1455 }
1456 }
1457
1458 #[allow(clippy::too_many_arguments)]
1459 fn build_actor_context(
1460 &self,
1461 handle: EnvoyHandle,
1462 actor_id: &str,
1463 generation: u32,
1464 actor_name: &str,
1465 key: ActorKey,
1466 factory: &ActorFactory,
1467 ) -> Result<ActorContext> {
1468 let formatted_key = format_actor_key(&key);
1469 #[cfg_attr(not(feature = "sqlite-local"), allow(unused_mut))]
1470 let mut sql = SqliteDb::new_with_remote_sqlite(
1471 handle.clone(),
1472 actor_id.to_owned(),
1473 Some(formatted_key),
1474 Some(generation as u64),
1475 factory.config().has_database,
1476 factory.config().remote_sqlite,
1477 )?;
1478 #[cfg(feature = "sqlite-local")]
1479 sql.set_worker_pool(self.sqlite_pool.clone());
1480 let ctx = ActorContext::build(
1481 actor_id.to_owned(),
1482 actor_name.to_owned(),
1483 key,
1484 self.region.clone(),
1485 Some(generation),
1486 handle.get_envoy_key().to_owned(),
1487 factory.config().clone(),
1488 LegacyActorKv::new(handle.clone(), actor_id.to_owned()),
1489 sql,
1490 );
1491 ctx.configure_envoy(handle, Some(generation));
1492 Ok(ctx)
1493 }
1494}
1495
1496fn map_envoy_stop_reason(reason: &protocol::StopActorReason) -> ShutdownKind {
1501 match reason {
1502 protocol::StopActorReason::SleepIntent => ShutdownKind::Sleep,
1504 protocol::StopActorReason::GoingAway => ShutdownKind::Sleep,
1507 protocol::StopActorReason::Lost => ShutdownKind::Sleep,
1510 protocol::StopActorReason::StopIntent => ShutdownKind::Destroy,
1512 protocol::StopActorReason::Destroy => ShutdownKind::Destroy,
1514 }
1515}
1516
1517