Skip to content

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

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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