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

This commit is contained in:
thzxx
2026-08-10 12:07:00 +08:00
parent cff98b0903
commit 334067d89e
88 changed files with 15713 additions and 10626 deletions
+181
View File
@@ -0,0 +1,181 @@
/**
* AriaEngine — SSTable CRC-32 损坏检测集成测试
*
* 覆盖:
* 1. 打开时整文件 CRC 校验失败 → 损坏 SSTable 被清理(自愈),打开不阻塞
* 2. 运行期预加载 CRC 校验失败 → 损坏文件不缓存并被清理
* 3. 旧版无校验文件(checksum=0)在 LSM 层仍正常加载
*/
import { AriaEngine } from '../../src/engine/aria/index';
import { createSchema } from '../../src/table/schema';
import 'fake-indexeddb/auto';
let idbCounter = 0;
function uniqueDB(): string {
return `crc-${Date.now()}-${++idbCounter}-${Math.random().toString(36).slice(2, 8)}`;
}
const SCHEMA = () => createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
age: { type: 'number' },
});
/** 篡改 MemoryBackend 中指定 key 的一个字节 */
async function corruptKey(engine: AriaEngine, key: string, byteOffset: number): Promise<void> {
const backend = (engine as any).backend;
const raw = await backend.read(key);
expect(raw).not.toBeNull();
const buf = new Uint8Array(raw as ArrayBuffer);
buf[Math.min(byteOffset, buf.length - 1)] ^= 0xff;
await backend.write(key, buf.buffer as ArrayBuffer);
}
/** 列出 backend 中所有 sst_ 前缀 key */
async function listSSTKeys(engine: AriaEngine): Promise<string[]> {
const backend = (engine as any).backend;
const keys = await backend.listKeys();
return (keys as string[]).filter((k) => k.startsWith('sst_'));
}
describe('AriaEngine — SSTable CRC 损坏检测(集成)', () => {
it('打开时 CRC 损坏的 SSTable 被清理,其余数据可读', async () => {
const engine = new AriaEngine({ storageBackend: 'indexeddb', memtableSizeThreshold: 64 * 1024 * 1024 });
const dbName = uniqueDB();
await engine.open(dbName, 1);
await engine.createTable(SCHEMA());
// 写入 200 行(触发多次 flush → 多个 SSTable
for (let batch = 0; batch < 3; batch++) {
const rows = [] as Record<string, unknown>[];
for (let i = 0; i < 50; i++) {
rows.push({ id: `u-${batch * 50 + i}`, name: `User${batch * 50 + i}`, age: batch * 50 + i });
}
await engine.insert('users', rows);
await (engine as any).lsm.flush();
}
await engine.close();
// 重新打开,篡改第一个 SSTable 文件的数据区
const engine2 = new AriaEngine({ storageBackend: 'indexeddb', memtableSizeThreshold: 64 * 1024 * 1024 });
await engine2.open(dbName, 1);
const sstKeys = await listSSTKeys(engine2);
expect(sstKeys.length).toBeGreaterThanOrEqual(2);
await corruptKey(engine2, sstKeys[0], 64);
// 再次打开:损坏文件应被跳过并清理,打开不抛错
await engine2.close();
const engine3 = new AriaEngine({ storageBackend: 'indexeddb', memtableSizeThreshold: 64 * 1024 * 1024 });
await engine3.open(dbName, 1);
// 剩余未损坏文件的数据应可查询
const remaining = await engine3.find('users', { table: 'users' });
expect(remaining.length).toBeGreaterThan(0);
expect(remaining.length).toBeLessThan(200);
// 损坏文件已被清理(meta 移除)
const afterKeys = await listSSTKeys(engine3);
expect(afterKeys).not.toContain(sstKeys[0]);
const lsm = (engine3 as any).lsm;
const metas = await lsm.sstableStore.listMeta();
expect(metas.some((m: { id: number }) => m.id === Number(sstKeys[0].replace('sst_', '')))).toBe(false);
await engine3.close();
});
it('打开时损坏全部 SSTable → 库仍可打开,数据为空但不崩溃', async () => {
const engine = new AriaEngine({ storageBackend: 'indexeddb', memtableSizeThreshold: 64 * 1024 * 1024 });
const dbName = uniqueDB();
await engine.open(dbName, 1);
await engine.createTable(SCHEMA());
for (let batch = 0; batch < 2; batch++) {
const rows = [] as Record<string, unknown>[];
for (let i = 0; i < 50; i++) {
rows.push({ id: `u-${batch * 50 + i}`, name: `User${batch * 50 + i}`, age: batch * 50 + i });
}
await engine.insert('users', rows);
await (engine as any).lsm.flush();
}
await engine.close();
const engine2 = new AriaEngine({ storageBackend: 'indexeddb', memtableSizeThreshold: 64 * 1024 * 1024 });
await engine2.open(dbName, 1);
for (const key of await listSSTKeys(engine2)) {
await corruptKey(engine2, key, 16);
}
await engine2.close();
const engine3 = new AriaEngine({ storageBackend: 'indexeddb', memtableSizeThreshold: 64 * 1024 * 1024 });
// 不应抛 ARIA_OPEN_ERROR
await engine3.open(dbName, 1);
const rows3 = await engine3.find('users', { table: 'users' });
expect(rows3.length).toBe(0);
await engine3.close();
});
it('运行期 CRC 损坏 → 预加载不缓存损坏文件并清理(自愈)', async () => {
const engine = new AriaEngine({ storageBackend: 'indexeddb', memtableSizeThreshold: 64 * 1024 * 1024 });
const dbName = uniqueDB();
await engine.open(dbName, 1);
await engine.createTable(SCHEMA());
for (let batch = 0; batch < 2; batch++) {
const rows = [] as Record<string, unknown>[];
for (let i = 0; i < 50; i++) {
rows.push({ id: `u-${batch * 50 + i}`, name: `User${batch * 50 + i}`, age: batch * 50 + i });
}
await engine.insert('users', rows);
await (engine as any).lsm.flush();
}
// 运行期:直接篡改后端中的某个 SSTable 文件(不触发打开校验路径)
const sstKeys = await listSSTKeys(engine);
expect(sstKeys.length).toBeGreaterThanOrEqual(2);
const victim = sstKeys[sstKeys.length - 1]; // 篡改最新(memtable 已 flush 后的文件)
await corruptKey(engine, victim, 100);
// 缓存里已有该文件(flush 时缓存)→ 先清缓存模拟运行期重载
const lsm = (engine as any).lsm;
lsm.sstableCache.delete(Number(victim.replace('sst_', '')));
// 查询触发 prefetchRange → preloadSSTable 发现 CRC 失败 → 清理 + 不缓存
const remaining = await engine.find('users', { table: 'users' });
expect(remaining.length).toBeLessThan(100);
expect(lsm.sstableCache.has(Number(victim.replace('sst_', '')))).toBe(false);
const metas = await lsm.sstableStore.listMeta();
expect(metas.some((m: { id: number }) => m.id === Number(victim.replace('sst_', '')))).toBe(false);
await engine.close();
});
it('旧版无校验文件(checksum=0)在 LSM 中正常加载', async () => {
const engine = new AriaEngine({ storageBackend: 'indexeddb', memtableSizeThreshold: 64 * 1024 * 1024 });
const dbName = uniqueDB();
await engine.open(dbName, 1);
await engine.createTable(SCHEMA());
await engine.insert('users', [
{ id: 'a', name: 'Alice', age: 30 },
{ id: 'b', name: 'Bob', age: 25 },
]);
await (engine as any).lsm.flush();
await engine.close();
const engine2 = new AriaEngine({ storageBackend: 'indexeddb', memtableSizeThreshold: 64 * 1024 * 1024 });
await engine2.open(dbName, 1);
const sstKeys = await listSSTKeys(engine2);
expect(sstKeys.length).toBe(1);
const backend = (engine2 as any).backend;
const raw = await backend.read(sstKeys[0]);
const buf = new Uint8Array(raw as ArrayBuffer);
new DataView(buf.buffer).setUint32(buf.byteLength - 4, 0, false); // checksum 清零 → 旧版
await backend.write(sstKeys[0], buf.buffer as ArrayBuffer);
await engine2.close();
// 重开:checksum=0 跳过校验,数据完整可读
const engine3 = new AriaEngine({ storageBackend: 'indexeddb', memtableSizeThreshold: 64 * 1024 * 1024 });
await engine3.open(dbName, 1);
const rows3 = await engine3.find('users', { table: 'users' });
expect(rows3.length).toBe(2);
expect(rows3.map((r) => r.id).sort()).toEqual(['a', 'b']);
await engine3.close();
});
});