running_process/broker/brokered_backend.rs
1//! Structurally-enforced fast-bind contract for v2 brokered daemons (#497).
2//!
3//! ## Why
4//!
5//! The v1 launcher (`BackendLauncher::probe_with_service`) requires the
6//! spawned daemon to answer an IPC probe within
7//! `DEFAULT_ENDPOINT_PROBE_TIMEOUT` (~500 ms) after spawn. That budget is
8//! hard-coded in the launcher; nothing in the type system prevents a
9//! daemon implementer from doing 3 s of state loading inside its
10//! bootstrap before the IPC endpoint becomes probe-able. zccache#640
11//! and zccache#784 fix this consumer-side; #497 lifts the invariant
12//! into a broker-owned contract so future brokered services (fbuild
13//! daemon, soldr cache-daemon) cannot silently regress.
14//!
15//! ## Shape (Option A from #497)
16//!
17//! ```text
18//! bind(&endpoint) -> IpcListener // SYNC, microseconds, no state access
19//! |
20//! v
21//! write_lock_file // broker-orchestrated
22//! |
23//! v
24//! serve(listener) -> ! // free to spawn_blocking, take 30s warming
25//! ```
26//!
27//! `bind` has no access to `State` — the daemon physically cannot do
28//! disk I/O before the endpoint is up. The fast-bind property becomes
29//! a compile-time consequence of the trait shape.
30//!
31//! Option B (broker-owned bind via inherited file descriptors / named-
32//! pipe handles) is a strictly stronger refinement deferred to a
33//! follow-up; this slice lands Option A as the minimum viable trait
34//! shape so downstream conformance tests + daemon migrations have a
35//! stable target.
36
37use std::error::Error;
38use std::fmt;
39
40/// Reasons a [`BrokeredBackend::bind`] call can fail.
41///
42/// Deliberately small. The broker treats every variant identically
43/// (declare the spawn dead, surface the error to the operator); the
44/// taxonomy exists so daemons can produce useful logs without inventing
45/// their own error types.
46#[derive(Debug)]
47pub enum BindError {
48 /// The endpoint string was not a valid platform path / pipe name.
49 InvalidEndpoint(String),
50
51 /// Another process already holds the endpoint (`EADDRINUSE`,
52 /// `ERROR_PIPE_BUSY`, etc.).
53 AlreadyBound(String),
54
55 /// Underlying OS error from the bind syscall.
56 Io(std::io::Error),
57
58 /// Catch-all for daemon-specific bind failures (permission
59 /// denied via custom security policy, etc.).
60 Other(String),
61}
62
63impl fmt::Display for BindError {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 match self {
66 Self::InvalidEndpoint(s) => write!(f, "invalid endpoint: {s}"),
67 Self::AlreadyBound(s) => write!(f, "endpoint already bound: {s}"),
68 Self::Io(e) => write!(f, "bind io error: {e}"),
69 Self::Other(s) => write!(f, "bind error: {s}"),
70 }
71 }
72}
73
74impl Error for BindError {
75 fn source(&self) -> Option<&(dyn Error + 'static)> {
76 match self {
77 Self::Io(e) => Some(e),
78 _ => None,
79 }
80 }
81}
82
83impl From<std::io::Error> for BindError {
84 fn from(e: std::io::Error) -> Self {
85 Self::Io(e)
86 }
87}
88
89/// Uninhabited type used as the return type of [`BrokeredBackend::serve`]
90/// and [`bootstrap`] to express "this function never returns" on
91/// stable Rust.
92///
93/// `!` (the bare never type) is nightly-only as a return-position type;
94/// an empty enum has the same uninhabitedness guarantee and compiles
95/// on stable. Implementers cannot construct one — the only way to
96/// satisfy the signature is to diverge (loop, panic, exit).
97#[derive(Debug)]
98pub enum Never {}
99
100/// Opaque listener owned by the selected local-IPC platform facade.
101pub type IpcListener = crate::platform::ipc::Listener;
102
103/// Opaque endpoint identifier the broker hands the daemon's [`bind`]
104/// method. Today a plain string (matches `ServiceDefinition`'s endpoint
105/// field shape); will gain structure as the v2 broker baseline grows.
106///
107/// [`bind`]: BrokeredBackend::bind
108pub type Endpoint = str;
109
110/// The fast-bind contract a v2 brokered daemon implements.
111///
112/// Trait method ordering matches the orchestration the broker runs
113/// inside [`bootstrap`]:
114///
115/// 1. [`bind`](BrokeredBackend::bind) — claim the kernel resource.
116/// Synchronous, expected to complete in microseconds. **Takes only
117/// the endpoint** — no `&mut self`, no associated-`State` parameter
118/// — so it is structurally impossible to perform daemon-state
119/// initialization before this returns.
120/// 2. (broker-orchestrated) write the lockfile + report spawn success
121/// to the operator.
122/// 3. [`serve`](BrokeredBackend::serve) — accept connections forever.
123/// Free to `spawn_blocking` for arbitrarily slow state loads; the
124/// broker does not observe this. Clients connecting during the
125/// warmup window queue in the OS accept backlog or see whatever
126/// cold-path semantics the daemon defines.
127pub trait BrokeredBackend {
128 /// Daemon-specific state that survives between requests.
129 ///
130 /// Allocated and consumed inside `serve` — never visible to
131 /// `bind`. The trait's structural guarantee is precisely that
132 /// state initialization cannot run before the endpoint is bound.
133 type State: Send + 'static;
134
135 /// Bind the IPC listener. **No state access, no disk I/O.**
136 ///
137 /// The broker enforces hang detection by failing the spawn if this
138 /// does not return (or if the resulting listener is not probe-able)
139 /// within `DEFAULT_ENDPOINT_PROBE_TIMEOUT`.
140 fn bind(endpoint: &Endpoint) -> Result<IpcListener, BindError>;
141
142 /// Serve forever on the bound listener.
143 ///
144 /// Free to initialize `State` synchronously, `spawn_blocking` heavy
145 /// loads, or anything else — the broker has already declared the
146 /// daemon "spawned successfully" by this point.
147 ///
148 /// Returns `!` because a brokered daemon's normal control flow is
149 /// to serve until termination signal; clean shutdown is via process
150 /// exit. Implementers that want graceful shutdown plumb it through
151 /// `State` (e.g. an `AtomicBool` checked between accept loops).
152 fn serve(listener: IpcListener) -> Never;
153}
154
155/// Run the broker-side fast-bind orchestration for a `BrokeredBackend`.
156///
157/// 1. Call `B::bind(endpoint)`. Failure → propagate the [`BindError`].
158/// 2. Hand the listener to `B::serve`, which never returns.
159///
160/// The function's signature is `Result<(), BindError>` rather than
161/// `Result<Never, …>` so callers don't have to spell `Never` in their
162/// return type just to call `bootstrap`. The body still proves
163/// divergence: `B::serve(listener)` returns the uninhabited [`Never`],
164/// which coerces to `()` via Rust's never-type coercion — control
165/// flow that reaches the end of the body would require constructing
166/// a `Never` value, which the type system forbids.
167///
168/// In the full v2 baseline, the broker calls this from inside the
169/// spawned daemon's `main()`. Slice 3c–4 of #488 has the v2 broker
170/// scaffold but does not yet exercise this trait; this function is the
171/// integration seam future slices will use.
172// `match B::serve(listener) {}` is the documented pattern for proving
173// divergence via an uninhabited type, but rustc's `unreachable_code`
174// lint still flags the match itself because flow analysis types
175// `B::serve` as `Never`. The lint help text recommends precisely this
176// `#[allow]` for the case.
177#[allow(unreachable_code)]
178pub fn bootstrap<B: BrokeredBackend>(endpoint: &Endpoint) -> Result<(), BindError> {
179 // Broker-owned bind (#500 slice 32): when the broker bound the endpoint
180 // and handed the listener over, adopt it instead of binding a second
181 // one. Binding again would fail with `AlreadyBound` against the broker's
182 // own listener — the two paths cannot both claim the endpoint.
183 //
184 // Today nothing sets that variable, so this is `Ok(None)` and the daemon
185 // binds for itself exactly as before. The launcher half lands separately;
186 // this side has to exist first, or enabling it there would break every
187 // launch at once.
188 let listener = match crate::broker::broker_owned_bind::recover_from_env() {
189 Ok(Some(inherited)) => inherited,
190 Ok(None) => B::bind(endpoint)?,
191 // A descriptor was advertised and could not be adopted. Falling back
192 // to `B::bind` here would leave the broker holding a listener nobody
193 // serves while this process binds a second endpoint — a daemon that
194 // looks healthy and is unreachable. Failing closed is the honest
195 // outcome.
196 Err(error) => return Err(BindError::Io(error)),
197 };
198 // Lockfile write lands in the v2 broker baseline; once it does,
199 // it goes here, between `bind` and `serve`.
200 match B::serve(listener) {}
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 /// Reference implementation used to verify the trait shape compiles.
208 struct StubBackend;
209
210 impl BrokeredBackend for StubBackend {
211 type State = ();
212
213 fn bind(endpoint: &Endpoint) -> Result<IpcListener, BindError> {
214 let endpoint = crate::platform::ipc::Endpoint::test(endpoint)?;
215 crate::platform::ipc::Listener::bind(&endpoint).map_err(Into::into)
216 }
217
218 fn serve(_listener: IpcListener) -> Never {
219 // Reference impl returns by panic. Real implementers run an
220 // accept loop and never return; the `Never` return type
221 // means there is no `return` value they could construct.
222 panic!("StubBackend::serve called");
223 }
224 }
225
226 /// Conformance test #1 (#497 acceptance): the `bind` method has no
227 /// state parameter. Verified by the trait signature itself — if
228 /// this test compiles, the property holds. Equivalent to the
229 /// `trybuild` UI test in #497's acceptance criteria, expressed via
230 /// the type system rather than a separate harness.
231 #[test]
232 fn brokered_backend_bind_returns_listener_from_endpoint_only() {
233 // The line `fn bind(endpoint: &Endpoint) -> Result<...>`
234 // structurally denies a `state` parameter. If a future revision
235 // of the trait added one, this test would fail to compile.
236 fn _shape_check<B: BrokeredBackend>() -> fn(&Endpoint) -> Result<IpcListener, BindError> {
237 B::bind
238 }
239 let _ = _shape_check::<StubBackend>();
240 }
241
242 /// Conformance test #3 (#497 acceptance): an implementation that
243 /// returns an actual listener from `bind` produces a probe-able
244 /// endpoint immediately (no `serve` call required).
245 #[test]
246 fn bind_alone_yields_a_listening_endpoint() {
247 let listener = StubBackend::bind("bind-alone").expect("bind succeeds");
248 // The listener's `accept` is the broker's hang-detection probe
249 // primitive. We don't call `accept` here (would block); we just
250 // verify the listener was constructed by the daemon-side code
251 // path without any state allocation.
252 drop(listener);
253 }
254
255 /// With nothing handed over, `bootstrap` binds for itself — the
256 /// behaviour every daemon has today, asserted so the adoption path
257 /// added for #500 cannot silently change it.
258 ///
259 /// Reads the ambient environment rather than setting it: env-mutating
260 /// tests race under a parallel runner, and this crate has been bitten by
261 /// that. The variable is unset in a normal test process, which is exactly
262 /// the case being asserted.
263 #[test]
264 fn without_a_handover_bootstrap_still_binds_for_itself() {
265 if std::env::var_os(crate::broker::broker_owned_bind::INHERITED_LISTENER_FD_ENV).is_some() {
266 eprintln!("skipping: a listener descriptor is set in this environment");
267 return;
268 }
269 // Reaching `serve` proves a listener was obtained. With no handover
270 // the only way to get one is `B::bind`, and `StubBackend::serve`
271 // panics — so the panic is the evidence.
272 let result = std::panic::catch_unwind(|| bootstrap::<StubBackend>("no-handover"));
273 let payload = result.expect_err("bootstrap should reach the serve panic");
274 let message = payload
275 .downcast_ref::<&str>()
276 .copied()
277 .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
278 .unwrap_or("");
279 assert!(
280 message.contains("StubBackend::serve called"),
281 "expected the self-bind path to reach serve, got: {message:?}"
282 );
283 }
284
285 /// `bootstrap` orchestration calls `bind` first; only if `bind`
286 /// succeeds does it hand off to `serve`. With this stub, `serve`
287 /// panics — so a successful `bind` followed by the panic proves
288 /// the orchestration ordering.
289 #[test]
290 fn bootstrap_calls_bind_then_serve() {
291 let result = std::panic::catch_unwind(|| bootstrap::<StubBackend>("bootstrap-ordering"));
292 let payload = result.expect_err("bootstrap should reach the serve panic");
293 let message = payload
294 .downcast_ref::<&str>()
295 .copied()
296 .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
297 .unwrap_or("");
298 assert!(
299 message.contains("StubBackend::serve called"),
300 "expected to reach serve, got panic payload: {message:?}"
301 );
302 }
303
304 /// A `bind` failure short-circuits before `serve` runs. Uses a
305 /// custom failing backend rather than tweaking the stub so the
306 /// stub's "real bind succeeds" property stays intact for the other
307 /// tests in this module.
308 #[test]
309 fn bootstrap_propagates_bind_failure_without_invoking_serve() {
310 struct FailingBackend;
311 impl BrokeredBackend for FailingBackend {
312 type State = ();
313 fn bind(_endpoint: &Endpoint) -> Result<IpcListener, BindError> {
314 Err(BindError::Other("synthetic failure".into()))
315 }
316 fn serve(_listener: IpcListener) -> Never {
317 panic!("serve must not run when bind fails");
318 }
319 }
320
321 let result = std::panic::catch_unwind(|| bootstrap::<FailingBackend>("bootstrap-failure"));
322 // The orchestrator returns Err — never panics — when bind fails.
323 // catch_unwind preserves that as Ok(Err(...)).
324 let inner = result.expect("bind error should be returned, not a panic");
325 match inner {
326 Err(BindError::Other(msg)) => assert_eq!(msg, "synthetic failure"),
327 other => panic!("expected BindError::Other, got: {other:?}"),
328 }
329 }
330}