1use tracing::Instrument;
2
3use super::*;
4use crate::error::ActorRuntime;
5use crate::runtime::RuntimeSpawner;
6
7impl EnvoyCallbacks for RegistryCallbacks {
8 fn on_actor_start(
9 &self,
10 handle: EnvoyHandle,
11 actor_id: String,
12 generation: u32,
13 config: protocol::ActorConfig,
14 _preloaded_kv: Option<protocol::PreloadedKv>,
15 ) -> EnvoyBoxFuture<anyhow::Result<()>> {
16 let dispatcher = self.dispatcher.clone();
17 let actor_name = config.name.clone();
18 let key = actor_key_from_protocol(config.key.clone());
19 let input = config.input.clone();
20 let factory = dispatcher.factories.get(&actor_name).cloned();
21
22 Box::pin(async move {
23 let factory = factory.ok_or_else(|| {
24 ActorRuntime::NotRegistered {
25 actor_name: actor_name.clone(),
26 }
27 .build()
28 })?;
29 let ctx = dispatcher.build_actor_context(
30 handle,
31 &actor_id,
32 generation,
33 &actor_name,
34 key,
35 factory.as_ref(),
36 )?;
37
38 dispatcher
39 .start_actor(StartActorRequest {
40 actor_id: actor_id.clone(),
41 generation,
42 actor_name,
43 input,
44 ctx,
45 })
46 .await?;
47
48 Ok(())
49 })
50 }
51
52 fn on_actor_stop_with_completion(
53 &self,
54 _handle: EnvoyHandle,
55 actor_id: String,
56 generation: u32,
57 reason: protocol::StopActorReason,
58 stop_handle: ActorStopHandle,
59 ) -> EnvoyBoxFuture<anyhow::Result<()>> {
60 let dispatcher = self.dispatcher.clone();
61 Box::pin(async move {
62 RuntimeSpawner::spawn(
63 async move {
64 if let Err(error) = dispatcher
65 .stop_actor(&actor_id, generation, reason, stop_handle)
66 .await
67 {
68 tracing::error!(
69 ?error,
70 "actor stop failed after asynchronous completion handoff",
71 );
72 }
73 }
74 .in_current_span(),
75 );
76 Ok(())
77 })
78 }
79
80 fn on_shutdown(&self) {}
81
82 fn fetch(
83 &self,
84 _handle: EnvoyHandle,
85 actor_id: String,
86 _gateway_id: protocol::GatewayId,
87 _request_id: protocol::RequestId,
88 request: HttpRequest,
89 ) -> EnvoyBoxFuture<anyhow::Result<HttpResponse>> {
90 tracing::info!(
91 method = %request.method,
92 path = %request.path,
93 "envoy callback: fetch request"
94 );
95 let dispatcher = self.dispatcher.clone();
96 Box::pin(async move { dispatcher.handle_fetch(&actor_id, request).await })
97 }
98
99 fn websocket(
100 &self,
101 _handle: EnvoyHandle,
102 actor_id: String,
103 _gateway_id: protocol::GatewayId,
104 _request_id: protocol::RequestId,
105 _request: HttpRequest,
106 _path: String,
107 _headers: HashMap<String, String>,
108 _is_hibernatable: bool,
109 is_restoring_hibernatable: bool,
110 sender: WebSocketSender,
111 ) -> EnvoyBoxFuture<anyhow::Result<WebSocketHandler>> {
112 tracing::info!(
113 path = %_path,
114 is_hibernatable = _is_hibernatable,
115 is_restoring_hibernatable,
116 "envoy callback: websocket request"
117 );
118 let dispatcher = self.dispatcher.clone();
119 Box::pin(async move {
120 dispatcher
121 .handle_websocket(
122 &actor_id,
123 &_request,
124 &_path,
125 &_headers,
126 &_gateway_id,
127 &_request_id,
128 _is_hibernatable,
129 is_restoring_hibernatable,
130 sender,
131 )
132 .await
133 })
134 }
135
136 fn can_hibernate(
137 &self,
138 actor_id: &str,
139 _gateway_id: &protocol::GatewayId,
140 _request_id: &protocol::RequestId,
141 request: &HttpRequest,
142 ) -> EnvoyBoxFuture<anyhow::Result<bool>> {
143 let can_hibernate = self.dispatcher.can_hibernate(actor_id, request);
144 Box::pin(async move { Ok(can_hibernate) })
145 }
146}
147
148impl ServeSettings {
149 fn from_env() -> anyhow::Result<Self> {
150 let engine_host = env::var("RIVET_RUN_ENGINE_HOST").ok();
151 let engine_port = env::var("RIVET_RUN_ENGINE_PORT")
152 .ok()
153 .and_then(|value| value.parse().ok());
154 let parsed = env::var("RIVET_ENDPOINT")
158 .ok()
159 .map(extract_endpoint_auth)
160 .transpose()
161 .context("invalid RIVET_ENDPOINT")?;
162 let env_namespace = env::var("RIVET_NAMESPACE").ok();
163 let env_token = env::var("RIVET_TOKEN").ok();
164 let parsed = parsed.unwrap_or_else(|| ParsedEndpoint {
165 endpoint: default_engine_endpoint(
166 engine_host.as_deref().unwrap_or("127.0.0.1"),
167 engine_port.unwrap_or(6420),
168 ),
169 namespace: None,
170 token: None,
171 });
172 if parsed.namespace.is_some() && env_namespace.is_some() {
173 anyhow::bail!(
174 "cannot specify namespace both in the RIVET_ENDPOINT URL and as RIVET_NAMESPACE"
175 );
176 }
177 if parsed.token.is_some() && env_token.is_some() {
178 anyhow::bail!(
179 "cannot specify token both in the RIVET_ENDPOINT URL and as RIVET_TOKEN"
180 );
181 }
182
183 Ok(Self {
184 version: env::var("RIVET_ENVOY_VERSION")
185 .ok()
186 .and_then(|value| value.parse().ok())
187 .unwrap_or(1),
188 endpoint: parsed.endpoint,
189 token: Some(
190 parsed
191 .token
192 .or(env_token)
193 .unwrap_or_else(|| "dev".to_owned()),
194 ),
195 namespace: parsed
196 .namespace
197 .or(env_namespace)
198 .unwrap_or_else(|| "default".to_owned()),
199 pool_name: env::var("RIVET_POOL_NAME").unwrap_or_else(|_| "rivetkit-rust".to_owned()),
200 engine_binary_path: env::var_os("RIVET_ENGINE_BINARY_PATH").map(PathBuf::from),
201 start_services: matches!(env::var("RIVET_RUN_SERVICES").as_deref(), Ok("1")),
202 services_binary_path: env::var_os("RIVET_SERVICES_BINARY").map(PathBuf::from),
203 engine_host,
204 engine_port,
205 engine_spawn: super::EngineSpawnMode::from_env(),
206 engine_auto_download: matches!(
207 env::var("RIVETKIT_ENGINE_AUTO_DOWNLOAD").as_deref(),
208 Ok("1") | Ok("true") | Ok("TRUE") | Ok("yes") | Ok("YES")
209 ),
210 handle_inspector_http_in_runtime: false,
211 serverless_base_path: None,
212 serverless_package_version: env!("CARGO_PKG_VERSION").to_owned(),
213 serverless_client_endpoint: None,
214 serverless_client_namespace: None,
215 serverless_client_token: None,
216 serverless_validate_endpoint: true,
217 serverless_max_start_payload_bytes: 1_048_576,
218 })
219 }
220}
221
222#[derive(Debug)]
224struct ParsedEndpoint {
225 endpoint: String,
226 namespace: Option<String>,
227 token: Option<String>,
228}
229
230fn extract_endpoint_auth(endpoint: String) -> anyhow::Result<ParsedEndpoint> {
235 let mut url = url::Url::parse(&endpoint)
236 .with_context(|| format!("invalid URL: {endpoint}"))?;
237 if url.query().is_some() {
238 anyhow::bail!("endpoint cannot contain a query string");
239 }
240 if url.fragment().is_some() {
241 anyhow::bail!("endpoint cannot contain a fragment");
242 }
243
244 let namespace = if url.username().is_empty() {
245 None
246 } else {
247 Some(decode_url_auth(url.username())?)
248 };
249 let token = match url.password() {
250 None | Some("") => None,
251 Some(password) => Some(decode_url_auth(password)?),
252 };
253 if token.is_some() && namespace.is_none() {
254 anyhow::bail!("endpoint cannot have a token without a namespace");
255 }
256
257 if namespace.is_some() || token.is_some() {
261 if url.set_username("").is_err() || url.set_password(None).is_err() {
262 anyhow::bail!("endpoint URL does not support credentials");
263 }
264 }
265
266 Ok(ParsedEndpoint {
267 endpoint: url.to_string(),
268 namespace,
269 token,
270 })
271}
272
273fn decode_url_auth(value: &str) -> anyhow::Result<String> {
276 super::http::percent_decode_path_segment(value)
277 .context("invalid percent-encoding in endpoint URL auth")
278}
279
280fn default_engine_endpoint(host: &str, port: u16) -> String {
281 let url_host = if host.contains(':') && !host.starts_with('[') {
282 format!("[{host}]")
283 } else {
284 host.to_owned()
285 };
286 format!("http://{url_host}:{port}")
287}
288
289impl ServeConfig {
290 pub fn from_env() -> anyhow::Result<Self> {
291 let settings = ServeSettings::from_env()?;
292 Ok(Self {
293 version: settings.version,
294 endpoint: settings.endpoint,
295 token: settings.token,
296 namespace: settings.namespace,
297 pool_name: settings.pool_name,
298 engine_binary_path: settings.engine_binary_path,
299 start_services: settings.start_services,
300 services_binary_path: settings.services_binary_path,
301 engine_host: settings.engine_host,
302 engine_port: settings.engine_port,
303 engine_spawn: settings.engine_spawn,
304 engine_auto_download: settings.engine_auto_download,
305 handle_inspector_http_in_runtime: settings.handle_inspector_http_in_runtime,
306 serverless_base_path: settings.serverless_base_path,
307 serverless_package_version: settings.serverless_package_version,
308 serverless_client_endpoint: settings.serverless_client_endpoint,
309 serverless_client_namespace: settings.serverless_client_namespace,
310 serverless_client_token: settings.serverless_client_token,
311 serverless_validate_endpoint: settings.serverless_validate_endpoint,
312 serverless_max_start_payload_bytes: settings.serverless_max_start_payload_bytes,
313 serverless_cache_envoy: true,
314 ..Default::default()
315 })
316 }
317}
318
319fn actor_key_from_protocol(key: Option<String>) -> ActorKey {
320 key.as_deref()
321 .map(deserialize_actor_key_from_protocol)
322 .unwrap_or_default()
323}
324
325fn deserialize_actor_key_from_protocol(key: &str) -> ActorKey {
326 const EMPTY_KEY: &str = "/";
327 const KEY_SEPARATOR: char = '/';
328
329 if key.is_empty() || key == EMPTY_KEY {
330 return Vec::new();
331 }
332
333 let mut parts = Vec::new();
334 let mut current_part = String::new();
335 let mut escaping = false;
336 let mut empty_string_marker = false;
337
338 for ch in key.chars() {
339 if escaping {
340 if ch == '0' {
341 empty_string_marker = true;
342 } else {
343 current_part.push(ch);
344 }
345 escaping = false;
346 } else if ch == '\\' {
347 escaping = true;
348 } else if ch == KEY_SEPARATOR {
349 if empty_string_marker {
350 parts.push(String::new());
351 empty_string_marker = false;
352 } else {
353 parts.push(std::mem::take(&mut current_part));
354 }
355 } else {
356 current_part.push(ch);
357 }
358 }
359
360 if escaping {
361 current_part.push('\\');
362 parts.push(current_part);
363 } else if empty_string_marker {
364 parts.push(String::new());
365 } else if !current_part.is_empty() || !parts.is_empty() {
366 parts.push(current_part);
367 }
368
369 parts.into_iter().map(ActorKeySegment::String).collect()
370}
371
372#[cfg(test)]
374#[path = "../../tests/envoy_callbacks.rs"]
375mod tests;