docs(B-6): 附录 H(B-6 完整实施记录)+ README/CHANGELOG/site/CONTRIBUTING 同步
- PLAN 附录 H:交付物清单、提交顺序不变量、**实施中新发现的 10 个缺陷**(含分片号复用、 读快照与并发 flush 的窗口、checkpoint 仍等 compaction、takeover 世代竞争、提交中冻结表 误报 WRITE_LOST、底部层只剩墓碑的 TypeError、介质读故障被当缺失、元数据损坏静默空库等), 以及可复现的验收命令与实测数字。 - PLAN 待办表:B-6 行改为"完整实施(非降级选项)";原"B-1 遗留"给出结论 (compaction/merge 输入只来自已校验数据,补校验反而有害;触发条件写明)。 - README:架构图/核心机制加入单一提交点;新增"存储布局在 v0.8.0 变更"的已知限制与迁移说明; 测试 1935 / 覆盖率 90.34 · 82.16 · 94.06 · 93.23;新增变异验证命令。 - CHANGELOG:0.8.0 条目补齐 B-6 完整实现(含 9 个新错误码与恢复报告)。 - site:错误码表补 7 个新码;AriaEngine 与崩溃恢复卡片按实现改写(不再宣称"空洞截断"); 首页徽章数字同步。 - CONTRIBUTING:新增"变异验证"一节(修复类提交必须能回答"回退后用例会不会失败")。
This commit is contained in:
+100
-6
@@ -804,7 +804,7 @@ connection-manager/integrations/migration)两份完整报告,以及测试质
|
||||
| ~~P1~~ | ~~A37 未限定列名 / WHERE 列存在性~~ | ✅ `567d150` |
|
||||
| ~~P1~~ | ~~A38/A39 压缩被遮蔽 / O(n²)~~ | ✅ `97b9fa4` |
|
||||
| ~~P1~~ | ~~A41 DDL 原子性~~ | ✅ `bf93ec5`(A40 经探针未复现,已固化为护栏) |
|
||||
| ~~P0~~ | ~~**B-6 存储单一提交点(manifest)**~~ | ✅ KVStore 三处止血(`edd9f1d`/`68e4731`)+ Aria DDL WAL 意图(`bf93ec5`) |
|
||||
| ~~P0~~ | ~~**B-6 存储单一提交点(manifest)**~~ | ✅ **完整实施**(非降级选项):`__aria_manifest` 单一提交点 + LSM 44/45/47/49/50/51/55 全部改造 + 引擎层 prefetch 依赖删除 —— 见**附录 H** |
|
||||
| ~~P1~~ | ~~**B-4 统一表达式求值器**~~ | ✅ `3983aae`(CASE 结构化解析 + 列引用/聚合统一) |
|
||||
| ~~P1~~ | ~~**B-5 输出列序号 + 分隔标识符**~~ | ✅ `2c945ee` |
|
||||
| ~~—~~ | ~~PC-2 e2e 真崩溃注入(CDP `Page.crash`)~~ | ✅ `6a0cfeb`(14 项 e2e,含两个 OPFS 写入窗口) |
|
||||
@@ -812,7 +812,7 @@ connection-manager/integrations/migration)两份完整报告,以及测试质
|
||||
| — | PG 文档与站点全量同步 | 待做 |
|
||||
|
||||
**下一步建议**:v0.8.0 的三条工作流(A 全部缺陷 / B-1~B-6 结构根治 / C 验证
|
||||
基础设施)与 G1~G6 门禁均已完成(见附录 G)。后续版本方向:
|
||||
基础设施)与 G1~G6 门禁均已完成(见附录 G、附录 H)。后续版本方向:
|
||||
|
||||
1. **真跨表快照** — `backup()` 目前是逐表读取(已如实写进已知限制)。真快照需要
|
||||
行所有权改为 COW(引擎内所有写入路径统一"新对象替换"而非就地修改),
|
||||
@@ -820,15 +820,24 @@ connection-manager/integrations/migration)两份完整报告,以及测试质
|
||||
2. **MVCC 真快照隔离** — `snapshotLsn`/`prevVersion` 目前只写不读,事务串行。
|
||||
若确有多事务并发需求,需让读路径走版本链而不是 `txnSnapshot`;
|
||||
3. **简单 CASE 形式** — `CASE <表达式> WHEN <值> THEN ...`(当前仅搜索式);
|
||||
4. **复合主键** — 存储布局与外键引用目前假设单主键;
|
||||
5. **B-1 遗留** — `AriaEngine` 的 compaction / merge 写入路径尚未过
|
||||
`validatePayload`(数据来自本引擎内部生成的 SSTable,非用户输入)。
|
||||
4. **复合主键** — 存储布局与外键引用目前假设单主键。
|
||||
|
||||
**关于原"B-1 遗留(compaction/merge 未过 validatePayload)"的结论**:该项**不是缺陷,
|
||||
也无需补校验**,理由是数据来源而非实现惰性 —— LSM 的 compaction/merge 输入只可能
|
||||
是本引擎**已提交的 SSTable**(`levels` 中的 meta 由本进程写入),其中每一行都曾在
|
||||
唯一写入 choke point(`Executor` → `validatePayload` → `compileValidator`)上校验过;
|
||||
compaction 做的是"逐字节搬运 + 按 key 去重",不解析、不构造新的行语义。
|
||||
在合并路径上再跑一遍校验反而有害:`ALTER TABLE` 之前写入的旧行形状与当前 schema
|
||||
可以合法地不同(这正是 ALTER 的语义),重校验会把合法历史数据判成非法而**中断
|
||||
compaction**。触发条件明确写在这里:**一旦 LSM 接受外部输入(例如导入第三方
|
||||
SSTable / 跨库搬迁),必须在 `save()` 之前补校验**。
|
||||
|
||||
**已完成的验证基础设施(本轮重点)**:
|
||||
- PC-2 真崩溃注入(CDP `Page.crash`)+ OPFS `createWritable` 的两个写入窗口;
|
||||
- `FaultyBackend` 字节级故障注入(撕裂/bit-flip/掉电/写失败);
|
||||
- **变异验证**成为回归套件的标准做法:把修复回退到修复前的行为,对应用例
|
||||
必须失败。本轮 A15/B-6(①②③)/A13/A38/A39 全部通过该检查 ——
|
||||
必须失败。本轮 A15/A13/A38/A39 与 **B-6 全量 17 项**均通过该检查(可执行脚本
|
||||
`scripts/mutation-b6.py`,逐项打印"变异是否被拦住")——
|
||||
这是"测试真能拦住回归"与"测试只是陪跑"的分界线。
|
||||
- 测试介质的两处**忠实性**缺陷已被修正(`SharedMemoryBackend` 跨实例可见性、
|
||||
OPFS mock 的目录语义)—— 它们此前让"多实例/多库"的验证跑在错误语义上。
|
||||
@@ -849,3 +858,88 @@ connection-manager/integrations/migration)两份完整报告,以及测试质
|
||||
| **G6** | 文档与实现一致:README/CHANGELOG/site 中每一条能力宣称都有对应测试或已删除 | §8 的 **12 条**全部收口:订阅(已接线)、LZ4 页面压缩(A38 已修)、`./migration` 导出、react/vue 产物+d.ts+peerDeps、`disconnect` 进类型、priority(改为实现符合文档)、MVCC 快照隔离(改为如实描述)、backup 一致性快照(改为如实描述并列入已知限制)、"5 种引擎"(改为 4 模式 + 3 后端)、`diskEngine` 生效范围(改为仅 aria)、"在线一致性快照"(改为全库导出)、钩子契约(CONTRIBUTING 写明返回值语义) | ✅ |
|
||||
|
||||
**额外要求**:CI lint 已去掉 `continue-on-error`(真正阻断);`tsconfig.test.json` 对 `tests/` 做类型检查并修复了 **103 个**被 babel 剥离类型掩盖的类型错误。 ✅
|
||||
|
||||
---
|
||||
|
||||
## 附录 H · B-6「存储层单一提交点」完整实施记录(v0.8.0 收敛)
|
||||
|
||||
> **为什么还需要这一节**:附录 F 的"已完成"表里 B-6 只列了 ①②③(KVStore 三处止血)+
|
||||
> Aria DDL WAL 意图,而 §B-6 的规格要求的是 **`__aria_manifest` 单一提交点 + LSM 单项改造**;
|
||||
> §"B-6 的降级选项"也白纸黑字写着那五处止血是"若周期不够"的替代品。
|
||||
> 本次(周期不受限、且明确禁止降级方案)把 B-6 **按规格完整实现**,本节记录落地细节、
|
||||
> 实施中新发现的 10 个缺陷,以及可复现的验证方式。
|
||||
|
||||
### H.1 交付物
|
||||
|
||||
| 文件 | 作用 |
|
||||
|---|---|
|
||||
| `src/engine/aria/store/manifest.ts`(新增) | `__aria_manifest_<generation>`:magic + formatVersion + generation + 头部 CRC + 载荷 CRC;`ManifestStore.load/claimOwnership/commit`;先写后验;保留两代;损坏世代**显式失败** |
|
||||
| `src/engine/aria/index/lsm.ts` | 冻结表一等状态(id/LSN/重试/提交窗口);读路径 epoch + 结构版本重试;compaction 不再摘整层、底部层原地合并回收墓碑、按层 `compacting` 集合;flush/maintenance 双链;恢复报告 |
|
||||
| `src/engine/aria/index.ts` | manifest 装载 → 旧格式迁移 → 认领所有权;`commitManifest()`(唯一提交入口);`advanceWalCheckpoint()`(先算边界 → 提交 → 再删除);表结构随 manifest 提交;恢复报告 `getRecoveryReport()`;删除引擎层全部 `prefetch*` 依赖 |
|
||||
| `src/engine/aria/wal/log.ts` / `segmented_store.ts` | LSN 全库单调(manifest 记高水位);`planKeepFrom` / `truncateBefore`;**分片号只增不减**;空洞(含前缀缺失)如实上报;按水位清理旧格式键 |
|
||||
| `src/engine/aria/store/page_sstable_store.ts` | 活跃 + 退休两张页面映射(在途读者按 id 读取不再被 compaction 摘除影响);`registerPageIds` |
|
||||
| `src/engine/aria/store/file_manager.ts` | 页面水位取 manifest / 旧 meta / 现存最大 id 三者最大(单调、永不复用);`__aria_meta` 降级为兼容提示 |
|
||||
| `src/engine/aria/index/sstable.ts` | 三份解析循环合并为 `iterEntries()` 一份,越界策略统一 |
|
||||
| `src/engine/aria/index/merge_iterator.ts` | 胜出来源的补充推迟到下一次 `next()`(提前终止不多算) |
|
||||
| `src/engine/aria/wal/checkpoint.ts` | checkpoint 只落 memtable(`flushMemtables?`),不等 compaction |
|
||||
| `tests/v080-b6-single-commit-point.test.ts`(新增 63 项) | 上述每一条的回归 + manifest 严格校验表驱动 25 例;`scripts/mutation-b6.py`(22 项变异验证,全部被拦住) |
|
||||
|
||||
### H.2 提交顺序(唯一不变量)
|
||||
|
||||
```
|
||||
数据落盘(SSTable 页面 / 整 value 已 await 完成)
|
||||
↓
|
||||
__aria_manifest_<gen> 提交(单文件原子写 + 回读校验)
|
||||
↓
|
||||
才允许截断 WAL / 删除旧分片 / 删除旧 SSTable
|
||||
```
|
||||
|
||||
- manifest 载荷 = `{pageIdWatermark, namespaces{SSTable meta + nextSstableId}, schemas,
|
||||
wal{startSegment,startLsn,nextLsn}, frozen[冻结表意图], owner{instanceId,epoch}, committedAt}`
|
||||
- 恢复只认**最后一份 CRC 通过**的世代;存在 manifest 文件但全部世代无效 → `ARIA_MANIFEST_CORRUPT`
|
||||
(修复前:裸 JSON meta 解析失败 → `[]` → **静默空库**,随后 repair 还会把"没人引用"的活页删光)
|
||||
- `frozen` 意图的作用:`wal.startLsn` 不得越过最早冻结表的 `lsnAtFreeze`;
|
||||
恢复时若"manifest 声称有未落盘数据 + 一条 WAL 记录都没重放到" → `ARIA_WRITE_LOST`
|
||||
(把"已确认写入是否真的丢了"从不可观测变成可判定)
|
||||
|
||||
### H.3 实施中新发现的缺陷(方案未列出,已一并修复)
|
||||
|
||||
| # | 现象(实测) | 根因 | 修复 |
|
||||
|---|---|---|---|
|
||||
| 1 | **删掉的行复活 / 已确认写入丢失**(随机压力套件:76≠68、53≠55 两个方向都出现) | WAL 全量截断后 `currentSegment` 重置为 0,而 manifest 里的 `startSegment` 是删除前算好的(= 旧最大号+1)→ 新记录写进被跳过的分片号;分片号复用还让同一序号对应两代记录,恢复按序号排序时旧 INSERT 排在 DELETE 之后 | 分片号**只增不减**(`truncateBefore`/`truncate` 都只往上走;只有整库清空 `clearAll` 才 `reset()`);正则放宽到 `\d{6,}` |
|
||||
| 2 | 300 行全部改名后,紧接着的全表扫描有 25 行仍是旧值(几毫秒后又变新值) | 读路径"同步取 meta 快照 → await 加载",而并发 flush 会在这个窗口里把新 SSTable 发布到 `levels[0]` **并同时**从 `frozenMemtables` 摘掉 → 该批数据既不在快照里也不在前台 | `structureVersion` + 乐观重试(加载后版本变了就重来;极端情况退回"等两条链静默") |
|
||||
| 3 | `close()` 之后 `__wal_000000.bin` 仍在(每次打开都要重读一遍) | `planKeepFrom` 只在"下一条记录也在水位之内"时才敢删边界分片,而**最后一个分片**没有"下一片"可比 → 永不删除 | `truncateBefore` 接受 `latestLsn`:末片以"当前 LSN ≤ 水位"判定整片可删 |
|
||||
| 4 | 第二个实例打开同一库时 `open()` 直接失败(两个实例互相判 STALE_INSTANCE) | 认领所有权只 +1 个世代号,旧实例**在途**的提交正好落在同一个号上并覆盖认领 | 认领时一次跨过 `MANIFEST_TAKEOVER_STRIDE=1000` 个世代号 |
|
||||
| 5 | 从上一代 manifest 回退时报 `ARIA_WRITE_LOST`(数据其实完好) | `saveMeta` 正在提交这张冻结表,而 `frozen` 意图仍把它列为"未落盘" | `FrozenTable.committing` 窗口:提交中的表由本次提交负责,不进意图;失败时标志复位、可重试 |
|
||||
| 6 | 底部层"整层只剩墓碑"时 compaction 抛 `TypeError`(读 `merged[0][0]`)→ 该层永远无法合并、墓碑永远回收不掉 | 空产物没有专门的退休分支 | 新增"全部是墓碑 → 直接退休旧文件"分支(`retireMetas`) |
|
||||
| 7 | item 55 实际未生效:checkpoint 仍然等整条 compaction | `CheckpointManager.checkpoint()` 里还有一句 `await this.lsm.flush()`(会 drain 维护链) | `Flushable.flushMemtables?()` + 引擎提供 `flushAllLsms(true)`;旧调用方保留原语义 |
|
||||
| 8 | 一个损坏的更高世代把库**永久锁死**(之后每次提交都 STALE_INSTANCE) | 陈旧检查拿"磁盘上最大世代号"与自己的世代比,损坏世代不是"别人的提交" | 陈旧判定跳过**已知损坏**的世代;但新世代号仍严格大于任何已存在文件(绝不覆盖) |
|
||||
| 9 | 旧格式迁移遇到坏 JSON 静默当空库 | `catch { return [] }`(meta 与 schema 各一处) | `ARIA_LEGACY_META_CORRUPT` 显式失败(迁移全有或全无) |
|
||||
| 10 | 介质读故障被折叠成"文件不存在",于是 `dropInvalidSSTable` 把 meta 删掉(不可逆) | `load()` 的 catch 一律 `return null`,调用方无法区分"读失败"与"确实没有" | 读失败**向上抛** `ARIA_SSTABLE_READ_FAILED`;只有"确定不存在且仍被 manifest 引用"才自愈清理,并进恢复报告 |
|
||||
|
||||
### H.4 验收(可复现)
|
||||
|
||||
```bash
|
||||
# 1) B-6 回归(63 项)
|
||||
npx jest tests/v080-b6-single-commit-point.test.ts
|
||||
|
||||
# 2) 变异验证:把每个修复回退到修复前行为,对应用例必须失败(22 项)
|
||||
python3 scripts/mutation-b6.py
|
||||
|
||||
# 3) 随机压力 / 检查点崩溃恢复(发现缺陷 1 的套件)
|
||||
npx jest tests/engine/aria-repair-hardening.test.ts tests/engine/aria-kv-backend.test.ts
|
||||
|
||||
# 4) 全量常规套件(不含重型与 e2e)
|
||||
npx jest --testPathIgnorePatterns='/node_modules/|/tests/e2e/|aria-prod-load|kvstore-stress|aria-matrix-audit|aria-idx-flush-race'
|
||||
```
|
||||
|
||||
**实测数字**:常规套件 **1935 通过 / 91 套件**;覆盖率 语句 90.34% / 分支 82.16% /
|
||||
函数 94.06% / 行 93.23%(阈值 90/82/94/93,全部通过)。
|
||||
|
||||
**恢复报告**:`engine.getRecoveryReport()` 返回
|
||||
`{droppedSSTables, dataLossSuspected, walGaps, legacyImported, manifestFallback}` ——
|
||||
"损坏被自愈了多少、有没有真正丢数据"从此是**可读的返回值**,而不是只能翻日志。
|
||||
|
||||
**新增错误码**:`ARIA_MANIFEST_CORRUPT`、`ARIA_MANIFEST_WRITE_FAILED`、
|
||||
`ARIA_MANIFEST_NOT_LOADED`、`ARIA_LEGACY_META_CORRUPT`、`ARIA_SSTABLE_READ_FAILED`、
|
||||
`ARIA_SSTABLE_SAVE_CONTRACT`、`ARIA_WAL_GAP`、`ARIA_WRITE_LOST`、`STALE_INSTANCE`。
|
||||
|
||||
Reference in New Issue
Block a user