feat(platform): 控制连接 WS ping/pong 保活 + 半开探测#396
Conversation
长时间不访问平台后第一次访问失败的元凶:控制连接空闲期被 LB/NAT 静默掐成半开,daemon 这头发现不了(心跳是单向应用消息、掩盖不了平台→daemon 方向断),访问时平台发 open-stream 到不了 daemon → 首次失败。加 WS 层 ping/pong:空闲也定时 ping(平台自动回 pong,双向有流量 防 idle 掐断),一个周期没等到 pong 就 terminate 触发重连(~30-60s 发现,不用干等几分钟 TCP 超时)。 真机 e2e 验证:连上后跨 2 个 ping 周期保活稳定(无误判/无崩溃);kill -STOP 冻住平台模拟半开, daemon ~52s 内探到 ping 无 pong 并重连。
deepcoldy
left a comment
There was a problem hiding this comment.
Codex review: checked src/platform/tunnel-client.ts keepalive lifecycle around adoptControl/cleanupSock/close. I don't see a blocker. pingTimer is owned at the tunnel-client instance level and cleared by cleanupSock on close/stop/unbound before reconnect; the interval has an OPEN guard and terminate is caught. pongAlive is per adopted socket closure, so reconnects don't share liveness state. The one-missed-pong logic sends ping only after setting pongAlive=false and terminates on the next interval if no pong arrived, which gives a full 30s response window and should not race normal pong handling. terminate() flows through close -> cleanupSock -> scheduleReconnect, and scheduleReconnect's reconnectTimer guard prevents duplicate reconnect timers. pnpm build passes locally. Proceeding with the requested admin squash merge.
背景
用户反馈:长时间不访问平台后,第一次访问失败、刷新就好。根因是控制连接空闲期被 LB/NAT 静默掐成半开——daemon 这头发现不了(app 心跳是单向 daemon→平台消息,掩盖不了平台→daemon 方向断),等你访问时平台发 open-stream 到不了 daemon → 首次失败;等 TCP 慢慢超时重连、或刷新那会儿刚好重连上了 → 又好了。
改动
控制连接加 WS 层 ping/pong(
tunnel-client.ts,adoptControl 后):terminate()触发重连(~30-60s 探到半开,不用干等几分钟 TCP 超时)。配合平台侧同款 ping/pong(内网 platform 仓另提)。纯 WS 控制帧,不动 JSON 协议、不涉两仓字节兼容。
验证(真机 e2e)
本地起平台(带 server ping/pong) + 跑真
startPlatformTunnelClient(带本改动):kill -STOP冻住平台模拟半开(不发 FIN),daemon ~52s 内日志控制连接 ping 无 pong,判定半开,重连并重连。tsc 通过。