Skip to content

Latest commit

 

History

History
101 lines (79 loc) · 5.97 KB

concurrency.md

File metadata and controls

101 lines (79 loc) · 5.97 KB

SPEC-011: 并发运行

项 值
规范编号 SPEC-011
标题 同一工程上的并发运行:构建目录锁、原子发布、乐观写入与取消
状态 评审中 v1.0
版本 1.0
最后修改 2026-10-26
对应实现 mcpp >= 2026.10.26.1
相关设计文档 .agents/docs/2026-10-26-control-contract-runtime-and-ide-ecosystem-design.md §3、§4、§5.4
相关 issue #839
使用文档 50 — 机器可读输出 §9、§11

用语按 RFC 2119:必须 / 禁止为强制,应当为建议,可以为可选。

0. 这份规范存在的原因

一个工程同时被多条调用链驱动:终端里的人、只有 shell 的 agent、IDE 的保存时构建、 CI。在 2026.10.24.1 上,同一配置的两次冷构建相隔 0.1–0.5 s 启动,5 轮中有 3 轮双方 都失败(failed to write/read compiled module,#839)。全局一侧(索引刷新、BMI 缓存、 工具链目录)已有细粒度保护,工程的构建目录是唯一没有跨进程保护的共享可变资源。本规范 规定每一种共享资源的保护方式,以及读者可以依赖的保证。

1. 三段

一次构建分为三段:

段 内容 写入 锁
规划 读清单、解析依赖与工具链(可联网)、目标侧、扫描,直到配置指纹确定 不写构建目录 无
修改 准备的收尾(动作、resolution.json、build.ninja、配置级编译数据库)、ninja、产物校验、配置记录与结果记录 构建目录 构建目录锁
执行 mcpp run 启动程序;mcpp test 之后的程序运行由测试命令持锁(§2.3) 无 run:无

配置目录 target/<triple>/<fingerprint>/ 由指纹决定,指纹在规划末尾才确定。规划段 禁止写入构建目录。

2. 构建目录锁 已实现

  1. 每个构建目录一把锁,文件为 <outputDir>/.mcpp-build.lock,旁边的 .owner 记录持有者: pid、开始时间、命令行与调用方标签(MCPP_CLIENT)。
  2. 锁必须在修改段开始时获取(完整规划时在准备的收尾阶段之前,快速路径在接纳记录的 计划之前),持有到修改段结束。规划段禁止持有它。
  3. 后到者必须等待而不是失败。等待超过 300 ms 时,标准错误上出现一行 Waiting for the build directory, held by pid N (<command>, from <client>, Ns), --message-format json 下另有 waiting 事件。取得锁后,ninja 只做前者未做完的事。
  4. 不同配置的构建使用不同的目录与不同的锁,禁止相互等待。
  5. 锁描述符必须带 close-on-exec;mcpp run 必须在 exec 程序之前释放锁,程序 运行期间同配置的构建不被阻塞。mcpp test 持锁直到测试结束,因为它在同一构建目录中 运行刚链接的测试程序。
  6. 进程崩溃或被 SIGKILL 时由操作系统释放锁,不存在僵死锁。Windows 使用 LockFileEx, 语义相同(实测见设计 E24)。

3. 原子发布 已实现

资源 规则
链接产物(可执行文件、共享库、测试程序) POSIX 非 PE 目标:链接写 <out>.tmp,再 mv -f 改名。Windows 宿主:mcpp link-output 先把旧产物改名为 <out>.old-<n>,再原地链接;运行中的程序保留旧文件,以后的链接清理不再被占用的 .old-*。POSIX 宿主上的 PE 目标:原地链接
result.json、target/.last-result.json、resolution.json、配置级与根目录的 compile_commands.json 写临时文件后改名替换
target/.build_cache(配置记录) 读改写在 target/.build_cache.lock 下进行,写临时文件后改名替换;格式不变
mcpp.lock 写临时文件后改名替换;乐观并发(§4)

读者(查询命令、IDE、mcpp emit build-database、读取记录的程序)禁止加锁,并且 必须只会看到某一次完整写入的内容。链接产物在任何时刻要么是旧文件、要么是完整的 新文件;在链接进行时反复启动程序,不会出现 ENOEXEC、ETXTBSY 或执行半个文件 (设计 E23)。

4. mcpp.lock 的乐观写入 已实现

解析依赖可能联网,持锁会把一次慢的下载变成对整个工程的阻塞。因此:

  1. 规划开始读取 mcpp.lock 时,记录读到的内容(换行统一为 LF;文件不存在也是一种内容)。
  2. 写入前必须重新读取。当前内容与记录相同,或与本次将要写入的内容相同,则写入 (后一种情况下无需改写)。
  3. 否则禁止覆盖,运行以 [lockfile-concurrent] 失败(退出 1),并以 help: run the command again 给出补救:重跑即按当前的锁文件重新解析。

5. 取消 已实现

  1. SIGINT、SIGTERM、SIGHUP 结束修改段中的运行时,mcpp 必须先结束它启动的进程组, 再写下 cancelled 的结果记录,然后按信号的默认动作结束(退出状态见 SPEC-003 §3.7)。记录的这一形态在修改段开始时预先生成,信号处理器 只调用异步信号安全的 open、write、close、rename。
  2. 锁由进程结束释放。构建目录保持一致:ninja 的日志只记录完成的步骤,被中断的步骤在 下一次构建中重做。
  3. SIGKILL 不经过处理器;status 为 running 而 pid 已不存在的记录,读者应当 读作"被强制结束"。

6. 全局一侧

全局 MCPP_HOME 保持既有的细粒度保护:索引刷新使用非阻塞锁,BMI 缓存按条目加锁, 工具链按版本目录安装。编译器探测缓存(<home>/cache/probe)与提供者握手缓存 (<home>/cache/provider-handshake)的条目以写临时文件后改名的方式发布,并发的写入者 写出相同的内容。它们只由声明了 write-global-cache 的运行(规划或构建)写入,其他命令 只读取:self doctor、self env、toolchain list 不改变 home。

7. 变更记录

版本 日期 变更
1.0 2026-10-26 首版:三段、构建目录锁、原子发布、mcpp.lock 的乐观写入、取消(#839)