Skip to content

关于Node.doneWait造成WaitGroup的panic("sync: negative WaitGroup counter")问题 #77

@idealeak

Description

@idealeak

服务关闭时,出现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 导致等待不收敛。
  • 按调用点落地 四类路径分别处理
    1. Invoke/AfterInvoke/AfterFunc:add 成功才允许异步投递;回调/dispatch 用同一 ticket done。
    2. request/event Task:把 ticket 捕获进 defer,保证 exactly-once done。
    3. scheduler spawn/kill:不要用 act.opts.wait 决定 done,改为“spawn 时是否拿到 ticket”的 actor 级标记。
    4. NodeLinker WaitHandler/DoneHandler:对每个 (uid,name) 做本地 ref 标记,仅在“曾加过”时允许 done。
  • 最小改动优先级
    先做 scheduler + proxy 两块(最容易出现负值),再做 NodeLinker 的配对防抖;这两步基本能把负值 panic 打掉。
  • 验证方案
    增加 4 组测试:
    1. Shut 阶段调用 Invoke 后 Start 执行;
    2. Shut 阶段 spawn,Work 阶段 kill;
    3. AfterFunc 与 Stop() 并发;
    4. 重复/乱序 Bind/Unbind 事件。
      验证目标:wg 不负值、Close 可收敛、无新死锁。

代码详见附件:code.zip

code.zip

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions