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

ChangeSocketEffect
TLS certificates, names, PROXY protocol, routes, tenantskeptrolling reload only
add a port / address (TCP)bound before the generation is publishednew instances serve it; old ones never did
remove a port / addresskept until the last instance of a generation that uses it has leftthen 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: trueUDP socket bound / closed the same way; the QUIC demux is restarted with the new UDP set (ADR-0039)
IPv6like 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; status shows the error in listeners.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 reload recovers.
  • 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.