Skip to content

Add address-agnostic Control for multiple listeners - #203

Merged
abs0luty merged 1 commit into
masterfrom
feature/multi-listener-control
Nov 10, 2026
Merged

abs0luty merged 1 commit into
masterfrom
feature/multi-listener-control

Conversation

@abs0luty

@abs0luty abs0luty commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Adds Control, an address-agnostic handle that shares shutdown and connection counting across servers listening on different address types, and a multiple_listeners example. This closes #66.

Why this closes the issue

The issue asks for one app on an arbitrary list of TCP and Unix sockets, with TLS on TCP and listeners inherited from systemd socket activation. Every part of that is now supported:

  • Inherited listeners. from_tcp and from_unix (plus from_tcp_rustls and from_unix_rustls) accept std listeners, so listenfd::ListenFd output can be passed straight in.
  • Same app on every socket. The router's make service is Clone; clone it per server.
  • Mixed types and TLS per listener. One Server per listener, each with its own acceptor, so TCP and Unix, TLS or plain, live in one process.
  • One lifecycle for the set. Control is new here: Handle::with_control attaches each server to it, and one graceful_shutdown or shutdown drains all of them while connection_count sums their connections.
  • Per-listener addresses. Each server gets its own Handle<A>, so listening() still reports that server's address instead of racing with other listeners.

examples/multiple_listeners.rs is the full program: one router over TCP and a Unix socket, drained together on Ctrl-C, with a failing listener shutting the rest down.

let control = Control::new();

let tcp_handle = Handle::<SocketAddr>::with_control(control.clone());
let tcp = axum_server::bind(tcp_addr).handle(tcp_handle.clone());

let unix_handle = Handle::<UnixSocketAddr>::with_control(control.clone());
let unix = axum_server::bind(unix_addr).handle(unix_handle.clone());

// Serve both, then:
control.graceful_shutdown(Some(Duration::from_secs(30)));
control.connection_count();

How the earlier changes got here

  • Make Server generic over streams and support Unix sockets #167 (0.8.0) made Server generic over its connections and added Unix sockets, from_unix, from_unix_rustls, and the Address/AddrListener traits. Before it the crate could only listen on TCP, so mixed listeners were not even representable.
  • Expose TLS connection info and local address to handlers #196 (0.9.0) added TlsInfo and LocalAddrAcceptor. The 2023 discussion on this issue objected that axum's ConnectInfo forced hyper's AddrStream type; per-connection TLS details, client certificates and the local address are now available to handlers through extensions, and ConnectInfo<SocketAddr> still works per TCP server because every server passes its own address as the make-service target.
  • Add first-request timeout and accept error handler #195 and Add TCP socket option acceptor #198 added per-server settings (first_request_timeout, accept_error_handler, TCP socket options) that fit the composition model without changes.
  • What was still missing was a handle that is not generic over the listener's address type, so mixed listeners could share one shutdown and one count. That is the only thing this PR adds; Handle<A>, Server::handle and listening() are unchanged.

Why not a single Server with many listeners

A Server is generic over one Address, that address's stream, its acceptor, and the service the acceptor produces (TLS wraps the service in TlsService). Putting TCP and Unix in one server would require boxed streams, boxed services and boxed accept futures, weakening the typed Accept trait and the per-listener TLS configuration. One server per listener keeps typed acceptors, per-listener TLS and socket options, timeouts, and ConnectInfo, and composes with tokio.

ConnectInfo remains TCP-only, because axum has no Connected implementation for Unix addresses. Per-server settings such as first_request_timeout and accept_error_handler are configured on each server.

Split the handle's shared state from its address latch. `Control` is not
tied to an address type, so one TCP and one Unix server can share shutdown
and connection counting while each `Handle` keeps its own `listening()`.

`Handle::with_control` builds a per-listener handle on top of a `Control`.
Also adds a multiple_listeners example that serves one app over TCP and a
Unix socket and drains both on shutdown.
@abs0luty
abs0luty merged commit c2724fe into master Nov 10, 2026
29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Listen on multiple sockets

1 participant