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!("cannot specify token both in the RIVET_ENDPOINT URL and as RIVET_TOKEN");
179 }
180
181 Ok(Self {
182 version: env::var("RIVET_ENVOY_VERSION")
183 .ok()
184 .and_then(|value| value.parse().ok())
185 .unwrap_or(1),
186 endpoint: parsed.endpoint,
187 token: Some(
188 parsed
189 .token
190 .or(env_token)
191 .unwrap_or_else(|| "dev".to_owned()),
192 ),
193 namespace: parsed
194 .namespace
195 .or(env_namespace)
196 .unwrap_or_else(|| "default".to_owned()),
197 pool_name: env::var("RIVET_POOL_NAME").unwrap_or_else(|_| "rivetkit-rust".to_owned()),
198 engine_binary_path: env::var_os("RIVET_ENGINE_BINARY_PATH").map(PathBuf::from),
199 start_services: matches!(env::var("RIVET_RUN_SERVICES").as_deref(), Ok("1")),
200 services_binary_path: env::var_os("RIVET_SERVICES_BINARY").map(PathBuf::from),
201 engine_host,
202 engine_port,
203 engine_spawn: super::EngineSpawnMode::from_env(),
204 engine_auto_download: matches!(
205 env::var("RIVETKIT_ENGINE_AUTO_DOWNLOAD").as_deref(),
206 Ok("1") | Ok("true") | Ok("TRUE") | Ok("yes") | Ok("YES")
207 ),
208 handle_inspector_http_in_runtime: false,
209 serverless_base_path: None,
210 serverless_package_version: env!("CARGO_PKG_VERSION").to_owned(),
211 serverless_client_endpoint: None,
212 serverless_client_namespace: None,
213 serverless_client_token: None,
214 serverless_validate_endpoint: true,
215 serverless_max_start_payload_bytes: 1_048_576,
216 })
217 }
218}
219
220#[derive(Debug)]
222struct ParsedEndpoint {
223 endpoint: String,
224 namespace: Option<String>,
225 token: Option<String>,
226}
227
228fn extract_endpoint_auth(endpoint: String) -> anyhow::Result<ParsedEndpoint> {
233 let mut url = url::Url::parse(&endpoint).with_context(|| format!("invalid URL: {endpoint}"))?;
234 if url.query().is_some() {
235 anyhow::bail!("endpoint cannot contain a query string");
236 }
237 if url.fragment().is_some() {
238 anyhow::bail!("endpoint cannot contain a fragment");
239 }
240
241 let namespace = if url.username().is_empty() {
242 None
243 } else {
244 Some(decode_url_auth(url.username())?)
245 };
246 let token = match url.password() {
247 None | Some("") => None,
248 Some(password) => Some(decode_url_auth(password)?),
249 };
250 if token.is_some() && namespace.is_none() {
251 anyhow::bail!("endpoint cannot have a token without a namespace");
252 }
253
254 if namespace.is_some() || token.is_some() {
258 if url.set_username("").is_err() || url.set_password(None).is_err() {
259 anyhow::bail!("endpoint URL does not support credentials");
260 }
261 }
262
263 Ok(ParsedEndpoint {
264 endpoint: url.to_string(),
265 namespace,
266 token,
267 })
268}
269
270fn decode_url_auth(value: &str) -> anyhow::Result<String> {
273 super::http::percent_decode_path_segment(value)
274 .context("invalid percent-encoding in endpoint URL auth")
275}
276
277fn default_engine_endpoint(host: &str, port: u16) -> String {
278 let url_host = if host.contains(':') && !host.starts_with('[') {
279 format!("[{host}]")
280 } else {
281 host.to_owned()
282 };
283 format!("http://{url_host}:{port}")
284}
285
286impl ServeConfig {
287 pub fn from_env() -> anyhow::Result<Self> {
288 let settings = ServeSettings::from_env()?;
289 Ok(Self {
290 version: settings.version,
291 endpoint: settings.endpoint,
292 token: settings.token,
293 namespace: settings.namespace,
294 pool_name: settings.pool_name,
295 engine_binary_path: settings.engine_binary_path,
296 start_services: settings.start_services,
297 services_binary_path: settings.services_binary_path,
298 engine_host: settings.engine_host,
299 engine_port: settings.engine_port,
300 engine_spawn: settings.engine_spawn,
301 engine_auto_download: settings.engine_auto_download,
302 handle_inspector_http_in_runtime: settings.handle_inspector_http_in_runtime,
303 serverless_base_path: settings.serverless_base_path,
304 serverless_package_version: settings.serverless_package_version,
305 serverless_client_endpoint: settings.serverless_client_endpoint,
306 serverless_client_namespace: settings.serverless_client_namespace,
307 serverless_client_token: settings.serverless_client_token,
308 serverless_validate_endpoint: settings.serverless_validate_endpoint,
309 serverless_max_start_payload_bytes: settings.serverless_max_start_payload_bytes,
310 serverless_cache_envoy: true,
311 ..Default::default()
312 })
313 }
314}
315
316fn actor_key_from_protocol(key: Option<String>) -> ActorKey {
317 key.as_deref()
318 .map(deserialize_actor_key_from_protocol)
319 .unwrap_or_default()
320}
321
322fn deserialize_actor_key_from_protocol(key: &str) -> ActorKey {
323 const EMPTY_KEY: &str = "/";
324 const KEY_SEPARATOR: char = '/';
325
326 if key.is_empty() || key == EMPTY_KEY {
327 return Vec::new();
328 }
329
330 let mut parts = Vec::new();
331 let mut current_part = String::new();
332 let mut escaping = false;
333 let mut empty_string_marker = false;
334
335 for ch in key.chars() {
336 if escaping {
337 if ch == '0' {
338 empty_string_marker = true;
339 } else {
340 current_part.push(ch);
341 }
342 escaping = false;
343 } else if ch == '\\' {
344 escaping = true;
345 } else if ch == KEY_SEPARATOR {
346 if empty_string_marker {
347 parts.push(String::new());
348 empty_string_marker = false;
349 } else {
350 parts.push(std::mem::take(&mut current_part));
351 }
352 } else {
353 current_part.push(ch);
354 }
355 }
356
357 if escaping {
358 current_part.push('\\');
359 parts.push(current_part);
360 } else if empty_string_marker {
361 parts.push(String::new());
362 } else if !current_part.is_empty() || !parts.is_empty() {
363 parts.push(current_part);
364 }
365
366 parts.into_iter().map(ActorKeySegment::String).collect()
367}
368
369#[cfg(test)]
371#[path = "../../tests/envoy_callbacks.rs"]
372mod tests;