## 背景 v2.3.1 开发浸泡/混沌套件(PR #46)时发现:混沌注入"用户显式 `Reconnect()`"动作后,被动端服务端会**长达 60 秒以上无法接受新连接**(对端 connect 持续被拒),期间无任何 accept 失败日志、无状态机自愈。当前混沌套件刻意排除了该动作(`test/StreamFrame.Tests/SoakTests.cs` 的注释有记录)。 ## 复现条件(已观察到的形态) - 被动模式(isActive=false)+ 默认 `AcceptFirstClientOnly=true`; - 服务端发起的拆除(用户 `Reconnect()`,或与自动重连形成双 `StartAsync` 竞速)反复发生; - 拆除后 `InitServer` 对同一端口的重新 `Bind` 失败(疑似 `WSAEADDRINUSE`:服务端主动关闭留下的 TIME_WAIT 占用本地端口;Windows 默认 TIME_WAIT 时长 60–240s)。 对照实验:纯对端死亡(客户端先关,服务端被动关闭,无 TIME_WAIT 落在监听端口)的混沌**不会**楔死——20s/60s 两档浸泡全部通过;加入 `server.Reconnect()` 动作后稳定复现。 ## 根因分析(两层叠加,待进一步证实) **网络层**:`InitServer`(`src/StreamFrame/Connection/StreamConnection.cs`)创建监听 socket 时未设置 `SO_REUSEADDR`——服务端主动关闭的连接在监听端口上留下 TIME_WAIT,重新绑定同端口失败。 **状态机层(放大器)**: 1. `AcceptAsync` 的重试延迟(`AcceptRetryDelayMs`,默认 2s)发生在持有 `_acceptLock` 的 try 块内部——绑定持续失败期间,锁被一直持有,后续任何接受循环都被堵在锁上,故障时长 = TIME_WAIT 存续期; 2. 用户 `Reconnect()` 与自动重连(`ScheduleReconnect`)并发可产生双 `StartAsync` 接受循环(`_retryInProgress` 的 CAS 在 `StartCore` fire-and-forget 后即释放),加剧服务端主动关闭的频率与 TIME_WAIT 堆积; 3. v2.3.0 评审的"附带发现"(Connected 发布与 StartSession 之间的瞬态窗口)同属"状态过渡未原子化"一族。 ## 待确认(诊断步骤) 1. 写确定性复现:循环"连接 → `server.Reconnect()` → 重连"×N,楔死窗口内 `netstat -ano | findstr :port` 确认 TIME_WAIT/绑定状态;监听 `accept failed` 日志(现有 Warning 日志应能看到 `WSAEADDRINUSE`); 2. 若确认绑定失败:验证监听 socket 设置 `ReuseAddress` 后是否消除。 ## 修复方向(建议分阶段) 1. **阶段一(低成本高确定性)**: - 监听 socket 设置 `SO_REUSEADDR`(服务端标准做法,允许绑定覆盖本进程遗留的 TIME_WAIT;文档注明 Windows 上该选项的安全权衡); - 把 accept 重试延迟移出 `_acceptLock` 持有区(或改为失败后释放锁再延迟),避免重试期堵死其它接受循环。 2. **阶段二(状态机)**:单一飞行重连——`StartCore` 携带代次(generation),接受循环每次重试前校验自己是否仍是当代,被取代的循环静默退出;用户 `Reconnect()` 与自动重连统一走同一条请求通道。 3. **阶段三(可选)**:状态过渡原子化的整体设计(评审"附带发现"的彻底收口)。 ## 验收标准 - 确定性复现测试:快速服务端主动重连 × N 次后,被动端在秒级内可接受新连接; - 混沌套件恢复"用户 Reconnect()"动作后依然全绿; - 现有全部测试不回归(net8.0/net10.0/net48)。
Activity
CSJ608 commentedon Aug 29, 2026
排查结果(PR #48)
按本 issue 的诊断步骤完成排查,结论与预期不同:
原始"楔死"无法复现
Reconnect()(服务端主动关闭)→ 端口须 10s 内可重新接受"——全程秒级可用,零 accept 失败日志。Windows 实测允许重绑覆盖服务端主动关闭留下的 TIME_WAIT,原 TIME_WAIT/WSAEADDRINUSE假设在本机不成立;Reconnect()"的竞速形态——3× 全过;Reconnect()动作加回混沌套件,20s/30s 多轮全绿。回溯判定:开发浸泡套件时观察到的 60s+ 拒绝,是当时混沌测试自身三个缺陷的组合(
client is null || action < 10的双重连接泄漏占住单客户端、循环收尾未释放存活对端、变量遮蔽导致的静默编译失败使我们一度在跑旧二进制)——均已在 PR #46 修复。库的接受机制(含用户重连与自动重连竞速)经证实收敛正确。仍然落地的防御性加固(PR #48)
排查中识别的两个真实弱点(即使本次未成为触发器):
_acceptLock:旧实现整个重试 while 持锁——将来若因任何外部原因(端口被占、防火墙、平台差异)持续绑定/接受失败,重试期会堵死其它接受循环,故障期被放大为全停。现改为逐次尝试持锁;SO_REUSEADDR:Windows 实测宽松,但 Linux 上无此选项时重绑遇 TIME_WAIT 会EADDRINUSE(跨平台部署经典坑,当前 ubuntu CI 未踩中属时序运气)。设置后跨平台行为一致——CI(ubuntu) 已验证。阶段二/三的处置
"单一飞行重连(generation)"与"状态过渡原子化"在楔死不可复现的前提下收益只剩理论值,暂不实施。若未来真实环境再次出现类似症状:现在 accept 失败有 Warning 日志且不再持锁,按加固后的两处排查即可定位。
新增确定性回归测试
RapidServerInitiatedReconnects_PortStaysAcceptable常驻防回归。本 issue 将随 PR #48 合并关闭。fix: 被动端监听韧性加固(#47)——重试延迟出锁 + SO_REUSEADDR
CSJ608 commentedon Aug 29, 2026
后续:竞速泄漏监听器 bug 已确认并修复(PR #52)
当初"无法复现"的结论需要修正:问题真实存在,只是复现强度门槛高。
Reconnect()与自动重连的并发竞速提到混沌动作的 60% 后,第一轮即稳定复现;_server已被新循环替换,跳过单客户端的监听器关闭——泄漏一个仍在监听但永无人 accept 的 socket,客户端 SYN 进入无人消费的 backlog 后ConnectAsync永久挂起;_acceptLoopId代次门控(本 issue 的"阶段二:单一飞行重连"方案),监听器的创建/关闭/accept 只归属当代循环;Soak_ReconnectRacing_LongRun(竞速专用混沌 + 状态机转移合法性/编号线性化全程校验)+ 每夜 600s 双平台 soak CI(.github/workflows/soak.yml)。当初 PR #48 的两个防御性加固(重试出锁、SO_REUSEADDR)依然有效且必要;本条为第三处、也是唯一有可复现故障的加固点。三处齐备后,issue 的三个阶段全部落地。
fix: 接受循环代次门控——修复重连竞速泄漏监听器导致的客户端永久挂起(#47 阶段二)