Commit Graph
4 Commits
Author SHA1 Message Date
thzxx c5694b1d23 feat(B-6): 存储层单一提交点(__aria_manifest)+ LSM 结构根治
按 PLAN-v0.7.5.md §B-6 的**完整规格**实施(此前只落地了"降级选项"里的五处止血):
B-6 要求的是 `__aria_manifest` 单一提交点 + LSM 单项改造。完整记录见方案附录 H。

一、单一提交点
  - 新增 `src/engine/aria/store/manifest.ts`:`__aria_manifest_<generation>`
    (magic + formatVersion + generation + 头部 CRC + 载荷 CRC;先写后验;保留两代)。
    载荷 = 页面水位 + 各命名空间 SSTable 元数据 + 表结构 + WAL 起始位置 + 待落盘冻结表意图。
  - 顺序固定:**数据落盘 → manifest 提交 → 才允许截断 WAL / 删除旧文件 / 删除旧 SSTable**。
  - 恢复只认最后一份 CRC 通过的世代;全部世代无效 → `ARIA_MANIFEST_CORRUPT`
    (修复前:裸 JSON meta 解析失败 → `[]` → 静默空库,随后 repair 还会删光活页)。
  - 旧格式(__aria_lsm_meta/__aria_schemas/__aria_meta)首次打开自动迁移,旧键保留;
    迁移遇到损坏 → `ARIA_LEGACY_META_CORRUPT`。
  - 陈旧实例保护(STALE_INSTANCE):认领时一次跨过 MANIFEST_TAKEOVER_STRIDE 个世代,
    杜绝"旧实例在途提交落在同一世代号上"(实测第二个实例 open 直接失败)。

二、LSM
  - 44 冻结表成为一等状态:失败保留 + 可重试(修复前失败即永久失去落盘机会)。
  - 45 `flush()` 先入链再报告后台错误(修复前一次后台失败会让之后每次 flush 直接抛错、
       数据永远等不到落盘);被重试修复的失败进 `getBackgroundWarnings()`(可见但不误报失败)。
  - 47 `MergeIterator` 胜出来源的补充推迟到下一次 `next()`:提前终止不再多算一条。
  - 49 `compacting` 由单 boolean 改为按层集合(跨层触发不再被静默丢弃)。
  - 50 compaction 不再"先 splice 整层再合并"(窗口内该层对读者可见);
       被取代的 SSTable 进"退休表" + 读者 epoch,等更早读者退出才物理删除。
  - 51 底部层原地合并回收墓碑(删除密集场景空间不再无界增长);"整层只剩墓碑" 有专门分支
       (修复前会读 `merged[0][0]` 抛 TypeError,compaction 永久失败)。
  - 55 flush 与 compaction 拆成两条链,checkpoint 只落 memtable;删除引擎层全部
       `prefetch*`/`drainChain` 依赖,改为"快照 + 结构版本乐观重试"
       (版本号同时覆盖 levels 与前台 memtable/frozen 的变化)。
  - 读路径自洽:介质读故障抛 `ARIA_SSTABLE_READ_FAILED`,不再折叠成"文件不存在"误删元数据。

三、WAL
  - LSN 全库单调(manifest 记高水位);按水位删除旧分片(`planKeepFrom` → 提交 → 再删除)。
  - **分片号只增不减**:修复前全量截断后重置为 0,会与 manifest 记录的 startSegment 错位,
    实测造成两个方向的损坏(删掉的行复活 / 已确认写入丢失,见随机压力套件)。
  - 分片空洞(含前缀缺失)显式报 `ARIA_WAL_GAP`,不再静默丢弃尾部。

四、其它
  - `sstable.ts` 三份解析循环合并为 `iterEntries()`,越界策略统一。
  - `vacuum()` 返回真实压缩层数(修复前硬编码 6 且底部层永不压缩)。
  - `close()` 加 try/finally(落盘失败也必须释放后端/锁并复位状态)。
  - `getRecoveryReport()`:{droppedSSTables, dataLossSuspected, walGaps, legacyImported,
    manifestFallback} —— "自愈了什么、有没有真丢数据"成为可读返回值。

五、验证
  - 新增 `tests/v080-b6-single-commit-point.test.ts`(63 项,含 manifest 严格校验表驱动 25 例)。
  - 新增 `scripts/mutation-b6.py`:22 项变异验证(把每个修复回退到修复前行为,对应用例必须失败),
    全部被拦住 —— 这批用例不是陪跑。
  - 常规套件 1935 通过 / 91 套件;覆盖率 90.34 / 82.16 / 94.06 / 93.23(阈值 90/82/94/93);
    e2e 14/14;重型套件 4 套件 27 项全绿。
2026-09-15 10:29:03 +08:00
thzxx 97b9fa486d fix(A38/A39): 页面化路径真正压缩 + compressLZ4 去除二次复杂度(含测试介质目录语义修正)
A38 `compression` 在页面化路径上被静默忽略
  压缩只写在"整 value 存一个 backend value"的分支里,而 `save()` 在页面化
  分支**提前 return** —— `pageStorage` 默认自动(OPFS 后端下为 true),
  于是 `compression: true` 在默认配置下完全无效且无任何提示。
  修法:`compression` 传入 `PageSSTableStore`,在**切页之前**整体压缩
  (压缩率优于逐页压缩),加载时对称解压。
  连带修正一个会静默损坏数据的接口问题:`SSTableMeta.totalSize` 的语义是
  "页面里存了多少字节",加载时按它截断 —— 压缩后必须写**压缩长度**。
  为此 `SSTableStore.save` 改为返回 `{ storedSize }`,两处 flush 流程与
  整 value 路径都用它回填 totalSize(写未压缩长度会让压缩数据被 0 填充撑大)。

  为什么此前没被发现:既有测试只断言"压缩后能读回来",而"根本没压缩"
  同样能正确读回 —— 断言太弱。新用例改为**结构性断言**:
  开启压缩后落盘字节数必须显著下降(>5×),与实现细节无关。

A39 `compressLZ4` 匹配搜索为 O(n²)
  旧实现逐字节向前扫描最多 65535 个候选位置、每个位置再逐字节比较 ——
  在低压缩率数据上退化为二次复杂度。实测 60KB 伪随机输入耗时 **2345ms**;
  而 SSTable 页/日志段正是几百 KB 到几 MB,属于普通写入路径上的真实卡顿。
  修法:改为 LZ4 标准的 **4 字节哈希链**(`head[]`/`prev[]`,单点最多
  `MAX_CHAIN=32` 次探测)→ 实测 6ms(约 390×)。
  **输出格式完全不变**,既有落盘数据无需迁移;旧实现保留为
  `compressLZ4LinearReference` 并作为测试对照物(证明两者可互解)。
  另加"全字面量"兜底:任何异常都产出合法可解压的流(数据正确性优先于压缩率)。

测试介质修正(同源发现,影响所有 OPFS 多库场景)
  `installOPFSMock` 把 `getDirectoryHandle(name)` 的 `name` **丢弃**,
  所有库共用一棵扁平文件树。实测:`open('db-alpha')` 建表后
  `open('db-beta').getTableNames()` 返回 `["alpha_only"]`。
  真实 OPFS 下 `OPFSBackend.open(name)` 是 `root.getDirectoryHandle(name)`,
  因此 mock 现在实现真实的**目录语义**,并提供 `dir(dbName)` 视图让测试与
  生产代码使用同一个 API(此前的 `listKeys/createFile` 是根目录假 API,
  两个依赖它的用例已改为目录视图)。

验证:新增 tests/engine/aria-compression.test.ts(12 项,含 1MB 大输入与
6 组格式兼容用例);两处修复都做**变异验证**:回退 A38 的接线 → 页面化压缩
用例失败;回退 A39 到线性实现 → "60KB < 1s" 用例失败(实测 2397ms)。
全量 89 套件 / 1742 测试通过;typecheck、lint、build 零错误/零告警;dist 已重建。
2026-09-15 01:52:57 +08:00
thzxx 0dba1abf2a test(P0): v0.8.0 验证基座与工程门禁根治
工作流 C-1 / C-3 前半 + 测试代码类型检查。

【故障注入基座】新增 tests/helpers/storage-harness.ts + faulty-backend.ts
- TransactionalFileStore:忠实 OPFS 提交语义(close 才可见)+ 字节级故障注入
  (failNextWrite/Append/Delete、truncateAppendTo 撕裂写、crashPending 真崩溃)
- 删除旧 opfs-mock:读返回内部引用、keepExistingData:false 不截断、close 空实现
  导致"提交前可见"等真实缺陷无法被测出(31 个测试文件迁移至新 harness)
- 删除 aria-opfs-backend 内的第三份重复 mock(含从未被断言使用的 writeCalls 死代码
  与 entry.content.subarray 恒等分支)
- FaultyBackend:包装任意 IStorageBackend 注入故障;crash() 明确区别于 close()
  (后者是优雅停机,会刷完写队列 —— 这正是此前所有"崩溃恢复"测试的真相)
- 16 条基座自测证明注入真的生效(含 close 不能当崩溃的对照组)

【覆盖率口径】jest.config.cjs
- 移除 '!src/**/index.ts'(该 glob 把 AriaEngine 主实现等 15 个实现文件整体
  排除出统计,与 v0.2.6 曾承认过的问题同源),改为只排除纯类型声明文件并附理由
- 新增 coverageThreshold 门禁(此前完全不存在)
- 真实基线:语句 90.66% / 分支 82.94% / 函数 94.36% / 行 93.43%
- 修正 testMatch 使 tests/helpers 下的测试可被发现

【测试代码类型检查】tsconfig.test.json + npm run typecheck:tests
- 修复 103 个测试代码类型错误(此前 babel 剥离类型 + tsconfig 排除 tests,全部隐藏)
- 新增 tests/helpers/assertions.ts:nonNull/decode/rows/object/engineMethod/expectCode
  以断言收窄替代 as any
- 消除 21 个 lint warning(含 v043-hardening 中定义后从未调用的 mockOPFS 死代码)
- parser.test.ts 12 处 toBeDefined() 空断言升级为结构断言(并新增 AND/OR 优先级用例,
  当前红灯,对应总账第 11 项,将在工作流 A 修复)

【版本契约】新增 tests/version-contract.test.ts
- 校验 src VERSION / package.json / dist 三者一致,替代两处硬编码版本字面量

【CI 门禁】.gitea/workflows/ci.yml
- lint 去掉 continue-on-error(此前永远不让 CI 变红)
- 新增 tests 类型检查、--coverage 覆盖率门禁、dist 与源码同步校验
- 版本 0.7.4 升至 0.8.0
2026-09-14 21:03:06 +08:00
thzxx 334067d89e release: v0.5.1 — 存储后端生产级硬化(CRC-32/全库加密/WAL分片/页面化存储/多标签页锁/e2e)+ 深度审查修复(假实现接线/死代码清理)
CI / test (18.x) (push) Successful in 10m10s
CI / test (20.x) (push) Successful in 10m10s
CI / test (22.x) (push) Successful in 10m6s
CI / e2e (push) Successful in 9m51s
CI / test (24.x) (push) Successful in 10m28s
2026-08-10 12:07:00 +08:00