Files
MetonaSqlark/tests/engine/aria-prod-load.test.ts
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

379 lines
14 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* AriaEngine — 生产负载验证(正确性优先,规模可完成)
*
* 覆盖 README 宣称的核心能力在生产负载下的正确性:
* 1. 5 万行写入(多级 Compaction)→ 完整查询 → 崩溃恢复
* 2. 高频更新/删除(Compaction 回收墓碑)→ 重启后一致
* 3. 大 value100KB×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);
// 多级 compactionVACUUM 语义)
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('大 value100KB × 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;
// 性能护栏:修复前 353sbatch 32 起每批 8~11s 性能悬崖),
// 修复后本机 ~12.5s。CIdebian 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);
});