Skip to content

CI infra: @objectstack/spec 的 DTS 构建贴着 runner 内存天花板 —— 每个 spec PR 首跑都被 OOM 杀掉一次(--max-old-space-size=12288 on a 16GB runner) #4845

Description

@os-zhuang

协议线 PM 会话在 v17 窗口连推 spec PR 时观测到的高频基建 flake,不是任何一个 PR 的内容问题。只记录证据与分析,不自行改 CI 配置(影响面是全仓所有 PR,且同时段有另一个 PM 会话在跑)。

现象:两个内容完全无关的 PR,同一步、同一签名

PR 内容 CJS/ESM 失败点 同 job 测试
#4831 (4f136c55) activationEvents 退役(纯删除) ✅ Build success in 17882ms DTS Build start → ELIFECYCLE Command failed. 7358 test(s) declared and all 7358 accounted for
#4841 (f3e2f404) batch 行结果形状迁移 ✅ Build success in 18043ms DTS Build start → ELIFECYCLE Command failed. 7362 test(s) declared and all 7362 accounted for

两者都是 Test Core (1/2),耗时同为 ~1m40s。关键特征:

分析:堆上限设得比 runner 物理内存还接近

packages/spec/package.json:

build: pnpm gen:schema && pnpm gen:openapi && tsup && \
       if [ -z "$OS_SKIP_DTS" ]; then NODE_OPTIONS="--max-old-space-size=12288" BUILD_DTS=true tsup; fi

tsup.config.ts 里的注释写着 // Generate DTS separately to avoid memory issues —— 拆开单跑本身是对的处置,但那一趟给了 12288 MB(12 GB) 的老生代上限。

.github/workflows/ci.yml 的 Test Core 是 runs-on: ubuntu-latest,公开仓标准 runner 为 4 vCPU / 16 GB。

--max-old-space-size 是告诉 V8 可以涨到多少,不是预留。设成 12 GB 意味着 V8 在逼近 12 GB 之前不会激进 GC;叠加 tsup/tsc 自身的堆外内存、pnpm/turbo 进程与系统占用,总量越过 16 GB 物理内存时,内核先于 V8 的 OOM 处理把进程杀掉 —— 于是没有任何 Node 侧的错误输出。

也就是说:这个设置在 16 GB runner 上不是防 OOM,而是 OOM 的成因之一。随着 spec 类型图增长,越界从偶发变成了几乎每次首跑必现。

为什么现在才密集出现

turbo 对 @objectstack/spec#build 有缓存,只有改动 spec 的 PR 才真正跑这一趟。v17 窗口内 spec PR 密集,于是暴露频率陡增。非 spec PR(如 #4823)命中缓存,完全看不到这个问题。

可能的处置方向(供维护者裁决,未实施)

  1. 调低 DTS 堆上限到与物理内存相称(如 --max-old-space-size=8192 甚至 6144),让 V8 在越过物理内存前先积极 GC —— 反直觉但通常正是修法;
  2. 给 DTS 步骤换更大的 runner(runs-on: ubuntu-latest-8-cores 之类,若组织有配额);
  3. 进一步拆分 DTS 入口(现在 16 个入口一趟出),按入口分批;
  4. 三者可组合。

任一方向都建议先复现再改:在 CI 上跑一次带 /usr/bin/time -v 或 --max-old-space-size 探测的 job,拿到真实峰值 RSS,再定数字 —— 否则又是一次凭猜调参。

影响

不阻塞合并(重跑可过),但让每个 spec PR 多花一轮 CI,在发布窗口期是实打实的节流。协议线目前还有 #4756 / #4709 / #4001 / #4391 四个 spec 单在飞,预计都会命中。

关联:#4831、#4841、#4796(另一个已知 flaky,spec/src/cloud/tenant.test.ts 5s 超时,与本条无关)。


Generated by Claude Code — session session_0176qgxgCXTJCUv4YFLtusP9

Activity

  1. os-zhuang commented on Aug 3, 2026

    @os-zhuang
    ContributorAuthor

    第三次复现:PR #4846(ef8c17e3),同一步、同一签名

    job 91652570174 —— Test Core (1/2)。

    三个内容毫不相干的 spec PR 连续命中,进一步坐实这不是任一 PR 的内容问题:

    PR 内容性质 结果
    #4831 纯删除(退役两个键 + 一个 def) 首跑 OOM,重跑转绿
    #4841 wire 形状迁移(实现改形) 首跑 OOM
    #4846 纯文案纠错(patch,只改散文与注释) 首跑 OOM

    第三例尤其说明问题:#4846 一行运行时代码都没改,只改墓碑文案、注释与再生成的文档,packages/spec 的类型图与 v17 之前逐字节等价。它同样在 DTS 那一步被杀,说明触发条件与改动内容无关 —— 只要 turbo 缓存失效、真正跑一次 @objectstack/spec 的 DTS 构建,就有相当概率越界。

    即:命中率取决于「是否跑这一趟」,不取决于「改了什么」。这与正文分析(12 GB 堆上限 vs 16 GB runner,V8 被告知可涨到 12 GB 就不积极 GC)一致。

    三例首跑全中,重跑可过 —— 边际性、高频。协议线还有 #4001 / #4391 两个 spec 单在飞,预计同样命中。


    Generated by Claude Code

  2. os-zhuang commented on Aug 3, 2026

    @os-zhuang
    ContributorAuthor

    ⬆️ 升级:第四次复现,且发生在合并队列里 —— 从「多烧一轮 CI」变成「PR 反复被踢出队列」

    merge-queue run 30803140986(gh-readonly-queue/main/pr-4831-…),12 个 job 里只有 Test Core (1/2) 失败,其余全绿(Test Core 2/2、Build Core、Build Docs、Console Pin Gate、Dogfood ×2 + gate、Temporal Conformance、filter)。

    失败 job 91652260560 日志尾部,签名与前三次逐字一致:

    CJS ⚡️ Build success in 18085ms
    CLI Building entry: src/index.ts, …(16 entries)
    DTS Build start
     ELIFECYCLE  Command failed.
    ##[error]command (…/packages/spec) …/pnpm run test exited (1)
     Tasks:    3 successful, 5 total
       Time:    1m42.997s
    …
    check-test-completeness: OK (3 package(s), 7382 test(s) declared and all 7382 accounted for).
    

    耗时 1m43s,与前三次(1m39s / 1m40s / 1m40s)一致;测试阶段完整跑完、零遗漏;DTS Build start 后零 tsc 诊断。

    为什么这次要升级严重度

    前三次都是 PR 上的 CI,重跑就能过(#4831 attempt 2 已实证)。但合并队列会对合并结果重新跑一遍,这次就栽在这里 —— 结果是 PR 被 CI_FAILURE 踢出队列。重跑 PR 的 CI 帮不上忙:队列每次消化都是一次新的抽签,而每次没中就再踢出一次。

    四次命中记录:

    # 场景 PR / run 内容性质 结果
    1 PR CI #4831 纯删除 OOM → 重跑转绿
    2 PR CI #4841 wire 形状迁移 OOM
    3 PR CI #4846 patch,零运行时代码改动 OOM
    4 merge queue #4831 同 #1 OOM → 踢出队列

    第 3 例说明命中与改动内容无关;第 4 例说明它能实际阻止合并。

    现状与影响

    已重新入队 #4831。但 v17 窗口内协议线还有 #4846、#4841、#4391 三个 spec PR 要过同一道队列,加上 #4001 战役后续多批 —— 按目前命中率,每单都可能反复被踢。

    建议把本单的处置提前:正文给的三个方向里,最小改动是把 packages/spec/package.json 里 DTS 那趟的 --max-old-space-size=12288 降到与 16 GB runner 相称的数值(V8 会因此更早开始积极 GC,而不是一路涨到被内核杀掉)。仍建议先量一次真实峰值 RSS 再定数字,但如果要快,8192 是个保守且大概率有效的起点(留 8 GB 给堆外与系统)。

    本 PM 会话未自行改该配置 —— 它影响全仓所有 PR,且同时段另一个 PM 会话也在推 PR 过同一道队列,已上报维护者裁决。


    Generated by Claude Code

  3. os-zhuang commented on Aug 3, 2026

    @os-zhuang
    ContributorAuthor

    ⛔ 更正:本单的 OOM 诊断是错的,已被自己的修复 PR 证伪

    PR #4853 把 DTS 堆上限降到 8192 后,它自己的 CI 以完全相同的方式失败。修复无效 ⇒ 前提不成立。已把 #4853 退回 draft 并关掉 auto-merge。

    以下是重查后确证与推翻的部分,写清楚免得下一个人照着我的错结论再走一遍。

    被推翻的两条

    1. 「DTS Build start 紧接 ELIFECYCLE ⇒ DTS 被 OOM 杀掉」—— 这是误读因果。

    turbo.json 里 test 的依赖是 dependsOn: ["^build"](上游依赖的 build),packages/spec 没有 pretest,其 test 就是 vitest run。所以日志里 spec 自己的 gen:schema / tsup / DTS 是另一个并发任务 @objectstack/spec#build(由 shard 上依赖 spec 的包触发)的输出。CI 用 --concurrency=4,turbo 把多个任务的 stdout 交织在一起,而 GitHub 又把整个 group 用同一个时间戳一次性 flush —— 相邻不等于因果。真正失败的是 @objectstack/spec#test,日志自己写得很清楚:command (…/packages/spec) …/pnpm run test exited (1)。

    2. 「测试全绿,只是构建挂了」—— 我把 completeness 的语义读反了。

    scripts/check-test-completeness.mjs 的注释原文:它解析 vitest 的汇总行,防的是「worker 进程级死掉导致部分用例从未运行、而汇总仍以 passed 开头」。all N accounted for 只表示没有用例被静默吞掉,完全不表示全部通过。工作流自己的注释更直接:

    Runs even when the suite failed — that is when it earns its keep. A red suite plus a GREEN completeness check means real test failures

    我此前把这行读成「测试全绿」,并据此断定失败在构建 —— 这是这次误诊的起点。

    确证的两条

    3. 本地同分支、用 CI 的同一条命令,测试全过。

    $ pnpm turbo run test --filter=@objectstack/spec --concurrency=4     # 在 #4853 分支上
     Test Files  294 passed (294)
           Tests  7362 passed (7362)
     Tasks: 1 successful, 1 total        turbo rc=0
    

    4. DTS 构建本身在 8192 下健康,峰值 RSS 实测 6.29 GB(15 GB 机器,与 runner 同量级)。所以 12288 确实高于真实需求 —— 但这不是这些 PR 变红的原因。

    现在的结论

    CI 里 @objectstack/spec 有真实的测试失败,且本地复现不出来 —— 是环境相关的。 具体是哪个用例,我拿不到:GitHub 的 job log API 只回约 10 KB 的尾部,而尾部被 gen:schema 的 1675 行 schema 清单占满,vitest 的失败明细在更靠前的位置被截断了。

    下一步该做的是拿到完整日志(下载 run 的日志归档,或在 workflow 里让失败摘要落到尾部/单独 artifact),而不是继续对着截断的尾部猜。

    一个未经证实的怀疑方向,供接手的人参考、不要当结论:scripts/build-schemas-check-mode.test.ts(本地 12 个用例跑 77 秒)里有多条显式依赖 git 状态的断言 —— 「the anchor is the merge base with origin/main, not HEAD」「fails LOUDLY when origin/main cannot be resolved」。CI 的 merge-base 与本地 worktree 不同。但本地在 main 已领先分支的状态下这些用例是过的,所以这条线索本身也有反证,不能直接采信。

    对 #4853 的处置

    改动本身(12288 → 8192)仍有独立依据 —— 真实需求 6.29 GB,上限高于物理内存确实会把可诊断的 V8 heap OOM 变成不可诊断的内核 SIGKILL。但它不修本单,所以已退回 draft、摘掉 Fixes #4845,是否单独作为一个小改进合入由维护者定。

    对之前四条「命中记录」的更正

    那张表里的四例仍然都是真实的红,但归因是错的 —— 它们不是同一个 OOM,而是同一个尚未定位的测试失败。特此更正,以免这张表被当成 OOM 的证据链继续引用。


    Generated by Claude Code

  4. os-zhuang commented on Aug 3, 2026

    @os-zhuang
    ContributorAuthor

    PR #4853 已关闭 —— 把测量留在这里,免得连同错误结论一起丢掉

    维护者关掉了 #4853(把 DTS 堆上限 12288 → 8192)。那个 PR 的结论是错的、且已被它自己的 CI 推翻,但它做的那次测量是对的,值得单独留档 —— 下次真有人撞到 packages/spec 的构建内存问题时,不必从头再量一遍。

    实测数据(与 CI runner 同量级的机器:15 GB 物理 / 可用 13 GB)

    项 读数
    packages/spec DTS 那一趟的峰值 RSS 6.29 GB
    当时的堆上限 --max-old-space-size=8192(实验值)
    结果 构建成功完成
    main 上现行上限 12288(packages/spec/package.json 的 build 脚本)
    CI runner ubuntu-latest,16 GB

    仍然成立的那部分推理

    --max-old-space-size 是允许 V8 涨到多少的上限,不是预留。12288 相对 6.29 GB 的真实需求高出近一倍,意味着 V8 在逼近 12 GB 前都不会积极 GC;Test Core 又是 turbo run test --concurrency=4,并发下总量理论上可以逼近物理内存。所以「把上限降到 8192」作为预防性卫生是有依据的,8 GB 相对实测需求留约 27% 余量。

    ⛔ 已被推翻、不要再引用的那部分

    我曾据此断定今晨四次 CI 红是内核 OOM-killer 杀掉 DTS 进程。这是错的。 #4853 自己挂着 8192 跑,红得一模一样 —— 同一个位置、同一个签名。

    真因是 #4796 那一族:13 个 compiler-API 导出面 pin 贴着 vitest 默认 5000ms,CI 上 turbo 把 spec#build 与 spec#test 并发到同 4 vCPU,把它们推过 5.0s。修复是 #4856 的 testTimeout: 60_000(现在在 packages/spec/vitest.config.ts:15)。

    我当时叠加犯了三个错,记在这里供后来者对照:

    1. 把 check-test-completeness.mjs 的 “all N accounted for” 读成「测试通过」。它只断言没有 worker 静默死掉,workflow 自己的注释写得很清楚:“A red suite plus a GREEN completeness check means real test failures”。
    2. 把 turbo 并发输出的相邻当成因果。test 的 dependsOn 只有 ["^build"](上游),packages/spec 没有 pretest,所以 spec#build(DTS)和 spec#test 在 --concurrency=4 下同时跑,GitHub 又给整组打同一个时间戳 —— DTS Build start 紧接着 ELIFECYCLE 完全可能来自两个无关的进程。
    3. 从日志尾巴下结论。那 ~10 KB 的 tail 被 gen:schema 的 1675 行清单吃光了,真正的失败行根本不在里面。下完整日志归档才看见。

    后续

    如果哪天真要降这个上限:改动本身是一个 token(packages/spec/package.json 的 build 脚本),依据就是上面那张表。但要写实话 —— 它不修任何已观测到的故障,是预防性的。别再把它和这个 issue 里的 OOM 叙事绑在一起。


    Generated by Claude Code

  5. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    ContributorAuthor

    重新定性:本单的可派发余量只剩「预防性堆上限」,不是 OOM 修复 —— 建议改为决策位

    分诊(会话 session_01FTszibd6C8sUCCZnM4VcrL,v17 D 组排查)。本单被 D 组列为「spec DTS 构建贴 OOM 天花板,v17 链每个 spec PR 首跑被杀一次」,但该定性来自已被本单自己推翻的诊断,不能照原样派发。核验结果:

    1. 真因已在别处修复,今天仍在 main 上。 本单 10:25Z / 13:30Z 两条更正确认真因是 #4796 一族的 5000ms 超时,修复是 testTimeout: 60_000。实测 origin/main:

    packages/spec/vitest.config.ts:15:    testTimeout: 60_000,
    

    修复健在。「每个 spec PR 首跑被杀一次」这个 D 组描述不再有观测支撑 —— 它继承自被推翻的 OOM 叙事。

    2. 堆上限确实还是 12288,但它不修任何已观测故障。 实测 origin/main:

    packages/spec/package.json:185:  "build": "... NODE_OPTIONS=\"--max-old-space-size=12288\" BUILD_DTS=true tsup; fi"
    

    而 13:30Z 评论已明确要求接手者「写实话」:降它是预防性的,别再和 OOM 叙事绑定。同时 10:25Z 记录了实测数据 —— 8192 下 DTS 峰值 RSS 6.29GB,构建健康。也就是说 12288 这个数字在 16GB runner 上确实没有余量意义(峰值离 8192 都还有 1.7GB),但把它降下来修的是风险,不是故障。

    3. 为什么不由 PM 直接拍板派发。 按 skill 的升级标准,这本该是 PM 的判断(hygiene 类改动),但有一条硬性反例:#4853 就是这个改动,它被维护者关掉了 —— 关闭理由是「结论错误且被自己的 CI 推翻」。在同一文件、同一数值上重开一个 PR,而唯一的新论据是「这次我们承认它不修故障」,需要维护者知情。这是「重开一个被维护者关闭过的改动」,不是常规 hygiene。

    给维护者的两个选项(两轴分析)

    A. 关闭本单(not planned) —— 根因已在 #4796/#4856 修复,预防性调参无观测需求。

    B. 改标题与正文后派发一行改动(12288 → 8192) —— 明确定性为预防性。

    • 项目长远合理性:12288 在 16GB runner 上是把上限设到物理内存的 75%,实测峰值只有 6.29GB;留着它意味着未来 spec 体积增长时,第一个撞上的不是清晰的 heap OOM 而是内核 OOM-killer(无诊断信息)。降到 8192 让失败模式从「静默被杀」变成「V8 报 heap out of memory」——可读的失败优于沉默的失败,这条本身站得住。
    • 防 AI 犯错:需要在 PR 与 changeset 里写死「本改动不修任何已观测故障」,否则这个数值变更会被后来者当作某次事故的修复来引用,错误叙事就借尸还魂了。

    我的建议:B,但要求把 issue 标题与正文一并改写(标题去掉 OOM 定性,正文首段就写清「原诊断已证伪,本单只剩预防性调参」)。理由:8192 的可读失败论据独立于被推翻的叙事,自身成立;而只要标题还写着 OOM,选 A 关掉也不能阻止下一个人从 D 组清单里再把它翻出来 —— 改写正文才是真正拆掉复现路径的动作。若维护者认为发版期不该动全仓构建参数,A 同样干净,本会话即刻执行。

    已挂 needs-user-decision,退出本轮派发队列。⛔ 本会话不会在无裁决的情况下派任何人改这个数值。


    Generated by Claude Code

  6. claude commented on Aug 6, 2026

    @claude
    Contributor

    处置(2026-08-06):关闭(not planned)——前提已死。

    原「spec DTS 构建贴 12GB 堆上限被 OOM 杀」的诊断已被本单评论链完整证伪(三层错误:completeness check 绿≠测试过、turbo 并发输出相邻≠因果、日志 tail 被 gen:schema 清单吃掉);真因是 #4796 族 5000ms 超时,已由 #4856 的 testTimeout: 60_000 修复并在 main(packages/spec/vitest.config.ts:15)。维护者已关闭对应 PR #4853。

    留此单的最大风险是下一个 agent 读标题重走 #4853 老路(上一轮已差点发生)。预防性堆上限调整(12288→8192)若将来需要,请另开干净单,并引用本单 2026-08-04 评论中的 6.29GB 峰值 RSS 实测数据——测量保留,错误叙事拆除。

    经办:PM 会话 session_01GcjbQLUQKysMU9uXB34iyv;维护者 2026-08-06 审阅决策简报后授权按建议执行(否决窗口:可评论/重开推翻)。


    Generated by Claude Code

  7. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    【裁决落地】维护者 2026-08-06 批复全舰队决策箱评估报告(批复「同意」),本单裁定:

    裁 B——改写标题与正文后派一行预防性改动:改写必须写明「原 OOM 叙事已被 PR #4853 证伪、真因(#4796 族测试超时)已由 #4856 修复且健在于 main,本单仅剩预防性降堆 --max-old-space-size 12288→8192(实测 DTS 峰值 RSS 6.29GB),不修任何已观测故障」——防止后续 agent 从 D 组清单翻出重走 #4853 老路。

    同时摘 target:v17(名不副实:实际故障已修,不挡发布)。

    流转:摘 needs-user-decision、target:v17 → pm:queue(改写正文 + 一行改动,归 devx/ci 面承接)。

    评估与落地会话:session_01N3uGFF8teXbpgtbEJ1aYXu


    Generated by Claude Code

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions