dragoman 0.5.12

Server for scholarly metadata in commonmeta format stored in a SQLite database.
//! Listening sockets, built in one place so that every listener can share its
//! port with other dragoman processes.
//!
//! With `--reuse-port`, each listener sets `SO_REUSEPORT` before it binds, so
//! several processes — say four, one per `dragoman@N` unit — bind the same
//! port, and the kernel hands each new connection to one of them. Each process
//! does its own TLS; no proxy sits in front.

use std::io;
use std::net::SocketAddr;

use socket2::{Domain, Protocol, Socket, Type};

/// How many connections may wait to be accepted.
const BACKLOG: i32 = 1024;

/// A listening socket on `addr`, shared with other processes when
/// `reuse_port` is set. Non-blocking, as tokio and axum-server expect.
pub fn bind(addr: SocketAddr, reuse_port: bool) -> io::Result<std::net::TcpListener> {
    let socket = Socket::new(Domain::for_address(addr), Type::STREAM, Some(Protocol::TCP))?;
    // As tokio's own bind does: a restart need not wait out TIME_WAIT.
    socket.set_reuse_address(true)?;
    if reuse_port {
        set_reuse_port(&socket)?;
    }
    socket.set_nonblocking(true)?;
    socket.bind(&addr.into())?;
    socket.listen(BACKLOG)?;
    Ok(socket.into())
}

/// [`bind`] as a tokio listener. Must run inside the runtime.
pub fn bind_tokio(addr: SocketAddr, reuse_port: bool) -> io::Result<tokio::net::TcpListener> {
    tokio::net::TcpListener::from_std(bind(addr, reuse_port)?)
}

#[cfg(unix)]
fn set_reuse_port(socket: &Socket) -> io::Result<()> {
    socket.set_reuse_port(true)
}

#[cfg(not(unix))]
fn set_reuse_port(_socket: &Socket) -> io::Result<()> {
    Err(io::Error::new(
        io::ErrorKind::Unsupported,
        "--reuse-port needs SO_REUSEPORT, which this platform lacks",
    ))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn listeners_share_a_port_only_when_both_ask() {
        let first = bind("127.0.0.1:0".parse().unwrap(), true).unwrap();
        let addr = first.local_addr().unwrap();
        // A second process — here a second socket — binds the same port.
        let second = bind(addr, true).unwrap();
        assert_eq!(second.local_addr().unwrap(), addr);
        // One that does not ask is refused, as without the flag.
        assert!(bind(addr, false).is_err());
        drop((first, second));

        let alone = bind("127.0.0.1:0".parse().unwrap(), false).unwrap();
        assert!(bind(alone.local_addr().unwrap(), true).is_err());
    }
}