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: false,
681 debug_latency_ms: None,
682 max_queued_requests_per_actor:
683 rivet_envoy_client::config::DEFAULT_MAX_QUEUED_REQUESTS_PER_ACTOR,
684 stopped_generation_floor_ttl:
685 rivet_envoy_client::config::DEFAULT_STOPPED_GENERATION_FLOOR_TTL,
686 stopped_generation_floor_capacity:
687 rivet_envoy_client::config::DEFAULT_STOPPED_GENERATION_FLOOR_CAPACITY,
688 callbacks,
689 connector: None,
690 });
691 let envoy_ready: Result<bool> = tokio::select! {
692 biased;
693 _ = shutdown.cancelled() => Ok(false),
694 result = timeout(ENVOY_START_TIMEOUT, handle.started()) => {
695 result.context("serverful envoy startup timed out").and_then(|result| result).map(|_| true)
696 }
697 };
698 if !matches!(&envoy_ready, Ok(true)) {
699 handle.shutdown_and_wait(true).await;
700 #[cfg(feature = "native-runtime")]
701 if let Some(development_processes) = development_processes {
702 development_processes.shutdown().await;
703 }
704 return envoy_ready.map(|_| ());
705 }
706 on_handle(CoreEnvoyHandle::new(handle.clone()));
707
708 shutdown.cancelled().await;
714
715 let shutdown_envoy = async {
716 if manage_engine {
717 if !handle.mark_stopping().await {
722 tracing::warn!("could not mark local development envoy as stopping");
723 }
724 let mut actors = Vec::new();
728 dispatcher
729 .actor_instances
730 .iter_async(|actor_id, state| {
731 if let Some(instance) = state.active_instance() {
732 actors.push((actor_id.clone(), instance.generation));
733 }
734 true
735 })
736 .await;
737 let actor_count = actors.len();
738 tracing::info!(
739 actors = actor_count,
740 "sleeping local development actors for shutdown"
741 );
742 for (actor_id, generation) in actors {
743 handle.sleep_actor(actor_id, Some(generation), None);
744 }
745 if actor_count > 0 {
746 let acknowledged = timeout(Duration::from_secs(3), async {
749 handle.wait_actors_drained().await;
750 handle.wait_for_events_acked().await
751 })
752 .await;
753 if !matches!(acknowledged, Ok(true)) {
754 tracing::warn!(
755 "local development actor stops were not acknowledged before shutdown"
756 );
757 }
758 }
759 }
760
761 let stop_threshold = handle
764 .get_protocol_metadata()
765 .await
766 .map(|x| x.actor_stop_threshold)
767 .unwrap_or(30 * 60 * 1000);
768 match timeout(
772 Duration::from_millis(stop_threshold as u64),
773 handle.shutdown_and_wait(false),
774 )
775 .await
776 {
777 Ok(()) => {}
778 Err(_) => {
779 tracing::warn!("envoy shutdown drain exceeded timeout; forcing immediate stop");
780 handle.shutdown(true);
781 handle.wait_stopped().await;
782 }
783 }
784 };
785 #[cfg(feature = "native-runtime")]
786 let shutdown_development_processes = async move {
787 if let Some(development_processes) = development_processes {
788 development_processes.shutdown().await;
789 }
790 };
791 #[cfg(feature = "native-runtime")]
792 {
793 shutdown_envoy.await;
794 shutdown_development_processes.await;
795 }
796 #[cfg(not(feature = "native-runtime"))]
797 shutdown_envoy.await;
798
799 Ok(())
800 }
801
802 fn into_dispatcher(self, config: &ServeConfig) -> Arc<RegistryDispatcher> {
803 Arc::new(RegistryDispatcher::new(
804 self.factories,
805 config.handle_inspector_http_in_runtime,
806 ))
807 }
808
809 pub async fn into_serverless_runtime(
810 self,
811 config: ServeConfig,
812 ) -> Result<crate::serverless::CoreServerlessRuntime> {
813 crate::serverless::CoreServerlessRuntime::new(self.factories, config).await
814 }
815}
816
817impl RegistryDispatcher {
818 pub(crate) fn new(
819 factories: HashMap<String, Arc<ActorFactory>>,
820 handle_inspector_http_in_runtime: bool,
821 ) -> Self {
822 Self {
823 factories,
824 actor_instances: SccHashMap::new(),
825 starting_instances: SccHashMap::new(),
826 pending_stops: SccHashMap::new(),
827 region: env::var("RIVET_REGION").unwrap_or_default(),
828 handle_inspector_http_in_runtime,
829 #[cfg(feature = "sqlite-local")]
830 sqlite_pool: sqlite_embedded::worker::SqlitePool::from_env(Some(Arc::new(
831 crate::actor::sqlite::pool_metrics::PoolMetrics,
832 ))),
833 }
834 }
835
836 pub(crate) fn build_actor_metadata_map(&self) -> HashMap<String, JsonValue> {
837 build_actor_metadata_map_from_factories(&self.factories)
838 }
839}
840
841pub(crate) fn serverless_metadata_payload(
842 actor_metadata: HashMap<String, JsonValue>,
843 config: &ServeConfig,
844 envoy_kind: ServerlessMetadataEnvoyKind,
845) -> ServerlessMetadataPayload {
846 let actor_names = actor_metadata
847 .into_iter()
848 .map(|(name, metadata)| {
849 (
850 name,
851 ActorName {
852 metadata: Some(metadata),
853 },
854 )
855 })
856 .collect::<HashMap<_, _>>();
857
858 ServerlessMetadataPayload {
859 runtime: "rivetkit".to_owned(),
860 version: config.serverless_package_version.clone(),
861 envoy_protocol_version: Some(protocol::PROTOCOL_VERSION),
862 actor_names,
863 envoy: Some(ServerlessMetadataEnvoy {
864 kind: Some(envoy_kind),
865 version: Some(config.version),
866 }),
867 runner: None,
868 client_endpoint: config.serverless_client_endpoint.clone(),
869 client_namespace: config.serverless_client_namespace.clone(),
870 client_token: config.serverless_client_token.clone(),
871 }
872}
873
874fn build_actor_metadata_map_from_factories(
875 factories: &HashMap<String, Arc<ActorFactory>>,
876) -> HashMap<String, JsonValue> {
877 factories
878 .iter()
879 .map(|(actor_name, factory)| {
880 let config = factory.config();
881 let mut metadata = serde_json::Map::new();
882 if let Some(icon) = &config.icon {
883 metadata.insert("icon".to_owned(), json!(icon));
884 }
885 if let Some(name) = &config.name {
886 metadata.insert("name".to_owned(), json!(name));
887 }
888 (actor_name.clone(), JsonValue::Object(metadata))
889 })
890 .collect()
891}
892
893impl RegistryDispatcher {
894 async fn start_actor(self: &Arc<Self>, request: StartActorRequest) -> Result<()> {
895 let factory = self
896 .factories
897 .get(&request.actor_name)
898 .cloned()
899 .ok_or_else(|| {
900 ActorRuntime::NotRegistered {
901 actor_name: request.actor_name.clone(),
902 }
903 .build()
904 })?;
905 let _ = self
906 .starting_instances
907 .insert_async(
908 request.actor_id.clone(),
909 StartingInstance {
910 generation: request.generation,
911 pending_stop: None,
912 },
913 )
914 .await;
915 let (lifecycle_tx, lifecycle_rx) = mpsc::unbounded_channel();
920 let (dispatch_tx, dispatch_rx) = mpsc::unbounded_channel();
921 let (lifecycle_events_tx, lifecycle_events_rx) = mpsc::unbounded_channel();
922 request
923 .ctx
924 .configure_lifecycle_events(Some(lifecycle_events_tx));
925 request.ctx.cancel_sleep_timer();
926 request.ctx.set_local_alarm_callback(Some(Arc::new({
927 let lifecycle_tx = lifecycle_tx.clone();
928 move || {
929 let lifecycle_tx = lifecycle_tx.clone();
930 Box::pin(async move {
931 let (reply_tx, reply_rx) = oneshot::channel();
932 if let Err(error) = try_send_lifecycle_command(
933 &lifecycle_tx,
934 LifecycleCommand::FireAlarm { reply: reply_tx },
935 ) {
936 tracing::warn!(?error, "failed to enqueue actor alarm");
937 return;
938 }
939 let _ = reply_rx.await;
940 })
941 }
942 })));
943 let task = ActorTask::new(
944 request.actor_id.clone(),
945 request.generation,
946 lifecycle_rx,
947 dispatch_rx,
948 lifecycle_events_rx,
949 factory.clone(),
950 request.ctx.clone(),
951 request.input,
952 );
953 let join = RuntimeSpawner::spawn(task.run());
954
955 let (start_tx, start_rx) = oneshot::channel();
956 let result: Result<Arc<ActorTaskHandle>> = async {
957 try_send_lifecycle_command(&lifecycle_tx, LifecycleCommand::Start { reply: start_tx })
958 .context("send actor task start command")?;
959 start_rx
960 .await
961 .context("receive actor task start reply")?
962 .context("actor task start")?;
963 let inspector = build_actor_inspector();
964 request.ctx.configure_inspector(Some(inspector.clone()));
965 Ok::<Arc<ActorTaskHandle>, anyhow::Error>(Arc::new(ActorTaskHandle {
966 actor_id: request.actor_id.clone(),
967 actor_name: request.actor_name.clone(),
968 generation: request.generation,
969 ctx: request.ctx.clone(),
970 factory,
971 inspector,
972 lifecycle: lifecycle_tx,
973 dispatch: dispatch_tx,
974 join: Arc::new(TokioMutex::new(Some(join))),
975 }))
976 }
977 .await
978 .with_context(|| format!("start actor `{}`", request.actor_id));
979
980 match result {
981 Ok(instance) => {
982 self.set_actor_instance_state(
985 request.actor_id.clone(),
986 ActorInstanceState::Active(instance),
987 )
988 .await;
989 let parked_during_start = self
990 .starting_instances
991 .remove_async(&request.actor_id)
992 .await
993 .and_then(|(_, starting)| starting.pending_stop);
994 let parked_before_start = self
995 .pending_stops
996 .remove_async(&request.actor_id)
997 .await
998 .map(|(_, pending_stop)| pending_stop);
999
1000 let mut current_generation_stop = None;
1004 for pending_stop in [parked_before_start, parked_during_start]
1005 .into_iter()
1006 .flatten()
1007 {
1008 if pending_stop.generation == request.generation
1009 && current_generation_stop.is_none()
1010 {
1011 current_generation_stop = Some(pending_stop);
1012 } else {
1013 let _ = pending_stop.stop_handle.complete();
1014 }
1015 }
1016
1017 if let Some(pending_stop) = current_generation_stop {
1018 self.apply_stop_queued_during_startup(&request.actor_id, pending_stop)
1019 .await;
1020 }
1021 Ok(())
1022 }
1023 Err(error) => {
1024 if let Some((_, starting)) = self
1028 .starting_instances
1029 .remove_async(&request.actor_id)
1030 .await && let Some(pending_stop) = starting.pending_stop
1031 {
1032 let _ = pending_stop.stop_handle.complete();
1033 }
1034 if let Some((_, pending_stop)) =
1035 self.pending_stops.remove_async(&request.actor_id).await
1036 {
1037 let _ = pending_stop.stop_handle.complete();
1038 }
1039 Err(error)
1040 }
1041 }
1042 }
1043
1044 async fn apply_stop_queued_during_startup(
1047 self: &Arc<Self>,
1048 actor_id: &str,
1049 pending_stop: PendingStop,
1050 ) {
1051 let stop_reason = map_envoy_stop_reason(&pending_stop.reason);
1052 let instance = match self
1053 .transition_actor_to_stopping(actor_id, pending_stop.generation, stop_reason)
1054 .await
1055 {
1056 TransitionResult::Transitioned(instance) => instance,
1057 TransitionResult::Stale | TransitionResult::Vacant => {
1058 let _ = pending_stop.stop_handle.complete();
1061 return;
1062 }
1063 };
1064
1065 let dispatcher = self.clone();
1066 let actor_id = actor_id.to_owned();
1067 RuntimeSpawner::spawn(async move {
1068 if let Err(error) = dispatcher
1069 .shutdown_started_instance(
1070 &actor_id,
1071 instance.clone(),
1072 pending_stop.reason,
1073 pending_stop.stop_handle,
1074 )
1075 .await
1076 {
1077 tracing::error!(
1078 actor_id,
1079 ?error,
1080 "failed to stop actor queued during startup"
1081 );
1082 }
1083 dispatcher
1084 .remove_stopping_actor_instance(&actor_id, &instance)
1085 .await;
1086 });
1087 }
1088
1089 async fn set_actor_instance_state(&self, actor_id: String, state: ActorInstanceState) {
1090 match self.actor_instances.entry_async(actor_id).await {
1091 SccEntry::Occupied(mut entry) => {
1092 entry.insert(state);
1093 }
1094 SccEntry::Vacant(entry) => {
1095 entry.insert_entry(state);
1096 }
1097 }
1098 }
1099
1100 async fn transition_actor_to_stopping(
1101 &self,
1102 actor_id: &str,
1103 generation: u32,
1104 reason: ShutdownKind,
1105 ) -> TransitionResult {
1106 match self.actor_instances.entry_async(actor_id.to_owned()).await {
1107 SccEntry::Occupied(mut entry) => {
1108 let instance = entry.get().instance();
1109 if instance.generation != generation {
1114 return TransitionResult::Stale;
1115 }
1116 if matches!(entry.get(), ActorInstanceState::Active(_)) {
1117 entry.insert(ActorInstanceState::Stopping {
1118 instance: instance.clone(),
1119 reason,
1120 });
1121 } else {
1122 instance
1123 .ctx
1124 .warn_work_sent_to_stopping_instance("stop_actor");
1125 }
1126 TransitionResult::Transitioned(instance)
1127 }
1128 SccEntry::Vacant(entry) => {
1129 drop(entry);
1130 TransitionResult::Vacant
1131 }
1132 }
1133 }
1134
1135 async fn remove_stopping_actor_instance(&self, actor_id: &str, expected: &ActiveActorInstance) {
1136 match self.actor_instances.entry_async(actor_id.to_owned()).await {
1137 SccEntry::Occupied(entry) => {
1138 let should_remove = match entry.get() {
1139 ActorInstanceState::Stopping { instance, .. } => {
1140 Arc::ptr_eq(instance, expected)
1141 }
1142 ActorInstanceState::Active(_) => false,
1143 };
1144 if should_remove {
1145 let _ = entry.remove_entry();
1146 }
1147 }
1148 SccEntry::Vacant(entry) => {
1149 drop(entry);
1150 }
1151 }
1152 }
1153
1154 async fn active_actor(&self, actor_id: &str) -> Result<Arc<ActorTaskHandle>> {
1155 if let Some(instance) = self.actor_instances.get_async(&actor_id.to_owned()).await {
1156 match instance.get() {
1157 ActorInstanceState::Active(instance) => {
1158 let instance = instance.clone();
1159 if instance.ctx.started() {
1161 if instance.ctx.destroy_requested() {
1162 instance
1163 .ctx
1164 .warn_work_sent_to_stopping_instance("active_actor");
1165 return Err(ActorLifecycleError::Destroying.build());
1166 }
1167 return Ok(instance);
1168 }
1169
1170 instance
1171 .ctx
1172 .warn_work_sent_to_stopping_instance("active_actor");
1173 return Err(if instance.ctx.destroy_requested() {
1174 ActorLifecycleError::Destroying.build()
1175 } else if instance.ctx.sleep_requested() {
1176 ActorLifecycleError::Stopping.build()
1177 } else {
1178 ActorLifecycleError::Starting.build()
1179 });
1180 }
1181 ActorInstanceState::Stopping { instance, reason } => {
1182 let instance = instance.clone();
1183 match reason {
1184 ShutdownKind::Sleep if instance.ctx.started() => return Ok(instance),
1185 ShutdownKind::Sleep => {
1186 instance
1187 .ctx
1188 .warn_work_sent_to_stopping_instance("active_actor");
1189 return Err(ActorLifecycleError::Stopping.build());
1190 }
1191 ShutdownKind::Destroy => {
1192 instance
1193 .ctx
1194 .warn_work_sent_to_stopping_instance("active_actor");
1195 return Err(ActorLifecycleError::Destroying.build());
1196 }
1197 }
1198 }
1199 }
1200 }
1201
1202 tracing::warn!(actor_id, "actor instance not found");
1203 Err(ActorRuntime::NotFound {
1204 resource: "instance".to_owned(),
1205 id: actor_id.to_owned(),
1206 }
1207 .build())
1208 }
1209
1210 async fn stop_actor(
1211 &self,
1212 actor_id: &str,
1213 generation: u32,
1214 reason: protocol::StopActorReason,
1215 stop_handle: ActorStopHandle,
1216 ) -> Result<()> {
1217 let mut pending_stop = PendingStop {
1218 generation,
1219 reason,
1220 stop_handle,
1221 };
1222 loop {
1223 match self
1226 .starting_instances
1227 .entry_async(actor_id.to_owned())
1228 .await
1229 {
1230 SccEntry::Occupied(mut entry) => {
1231 let starting = entry.get_mut();
1232 let rejected = if starting.generation != pending_stop.generation {
1233 Some(pending_stop)
1237 } else if starting.pending_stop.is_some() {
1238 Some(pending_stop)
1241 } else {
1242 starting.pending_stop = Some(pending_stop);
1243 None
1244 };
1245 drop(entry);
1246
1247 if let Some(rejected) = rejected {
1248 let _ = rejected.stop_handle.complete();
1249 }
1250 return Ok(());
1251 }
1252 SccEntry::Vacant(entry) => {
1253 drop(entry);
1254 }
1255 }
1256
1257 let task_stop_reason = map_envoy_stop_reason(&pending_stop.reason);
1258 match self
1259 .transition_actor_to_stopping(actor_id, pending_stop.generation, task_stop_reason)
1260 .await
1261 {
1262 TransitionResult::Transitioned(instance) => {
1263 let result = self
1264 .shutdown_started_instance(
1265 actor_id,
1266 instance.clone(),
1267 pending_stop.reason,
1268 pending_stop.stop_handle,
1269 )
1270 .await;
1271 self.remove_stopping_actor_instance(actor_id, &instance)
1272 .await;
1273 return result;
1274 }
1275 TransitionResult::Stale => {
1276 let _ = pending_stop.stop_handle.complete();
1280 return Ok(());
1281 }
1282 TransitionResult::Vacant => {}
1283 }
1284
1285
1286
1287 if self
1292 .pending_stops
1293 .insert_async(actor_id.to_owned(), pending_stop)
1294 .await
1295 .is_err()
1296 {
1297 return Ok(());
1298 }
1299 if !self
1300 .actor_instances
1301 .contains_async(&actor_id.to_owned())
1302 .await
1303 {
1304 return Ok(());
1305 }
1306 let Some((_, reclaimed)) = self
1307 .pending_stops
1308 .remove_if_async(&actor_id.to_owned(), |parked| {
1309 parked.generation == generation
1310 })
1311 .await
1312 else {
1313 return Ok(());
1315 };
1316 pending_stop = reclaimed;
1317 }
1318 }
1319
1320 async fn shutdown_started_instance(
1321 &self,
1322 actor_id: &str,
1323 instance: Arc<ActorTaskHandle>,
1324 reason: protocol::StopActorReason,
1325 stop_handle: ActorStopHandle,
1326 ) -> Result<()> {
1327 let task_stop_reason = map_envoy_stop_reason(&reason);
1328
1329 if matches!(task_stop_reason, ShutdownKind::Destroy) {
1330 instance.ctx.mark_destroy_requested();
1331 }
1332
1333 tracing::debug!(
1334 actor_id,
1335 handle_actor_id = %instance.actor_id,
1336 actor_name = %instance.actor_name,
1337 generation = instance.generation,
1338 ?reason,
1339 ?task_stop_reason,
1340 "stopping actor instance"
1341 );
1342
1343 let (reply_tx, reply_rx) = oneshot::channel();
1344 let shutdown_result = match try_send_lifecycle_command(
1345 &instance.lifecycle,
1346 LifecycleCommand::Stop {
1347 reason: task_stop_reason,
1348 reply: reply_tx,
1349 },
1350 ) {
1351 Ok(()) => reply_rx
1352 .await
1353 .context("receive actor task stop reply")
1354 .and_then(|result| result),
1355 Err(error) => Err(error),
1356 };
1357
1358 if matches!(task_stop_reason, ShutdownKind::Destroy) {
1359 let shutdown_deadline =
1360 Instant::now() + instance.factory.config().effective_sleep_grace_period();
1361 if !instance
1362 .ctx
1363 .wait_for_internal_keep_awake_idle(shutdown_deadline)
1364 .await
1365 {
1366 instance.ctx.record_direct_subsystem_shutdown_warning(
1367 "internal_keep_awake",
1368 "destroy_drain",
1369 );
1370 tracing::warn!(
1371 actor_id,
1372 "destroy shutdown timed out waiting for in-flight actions"
1373 );
1374 }
1375 if !instance
1376 .ctx
1377 .wait_for_http_requests_drained(shutdown_deadline)
1378 .await
1379 {
1380 instance
1381 .ctx
1382 .record_direct_subsystem_shutdown_warning("http_requests", "destroy_drain");
1383 tracing::warn!(
1384 actor_id,
1385 "destroy shutdown timed out waiting for in-flight http requests"
1386 );
1387 }
1388 }
1389
1390 let mut join_guard = instance.join.lock().await;
1391 let join_result = if let Some(join) = join_guard.take() {
1394 join.await
1395 .context("join actor task")
1396 .and_then(|result| result.context("actor task failed"))
1397 } else {
1398 Ok(())
1399 };
1400 instance.ctx.configure_lifecycle_events(None);
1401
1402 if let (Err(shutdown_error), Err(join_error)) = (&shutdown_result, &join_result) {
1403 tracing::warn!(
1404 actor_id,
1405 %shutdown_error,
1406 discarded_join_error = %join_error,
1407 "actor stop had both shutdown and join failures; only the shutdown error is returned"
1408 );
1409 }
1410
1411 let final_result = shutdown_result.and(join_result);
1412 match &final_result {
1413 Ok(_) => {
1414 let _ = stop_handle.complete();
1415 }
1416 Err(error) => {
1417 let _ = stop_handle.fail(anyhow::Error::new(RivetError::extract(error)));
1418 }
1419 }
1420
1421 final_result.with_context(|| format!("stop actor `{actor_id}`"))
1422 }
1423}
1424
1425impl RegistryDispatcher {
1426 fn can_hibernate(&self, actor_id: &str, request: &HttpRequest) -> bool {
1427 let Some(instance) = self
1428 .actor_instances
1429 .read_sync(actor_id, |_, state| state.active_instance())
1430 .flatten()
1431 else {
1432 return false;
1433 };
1434
1435 match &instance.factory.config().can_hibernate_websocket {
1436 CanHibernateWebSocket::Bool(value) => *value,
1437 CanHibernateWebSocket::Callback(callback) => callback(request),
1438 }
1439 }
1440
1441 #[allow(clippy::too_many_arguments)]
1442 fn build_actor_context(
1443 &self,
1444 handle: EnvoyHandle,
1445 actor_id: &str,
1446 generation: u32,
1447 actor_name: &str,
1448 key: ActorKey,
1449 factory: &ActorFactory,
1450 ) -> Result<ActorContext> {
1451 let formatted_key = format_actor_key(&key);
1452 #[cfg_attr(not(feature = "sqlite-local"), allow(unused_mut))]
1453 let mut sql = SqliteDb::new_with_remote_sqlite(
1454 handle.clone(),
1455 actor_id.to_owned(),
1456 Some(formatted_key),
1457 Some(generation as u64),
1458 factory.config().has_database,
1459 factory.config().remote_sqlite,
1460 )?;
1461 #[cfg(feature = "sqlite-local")]
1462 sql.set_worker_pool(self.sqlite_pool.clone());
1463 let ctx = ActorContext::build(
1464 actor_id.to_owned(),
1465 actor_name.to_owned(),
1466 key,
1467 self.region.clone(),
1468 Some(generation),
1469 handle.get_envoy_key().to_owned(),
1470 factory.config().clone(),
1471 LegacyActorKv::new(handle.clone(), actor_id.to_owned()),
1472 sql,
1473 );
1474 ctx.configure_envoy(handle, Some(generation));
1475 Ok(ctx)
1476 }
1477}
1478
1479fn map_envoy_stop_reason(reason: &protocol::StopActorReason) -> ShutdownKind {
1484 match reason {
1485 protocol::StopActorReason::SleepIntent => ShutdownKind::Sleep,
1487 protocol::StopActorReason::GoingAway => ShutdownKind::Sleep,
1490 protocol::StopActorReason::Lost => ShutdownKind::Sleep,
1493 protocol::StopActorReason::StopIntent => ShutdownKind::Destroy,
1495 protocol::StopActorReason::Destroy => ShutdownKind::Destroy,
1497 }
1498}
1499
1500