Skip to main content

running_process/broker/
broker_http_port.rs

1//! v2 broker HTTP port mode resolution (slice 9 of #488).
2//!
3//! Implements the three broker port modes from #483 §3 plus the Docker
4//! env overrides from #483 §3 ("Env override for Docker / sidecar
5//! deployments"). Single resolution point — `BrokerHttpPort::resolve` —
6//! so the env-override surface is visible exactly once in the code path
7//! and the rest of the broker handles only the resolved enum.
8
9use std::env;
10use std::net::{IpAddr, Ipv4Addr};
11
12/// Broker HTTP port mode declared in `BrokerConfig`.
13///
14/// Per #483 §3, the v2 broker's HTTP server picks its port via one of
15/// these strategies. `BrokerHttpPort::resolve` overlays the env vars
16/// from §3's table so container deployments can pin the port from
17/// outside the binary.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum BrokerHttpPort {
20    /// Bind exactly this port; fail if unavailable.
21    Static {
22        /// The port the operator wants.
23        port: u16,
24    },
25    /// Always bind whatever the OS gives us (`bind(0)` semantics).
26    Dynamic,
27    /// Try `preferred`; if EADDRINUSE, fall back to OS-allocated.
28    StaticOrFallback {
29        /// The preferred port. Falls back to OS-allocated when taken.
30        preferred: u16,
31    },
32}
33
34/// Env var that overrides the configured port — when set & parseable,
35/// resolution collapses to [`BrokerHttpPort::Static`] regardless of
36/// the surrounding `BrokerConfig`.
37pub const PORT_OVERRIDE_ENV: &str = "RUNNING_PROCESS_BROKER_HTTP_PORT";
38
39/// Env var that overrides the bound IP. Defaults to `127.0.0.1`.
40pub const BIND_OVERRIDE_ENV: &str = "RUNNING_PROCESS_BROKER_HTTP_BIND";
41
42/// Resolved bind state — single source of truth for the rest of the
43/// broker after [`BrokerHttpPort::resolve`] runs.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct ResolvedHttpBind {
46    /// The port mode after env override.
47    pub port: BrokerHttpPort,
48    /// The IP to bind on (defaults to loopback).
49    pub addr: IpAddr,
50}
51
52impl BrokerHttpPort {
53    /// Resolve config + env into the canonical bind state.
54    ///
55    /// Precedence:
56    /// 1. If `RUNNING_PROCESS_BROKER_HTTP_PORT` is set and parses as a
57    ///    `u16` → return [`BrokerHttpPort::Static`] for the override
58    ///    (no silent fallback — defeating the container port-mapping
59    ///    is the user's whole reason for setting it).
60    /// 2. Otherwise → return `config` unchanged.
61    /// 3. If `RUNNING_PROCESS_BROKER_HTTP_BIND` is set and parses as
62    ///    an `IpAddr` → use that; otherwise default `127.0.0.1`.
63    /// 4. Empty / invalid env values are treated as unset (config wins).
64    pub fn resolve(config: BrokerHttpPort) -> ResolvedHttpBind {
65        let port = match parse_port_env() {
66            Some(p) => BrokerHttpPort::Static { port: p },
67            None => config,
68        };
69        let addr = parse_bind_env().unwrap_or(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
70        ResolvedHttpBind { port, addr }
71    }
72}
73
74fn parse_port_env() -> Option<u16> {
75    let raw = env::var(PORT_OVERRIDE_ENV).ok()?;
76    let trimmed = raw.trim();
77    if trimmed.is_empty() {
78        return None;
79    }
80    trimmed.parse::<u16>().ok()
81}
82
83fn parse_bind_env() -> Option<IpAddr> {
84    let raw = env::var(BIND_OVERRIDE_ENV).ok()?;
85    let trimmed = raw.trim();
86    if trimmed.is_empty() {
87        return None;
88    }
89    trimmed.parse::<IpAddr>().ok()
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use std::sync::Mutex;
96
97    // The env mutation tests share global state (`std::env`). Serialize
98    // them through a mutex so parallel test threads can't trample each
99    // other's env state.
100    static ENV_LOCK: Mutex<()> = Mutex::new(());
101
102    fn with_env<F: FnOnce()>(port: Option<&str>, bind: Option<&str>, f: F) {
103        let _g = ENV_LOCK.lock().expect("env mutex poisoned");
104        // Save + clear.
105        let prev_port = env::var(PORT_OVERRIDE_ENV).ok();
106        let prev_bind = env::var(BIND_OVERRIDE_ENV).ok();
107        match port {
108            Some(p) => env::set_var(PORT_OVERRIDE_ENV, p),
109            None => env::remove_var(PORT_OVERRIDE_ENV),
110        }
111        match bind {
112            Some(b) => env::set_var(BIND_OVERRIDE_ENV, b),
113            None => env::remove_var(BIND_OVERRIDE_ENV),
114        }
115        f();
116        // Restore.
117        match prev_port {
118            Some(p) => env::set_var(PORT_OVERRIDE_ENV, p),
119            None => env::remove_var(PORT_OVERRIDE_ENV),
120        }
121        match prev_bind {
122            Some(b) => env::set_var(BIND_OVERRIDE_ENV, b),
123            None => env::remove_var(BIND_OVERRIDE_ENV),
124        }
125    }
126
127    #[test]
128    fn no_env_returns_config_and_loopback_default() {
129        with_env(None, None, || {
130            let r = BrokerHttpPort::resolve(BrokerHttpPort::Dynamic);
131            assert_eq!(r.port, BrokerHttpPort::Dynamic);
132            assert_eq!(r.addr, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
133        });
134    }
135
136    #[test]
137    fn port_env_set_overrides_to_static() {
138        with_env(Some("8080"), None, || {
139            let r = BrokerHttpPort::resolve(BrokerHttpPort::StaticOrFallback {
140                preferred: 12_345,
141            });
142            assert_eq!(r.port, BrokerHttpPort::Static { port: 8080 });
143        });
144    }
145
146    #[test]
147    fn bind_env_set_overrides_addr() {
148        with_env(None, Some("0.0.0.0"), || {
149            let r = BrokerHttpPort::resolve(BrokerHttpPort::Static { port: 4242 });
150            assert_eq!(r.port, BrokerHttpPort::Static { port: 4242 });
151            assert_eq!(r.addr, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)));
152        });
153    }
154
155    #[test]
156    fn invalid_port_env_falls_back_to_config() {
157        with_env(Some("not-a-port"), None, || {
158            let r = BrokerHttpPort::resolve(BrokerHttpPort::Dynamic);
159            assert_eq!(r.port, BrokerHttpPort::Dynamic);
160        });
161    }
162
163    #[test]
164    fn empty_port_env_falls_back_to_config() {
165        with_env(Some(""), None, || {
166            let r = BrokerHttpPort::resolve(BrokerHttpPort::Dynamic);
167            assert_eq!(r.port, BrokerHttpPort::Dynamic);
168        });
169    }
170
171    #[test]
172    fn invalid_bind_env_falls_back_to_loopback() {
173        with_env(None, Some("not-an-ip"), || {
174            let r = BrokerHttpPort::resolve(BrokerHttpPort::Dynamic);
175            assert_eq!(r.addr, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
176        });
177    }
178
179    #[test]
180    fn both_env_overrides_compose() {
181        with_env(Some("9999"), Some("0.0.0.0"), || {
182            let r = BrokerHttpPort::resolve(BrokerHttpPort::Dynamic);
183            assert_eq!(r.port, BrokerHttpPort::Static { port: 9999 });
184            assert_eq!(r.addr, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)));
185        });
186    }
187}