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