1use std::collections::HashMap;
10use std::sync::Mutex;
11use std::time::{Duration, Instant};
12
13use crate::broker::protocol::{
14 hello_reply::Result as HelloReplyResult, ErrorCode, Frame, HelloReply, Refused,
15 ServiceDefinition, PROTOCOL_VERSION,
16};
17use crate::broker::server::hello_handler::validate_hello_shape;
18use crate::broker::server::session_token::SessionTokenAuthority;
19use crate::broker::server::{
20 check_version_allowed, BackendKey, BackendLaunchRequest, BackendLauncher, BackendRegistry,
21 BrokerInstanceKey, HelloHandler, HelloHandlerError, HelloRequest, PeerIdentity,
22 RegisteredBackend, ServiceDefinitionError, ServiceDefinitionSource, SpawnBeginError,
23 SpawnCoordinator, SpawnOutcome, TraceContext, VersionPolicyBlock,
24};
25
26#[derive(Clone, Copy)]
33pub struct HelloRouter<'a> {
34 service_definitions: &'a dyn ServiceDefinitionSource,
35 backends: BackendRegistryView<'a>,
36 spawn_coordinator: Option<&'a Mutex<SpawnCoordinator>>,
37 backend_launcher: Option<&'a dyn BackendLauncher>,
38 session_tokens: Option<&'a Mutex<SessionTokenAuthority>>,
39}
40
41#[derive(Clone, Copy)]
42enum BackendRegistryView<'a> {
43 Static(&'a BackendRegistry),
44 Live(&'a Mutex<BackendRegistry>),
45}
46
47impl<'a> HelloRouter<'a> {
48 pub fn new(
50 service_definitions: &'a dyn ServiceDefinitionSource,
51 backends: &'a BackendRegistry,
52 ) -> Self {
53 Self {
54 service_definitions,
55 backends: BackendRegistryView::Static(backends),
56 spawn_coordinator: None,
57 backend_launcher: None,
58 session_tokens: None,
59 }
60 }
61
62 pub fn with_lifecycle_monitor(
65 service_definitions: &'a dyn ServiceDefinitionSource,
66 backends: &'a Mutex<BackendRegistry>,
67 ) -> Self {
68 Self {
69 service_definitions,
70 backends: BackendRegistryView::Live(backends),
71 spawn_coordinator: None,
72 backend_launcher: None,
73 session_tokens: None,
74 }
75 }
76
77 pub fn with_spawn_coordinator(
79 mut self,
80 spawn_coordinator: &'a Mutex<SpawnCoordinator>,
81 ) -> Self {
82 self.spawn_coordinator = Some(spawn_coordinator);
83 self
84 }
85
86 pub fn with_backend_launcher(mut self, backend_launcher: &'a dyn BackendLauncher) -> Self {
88 self.backend_launcher = Some(backend_launcher);
89 self
90 }
91
92 pub fn with_session_token_authority(
99 mut self,
100 session_tokens: &'a Mutex<SessionTokenAuthority>,
101 ) -> Self {
102 self.session_tokens = Some(session_tokens);
103 self
104 }
105
106 pub fn handle_frame(&self, frame: Frame, peer: PeerIdentity) -> HelloReply {
108 match HelloRequest::decode(frame, peer) {
109 Ok(request) => self.handle_request(&request),
110 Err(refused) => refused_reply(refused),
111 }
112 }
113
114 pub fn handle_request(&self, request: &HelloRequest) -> HelloReply {
122 if let Some(refused) = validate_hello_shape(&request.hello, &request.peer) {
123 return refused_reply(refused);
124 }
125 match self.route_request(request) {
126 Ok(registered) => match HelloHandler::new().with_backend(registered) {
127 Ok(handler) => handler.handle_request(request),
128 Err(err) => refused_reply(refused_from_handler_error(err)),
129 },
130 Err(refused) => refused_reply(refused),
131 }
132 }
133
134 fn route_request(&self, request: &HelloRequest) -> Result<RegisteredBackend, Refused> {
135 let service_definition = self
136 .service_definitions
137 .lookup_or_reload(&request.hello.service_name)
138 .map_err(refused_from_service_definition_error)?;
139
140 if let Err(block) =
141 check_version_allowed(&request.hello.wanted_version, &service_definition)
142 {
143 return Err(refused_from_version_policy(block));
144 }
145
146 let instance =
147 BrokerInstanceKey::from_service_definition(&service_definition).map_err(|err| {
148 refused(
149 ErrorCode::ErrorInternal,
150 format!("service isolation could not be resolved: {err}"),
151 0,
152 )
153 })?;
154
155 let expected_exe = on_disk_exe_hash_hex(&service_definition.binary_path);
160
161 if let Some(registered) = self.registered_backend_for(
162 &instance,
163 &service_definition,
164 &request.hello.wanted_version,
165 &expected_exe,
166 ) {
167 return Ok(registered);
168 }
169
170 let key = BackendKey::new(
171 instance,
172 request.hello.service_name.clone(),
173 request.hello.wanted_version.clone(),
174 expected_exe,
175 );
176 let trace_context = request.trace_context();
177 self.launch_backend(&key, &service_definition, &trace_context)
178 }
179
180 fn registered_backend_for(
181 &self,
182 instance: &BrokerInstanceKey,
183 service_definition: &ServiceDefinition,
184 service_version: &str,
185 expected_exe_hash: &str,
186 ) -> Option<RegisteredBackend> {
187 match self.backends {
188 BackendRegistryView::Static(registry) => registry.registered_backend_for(
189 instance,
190 service_definition,
191 service_version,
192 expected_exe_hash,
193 ),
194 BackendRegistryView::Live(registry) => {
195 let mut registry = registry
196 .lock()
197 .unwrap_or_else(|poisoned| poisoned.into_inner());
198 let _removed = registry.prune_stale();
199 registry.registered_backend_for(
200 instance,
201 service_definition,
202 service_version,
203 expected_exe_hash,
204 )
205 }
206 }
207 }
208
209 fn launch_backend(
210 &self,
211 key: &BackendKey,
212 service_definition: &ServiceDefinition,
213 trace_context: &TraceContext,
214 ) -> Result<RegisteredBackend, Refused> {
215 self.begin_spawn(key.clone())?;
216
217 let Some(backend_launcher) = self.backend_launcher else {
218 self.finish_spawn(key, SpawnOutcome::Failed);
219 return Err(refused(
220 ErrorCode::ErrorBackendSpawnFailed,
221 "backend is not registered",
222 1_000,
223 ));
224 };
225
226 let minted_session_token = self.session_tokens.and_then(|authority| {
237 let mut authority = authority
238 .lock()
239 .unwrap_or_else(|poisoned| poisoned.into_inner());
240 authority
241 .register_daemon(key.service_name.clone())
242 .ok()
243 .map(|daemon_half| {
244 crate::broker::server::session_token::compose_presented_token(
245 authority.broker_token(),
246 &daemon_half,
247 )
248 })
249 });
250
251 let request = BackendLaunchRequest {
252 key,
253 service_definition,
254 trace_context,
255 session_token: minted_session_token.as_deref(),
256 };
257 match backend_launcher.launch(&request) {
258 Ok(handle) => match self.register_launched_backend(key, service_definition, handle) {
259 Ok(registered) => {
260 self.finish_spawn(key, SpawnOutcome::Success);
261 Ok(registered)
262 }
263 Err(refused) => {
264 self.finish_spawn(key, SpawnOutcome::Failed);
265 Err(refused)
266 }
267 },
268 Err(err) => {
269 self.finish_spawn(key, SpawnOutcome::Failed);
270 Err(refused(
271 ErrorCode::ErrorBackendSpawnFailed,
272 format!("backend spawn failed: {err}"),
273 1_000,
274 ))
275 }
276 }
277 }
278
279 fn begin_spawn(&self, key: BackendKey) -> Result<(), Refused> {
280 let Some(spawn_coordinator) = self.spawn_coordinator else {
281 return Ok(());
282 };
283
284 let now = Instant::now();
285 let mut coordinator = spawn_coordinator
286 .lock()
287 .unwrap_or_else(|poisoned| poisoned.into_inner());
288 match coordinator.try_begin(key.clone(), now) {
289 Ok(_) => Ok(()),
290 Err(SpawnBeginError::AlreadyInProgress) => Err(refused(
291 ErrorCode::ErrorRateLimited,
292 "backend spawn already in progress",
293 1_000,
294 )),
295 Err(SpawnBeginError::BudgetExhausted {
296 retry_after,
297 is_storm_trip,
298 ..
299 }) => {
300 if is_storm_trip {
309 if let Some(session_tokens) = self.session_tokens {
310 let mut authority = session_tokens
311 .lock()
312 .unwrap_or_else(|poisoned| poisoned.into_inner());
313 let _ = authority.rotate_broker_token();
314 }
315 }
316 Err(refused(
317 ErrorCode::ErrorRateLimited,
318 "backend spawn budget exhausted",
319 duration_to_retry_ms(retry_after),
320 ))
321 }
322 }
323 }
324
325 fn finish_spawn(&self, key: &BackendKey, outcome: SpawnOutcome) {
326 let Some(spawn_coordinator) = self.spawn_coordinator else {
327 return;
328 };
329
330 let mut coordinator = spawn_coordinator
331 .lock()
332 .unwrap_or_else(|poisoned| poisoned.into_inner());
333 coordinator.finish(key, outcome, Instant::now());
334 }
335
336 fn register_launched_backend(
337 &self,
338 key: &BackendKey,
339 service_definition: &ServiceDefinition,
340 handle: crate::broker::backend_handle::BackendHandle,
341 ) -> Result<RegisteredBackend, Refused> {
342 if handle.service_name != key.service_name || handle.service_version != key.service_version
343 {
344 return Err(refused(
345 ErrorCode::ErrorInternal,
346 "launched backend identity did not match request",
347 0,
348 ));
349 }
350
351 let registered = RegisteredBackend {
352 service_definition: service_definition.clone(),
353 daemon_version: handle.service_version.clone(),
354 backend_pipe: handle.daemon_process.ipc_endpoint.path.clone(),
355 server_capabilities: 0,
356 };
357
358 if let BackendRegistryView::Live(registry) = self.backends {
359 let mut registry = registry
360 .lock()
361 .unwrap_or_else(|poisoned| poisoned.into_inner());
362 registry.insert(key.instance.clone(), handle);
363 }
364
365 Ok(registered)
366 }
367}
368
369fn refused_from_service_definition_error(error: ServiceDefinitionError) -> Refused {
370 match error {
371 ServiceDefinitionError::InvalidName(_) => {
372 refused(ErrorCode::ErrorPeerRejected, "invalid service_name", 0)
373 }
374 ServiceDefinitionError::Io(err) if err.kind() == std::io::ErrorKind::NotFound => refused(
375 ErrorCode::ErrorServiceUnknown,
376 "service definition was not found",
377 0,
378 ),
379 other => refused(
380 ErrorCode::ErrorServiceUnknown,
381 format!("service definition could not be loaded: {other}"),
382 0,
383 ),
384 }
385}
386
387fn refused_from_version_policy(block: VersionPolicyBlock) -> Refused {
388 match block {
389 VersionPolicyBlock::BelowMinVersion => refused(
390 ErrorCode::ErrorVersionBlocked,
391 "wanted_version is below min_version",
392 30_000,
393 ),
394 VersionPolicyBlock::OutsideAllowList => refused(
395 ErrorCode::ErrorVersionBlocked,
396 "wanted_version is not in version_allow_list",
397 30_000,
398 ),
399 }
400}
401
402fn refused_from_handler_error(error: HelloHandlerError) -> Refused {
403 refused(
404 ErrorCode::ErrorInternal,
405 format!("registered backend could not be installed: {error}"),
406 0,
407 )
408}
409
410fn refused(code: ErrorCode, reason: impl Into<String>, retry_after_ms: u64) -> Refused {
411 Refused {
412 reason: reason.into(),
413 daemon_min_protocol: PROTOCOL_VERSION,
414 daemon_max_protocol: PROTOCOL_VERSION,
415 code: code as i32,
416 details: HashMap::new(),
417 retry_after_ms,
418 }
419}
420
421fn duration_to_retry_ms(duration: Duration) -> u64 {
422 let millis = duration.as_millis().max(1);
423 u64::try_from(millis).unwrap_or(u64::MAX)
424}
425
426fn refused_reply(refused: Refused) -> HelloReply {
427 HelloReply {
428 result: Some(HelloReplyResult::Refused(refused)),
429 }
430}
431
432fn on_disk_exe_hash_hex(binary_path: &str) -> String {
444 use crate::broker::backend_lifecycle::identity::executable_hash_file;
445 use crate::broker::server::backend_registry::hex_lower;
446 use std::path::PathBuf;
447 use std::sync::OnceLock;
448 use std::time::UNIX_EPOCH;
449
450 type ExeHashCache = Mutex<HashMap<PathBuf, (u128, u64, String)>>;
452 static CACHE: OnceLock<ExeHashCache> = OnceLock::new();
453
454 let path = PathBuf::from(binary_path);
455 let Ok(meta) = std::fs::metadata(&path) else {
456 return String::new();
457 };
458 let size = meta.len();
459 let mtime = meta
460 .modified()
461 .ok()
462 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
463 .map(|d| d.as_nanos())
464 .unwrap_or(0);
465
466 let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
467 {
468 let map = cache
469 .lock()
470 .unwrap_or_else(|poisoned| poisoned.into_inner());
471 if let Some((c_mtime, c_size, hash)) = map.get(&path) {
472 if *c_mtime == mtime && *c_size == size {
473 return hash.clone();
474 }
475 }
476 }
477
478 let hash = match executable_hash_file(&path) {
479 Ok(bytes) => hex_lower(&bytes),
480 Err(_) => return String::new(),
481 };
482 cache
483 .lock()
484 .unwrap_or_else(|poisoned| poisoned.into_inner())
485 .insert(path, (mtime, size, hash.clone()));
486 hash
487}
488
489#[cfg(test)]
490mod tests {
491 use std::fs;
492
493 use prost::Message;
494
495 use crate::broker::backend_handle::{BackendHandle, DaemonProcess};
496 use crate::broker::protocol::{
497 BrokerIsolation, Endpoint, FrameKind, Hello, PayloadEncoding, ServiceDefinition,
498 };
499 use crate::broker::server::{
500 ensure_service_definition_dir, service_definition_path, PeerIdentity,
501 ServiceDefinitionLoader,
502 };
503
504 use super::*;
505
506 fn service_definition() -> ServiceDefinition {
507 let exe = std::env::current_exe().unwrap();
508 let dir = exe.parent().unwrap().to_path_buf();
509 ServiceDefinition {
510 service_name: "zccache".into(),
511 binary_path: exe.to_string_lossy().into_owned(),
512 isolation: BrokerIsolation::SharedBroker as i32,
513 explicit_instance: String::new(),
514 per_version_binary_dir: dir.to_string_lossy().into_owned(),
515 min_version: "1.10.0".into(),
516 version_allow_list: vec!["1.11.20".into()],
517 labels: Default::default(),
518 }
519 }
520
521 fn service_dir_with_definition(definition: &ServiceDefinition) -> tempfile::TempDir {
522 let tmp = tempfile::tempdir().unwrap();
523 let root = tmp.path().join("services");
524 ensure_service_definition_dir(&root).unwrap();
525 fs::write(
526 service_definition_path(&root, "zccache").unwrap(),
527 definition.encode_to_vec(),
528 )
529 .unwrap();
530 tmp
531 }
532
533 fn request() -> HelloRequest {
534 let hello = Hello {
535 client_min_protocol: 1,
536 client_max_protocol: 1,
537 service_name: "zccache".into(),
538 wanted_version: "1.11.20".into(),
539 client_version: "zccache-cli/1.11.20".into(),
540 client_capabilities: 0,
541 auth_token: Vec::new(),
542 request_id: "req-live-prune".into(),
543 connection_id: 0,
544 peer_pid: 0,
545 client_lib_name: "running-process".into(),
546 client_lib_version: env!("CARGO_PKG_VERSION").into(),
547 peer_attestation_nonce: Vec::new(),
548 capability_token: Vec::new(),
549 client_keepalive_secs: 60,
550 };
551 HelloRequest {
552 frame: Frame {
553 envelope_version: 1,
554 kind: FrameKind::Request as i32,
555 payload_protocol: 0,
556 payload: hello.encode_to_vec(),
557 request_id: 1,
558 payload_encoding: PayloadEncoding::None as i32,
559 deadline_unix_ms: 0,
560 traceparent: String::new(),
561 tracestate: String::new(),
562 },
563 hello,
564 peer: PeerIdentity {
565 pid: 0,
566 uid_or_sid: "test-peer".into(),
567 },
568 }
569 }
570
571 fn stale_backend_handle() -> BackendHandle {
572 let endpoint = Endpoint {
573 namespace_id: "shared".into(),
574 path: "rpb-v1-test-stale-backend".into(),
575 };
576 let mut daemon = DaemonProcess::current_process(endpoint, Some(30)).unwrap();
577 daemon.pid = u32::MAX;
578 BackendHandle {
579 service_name: "zccache".into(),
580 service_version: "1.11.20".into(),
581 daemon_process: daemon,
582 process_handle: None,
583 }
584 }
585
586 #[test]
587 fn live_registry_prunes_stale_backend_before_routing() {
588 let definition = service_definition();
589 let tmp = service_dir_with_definition(&definition);
590 let loader = ServiceDefinitionLoader::new(tmp.path().join("services"));
591 let mut registry = BackendRegistry::new();
592 registry.insert(BrokerInstanceKey::Shared, stale_backend_handle());
593 let registry = Mutex::new(registry);
594 let router = HelloRouter::with_lifecycle_monitor(&loader, ®istry);
595
596 let reply = router.handle_request(&request());
597
598 assert!(registry.lock().unwrap().is_empty());
599 match reply.result.unwrap() {
600 HelloReplyResult::Refused(refused) => {
601 assert_eq!(
602 ErrorCode::try_from(refused.code).unwrap(),
603 ErrorCode::ErrorBackendSpawnFailed
604 );
605 }
606 HelloReplyResult::Negotiated(negotiated) => {
607 panic!("stale backend must not negotiate: {negotiated:?}")
608 }
609 }
610 }
611}