Adaptive Runtime
Listener generations — changing listeners without restarting the controller
Design: ADR-0037. Applies to scalws-controller with listen: inherited (the default).
What a reload can change
| Change | Socket | Effect |
|---|---|---|
| TLS certificates, names, PROXY protocol, routes, tenants | kept | rolling reload only |
| add a port / address (TCP) | bound before the generation is published | new instances serve it; old ones never did |
| remove a port / address | kept until the last instance of a generation that uses it has left | then closed |
change a bind address (127.0.0.1:8081 → 127.0.0.2:8081) | new bound, old closed after the drain | |
add / remove http3: true | UDP socket bound / closed the same way; the QUIC demux is restarted with the new UDP set (ADR-0039) | |
| IPv6 | like IPv4 |
A socket is identified by transport and bound address. The same address in the old and the new generation is the same socket and the same accept queue: no gap, no reset.
Failure handling
- Bind failure (port in use, address not local, permission): the reload is rejected
with the reason (
listener alt (tcp 127.0.0.1:8090): Address in use); the generation number does not change; sockets opened for it are closed again;statusshows the error inlisteners.last_error. - New generation never becomes ready (for example an unreadable certificate): its
instances fail and are retried with backoff; the previous generation keeps serving.
A corrected
reloadrecovers. - Controller restart in the middle of a transition: the new controller takes the
sockets back from the running instances (
<runtime_dir>/<id>/listen-handback.sock, 0600, same user only), so there is still one socket per address; it binds only addresses no instance holds.
Status
scalws-controller status → listeners.sockets[]: socket (tcp 127.0.0.1:8080),
names, state (active = used by the current generation, draining = only by older
ones), generations; listeners.last_error.
Test
scripts/controller-listeners.sh: add a port, add HTTPS, add IPv6, change a bind
address, remove a listener, port in use, invalid certificate and recovery, controller
restart mid-transition — all under continuous keep-alive and new-connection load on the
address that stays.