roadrunner_listener (roadrunner v0.1.0)
View SourceListener gen_server — owns the listening socket and the acceptor pool for one named roadrunner instance.
Plain TCP is backed by gen_tcp with the legacy inet_drv backend.
The OTP-27 {inet_backend, socket} NIF path was tried but adds
significant own-time overhead on short-lived connections via
per-socket-option lookups. TLS is backed by ssl, gated by the
tls opt.
Both paths share the same roadrunner_transport tagged-socket abstraction.
On init/1 the listener opens the listen socket, builds the shared
roadrunner_conn:proto_opts() (dispatch + body limits + timeouts +
max_clients counter), and spawn-links num_acceptors (default 10)
roadrunner_acceptor processes that pull from the same listen socket.
Connection workers are unlinked from the acceptor so a single
connection crash doesn't take the pool down.
All duration and interval values in opts() are in milliseconds —
request_timeout, keep_alive_timeout, rate_check_interval,
hibernate_after, and slot_reconciliation.interval.
Summary
Types
HTTP/2 listener tunables (under {http2, ThisMap} in protocols).
Listener configuration map.
One protocol entry in the listener's protocols list. Either a
bare atom (http1 / http2) for default opts, or a tuple
{Proto, ProtoOpts} carrying protocol-specific tuning. HTTP/1
currently has no tunables (its opts map must be empty); HTTP/2
tunables live under http2_opts/0.
Functions
Graceful shutdown. Closes the listen socket immediately so no new
connections are accepted, broadcasts {roadrunner_drain, Deadline} to
every active conn (so {loop, ...} handlers can opt to honor it),
and then polls the live-connection counter until it hits zero or
Timeout milliseconds elapse. Conns still alive at the deadline are
hard-killed via exit(Pid, shutdown).
Return runtime introspection for a listener
Return the actual TCP port the listener is bound to.
Atomically swap the listener's compiled route table without
restarting it. The new Routes are compiled via
roadrunner_router:compile/2 (with the listener's middlewares
re-baked) and published to persistent_term;
in-flight conns keep using whatever they read at request-resolve
time, but every subsequent dispatch sees the new table.
Start a named listener that binds the given TCP port.
Return the listener's lifecycle phase
Stop a listener and release its port. In-flight conns are not waited on.
Types
-type http2_opts() ::
#{conn_window => 1..2147483647,
stream_window => 1..2147483647,
window_refill_threshold => 1..2147483647}.
HTTP/2 listener tunables (under {http2, ThisMap} in protocols).
conn_window— connection-level receive window peak in bytes (1..2^31-1). RFC 9113 default65535; values above the default emit an earlyWINDOW_UPDATE(0, peak - 65535)after the server SETTINGS. Worst-case memory ismax_clients × peak.stream_window— stream-level receive window peak in bytes (1..2^31-1). Advertised viaSETTINGS_INITIAL_WINDOW_SIZE. Default65535. Setting aboveconn_windowis allowed but not useful — the conn-level peak is the binding constraint.window_refill_threshold— refill trigger in bytes. When the remaining window drops below this, the conn refills back to the peak. Lower threshold = fewerWINDOW_UPDATEframes per byte consumed but a smaller live window between refills. Default32768.
-type opts() :: #{port := inet:port_number(), routes => module() | {module(), term()} | #{handler := module(), state => term(), middlewares => roadrunner_middleware:middleware_list()} | roadrunner_router:routes(), middlewares => roadrunner_middleware:middleware_list(), max_content_length => non_neg_integer(), request_timeout => non_neg_integer(), keep_alive_timeout => non_neg_integer(), num_acceptors => pos_integer(), max_keep_alive_requests => pos_integer(), max_clients => pos_integer(), min_bytes_per_second => non_neg_integer(), rate_check_interval => pos_integer(), body_buffering => auto | manual, slot_reconciliation => disabled | #{interval := pos_integer()}, graceful_drain => boolean(), hibernate_after => pos_integer(), protocols => [protocol_entry(), ...], tls => [ssl:tls_server_option()]}.
Listener configuration map.
Required:
port— TCP port to bind.0lets the kernel pick an ephemeral port; query it back withport/1.
Routing (pick one):
routes => module()— single-handler dispatch. Every request goes toModule:handle/1androadrunner_req:state/1returnsundefined.routes => {module(), term()}— single-handler dispatch with per-handler state. The opaque second element is reachable from the handler viaroadrunner_req:state/1.routes => #{handler := module(), state => term(), middlewares => [...]}— map form for single-handler dispatch; use it to attach per-handler middlewares (or future per-handler framework knobs) alongside the state.routes => roadrunner_router:routes()— list of route entries; each entry is either a{Path, Handler}/{Path, Handler, State}tuple or a#{path := Path, handler := Handler, state => ..., middlewares => [...]}map. First match wins.
Optional middleware and timing knobs (durations in milliseconds):
middlewares— listener-wide pipeline applied to every request.max_content_length— request-body cap; over-cap reads returnpayload_too_large. Default 10 MB.request_timeout— header-read timeout on a fresh conn. Default 30 s.keep_alive_timeout— idle timeout between requests on a keep-alive conn. Default 60 s.num_acceptors— size of the acceptor pool. Default 10.max_keep_alive_requests— requests served per conn before forced close. Default 1000.max_clients— concurrent connection cap. Default 150.min_bytes_per_second— slow-loris guard on the request-read phase (0 disables). Default 100.rate_check_interval— how often the rate guard re-checks (ms). Default 1000.body_buffering—auto(default; framework reads the full body before invoking the handler) ormanual(handler callsroadrunner_req:read_body/1,2).slot_reconciliation—disabled(default) or#{interval := Ms}to periodically reap slots orphaned by brutal-kill exits.graceful_drain— opt out of the per-conn pg drain group (truedefault;falsetrades drain notification for ~10 % lower per-conn overhead on short-lived workloads).hibernate_after— when set, idle conns hibernate after this many milliseconds of main-loop idle time.protocols— list ofprotocol_entry/0. Default[http1]. On TLS this drivesalpn_preferred_protocolsautomatically.tls—[ssl:tls_server_option()]for HTTPS. Empty / absent for plain HTTP.
The inline source comments next to each field carry the deeper ops-tuning rationale.
-type protocol_entry() :: http1 | http2 | {http1, #{}} | {http2, http2_opts()}.
One protocol entry in the listener's protocols list. Either a
bare atom (http1 / http2) for default opts, or a tuple
{Proto, ProtoOpts} carrying protocol-specific tuning. HTTP/1
currently has no tunables (its opts map must be empty); HTTP/2
tunables live under http2_opts/0.
On TLS the list drives alpn_preferred_protocols. On plain TCP,
[http2] means prior-knowledge h2c (client sends the h2 preface
directly); [http1, http2] on plain TCP is rejected at init/1
since there's no Upgrade: h2c implementation.
Functions
-spec drain(Name :: atom(), Timeout :: non_neg_integer()) -> {ok, drained} | {timeout, non_neg_integer()}.
Graceful shutdown. Closes the listen socket immediately so no new
connections are accepted, broadcasts {roadrunner_drain, Deadline} to
every active conn (so {loop, ...} handlers can opt to honor it),
and then polls the live-connection counter until it hits zero or
Timeout milliseconds elapse. Conns still alive at the deadline are
hard-killed via exit(Pid, shutdown).
Returns {ok, drained} when the counter reached zero before the
deadline, or {timeout, Remaining} with the count that was still
alive when the timeout fired (those processes are torn down before
returning).
After drain/2 returns the listener exits — call start_link/2
again to bring it back up.
-spec info(Name :: atom()) -> #{active_clients := non_neg_integer(), max_clients := pos_integer(), requests_served := non_neg_integer()}.
Return runtime introspection for a listener:
active_clients— current number of connections held open.max_clients— the configured cap.requests_served— cumulative count of requests whose headers parsed successfully since the listener started. Includes 4xx responses from the router (404) and the body-size pre-check (413); excludes wire-level parse failures, idle keep-alive timeouts, and silent slow-client closes.
Useful for ops dashboards / health endpoints.
-spec port(Name :: atom()) -> inet:port_number().
Return the actual TCP port the listener is bound to.
-spec reload_routes(Name :: atom(), roadrunner_router:routes()) -> ok | {error, no_routes}.
Atomically swap the listener's compiled route table without
restarting it. The new Routes are compiled via
roadrunner_router:compile/2 (with the listener's middlewares
re-baked) and published to persistent_term;
in-flight conns keep using whatever they read at request-resolve
time, but every subsequent dispatch sees the new table.
Returns ok on success or {error, no_routes} if the listener was
started in single-handler mode (routes => Module or no routes
opt) — there's no router table to reload.
Start a named listener that binds the given TCP port.
port => 0 lets the kernel choose an ephemeral port — query it back
with port/1.
-spec status(Name :: atom()) -> accepting | draining.
Return the listener's lifecycle phase:
accepting— normal serving; new connections are being accepted.draining—drain/2is in progress; the listen socket is closed and active conns are finishing.
After drain/2 (or stop/1) returns the listener has exited and
this call would fail with a noproc.
-spec stop(Name :: atom()) -> ok.
Stop a listener and release its port. In-flight conns are not waited on.