Commit Graph
2 Commits
Author SHA1 Message Date
thzxx 68e4731788 fix(B-6 ③ + 测试介质): 陈旧实例拒绝提交覆盖 + SharedMemoryBackend 跨实例可见性
③ 陈旧实例的 checkpoint 会静默抹掉新实例的写入(P0)
   修复前多个 KVStore 实例同时打开同一库时,snapshot/meta 是全库共享的,而每个
   实例各有内存索引与 seq —— 落后实例的一次 checkpoint 会用它的旧索引覆盖介质:
     A.open → A.put(x) ; B.open(读到 x) → B.put(y) → B.close()
     → A.checkpoint()          // A 的索引里没有 y
     → 重开:x 在、**y 消失**,且 A.checkpoint() **没有报任何错**
   修法:meta 增加 `owner`(实例 id),open() 时领取所有权;checkpoint 前比对
   owner —— 不是自己即判为过期,抛 `STALE_INSTANCE` 并拒绝提交(而不是覆盖);
   此后该实例的写入也显式失败(否则只会写进永远无法提交的 WAL)。
   重新 open 即可恢复(陈旧状态不是永久的)。

   **测试介质本身的缺陷(同源发现,影响此前的所有多实例验证)**
   `SharedMemoryBackend` 每个实例各有一份私有拼接缓存,且只在**自己的**
   write/append 时失效 → 实例 A 读过的键永远命中 A 的缓存,**看不到** B 之后的
   写入。介质行为退化为"每个实例各有一份快照",于是所有"两实例共享介质"的
   崩溃/多标签页用例都跑在错误的介质上(这正是上面那条 bug 起初查不出来的原因:
   守卫读 owner 时拿到的是自己的旧值)。
   修法:拼接缓存按库名**共享**(挂在注册表条目上),任何实例的 write/append/
   delete 都清掉该库的共享缓存;删除用 `null` 哨兵,杜绝"删了还能读到旧值"。
   同时保持既有 close 契约(close 后读返回 null、写删清空不抛错且不持久化,
   跨实例持久化语义不变)—— `clearRegistry()` 换代,避免跨用例数据泄漏。

验证:tests/v080-kvstore-commit-point.test.ts 扩到 19 项(含 4 项陈旧实例 +
4 项介质可见性);全量 87 套件 / 1686 测试通过(含 Aria 10 万行生产负载);
typecheck(src+tests) 与 lint 零错误。
2026-09-15 00:34:45 +08:00
thzxx edd9f1dcd9 fix(B-6): KVStore 损坏尾部不再清空整库 + 后台 checkpoint 失败语义(两处 P0)
PLAN-v0.7.5.md §4 B-6 的 KVStore 三处止血中最严重的两处,均为**P0 数据丢失**。

① open() 遇损坏日志尾部会清空整个日志
   修复前 `open()` 检测到 corruptOffsets 后调用 `truncateLog()` —— 那是"写空文件",
   用于 checkpoint 之后(此时快照已覆盖全部数据)。用在崩溃恢复路径上,
   等价于把"尾部损坏"放大成"整库丢失"。实测(本提交的新用例锁定):
     写 a/b/c 三条 → 第 4 条撕裂(只落 20 字节)→ 重开:a/b/c 可见
     → **再重开一次:全部为空**
   修法:新增 `truncateLogTo(keepBytes)`,恢复路径截断到
   `findValidLogLength(log)`(与 repair() 同一口径),只丢弃损坏尾部。
   两个方法的语义差异写进注释:checkpoint 后可清空(快照是数据来源),
   恢复时只能截断(日志前缀才是唯一数据来源)。

② 自动 checkpoint 失败会让已确认写入报错
   修复前 `appendRecord` 末尾的自动 checkpoint 没有 try/catch:快照/meta 写失败
   会把异常冒泡到 `put()`,但那条写入**已经在 WAL 里**(WAL 是权威来源,崩溃后
   一定能重放)。用户看到"写入失败"、数据却在盘上 —— 报错与事实相反,
   调用方据此重试会写两次、据此丢弃业务状态会丢数据。
   修法:抽出 `autoCheckpoint()`,失败记录为 `lastBackgroundError` 而不抛出;
   由**下一次显式 `checkpoint()`** 报告(`KV_BACKGROUND_ERROR`)——
   那是用户主动要求压实数据的时机,此时失败才是真实问题。
   WAL 追加本身失败仍然照旧抛 `KV_LOG_ERROR` 且不回滚内存索引(原子性保持)。

验证方式:tests/v080-kvstore-commit-point.test.ts —— 11 项,使用
`FaultyBackend` 做**真实字节级**故障注入(撕裂写 / bit-flip / 掉电丢弃 /
写失败),而非"重开测试"。并做了**变异验证**:把两处修复分别回退到修复前的
行为,对应用例立即失败(①2 项失败、②3 项失败),恢复后全绿 ——
确认这些断言真的能拦住回归,不是假绿。

全量 85 套件 / 1657 测试通过;typecheck(src+tests) 与 lint 零错误。
2026-09-15 00:21:32 +08:00