v3.9.37
v3.9.37
这一版来自对 v3.9.36 的一次独立对抗审查(1 个 P1 + 4 个 P2,全部复现后修复并复核)。
核心是凭据库的锁被重写成"实例身份":并发写入不再可能丢记录,写者中途死亡留下的锁也不再
需要人工干预。默认路径逐字节不变(270 组完整请求 Buffer 对拍),无 API 破坏,ACU ^22 仍默认关。
用户可感知
并发写凭据不再可能丢记录(锁重写)
旧实现把锁的身份放在固定路径上(<凭据文件>.lock,里面一个 owner.json),回收死锁靠"重读那个
路径再删"。两个进程都读到同一个死 owner 时,先完成回收的那一个会重新拿锁并读取快照,而慢的那一个
继续按固定路径删除——删掉的是新持有者刚建立的锁,于是两个写者同时进入,后写的覆盖先写的。
确定性交错探针可复现。
现在:每次写入建立不可变的 claim 实例(目录名自带 host/pid/随机 token,身份就是名字本身),
回收只删除被证明确实已死的那一个实例(同机 + ESRCH),发布前再次确认自己仍持有该实例。
另外在固定路径上放了一个永久普通文件哨兵,旧版本既创建不了也删不掉它——因此升级后新旧版本
不会互相覆盖。
写者中途死亡不再需要人工解锁
旧实现在"创建目录成功、写 owner.json 之前"死亡(或该写入撞上 ENOSPC)会留下无法回收的锁:
新的身份写在名字里,不再有"第二次写决定身份"的窗口,这类残留会被作为可证明死亡的实例自动回收。
运维注意(唯一需要动手的地方):如果升级前旧版本崩溃留下了空的
<凭据文件>.lock目录,
新版本不会自动接管它(空目录可能是另一个旧写者的初始化窗口,年龄不能作为死亡证据)。
此时写入会明确返回 BUSY,删掉那个空目录即可恢复。这是有意的保守设计。
其他
- 登录响应里的
credentialStored只在实际落盘成功后才为true(此前凭据存储未启用也会报"已保存"); - 会话复用索引不再泄漏:resolve 路径写入的索引现在也登记归属,淘汰时清理得干净(复现中曾残留 29 条)。
工程与门禁
- 修掉 CI 门禁的自比较:
wire-byte-identity原用git describe --tags --abbrev=0,在一个已打
tag 的提交上它会返回该提交自己的 tag,于是"与上一个 release 比较"退化成与自己比较、门禁空转。
现在由scripts/wire-base.mjs选择严格早于待测提交的 annotated release tag,并把基线 SHA 写进
日志;无可选基线时失败关闭。 - 新增确定性交错与故障注入测试族:锁回收、悬停迁移、ENOSPC/EIO、发布前失去 claim 等 24 条;
两条最初没有任何测试能判红的安全层(guard 后二次读取旧身份、缺失身份永不算死亡)已补齐用例,
各自在对应突变下变红。
验证
npm test4683 tests / 4606 pass / 0 fail / 77 skipped(skip 全部带原因);- 270/270 完整请求字节对拍(vs 上一个 release),黄金帧 12/12;
- 突变规格 68 份 / 678 条,anchor 全唯一;两条被改动的基线实测复算吻合(317、50);
- 独立复核(由写这批代码的会话执行,只信复跑结果):发布围栏与索引归属两层在源码被改坏时测试确实变红。
破坏性变更
无公开接口、路由、字段或默认开关的变化。