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