roadrunner_listener (roadrunner v0.1.0)

View Source

Listener 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

http2_opts()

-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 default 65535; values above the default emit an early WINDOW_UPDATE(0, peak - 65535) after the server SETTINGS. Worst-case memory is max_clients × peak.
  • stream_window — stream-level receive window peak in bytes (1..2^31-1). Advertised via SETTINGS_INITIAL_WINDOW_SIZE. Default 65535. Setting above conn_window is 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 = fewer WINDOW_UPDATE frames per byte consumed but a smaller live window between refills. Default 32768.

opts()

-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. 0 lets the kernel pick an ephemeral port; query it back with port/1.

Routing (pick one):

  • routes => module() — single-handler dispatch. Every request goes to Module:handle/1 and roadrunner_req:state/1 returns undefined.
  • routes => {module(), term()} — single-handler dispatch with per-handler state. The opaque second element is reachable from the handler via roadrunner_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 return payload_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_bufferingauto (default; framework reads the full body before invoking the handler) or manual (handler calls roadrunner_req:read_body/1,2).
  • slot_reconciliationdisabled (default) or #{interval := Ms} to periodically reap slots orphaned by brutal-kill exits.
  • graceful_drain — opt out of the per-conn pg drain group (true default; false trades 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 of protocol_entry/0. Default [http1]. On TLS this drives alpn_preferred_protocols automatically.
  • 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.

protocol_entry()

-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

drain(Name, Timeout)

-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.

info(Name)

-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.

port(Name)

-spec port(Name :: atom()) -> inet:port_number().

Return the actual TCP port the listener is bound to.

reload_routes(Name, Routes)

-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_link(Name, Opts)

-spec start_link(Name :: atom(), opts()) -> {ok, pid()} | {error, term()}.

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.

status(Name)

-spec status(Name :: atom()) -> accepting | draining.

Return the listener's lifecycle phase:

  • accepting — normal serving; new connections are being accepted.
  • drainingdrain/2 is 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.

stop(Name)

-spec stop(Name :: atom()) -> ok.

Stop a listener and release its port. In-flight conns are not waited on.