From 104fd26b6dd381bcddb9792062696ee60e6a6129 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 19:18:09 +0000 Subject: [PATCH] =?UTF-8?q?Set=20SO=5FREUSEADDR=20on=20the=20web=20admin's?= =?UTF-8?q?=20TCPServer=20=E2=80=94=20the=20actual=20root=20cause?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live-confirmed the retry fix from the previous commit wasn't enough: still "Address already in use" after 20s of retries (10 attempts, 2s backoff), only to succeed on its own sometime after that. That delay pattern is TIME_WAIT, not a process-death race — and this code was never going to avoid it, because socketserver.TCPServer defaults allow_reuse_address to False. (http.server.HTTPServer sets this for you; the plain base class used here does not.) Without SO_REUSEADDR, the kernel can refuse to rebind a port with a lingering TIME_WAIT socket from the previous instance for up to 60s, regardless of whether that old process is even still alive — which is also why the entrypoint.sh fix waiting for the process to exit didn't help either. Set socketserver.TCPServer.allow_reuse_address = True before binding. This is the standard fix for exactly this symptom. Keeping the retry loop from the previous commit too, for the (now much smaller) window where network_mode: host still has no Docker-managed port mapping to instantly free. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi --- vendor/easy-asterisk/easy-asterisk-v0.10.0.sh | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/vendor/easy-asterisk/easy-asterisk-v0.10.0.sh b/vendor/easy-asterisk/easy-asterisk-v0.10.0.sh index d16fa12..a49ba67 100755 --- a/vendor/easy-asterisk/easy-asterisk-v0.10.0.sh +++ b/vendor/easy-asterisk/easy-asterisk-v0.10.0.sh @@ -6135,12 +6135,22 @@ class WebAdminHandler(http.server.BaseHTTPRequestHandler): self.wfile.write(json.dumps(data).encode()) def main(): - # A container recreate under network_mode: host depends on the *previous* - # container's web admin process actually dying before the port is free — - # there's no Docker-managed port mapping to instantly release it like - # there would be in bridge mode. A fast recreate-right-after-recreate can - # briefly race that teardown. Retry instead of crashing on the first - # failure, which otherwise means the web admin silently never comes up. + # socketserver.TCPServer (unlike http.server.HTTPServer, which sets this + # itself) defaults allow_reuse_address to False — without SO_REUSEADDR, + # the kernel can refuse to rebind this port for up to 60s while any + # TIME_WAIT socket from the previous instance lingers, regardless of + # whether that old process is even still alive. Live-confirmed: still + # failing after 20s of retries, only to succeed on its own well after + # that. This was the actual bug — SO_REUSEADDR is the standard fix for + # exactly this "restarted a TCP server, port still busy" symptom. + socketserver.TCPServer.allow_reuse_address = True + + # Still retry on top of that: under network_mode: host there's no + # Docker-managed port mapping to instantly release like there'd be in + # bridge mode, so a fast recreate-right-after-recreate can still + # briefly race the previous process's own shutdown. Retry instead of + # crashing on the first failure, which otherwise means the web admin + # silently never comes up. httpd = None last_err = None for attempt in range(10):