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