fix(web_ui): cap bind_with_retry at 12 attempts, then return AddrInUse

This commit is contained in:
müde 2026-07-05 20:41:37 +02:00
commit 7c851c5dd3

View file

@ -192,12 +192,16 @@ fn bind_unix(path: &Path) -> Result<tokio::net::UnixListener> {
Ok(listener)
}
/// Bind a TCP listener with `SO_REUSEADDR` set, retrying on
/// `AddrInUse` indefinitely with exponential backoff capped at 2s.
/// First 12 attempts log at WARN; subsequent attempts log at INFO so
/// a long-held stale socket doesn't flood the journal.
/// Maximum bind attempts before `bind_with_retry` gives up on `AddrInUse`.
const MAX_BIND_ATTEMPTS: u32 = 12;
/// Bind a TCP listener with `SO_REUSEADDR` set, retrying on `AddrInUse` with
/// exponential backoff capped at 2s, up to [`MAX_BIND_ATTEMPTS`] attempts. If
/// the port is still held after the final attempt, returns the `AddrInUse`
/// error rather than looping forever (a genuine collision needs the operator,
/// not an unbounded wait).
///
/// Uncapped retry + dashboard-banner-on-real-collision rationale:
/// Retry rationale + dashboard-banner-on-real-collision:
/// see [`docs/web-ui/shape.md::Listener bind`](../../../docs/web-ui/shape.md).
async fn bind_with_retry(addr: SocketAddr, label: &str) -> Result<tokio::net::TcpListener> {
let mut delay_ms = 250u64;
@ -215,17 +219,15 @@ async fn bind_with_retry(addr: SocketAddr, label: &str) -> Result<tokio::net::Tc
}
Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => {
let attempt = attempts + 1;
if attempt <= 12 {
tracing::warn!(
%addr, attempt,
"{label}: AddrInUse, retrying in {delay_ms}ms"
);
} else {
tracing::info!(
%addr, attempt,
"{label}: AddrInUse still holding, retrying in {delay_ms}ms"
);
if attempt >= MAX_BIND_ATTEMPTS {
return Err(e).with_context(|| {
format!("bind {label} on {addr}: still AddrInUse after {attempt} attempts")
});
}
tracing::warn!(
%addr, attempt,
"{label}: AddrInUse, retrying in {delay_ms}ms"
);
tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
attempts += 1;
delay_ms = (delay_ms * 2).min(2000);