Skip to main content

running_process/broker/server/
combined_service_def_loader.rs

1//! v2-first service-definition lookup for the shared (v2) broker serve path.
2//!
3//! soldr#2364. `serve_launching_backends` / [`super::HelloRouter`] historically
4//! read only v1 `.servicedef` files, but soldr — and every consumer that
5//! installs via [`crate::broker::protocol_v2::write_service_definition_v2`] —
6//! writes `.servicedef.v2` files. So a real Hello against the shared broker was
7//! `Refused { "service definition was not found" }`: the file was on disk under
8//! the v2 extension the v1 loader never looks for.
9//!
10//! This module closes that gap without disturbing the downstream routing chain.
11//! [`CombinedServiceDefinitionLoader`] tries the v2 file first, falls back to
12//! the v1 file, and returns the **v1** [`ServiceDefinition`] type that
13//! `check_version_allowed`, [`super::BrokerInstanceKey::from_service_definition`],
14//! `BackendLaunchRequest`, and the rest of the launch path already consume. The
15//! v2 schema is a strict superset of v1 (identical field numbers 1–8, plus the
16//! v2-only optional `http_server` capability at field 10), so the
17//! down-conversion is lossless for everything the broker launch path reads.
18//!
19//! [`super::HelloRouter`] takes its loader as a `&dyn `[`ServiceDefinitionSource`] so
20//! both the bare v1 [`ServiceDefinitionLoader`] (used by the crate's existing
21//! unit/integration tests, which install v1 files) and this combined loader
22//! satisfy it: a `&ServiceDefinitionLoader` unsize-coerces to the trait object
23//! at the call site, so those call sites need no change.
24
25use crate::broker::protocol::ServiceDefinition;
26use crate::broker::protocol_v2;
27use crate::broker::server::service_def_loader::{ServiceDefinitionError, ServiceDefinitionLoader};
28use std::path::{Path, PathBuf};
29
30/// A source of v1-typed service definitions for [`super::HelloRouter`].
31///
32/// Implemented by the bare v1 [`ServiceDefinitionLoader`] and by
33/// [`CombinedServiceDefinitionLoader`]. The router only ever needs to look a
34/// service up by name, so this is the whole surface.
35pub trait ServiceDefinitionSource: Sync {
36    /// Look up (always re-reading from disk) the service definition for
37    /// `service_name`, returning the v1 [`ServiceDefinition`] type.
38    fn lookup_or_reload(
39        &self,
40        service_name: &str,
41    ) -> Result<ServiceDefinition, ServiceDefinitionError>;
42}
43
44impl ServiceDefinitionSource for ServiceDefinitionLoader {
45    fn lookup_or_reload(
46        &self,
47        service_name: &str,
48    ) -> Result<ServiceDefinition, ServiceDefinitionError> {
49        ServiceDefinitionLoader::lookup_or_reload(self, service_name)
50    }
51}
52
53/// Reads `.servicedef.v2` first, then `.servicedef`, returning the v1
54/// [`ServiceDefinition`] the broker routing chain consumes.
55#[derive(Clone)]
56pub struct CombinedServiceDefinitionLoader {
57    root: PathBuf,
58}
59
60impl CombinedServiceDefinitionLoader {
61    /// Build a loader rooted at `root` (the service-definition directory).
62    pub fn new(root: impl Into<PathBuf>) -> Self {
63        Self { root: root.into() }
64    }
65
66    /// The service-definition directory this loader reads from.
67    pub fn root(&self) -> &Path {
68        &self.root
69    }
70
71    /// Load and validate one service definition, preferring the v2 file.
72    ///
73    /// A missing v2 file (only) falls back to the v1 file; any other v2 error
74    /// (insecure directory, decode failure, name mismatch) is returned as-is so
75    /// a genuinely broken v2 file is never silently masked by a v1 fallback.
76    pub fn load(&self, service_name: &str) -> Result<ServiceDefinition, ServiceDefinitionError> {
77        match protocol_v2::ServiceDefinitionLoader::new(&self.root).load(service_name) {
78            Ok(v2) => Ok(service_definition_v2_to_v1(v2)),
79            Err(err) if is_missing_file(&err) => {
80                ServiceDefinitionLoader::new(&self.root).load(service_name)
81            }
82            Err(err) => Err(err),
83        }
84    }
85
86    /// Re-read one service definition from disk (alias for [`Self::load`]).
87    pub fn reload(&self, service_name: &str) -> Result<ServiceDefinition, ServiceDefinitionError> {
88        self.load(service_name)
89    }
90
91    /// Lookup that always re-reads — mirrors the v1 loader's contract.
92    pub fn lookup_or_reload(
93        &self,
94        service_name: &str,
95    ) -> Result<ServiceDefinition, ServiceDefinitionError> {
96        self.load(service_name)
97    }
98}
99
100impl ServiceDefinitionSource for CombinedServiceDefinitionLoader {
101    fn lookup_or_reload(
102        &self,
103        service_name: &str,
104    ) -> Result<ServiceDefinition, ServiceDefinitionError> {
105        CombinedServiceDefinitionLoader::lookup_or_reload(self, service_name)
106    }
107}
108
109/// True when `err` is a plain "file not found" — the only v2 error that should
110/// trigger a v1 fallback rather than surface.
111fn is_missing_file(err: &ServiceDefinitionError) -> bool {
112    matches!(err, ServiceDefinitionError::Io(io) if io.kind() == std::io::ErrorKind::NotFound)
113}
114
115/// Down-convert a v2 [`protocol_v2::ServiceDefinition`] to the v1
116/// [`ServiceDefinition`] the broker launch chain consumes.
117///
118/// Fields 1–8 are identical across the two schemas (same field numbers, same
119/// wire types; `isolation` is a plain `i32` in both, with matching
120/// `BrokerIsolation` discriminants). The v2-only `http_server` capability
121/// (field 10) has no v1 equivalent and is dropped — the broker launch path
122/// never reads it (it is an aggregator-facing HTTP hint, not launch policy).
123pub fn service_definition_v2_to_v1(v2: protocol_v2::ServiceDefinition) -> ServiceDefinition {
124    ServiceDefinition {
125        service_name: v2.service_name,
126        binary_path: v2.binary_path,
127        isolation: v2.isolation,
128        explicit_instance: v2.explicit_instance,
129        per_version_binary_dir: v2.per_version_binary_dir,
130        min_version: v2.min_version,
131        version_allow_list: v2.version_allow_list,
132        labels: v2.labels,
133    }
134}
135
136#[cfg(test)]
137mod tests;