返回博客

多 Agent 并发抢 next build 锁,用隔离 distDir 绕过

某组件库文档站是 pnpm monorepo,多个并行 agent session 同时对 apps/www 执行 next build 验收各自的改动。我的任务是证明新增的 List 组件页面能通过 SSG 构建。然而每次一执行,终端就报:

⨯ Another next build process is already running.

轮询 pgrep -f "next build" 等待锁释放,永远等不到——一个 session 刚松手,另一个立刻抢占。即便偶尔跑到 compile 阶段,type-check 又以一种"每次报的错都不一样"的姿态炸掉:

Cannot find name 'headingShowcase'
Cannot find module '../../app/tltest/page.js'
Interface 'AffixProps' incorrectly extends interface ...

这些报错跟我的 diff 毫无关系——是隔壁 session 正在写一半的文件在 type-check 快照里漏进来了。

现象

  1. .next 目录持有一把全局构建锁,多个 next build 进程共享同一 distDir 时先到先得,后来者秒退。
  2. Next.js 的 type-check 阶段会扫描整个工作区的 TypeScript 文件,包括其他 session 尚未提交的半成品文件,导致报错内容随并发写入持续变化。
  3. output: "export" 模式下,SSG 结果会写到固定的 out/ 目录,多个 session 并发写会产生额外的文件竞争。

根因

Next.js 的构建锁粒度是 distDir:谁先持有 .next/.next_build_lock,其他进程就直接退出,不做排队。官方设计是单进程场景,没有提供"同项目多 distDir 并发"的原生支持。

在多 agent 工作流里,每个 session 都独立拥有代码改动,却共享同一个 .nextout/,于是:

  • 锁:先进者独占,后进者不等待直接失败
  • type-check:整棵源码树快照,包含任何 untracked / 未 commit 的 WIP 文件
  • export out/:多写者竞争,产物不可信
坑:distDir 共享 = 锁共享,改 output 也救不了你

很多人第一反应是"等锁释放再跑",但在并发 session retry-loop 的场景下这是无底洞。另一个常见误区是以为改 output 能隔离——output 控制的是 export 目标,锁依然在 .next。真正的隔离单元是 distDir,换掉它才能拿到独立的锁。

正解

next.config.mjs 里把三个关键字段改为环境变量门控、默认回退原值,这样不带环境变量的其他并发 session 读同一份配置文件时,行为完全不变:

// next.config.mjs
const nextConfig = {
  // 不带 env 时保持原来的 "export",带 env 时 undefined = 仅 SSG 不写 out/
  output: process.env.MY_NO_EXPORT ? undefined : "export",
  // 不带 env 时用原来的 .next,带 env 时用独立目录 = 独立锁
  distDir: process.env.MY_DISTDIR || ".next",
  // 不带 env 时保持严格类型检查,带 env 时跳过外部 WIP 噪声
  ...(process.env.MY_IGNORE_TS
    ? { typescript: { ignoreBuildErrors: true }, eslint: { ignoreDuringBuilds: true } }
    : {}),
  images: { unoptimized: true },
};

然后带环境变量运行,给这次验收分配一个独享的 distDir:

cd apps/www
rm -rf .next-mycheck
MY_NO_EXPORT=1 MY_DISTDIR=.next-mycheck MY_IGNORE_TS=1 \
  pnpm exec next build 2>&1 | grep -E "Compiled|Generating static|Finalizing|Failed|Type error"

验证自己的路由确实出现在 SSG 产物里:

grep '/components/list"' .next-mycheck/prerender-manifest.json

验收完毕后立即还原,不留痕迹在 commit 里:

git checkout -- apps/www/next.config.mjs
rm -rf apps/www/.next-mycheck
正解:独立 distDir = 独立锁,三个 env 门控互不影响
  • MY_DISTDIR 换掉锁根,是真正绕过锁竞争的核心
  • MY_NO_EXPORT 避免写 out/ 引入额外并发写竞争
  • MY_IGNORE_TS 跳过他人 WIP 文件的类型噪声——前提是已在本包单独跑 tsc --noEmit 证明自己代码类型干净,否则这个门控只是掩盖自己的 bug

一句话外卖

Next.js 的构建锁粒度是 distDir,并发验收时给每个 session 分配独立的 distDir,三行环境变量门控,既不阻塞他人也能证明自己的切片。