服务关闭时,出现panic("sync: negative WaitGroup counter"),以下是gpt-5.3-codex给出的分析结果和修改建议
分析结果:
关于Node.doneWait执行时,“减成负值”的问题很可能是Done 在 Work/Hang 执行了,但对应 Add 在 Shut 被跳过,不是传统 timer 双回调导致的双减。
最可疑、且代码上可闭环的路径有两条:
- 路径1(高概率):Proxy.Invoke/AfterFunc/AfterInvoke 在 Shut 阶段被调用
- addWait() 在 node.go:479-482,state==Shut 时会跳过 Add。
- 但这些任务会在后续 Start 后被执行并走 doneWait()(node.go:205 或 proxy.go:306),此时状态是 Work/Hang,Done 生效。
- 典型触发点是 Init 阶段 hook(Init 时确实是 Shut,见 node.go:94-121 + runHookFunc)。
- 结果:Done without Add => WaitGroup 负值 panic。
- 路径2(中高概率):Scheduler.spawn 在 Shut 创建 Actor,后续 kill 在 Work/Hang 执行
- spawn 调 s.node.addWait()(scheduler.go:52)会因 Shut 跳过。
- 后续 kill 调 s.node.doneWait()(scheduler.go:88)在非 Shut 生效。
- 结果同样是 Done without Add。
我排过的低概率项:
- Timer.Stop 与 AfterFunc 双减:当前实现用 Stop()==true 才 done(timer.go:16-18),而回调 done 在 proxy.go:306,两条路径基本互斥,不太像主因。
- locator 重复/乱序事件:doDeleteSource 有 map 存在与 nid 匹配校验(internal/link/node.go:410+),重复 unbind 不会持续 done,主因概率低于上面两条。
一句话结论:根因是 add/done 都被 state gate,但 gate 条件不是同一时刻、同一生命周期闭包,导致“Shut 时丢 Add,Work/Hang 时吃 Done”。
修改建议:
- 核心方案 引入“记账令牌(ticket)”机制,替代当前纯状态判断
把 addWait() 变成“申请令牌”:返回 ticket(是否成功计入 wg);doneWait(ticket) 只在 ticket=true 时执行。这样彻底避免“Add 被跳过但 Done 执行”的负值问题。
关键点:Done 依据 ticket,不依据当前 state。
- 生命周期闸门 关闭阶段禁止新增记账
在 Close() 开始时先关闭 admission(不再发放 ticket),再 wg.Wait();Destroy() 不再影响记账逻辑。
这可同时解决你之前看到的另一类风险:Close 期间继续 Add 导致等待不收敛。
- 按调用点落地 四类路径分别处理
- Invoke/AfterInvoke/AfterFunc:add 成功才允许异步投递;回调/dispatch 用同一 ticket done。
- request/event Task:把 ticket 捕获进 defer,保证 exactly-once done。
- scheduler spawn/kill:不要用 act.opts.wait 决定 done,改为“spawn 时是否拿到 ticket”的 actor 级标记。
- NodeLinker WaitHandler/DoneHandler:对每个 (uid,name) 做本地 ref 标记,仅在“曾加过”时允许 done。
- 最小改动优先级
先做 scheduler + proxy 两块(最容易出现负值),再做 NodeLinker 的配对防抖;这两步基本能把负值 panic 打掉。
- 验证方案
增加 4 组测试:
- Shut 阶段调用 Invoke 后 Start 执行;
- Shut 阶段 spawn,Work 阶段 kill;
- AfterFunc 与 Stop() 并发;
- 重复/乱序 Bind/Unbind 事件。
验证目标:wg 不负值、Close 可收敛、无新死锁。
代码详见附件:code.zip
code.zip
服务关闭时,出现panic("sync: negative WaitGroup counter"),以下是gpt-5.3-codex给出的分析结果和修改建议
分析结果:
关于Node.doneWait执行时,“减成负值”的问题很可能是Done 在 Work/Hang 执行了,但对应 Add 在 Shut 被跳过,不是传统 timer 双回调导致的双减。
最可疑、且代码上可闭环的路径有两条:
我排过的低概率项:
一句话结论:根因是 add/done 都被 state gate,但 gate 条件不是同一时刻、同一生命周期闭包,导致“Shut 时丢 Add,Work/Hang 时吃 Done”。
修改建议:
把 addWait() 变成“申请令牌”:返回 ticket(是否成功计入 wg);doneWait(ticket) 只在 ticket=true 时执行。这样彻底避免“Add 被跳过但 Done 执行”的负值问题。
在 Close() 开始时先关闭 admission(不再发放 ticket),再 wg.Wait();Destroy() 不再影响记账逻辑。
这可同时解决你之前看到的另一类风险:Close 期间继续 Add 导致等待不收敛。
先做 scheduler + proxy 两块(最容易出现负值),再做 NodeLinker 的配对防抖;这两步基本能把负值 panic 打掉。
增加 4 组测试:
验证目标:wg 不负值、Close 可收敛、无新死锁。
代码详见附件:code.zip
code.zip