Skip to content

fix(dag): httpapi exerciser teardown hang — DagSummaryPublisher event storm after dispose #316

Closed

Description

@LeXwDeX

现象(本地复现 + CI 实锤)

httpapi exerciser(bun run test:httpapi)在 summary 打印(pass=229 fail=0)之后进程不退出,挂起直至被 step timeout(15 分钟)杀掉。CI 实锤:PR #315 同 commit 两个 run 一红一绿(红 run 31994077328:summary 04:33:58 → step timeout 04:48:57,期间零输出)。本地抓网第一轮即复现(summary 后 120s 进程存活)。

挂起签名

挂起期间进程每 ~1.2 秒刷一条(dagID 各不相同、时间戳递减):

WARN DagSummaryPublisher: failed to publish summaries { dagID: dag_fe5ff1e2…, cause: … }

已定位的机制面

  • summary-publisher.ts:151-169:publisher 订阅 dag 触发事件,每个事件 forkIn(scope) 一个发布 fiber;发布体失败 → catchCause 打警告 → fiber 死亡
  • 警告流不止 = 拆伙期间仍有 dag 事件持续涌入;store 已 dispose → 发布必败
  • exerciser 尾段(index.ts:2154/2198):summary → Effect.scoped finalizer disposeApps() → process.exit(0);挂点在 disposeApps 的 Promise.all(backend.ts:60 并行 dispose 全部 app)不返回
  • Memory 实例状态本身是纯 Map(memory.ts:91)、无后台 fiber——嫌疑在于装配图变化改变了 teardown 时序/事件流

归因线索

#313(Memory.node 进 server.ts app 图)合入前 15+ 个 ci-test run 全绿;合入后首个 dev run 绿,第二批次出现一红一绿。强相关,机制未钉死——需要:谁在 dispose 期间持续发 dag 事件、哪个 fiber/scope 未了结。

风险

不止测试路径:真实 server 关停同样走 dispose——若同款事件风暴存在,生产实例关停也会 hang。需确认。

复现

/tmp/opencode-repro/hang-catcher.sh(10 轮循环 + summary 后 120s 存活判定 + SIGQUIT 抓栈),第一轮命中。日志:/tmp/opencode-repro/loop-1.log。

验收

  1. exerciser teardown 有界退出(summary 后 <60s 必 process.exit)
  2. 挂起根因证明(事件源 + 未了结 fiber 定位)
  3. 真实 server 关停路径确认不受同款风暴影响(或一并修复)
  4. flaky 消除验证:抓网脚本 10 轮无挂起

Activity

  1. LeXwDeX commented on Aug 17, 2026

    @LeXwDeX
    OwnerAuthor

    处置决策(2026-08-17):设防待割(选项 A)

    已武装的防线(#317,已入 main):

    1. 进程外看门狗静默漏洞修复——心跳文件缺失按停滞处理,超时照杀(旧版 !mtime → return 永久静默)
    2. teardown 阶段心跳标签——disposeApps 逐 app / cleanup / complete

    证据分级(本 issue 记录在案):

    • 铁证:CI hang 真实存在(run 31994077328:summary 04:33:58 → 超时 04:48:57,15 分钟零输出,孪生 run 绿);挂起期间事件循环存活(警告流跨分钟持续)
    • 已排除:DagSummaryPublisher 警告风暴(干净/挂起 run 同款,纯噪音)
    • 未钉死:本地复现(唯一候选样本杀得太早,已降级)、fix(server): wire Memory.node into the delivered httpapi app graph #313 引入说(时间强相关、机制未证)、具体卡点

    政策:不再烧本地机器堆复现。下一个真实挂起将被看门狗在 120s 内终结并带现场归因(哪个 app 的 dispose / 哪段 cleanup),届时按归因钉根因。

    关票标准(二选一):

    1. 根因钉死 + 修复落地;或
    2. 双防线武装后连续 2 周无挂起 → 降级为"已设防未复发"备注关闭
  2. LeXwDeX commented on Aug 18, 2026

    @LeXwDeX
    OwnerAuthor

    This was generated by AI during triage.

    机制面修复时间线补记(2026-08-18)

    审计 docs/audit-dag-memory-goal-2026-08-18.md(DAG-04 条目)复核了本 issue 的机制面,其两项修复已随 PR #335 合入 dev:

    1. publisher interrupt 再抛(summary-publisher.ts):外层 catchCause 依 F1 模式补 Cause.hasInterrupts 再抛——dispose 期间的 interrupt 不再被吞成 "failed to publish summaries" 成功日志行。该警告流此前已被归因排除为纯噪音,此修复让未来噪音不再掩盖真实信号。
    2. 生产 disposeAll 有界化(global-lifecycle.ts):disposeAllInstancesAndEmitGlobalDisposed 加 10s timeoutOption(超时即放弃、不产生错误通道、Disposed 事件仍落地)——这正面回答了本 issue 验收项 3(真实 server 关停路径)的脆弱性。

    仍未钉死(与 issue 政策一致):dispose 期间持续发 dag.* 事件的触发源。审计局限 §2 在案记录。看门狗双防线(#317)+ 上述有界化双保险下,下一个真实挂起应在 120s 内带现场归因终结。

    此注释仅为归因时间线补全,不改变「设防待割」处置决策与关票标准。

  3. LeXwDeX commented on Aug 18, 2026

    @LeXwDeX
    OwnerAuthor

    This was generated by AI during triage.

    诊断报告(2026-08-18,/diagnosing-bugs Phase 1 收口)

    按维护者裁决走选项 B:以治愈证据提前关票。

    复现回路(Phase 1 交付物)

    按本 issue 配方重建 /tmp/opencode-repro/hang-catcher.sh(单轮 coverage 模式 → 等 summary pass= 行 → 75s 存活窗口判定 → 挂起则 SIGQUIT 抓栈)。挂起捕获能力成立。

    三面观测,零复现

    观测面 结果
    本地基线 f1c2c8c33(#317 后、#335 前) 4 轮全 CLEAN(summary 后干净退出)
    本地当前 dev(含 #335/#336/#337) 5 轮全 CLEAN
    CI 时间线 #317 合入(08-17 06:10)后 15+ 个 dev ci-test run 全绿;期间 4 个 failure run 经核查全部是 Setup Bun 阶段 ghostty-web tarball 429 限流(基建问题,与挂起无关)

    关键考古

    1. 原始红 run(31994077328)现场已灭失:attempt 1(failure、挂起签名所在)被 re-run 覆盖,GitHub 仅存 attempt 2(success)日志——挂起现场不可再取。
    2. 挂起点已被 fix(dag): resolve audit defects DAG-01..04 (fixed-point loop converged) #335 有界化覆盖:本 issue 记录的卡点 disposeApps → disposeAllInstancesAndEmitGlobalDisposed,现 10s timeoutOption 超时即放弃、Disposed 事件仍落地——即使触发源潜伏,teardown 也不再表现为无限挂起。
    3. 当时「本地第一轮即复现」的环境已不可再现(今日基线 4 轮 0 复现)。

    关票依据(对应关票标准的强化形态)

    验证项对照:验收 1(teardown 有界)——#335 有界化 ✅;验收 3(生产路径确认)——#335 直接覆盖生产 disposeAll ✅;验收 2/4(根因证明/10 轮无挂起)——本地 9 轮 + 触发源不可复现,按「设防+兜底+不可复现」收口。

  4. LeXwDeX commented on Aug 19, 2026

    @LeXwDeX
    OwnerAuthor

    根因证明补录(2026-08-19 深挖审计,docs/audit-dag-deepdive-2026-08-19.md)

    关闭时遗留的验收项 2(「谁在 dispose 期间持续发 dag 事件」)现已补齐:

    1. 穷尽阅读后不存在自主持续发布者。生产代码 dag.* 发布点仅 dag.ts 命令方法(经 withWorkflowLock)与 loop.ts:543(init 期孤儿-pending 合法化);调用方要么在实例闭包内(随 teardown 死),要么是请求上下文。EventV2Bridge(纯转发)、InstanceStore.disposeAll(仅发 server.instance.disposed)、InstanceState scope-close(invalidate+interrupt)均已直读排除。
    2. 观察到的警告流机制唯一成立路径:publisher 内层 exit-重抛的自抛 interrupt cause(summary-publisher.ts:111-113)被旧外层 catchCause 捕获成 "failed to publish" 日志——db626d4ba 已修。
    3. 外部 teardown 中断在 Effect v4 下天然绕过 catchCause(effect-smol Effect.test.ts:1303-1321)→ 静默死亡而非事件风暴;dispose 期间迟到事件 forkIn 已关闭 scope → fiber 未启动即死,不 defect。机制经 effect-smol@3a1128c 源码验证。
    4. PR fix(dag): host-level supervision sweep settles nodes orphaned by instance teardown #338 生产事故(teardown 孤儿饿死)是本议题的镜像补集(放大 vs 静默),非触发源;其残留「工作流 rot」已另开 issue。
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