工作流 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
379 lines
14 KiB
TypeScript
379 lines
14 KiB
TypeScript
/**
|
||
* AriaEngine — 生产负载验证(正确性优先,规模可完成)
|
||
*
|
||
* 覆盖 README 宣称的核心能力在生产负载下的正确性:
|
||
* 1. 5 万行写入(多级 Compaction)→ 完整查询 → 崩溃恢复
|
||
* 2. 高频更新/删除(Compaction 回收墓碑)→ 重启后一致
|
||
* 3. 大 value(100KB×50)→ 编码/压缩/恢复
|
||
* 4. 混合操作 + 崩溃 → 已确认写入零丢失
|
||
* 5. 大量删除(90%)+ Compaction → 重启无残留
|
||
* 6. kv 后端 5 万行(页面化路径)
|
||
*
|
||
* 注:10 万级 kv 后端性能专项见 CHANGELOG v0.6.1 待办(KVStore 日志增长优化)。
|
||
*/
|
||
import { AriaEngine } from '../../src/engine/aria/index';
|
||
import { createSchema } from '../../src/table/schema';
|
||
import { SharedMemoryBackend } from '../../src/engine/kvstore/shared_memory_medium';
|
||
import { resetOPFSMock } from '../helpers/storage-harness';
|
||
|
||
let counter = 0;
|
||
function uniqueDB(): string {
|
||
return `pload-${Date.now()}-${++counter}-${Math.random().toString(36).slice(2, 6)}`;
|
||
}
|
||
|
||
const SCHEMA = () => createSchema('big', {
|
||
id: { type: 'string', primaryKey: true },
|
||
val: { type: 'number' },
|
||
tag: { type: 'string', index: true },
|
||
name: { type: 'string' },
|
||
});
|
||
|
||
beforeEach(() => {
|
||
SharedMemoryBackend.clearRegistry();
|
||
resetOPFSMock();
|
||
});
|
||
|
||
describe('AriaEngine — 生产负载验证', () => {
|
||
it('5 万行写入(多级 Compaction + 页面化)→ 完整查询 → 崩溃恢复(OPFS 后端)', async () => {
|
||
const dbName = uniqueDB();
|
||
const engine = new AriaEngine({
|
||
storageBackend: 'opfs',
|
||
memtableSizeThreshold: 256 * 1024,
|
||
checkpointInterval: 2000,
|
||
walSyncMode: 'full',
|
||
});
|
||
await engine.open(dbName, 1);
|
||
await engine.createTable(SCHEMA());
|
||
|
||
const TOTAL = 50000;
|
||
for (let batch = 0; batch < TOTAL / 1000; batch++) {
|
||
const rows = [] as Record<string, unknown>[];
|
||
for (let i = 0; i < 1000; i++) {
|
||
const idx = batch * 1000 + i;
|
||
rows.push({ id: `k${idx}`, val: idx, tag: `t${idx % 10}`, name: `User${idx}` });
|
||
}
|
||
await engine.insert('big', rows);
|
||
}
|
||
expect(await engine.count('big')).toBe(TOTAL);
|
||
|
||
// 多级 compaction(VACUUM 语义)
|
||
await (engine as any).lsm.flush();
|
||
for (let level = 0; level < 4; level++) {
|
||
await (engine as any).lsm.compactLevel(level);
|
||
}
|
||
const stats = (engine as any).lsm.getStats();
|
||
expect((stats.levelCounts as number[]).reduce((a: number, b: number) => a + b, 0)).toBeGreaterThanOrEqual(1);
|
||
|
||
// 完整查询
|
||
expect((await engine.find('big', { table: 'big' }))).toHaveLength(TOTAL);
|
||
|
||
// 崩溃恢复
|
||
await (engine as any).backend.close();
|
||
(engine as any).opened = false;
|
||
const engine2 = new AriaEngine({
|
||
storageBackend: 'opfs',
|
||
memtableSizeThreshold: 256 * 1024,
|
||
checkpointInterval: 2000,
|
||
walSyncMode: 'full',
|
||
});
|
||
await engine2.open(dbName, 1);
|
||
expect(await engine2.count('big')).toBe(TOTAL);
|
||
for (const id of ['k0', 'k25000', 'k49999']) {
|
||
expect(await engine2.find('big', { table: 'big', where: { id } })).toHaveLength(1);
|
||
}
|
||
// 索引(重启重建)
|
||
expect(await engine2.find('big', { table: 'big', where: { tag: 't5' } })).toHaveLength(5000);
|
||
await engine2.close();
|
||
}, 600000);
|
||
|
||
it('高频更新/删除(Compaction 回收墓碑)→ 重启后一致', async () => {
|
||
const dbName = uniqueDB();
|
||
const engine = new AriaEngine({
|
||
storageBackend: 'opfs',
|
||
memtableSizeThreshold: 128 * 1024,
|
||
checkpointInterval: 1000,
|
||
walSyncMode: 'full',
|
||
});
|
||
await engine.open(dbName, 1);
|
||
await engine.createTable(SCHEMA());
|
||
|
||
const rows = [] as Record<string, unknown>[];
|
||
for (let i = 0; i < 10000; i++) rows.push({ id: `k${i}`, val: i, tag: `t${i % 5}` });
|
||
await engine.insert('big', rows);
|
||
|
||
let seed = 7;
|
||
const rand = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff; };
|
||
for (let i = 0; i < 2000; i++) {
|
||
const id = `k${Math.floor(rand() * 10000)}`;
|
||
if (rand() < 0.5) {
|
||
await engine.update('big', { table: 'big', where: { id } }, { val: Math.floor(rand() * 1e9) });
|
||
} else {
|
||
await engine.delete('big', { table: 'big', where: { id } });
|
||
}
|
||
}
|
||
await (engine as any).lsm.flush();
|
||
await (engine as any).lsm.compactLevel(0);
|
||
|
||
const count = await engine.count('big');
|
||
expect(count).toBeGreaterThan(0);
|
||
await engine.close();
|
||
|
||
const engine2 = new AriaEngine({
|
||
storageBackend: 'opfs',
|
||
memtableSizeThreshold: 128 * 1024,
|
||
checkpointInterval: 1000,
|
||
walSyncMode: 'full',
|
||
});
|
||
await engine2.open(dbName, 1);
|
||
expect(await engine2.count('big')).toBe(count);
|
||
await engine2.close();
|
||
}, 600000);
|
||
|
||
it('大 value(100KB × 50)压缩写入/恢复完整', async () => {
|
||
const dbName = uniqueDB();
|
||
const engine = new AriaEngine({
|
||
storageBackend: 'opfs',
|
||
compression: true,
|
||
memtableSizeThreshold: 512 * 1024,
|
||
checkpointInterval: 5000,
|
||
walSyncMode: 'full',
|
||
});
|
||
await engine.open(dbName, 1);
|
||
await engine.createTable(createSchema('docs', {
|
||
id: { type: 'string', primaryKey: true },
|
||
body: { type: 'string' },
|
||
}));
|
||
const chunk = '这是大段生产数据内容。'.repeat(5000); // ~100KB
|
||
for (let i = 0; i < 50; i++) {
|
||
await engine.insert('docs', [{ id: `d${i}`, body: chunk }]);
|
||
}
|
||
await engine.close();
|
||
|
||
const engine2 = new AriaEngine({
|
||
storageBackend: 'opfs',
|
||
compression: true,
|
||
memtableSizeThreshold: 512 * 1024,
|
||
checkpointInterval: 5000,
|
||
walSyncMode: 'full',
|
||
});
|
||
await engine2.open(dbName, 1);
|
||
expect(await engine2.count('docs')).toBe(50);
|
||
const one = await engine2.find('docs', { table: 'docs', where: { id: 'd25' } });
|
||
expect((one[0].body as string).length).toBe(chunk.length);
|
||
await engine2.close();
|
||
}, 600000);
|
||
|
||
it('混合操作 + 崩溃:已确认写入零丢失(20000 操作)', async () => {
|
||
const dbName = uniqueDB();
|
||
const engine = new AriaEngine({
|
||
storageBackend: 'opfs',
|
||
memtableSizeThreshold: 128 * 1024,
|
||
checkpointInterval: 1000,
|
||
walSyncMode: 'full',
|
||
});
|
||
await engine.open(dbName, 1);
|
||
await engine.createTable(SCHEMA());
|
||
|
||
let seed = 123;
|
||
const rand = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff; };
|
||
const confirmed = new Map<string, { val: number; tag: string }>();
|
||
for (let i = 0; i < 20000; i++) {
|
||
const r = rand();
|
||
const id = `k${Math.floor(rand() * 10000)}`;
|
||
if (r < 0.5) {
|
||
const row = { id, val: Math.floor(rand() * 1e9), tag: `t${Math.floor(rand() * 5)}` };
|
||
try {
|
||
await engine.insert('big', [row]);
|
||
confirmed.set(id, row);
|
||
} catch (e) {
|
||
if ((e as { code?: string }).code !== 'DUPLICATE_KEY') throw e;
|
||
}
|
||
} else if (r < 0.8) {
|
||
const val = Math.floor(rand() * 1e9);
|
||
await engine.update('big', { table: 'big', where: { id } }, { val });
|
||
if (confirmed.has(id)) confirmed.set(id, { ...confirmed.get(id)!, val });
|
||
} else {
|
||
await engine.delete('big', { table: 'big', where: { id } });
|
||
confirmed.delete(id);
|
||
}
|
||
}
|
||
|
||
await (engine as any).backend.close();
|
||
(engine as any).opened = false;
|
||
|
||
const engine2 = new AriaEngine({
|
||
storageBackend: 'opfs',
|
||
memtableSizeThreshold: 128 * 1024,
|
||
checkpointInterval: 1000,
|
||
walSyncMode: 'full',
|
||
});
|
||
await engine2.open(dbName, 1);
|
||
expect(await engine2.count('big')).toBe(confirmed.size);
|
||
let sampled = 0;
|
||
for (const [id, expected] of confirmed) {
|
||
if (sampled++ > 1000) break;
|
||
const rows = await engine2.find('big', { table: 'big', where: { id } });
|
||
expect(rows).toHaveLength(1);
|
||
expect(rows[0].val).toBe(expected.val);
|
||
}
|
||
await engine2.close();
|
||
}, 600000);
|
||
|
||
it('大量删除(90% 行)+ Compaction → 重启无残留(墓碑清理)', async () => {
|
||
const dbName = uniqueDB();
|
||
const engine = new AriaEngine({
|
||
storageBackend: 'opfs',
|
||
memtableSizeThreshold: 128 * 1024,
|
||
checkpointInterval: 1000,
|
||
walSyncMode: 'full',
|
||
});
|
||
await engine.open(dbName, 1);
|
||
await engine.createTable(SCHEMA());
|
||
const rows = [] as Record<string, unknown>[];
|
||
for (let i = 0; i < 10000; i++) rows.push({ id: `k${i}`, val: i, tag: `t${i % 5}` });
|
||
await engine.insert('big', rows);
|
||
|
||
const toDelete = [] as string[];
|
||
for (let i = 1000; i < 10000; i++) toDelete.push(`k${i}`);
|
||
await engine.delete('big', { table: 'big', where: { id: { $in: toDelete } } });
|
||
expect(await engine.count('big')).toBe(1000);
|
||
|
||
await (engine as any).lsm.flush();
|
||
for (let l = 0; l < 4; l++) await (engine as any).lsm.compactLevel(l);
|
||
await engine.close();
|
||
|
||
const engine2 = new AriaEngine({
|
||
storageBackend: 'opfs',
|
||
memtableSizeThreshold: 128 * 1024,
|
||
checkpointInterval: 1000,
|
||
walSyncMode: 'full',
|
||
});
|
||
await engine2.open(dbName, 1);
|
||
expect(await engine2.count('big')).toBe(1000);
|
||
const all = await engine2.find('big', { table: 'big' });
|
||
expect(all.every((r) => Number(String(r.id).slice(1)) < 1000)).toBe(true);
|
||
await engine2.close();
|
||
}, 600000);
|
||
|
||
it('kv 后端 5 万行(页面化路径):写入 → 崩溃 → 恢复完整', async () => {
|
||
const dbName = uniqueDB();
|
||
const engine = new AriaEngine({
|
||
storageBackend: 'kv',
|
||
memtableSizeThreshold: 512 * 1024,
|
||
checkpointInterval: 5000,
|
||
walSyncMode: 'full',
|
||
});
|
||
await engine.open(dbName, 1);
|
||
await engine.createTable(SCHEMA());
|
||
|
||
const TOTAL = 50000;
|
||
for (let batch = 0; batch < TOTAL / 1000; batch++) {
|
||
const rows = [] as Record<string, unknown>[];
|
||
for (let i = 0; i < 1000; i++) {
|
||
const idx = batch * 1000 + i;
|
||
rows.push({ id: `k${idx}`, val: idx, tag: `t${idx % 10}`, name: `User${idx}` });
|
||
}
|
||
await engine.insert('big', rows);
|
||
}
|
||
expect(await engine.count('big')).toBe(TOTAL);
|
||
await (engine as any).backend.close();
|
||
(engine as any).opened = false;
|
||
|
||
const engine2 = new AriaEngine({
|
||
storageBackend: 'kv',
|
||
memtableSizeThreshold: 512 * 1024,
|
||
checkpointInterval: 5000,
|
||
walSyncMode: 'full',
|
||
});
|
||
await engine2.open(dbName, 1);
|
||
expect(await engine2.count('big')).toBe(TOTAL);
|
||
expect(await engine2.find('big', { table: 'big', where: { tag: 't3' } })).toHaveLength(5000);
|
||
await engine2.close();
|
||
}, 600000);
|
||
|
||
it('10 万行 kv 后端(含索引):完整查询 + 崩溃恢复(v0.6.1-perf 回归)', async () => {
|
||
const dbName = uniqueDB();
|
||
const engine = new AriaEngine({
|
||
storageBackend: 'kv',
|
||
memtableSizeThreshold: 512 * 1024,
|
||
checkpointInterval: 30000,
|
||
walSyncMode: 'full',
|
||
});
|
||
await engine.open(dbName, 1);
|
||
await engine.createTable(SCHEMA());
|
||
|
||
const TOTAL = 100000;
|
||
const t0 = Date.now();
|
||
for (let batch = 0; batch < TOTAL / 1000; batch++) {
|
||
const rows = [] as Record<string, unknown>[];
|
||
for (let i = 0; i < 1000; i++) {
|
||
const idx = batch * 1000 + i;
|
||
rows.push({ id: `k${idx}`, val: idx, tag: `t${idx % 10}`, name: `User${idx}` });
|
||
}
|
||
await engine.insert('big', rows);
|
||
}
|
||
const insertMs = Date.now() - t0;
|
||
// 性能护栏:修复前 353s(batch 32 起每批 8~11s 性能悬崖),
|
||
// 修复后本机 ~12.5s。CI(debian runner 慢 2~3 倍、重型套件串行)下
|
||
// 健康耗时约 30~80s;护栏放宽到 240s —— 仍能拦截性能悬崖回归(353s >> 240s),
|
||
// 不误报健康慢环境。
|
||
// eslint-disable-next-line no-console -- 性能护栏需要输出实测耗时
|
||
console.log(`10万行 kv 插入耗时: ${insertMs}ms`);
|
||
expect(insertMs).toBeLessThan(240000);
|
||
expect(await engine.count('big')).toBe(TOTAL);
|
||
|
||
// 全部 10 个 tag 索引查询完整
|
||
for (let t = 0; t < 10; t++) {
|
||
const viaIdx = await engine.find('big', { table: 'big', where: { tag: `t${t}` } });
|
||
expect(viaIdx.length).toBe(10000);
|
||
}
|
||
|
||
// 崩溃恢复
|
||
await (engine as any).backend.close();
|
||
(engine as any).opened = false;
|
||
const engine2 = new AriaEngine({
|
||
storageBackend: 'kv',
|
||
memtableSizeThreshold: 512 * 1024,
|
||
checkpointInterval: 30000,
|
||
walSyncMode: 'full',
|
||
});
|
||
await engine2.open(dbName, 1);
|
||
expect(await engine2.count('big')).toBe(TOTAL);
|
||
expect(await engine2.find('big', { table: 'big', where: { tag: 't7' } })).toHaveLength(10000);
|
||
await engine2.close();
|
||
}, 600000);
|
||
|
||
it('10 万行 opfs 后端(含索引):完整查询(v0.6.1-perf 回归)', async () => {
|
||
const engine = new AriaEngine({
|
||
storageBackend: 'opfs',
|
||
memtableSizeThreshold: 512 * 1024,
|
||
checkpointInterval: 30000,
|
||
walSyncMode: 'full',
|
||
});
|
||
await engine.open(uniqueDB(), 1);
|
||
await engine.createTable(SCHEMA());
|
||
|
||
const TOTAL = 100000;
|
||
const t0 = Date.now();
|
||
for (let batch = 0; batch < TOTAL / 1000; batch++) {
|
||
const rows = [] as Record<string, unknown>[];
|
||
for (let i = 0; i < 1000; i++) {
|
||
const idx = batch * 1000 + i;
|
||
rows.push({ id: `k${idx}`, val: idx, tag: `t${idx % 10}`, name: `User${idx}` });
|
||
}
|
||
await engine.insert('big', rows);
|
||
}
|
||
const insertMs = Date.now() - t0;
|
||
// 同上:CI 慢环境护栏放宽(本机 ~25s;悬崖回归仍会被拦截)
|
||
// eslint-disable-next-line no-console -- 性能护栏需要输出实测耗时
|
||
console.log(`10万行 opfs 插入耗时: ${insertMs}ms`);
|
||
expect(insertMs).toBeLessThan(300000);
|
||
expect(await engine.count('big')).toBe(TOTAL);
|
||
for (let t = 0; t < 10; t++) {
|
||
const viaIdx = await engine.find('big', { table: 'big', where: { tag: `t${t}` } });
|
||
expect(viaIdx.length).toBe(10000);
|
||
}
|
||
await engine.close();
|
||
}, 600000);
|
||
});
|