Skip to content

fix(server): Windows 上改用独占绑定,堵住升级后的静默双开 - #155

Merged
shuishuipingan merged 2 commits into
shuishuipingan:mainfrom
StephenBoyMi:fix/windows-exclusive-bind
Nov 12, 2026
Merged

shuishuipingan merged 2 commits into
shuishuipingan:mainfrom
StephenBoyMi:fix/windows-exclusive-bind

Conversation

@StephenBoyMi

Copy link
Copy Markdown

问题

Windows 上「一键更新」后文件已经换成新版本,看板却一直提示「有新版本」,/health 回的 version 也一直是旧的。

实测现场:同一台机器上两个 qoder_proxy.py 进程,命令行完全相同;hub.pid 指向新进程,但 18096 上的连接全部落在旧进程。新进程日志里已经打印了 listening: http://127.0.0.1:18096/v1,实际一个请求都收不到。

根因

http.server.HTTPServer.allow_reuse_address = 1 → Windows 上 SO_REUSEADDR 允许第二个 socket 绑一个正在被监听的端口。

升级走 schedule_restart():起新进程后旧进程才退出。新进程的启动守卫探测 /health,把仍在应答的旧实例判成 stale(_pid_alive 为假),重试 3 次后继续 bind——而 Windows 上这个 bind 不会失败。结果两个实例同时 listen,连接静默分流到旧实例,新版本永远不生效。

代码里「bind 失败会保护我」的假设只在 POSIX 成立:那里第二个 bind 会直接 EADDRINUSE,进程退出。

修法

  1. Windows 用独占绑定:allow_reuse_address = False + SO_EXCLUSIVEADDRUSE,抢绑直接 WSAEADDRINUSE;
  2. 只有「独占绑定失败 且 端口上没人 accept」时才降级到兼容 socket —— 那说明只剩 TIME_WAIT 残留(Windows 关闭监听后 2 分钟内常见),否则重启会长时间绑不上;
  3. 只要有人在 accept 就绝不降级,不然等于把双开的大门重新打开。

测试

tests/_test_windows_bind_guard.py:7 项。windows-latest 上真实复现「端口被占 → 第二个实例 bind 失败」;POSIX 上断言仍保留 SO_REUSEADDR(复用 TIME_WAIT 不受影响)。

复现

Windows,两个实例交替存活时点「一键更新」:

netstat -ano | findstr :18096     # 两条 LISTENING,PID 不同
curl http://127.0.0.1:18096/health  # pid 与 hub.pid 不一致,version 是旧的

related: #89

Windows 的 SO_REUSEADDR 允许第二个 socket 绑一个正在被监听的端口,而
HTTPServer 默认 allow_reuse_address = 1。升级时 schedule_restart() 先起新进程、
旧进程随后才退出,新进程的启动守卫把应答者判成 stale 后继续 bind,在 Windows 上
这个 bind 不会失败:两个实例同时 listen,连接静默分流到旧实例,/health 一直回
旧版本,看板永远提示「有新版本」。

Windows 改用 SO_EXCLUSIVEADDRUSE 并关掉 allow_reuse_address,抢绑直接
WSAEADDRINUSE。若独占绑定失败而端口上没人 accept(只剩 TIME_WAIT 残留,
Windows 关监听后两分钟内常见),才降级到兼容 socket——只要有人 accept 就绝
不降级,否则等于把双开的大门重新打开。

测试:tests/_test_windows_bind_guard.py(7 项)。
@StephenBoyMi
StephenBoyMi force-pushed the fix/windows-exclusive-bind branch from 8ab43ac to e7f0f66 Compare November 12, 2026 02:54
@StephenBoyMi StephenBoyMi changed the title fix(server): Windows 独占绑定,堵住升级后的静默双开 fix(server): Windows 上改用独占绑定,堵住升级后的静默双开 Nov 12, 2026
@shuishuipingan

Copy link
Copy Markdown
Owner

方向有价值,但原兼容回退存在阻塞:Windows 本机可复现先由 _CompatHTTPServer 建监听,再由 SO_REUSEADDR socket 成功抢绑同端口;负向 _port_accepts 之后插入旧式 SO_REUSEADDR 监听者,fallback 同样可成功 bind。TCP 探测失败也不能证明仅剩 TIME_WAIT(可能 bind 未 listen、过滤或时序竞争)。维护者将保留 SO_EXCLUSIVEADDRUSE + 禁止 reuse 的正确部分,移除兼容回退,全程有限重试独占绑定,并补充 reuse=0/1 × bound/listening 与反向抢绑行为测试。修订通过后才合并。

@shuishuipingan

Copy link
Copy Markdown
Owner

维护者修订 e868e9b 已推送此分支:移除 _CompatHTTPServer/_port_accepts 回退,所有重试保持内核独占绑定;保留 POSIX reuse。Windows 本机行为测试11/11(含未listen占位、旧reuse监听者、反向抢绑、关闭后重绑),完整145套件/4034 checks通过。TCP探测失败不再被当作TIME_WAIT证据。即将合并修订版。

@shuishuipingan
shuishuipingan merged commit ab04621 into shuishuipingan:main Nov 12, 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.

3 participants