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 项全绿。
This commit is contained in:
@@ -20,7 +20,7 @@ import { OPFSBackend } from '../src/engine/aria/store/opfs_backend';
|
||||
import { KVStoreEngine } from '../src/engine/kvstore_engine';
|
||||
import type { SSTableMeta } from '../src/engine/aria/types';
|
||||
|
||||
import { resetOPFSMock } from './helpers/storage-harness';
|
||||
import { resetOPFSMock, readManifestNamespace } from './helpers/storage-harness';
|
||||
|
||||
beforeEach(() => { resetOPFSMock(); });
|
||||
|
||||
@@ -153,8 +153,8 @@ describe('P0-1b — AriaEngine 打开时完整性校验', () => {
|
||||
// 篡改存储:把第一个 SSTable 的第一个页面写成残缺内容(meta 仍引用它)
|
||||
const backend = new OPFSBackend();
|
||||
await backend.open(dbName);
|
||||
const metas = JSON.parse(new TextDecoder().decode(
|
||||
await backend.read('__aria_lsm_meta') as ArrayBuffer)) as { id: number; pageIds: number[] }[];
|
||||
// v0.8.0(B-6):SSTable meta 随 manifest 原子提交(不再是裸 JSON key)
|
||||
const metas = await readManifestNamespace(backend, 'main') as { id: number; pageIds: number[] }[];
|
||||
expect(metas.length).toBeGreaterThan(0);
|
||||
const pageId = metas[0].pageIds[0];
|
||||
await backend.write(`pg_${pageId}`, new TextEncoder().encode('truncated-garbage').buffer);
|
||||
@@ -174,8 +174,7 @@ describe('P0-1b — AriaEngine 打开时完整性校验', () => {
|
||||
const backend2 = new OPFSBackend();
|
||||
await backend2.open(dbName);
|
||||
const remaining = (await backend2.listKeys()).filter((k) => k.startsWith('pg_'));
|
||||
const metaRaw = await backend2.read('__aria_lsm_meta');
|
||||
const metaList = JSON.parse(new TextDecoder().decode(metaRaw ?? new Uint8Array())) as { pageIds?: number[] }[];
|
||||
const metaList = await readManifestNamespace(backend2, 'main');
|
||||
const livePages = new Set<number>();
|
||||
for (const m of metaList) if (m.pageIds) for (const pid of m.pageIds) livePages.add(pid);
|
||||
const orphan = remaining.filter((k) => !livePages.has(Number(k.slice(3))));
|
||||
@@ -414,8 +413,7 @@ describe('P2-9 — 统一自愈接口 repair / clearAll', () => {
|
||||
// 篡改一个 SSTable 的页面文件
|
||||
const backend = new OPFSBackend();
|
||||
await backend.open(dbName);
|
||||
const metas = JSON.parse(new TextDecoder().decode(
|
||||
await backend.read('__aria_lsm_meta') as ArrayBuffer)) as { id: number; pageIds: number[] }[];
|
||||
const metas = await readManifestNamespace(backend, 'main') as { id: number; pageIds: number[] }[];
|
||||
expect(metas.length).toBeGreaterThan(0);
|
||||
const victimPage = metas[0].pageIds[0];
|
||||
const raw = new Uint8Array(await backend.read(`pg_${victimPage}`) as ArrayBuffer);
|
||||
|
||||
Reference in New Issue
Block a user