Skip to content

fix(gateway): switching the listen address on the same port takes effect live - #188

Merged
fylorn merged 1 commit into
mainfrom
fix/listener-swap
Sep 24, 2026
Merged

fylorn merged 1 commit into
mainfrom
fix/listener-swap

Conversation

@fylorn

@fylorn fylorn commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Changing listen.gateway.bind between all and one interface (or back) failed on Linux: the new listener was bound before the old one released the port, and Linux does not let 0.0.0.0:8788 and 127.0.0.1:8788 listen side by side. The result was gw.listen.port_taken ("already in use by another program"), the old listener kept serving, and only a restart applied the setting. The control plane's pre-save check (tw_gateway::listen::check) gave the same answer, so the app could not save the setting at all. Found while testing the server deployment in #183.

Change

  • Still "bind the new one first" whenever possible.
  • When a new address fails with EADDRINUSE on the port of one of our listeners that is going away anyway, that listener gives up the port first: stop accepting, wait until axum has actually dropped the socket (a small Listener wrapper, Tracked, reports that moment), then bind the new addresses. In-flight requests are unaffected (their connections outlive the listening socket); new connections see a gap of milliseconds.
  • If the new address still cannot be bound (really taken), the old addresses are bound back and the failure goes through the existing ListenChanged { error } status path.
  • check() no longer reports our own listener as another program: an EADDRINUSE on an address overlapping one we listen on is left to the switch, which reports real failures.
  • ConnectInfo still works: the wrapper goes through ListenerExt::tap_io, the one custom-listener path axum gives ConnectInfo<SocketAddr> for.

Tests (tw-gateway/tests/hotreload.rs)

  • all → 127.0.0.1 → all → 127.0.0.1 on one port: each switch takes effect, no error, requests answered.
  • A slow request in flight survives the handover; the new listener accepts before it finishes.
  • New address really taken ([::1]:p held by another socket while switching from 0.0.0.0:p): the old one is taken back and gw.listen.port_taken reported.
  • Pre-save check: our own overlap is fine; a real squatter is still reported.

On Linux (Docker, rust:1-bookworm) three of these fail on main and all pass with the change (ran 3×). macOS allows the overlap, so it never showed there.

The remote control listener in #187 already handles this case (it releases the old listener, awaiting its accept task, and binds back on failure), so it needs no change.

🤖 Generated with Claude Code

…ect live

Changing `bind` between `all` and one interface (or back) failed on
Linux: the new listener was bound before the old one gave up the port,
and Linux does not let 0.0.0.0:8788 and 127.0.0.1:8788 listen side by
side. The bind failed with "already in use by another program", the old
listener stayed, and only a restart applied the setting. The pre-save
check in the control plane said the same, so the app could not even
save it.

When a new address fails with EADDRINUSE on the port of a listener that
is going away anyway, that listener now gives up the port first: axum
stops accepting and drops the socket (a wrapper around the listener
reports that moment), then the new addresses are bound. Requests in
flight are unaffected, since their connections outlive the listening
socket; new connections see a gap of milliseconds. If the new address
still cannot be bound, the old one is bound back and the failure is
reported as before. Every other case keeps "bind the new one first".

The pre-save check no longer reports our own listener as another
program: an EADDRINUSE on a port we listen on, overlapping one of our
addresses, is left to the switch, which reports a real failure.

Tests switch all → 127.0.0.1 → all → 127.0.0.1 on one port, keep a slow
request alive across the handover, take the old address back when the
new one is really taken, and check the pre-save answer. Three of them
fail on Linux without this change (run in Docker); macOS allows the
overlap and never showed the problem.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fylorn
fylorn merged commit 7f54cb3 into main Sep 24, 2026
4 checks passed
@fylorn
fylorn deleted the fix/listener-swap branch September 24, 2026 17:01
@fylorn fylorn mentioned this pull request Sep 24, 2026
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.

1 participant