Skip to main content

running_process/broker/protocol_v2/
mod.rs

1//! v2 broker protocol module.
2//!
3//! Houses the prost-generated types for the `running_process.broker.v2`
4//! package — currently the `ServiceDefinition` envelope and the
5//! `HttpServerCapability` optional sub-message introduced in #483.
6//!
7//! v2 runs in parallel with v1 (`super::protocol`) through the broker
8//! v2 rollout; v1's types are FROZEN FOREVER (#228) so all new
9//! capability fields land here instead.
10
11#[allow(missing_docs)]
12mod prost_generated {
13    include!(concat!(env!("OUT_DIR"), "/running_process.broker.v2.rs"));
14}
15
16pub use prost_generated::*;
17
18mod io;
19pub use io::{
20    service_definition_dir_v2, service_definition_path_v2, write_service_definition_v2,
21    ServiceDefinitionBuilder, SERVICE_DEF_V2_EXTENSION,
22};
23
24mod manifest_io;
25pub use manifest_io::{
26    central_manifest_path_v2, central_registry_dir_v2, write_to_central_in_dir_v2,
27    write_to_central_v2, write_to_root_v2, CacheManifestBuilder, BROKER_ENVELOPE_VERSION_V2,
28    CENTRAL_MANIFEST_EXTENSION_V2, ROOT_MANIFEST_FILE_V2,
29};
30
31pub mod backend_handle;
32
33pub mod client_compat;
34
35mod loader;
36pub use loader::{ServiceDefinitionLoader, ServiceDefinitionScanEntry};
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41    use prost::Message;
42
43    /// `ServiceDefinition` round-trips with no HTTP capability — the
44    /// optional field is absent on both sides.
45    #[test]
46    fn service_definition_without_http_round_trips() {
47        let original = ServiceDefinition {
48            service_name: "zccache".to_owned(),
49            http_server: None,
50            ..Default::default()
51        };
52
53        let bytes = original.encode_to_vec();
54        let decoded = ServiceDefinition::decode(bytes.as_slice())
55            .expect("encoded ServiceDefinition decodes");
56
57        assert_eq!(decoded.service_name, "zccache");
58        assert!(decoded.http_server.is_none());
59    }
60
61    /// `ServiceDefinition` round-trips with an `HttpServerCapability`
62    /// populated — all three fields survive.
63    #[test]
64    fn service_definition_with_http_round_trips() {
65        let original = ServiceDefinition {
66            service_name: "fbuild".to_owned(),
67            http_server: Some(HttpServerCapability {
68                bind_addr: "127.0.0.1".to_owned(),
69                health_path: "/healthz".to_owned(),
70                display_name: "fbuild status".to_owned(),
71            }),
72            ..Default::default()
73        };
74
75        let bytes = original.encode_to_vec();
76        let decoded = ServiceDefinition::decode(bytes.as_slice())
77            .expect("encoded ServiceDefinition decodes");
78
79        let cap = decoded
80            .http_server
81            .expect("http_server survives round-trip");
82        assert_eq!(decoded.service_name, "fbuild");
83        assert_eq!(cap.bind_addr, "127.0.0.1");
84        assert_eq!(cap.health_path, "/healthz");
85        assert_eq!(cap.display_name, "fbuild status");
86    }
87
88    /// Empty `HttpServerCapability` survives a round-trip — defaults are
89    /// applied by the loader/consumer, not by the proto encoder.
90    #[test]
91    fn http_server_capability_empty_defaults_survive_round_trip() {
92        let original = ServiceDefinition {
93            service_name: "minimal".to_owned(),
94            http_server: Some(HttpServerCapability::default()),
95            ..Default::default()
96        };
97
98        let bytes = original.encode_to_vec();
99        let decoded = ServiceDefinition::decode(bytes.as_slice())
100            .expect("encoded ServiceDefinition decodes");
101
102        let cap = decoded
103            .http_server
104            .expect("http_server survives round-trip");
105        assert!(cap.bind_addr.is_empty());
106        assert!(cap.health_path.is_empty());
107        assert!(cap.display_name.is_empty());
108    }
109
110    /// Slice 22 (zackees/zccache#782): the launcher / isolation fields
111    /// ported from v1 round-trip cleanly. Pins every new field plus the
112    /// `BrokerIsolation` enum mapping so a future proto regression
113    /// surfaces here instead of at the first downstream loader.
114    #[test]
115    fn service_definition_v1_fields_round_trip() {
116        use std::collections::HashMap;
117        let mut labels = HashMap::new();
118        labels.insert("env".to_owned(), "prod".to_owned());
119        labels.insert("deploy".to_owned(), "blue".to_owned());
120
121        let original = ServiceDefinition {
122            service_name: "zccache".to_owned(),
123            binary_path: "/usr/local/bin/zccache-daemon".to_owned(),
124            isolation: BrokerIsolation::SharedBroker as i32,
125            explicit_instance: String::new(),
126            per_version_binary_dir: "/usr/local/bin".to_owned(),
127            min_version: "1.0.0".to_owned(),
128            version_allow_list: vec!["1.12.9".to_owned(), "1.13.0".to_owned()],
129            labels,
130            http_server: None,
131        };
132
133        let bytes = original.encode_to_vec();
134        let decoded = ServiceDefinition::decode(bytes.as_slice())
135            .expect("encoded ServiceDefinition decodes");
136
137        assert_eq!(decoded.service_name, "zccache");
138        assert_eq!(decoded.binary_path, "/usr/local/bin/zccache-daemon");
139        assert_eq!(decoded.isolation, BrokerIsolation::SharedBroker as i32);
140        assert!(decoded.explicit_instance.is_empty());
141        assert_eq!(decoded.per_version_binary_dir, "/usr/local/bin");
142        assert_eq!(decoded.min_version, "1.0.0");
143        assert_eq!(
144            decoded.version_allow_list,
145            vec!["1.12.9".to_owned(), "1.13.0".to_owned()]
146        );
147        assert_eq!(decoded.labels.len(), 2);
148        assert_eq!(decoded.labels.get("env"), Some(&"prod".to_owned()));
149        assert_eq!(decoded.labels.get("deploy"), Some(&"blue".to_owned()));
150        assert!(decoded.http_server.is_none());
151    }
152
153    /// Slice 22 (zackees/zccache#782): every `BrokerIsolation` enum
154    /// variant survives the round-trip. Pins the proto-int mapping so
155    /// future variant additions get an explicit failure here instead
156    /// of misclassifying as `PrivateBroker` (the proto3 zero value).
157    #[test]
158    fn broker_isolation_enum_values_round_trip() {
159        for iso in [
160            BrokerIsolation::PrivateBroker,
161            BrokerIsolation::SharedBroker,
162            BrokerIsolation::ExplicitInstance,
163        ] {
164            let original = ServiceDefinition {
165                service_name: format!("svc-{}", iso as i32),
166                isolation: iso as i32,
167                ..Default::default()
168            };
169            let bytes = original.encode_to_vec();
170            let decoded = ServiceDefinition::decode(bytes.as_slice())
171                .expect("encoded ServiceDefinition decodes");
172            assert_eq!(decoded.isolation, iso as i32, "round-trip of {iso:?}");
173        }
174    }
175
176    /// Slice 22: `explicit_instance` is only meaningful when
177    /// `isolation == ExplicitInstance`, but the proto encoder doesn't
178    /// enforce the gating — the consumer (broker / loader) does. Pin
179    /// that the field round-trips regardless of the isolation value
180    /// so a future broker policy change can rely on the bytes round-tripping
181    /// faithfully even for "invalid" combinations.
182    #[test]
183    fn service_definition_explicit_instance_round_trips_with_any_isolation() {
184        for iso in [
185            BrokerIsolation::PrivateBroker,
186            BrokerIsolation::SharedBroker,
187            BrokerIsolation::ExplicitInstance,
188        ] {
189            let original = ServiceDefinition {
190                service_name: "svc".to_owned(),
191                isolation: iso as i32,
192                explicit_instance: "ci-trusted".to_owned(),
193                ..Default::default()
194            };
195            let bytes = original.encode_to_vec();
196            let decoded = ServiceDefinition::decode(bytes.as_slice())
197                .expect("encoded ServiceDefinition decodes");
198            assert_eq!(
199                decoded.explicit_instance, "ci-trusted",
200                "explicit_instance must survive round-trip even with isolation={iso:?}"
201            );
202        }
203    }
204
205    /// `BackendHttpReady` carries the daemon's OS-allocated port back to
206    /// the broker; encodes/decodes without loss.
207    #[test]
208    fn backend_http_ready_round_trips() {
209        let original = BackendHttpReady { port: 49_152 };
210
211        let bytes = original.encode_to_vec();
212        let decoded =
213            BackendHttpReady::decode(bytes.as_slice()).expect("BackendHttpReady decodes");
214
215        assert_eq!(decoded.port, 49_152);
216    }
217
218    /// `GetBrokerHttpEndpointRequest` is an empty marker; encoding +
219    /// decoding it produces the same default-constructed message.
220    #[test]
221    fn get_broker_http_endpoint_request_round_trips_empty() {
222        let original = GetBrokerHttpEndpointRequest::default();
223
224        let bytes = original.encode_to_vec();
225        let decoded = GetBrokerHttpEndpointRequest::decode(bytes.as_slice())
226            .expect("GetBrokerHttpEndpointRequest decodes");
227
228        assert_eq!(decoded, GetBrokerHttpEndpointRequest::default());
229    }
230
231    /// `GetBrokerHttpEndpointResponse` round-trips both fields (port + pid).
232    #[test]
233    fn get_broker_http_endpoint_response_round_trips() {
234        let original = GetBrokerHttpEndpointResponse {
235            port: 8765,
236            pid: 12_345,
237        };
238
239        let bytes = original.encode_to_vec();
240        let decoded = GetBrokerHttpEndpointResponse::decode(bytes.as_slice())
241            .expect("GetBrokerHttpEndpointResponse decodes");
242
243        assert_eq!(decoded.port, 8765);
244        assert_eq!(decoded.pid, 12_345);
245    }
246}