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