fix(P0): SSTable rangeScan 尾块漏读 — 索引块记录块尾 key 但二分按块首语义定位,endKey 落在块尾时下一块被排除导致二级索引查询丢数据(5万行丢106~771条);endBlockIdx 多扫一块 + 4 个 sstable 边界回归 + 重建二级索引大数据量回归(含崩溃恢复)。附带:checkpoint 同步 flush 二级索引 LSM(P1)、kv 后端页面化 + SharedMemoryBackend chunk 化、生产负载验证测试
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* v0.6.1-fix(P0) — 二级索引大数据量完整性回归测试
|
||||
*
|
||||
* 覆盖两类根因:
|
||||
* 1. LSM.put 的 `!this.flushing` 条件竞态(修复:每次 freeze 一律挂链)
|
||||
* 2. SSTable rangeScan 尾块漏读(索引键为块尾 key,locateBlockLE 排除
|
||||
* 尾 key 越界的下一块 → 范围扫描丢尾部数据;修复:endBlockIdx 多扫一块)
|
||||
*/
|
||||
import { AriaEngine } from '../../src/engine/aria/index';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
import { installOPFSMock } from '../helpers/opfs-mock';
|
||||
|
||||
let counter = 0;
|
||||
function uniqueDB(): string {
|
||||
return `idxrace-${Date.now()}-${++counter}-${Math.random().toString(36).slice(2, 6)}`;
|
||||
}
|
||||
|
||||
beforeEach(() => { installOPFSMock(new Map()); });
|
||||
|
||||
describe('AriaEngine — 二级索引完整性(P0 回归)', () => {
|
||||
it('5 万行写入:索引查询与主表一致(修复前丢 106~771 条)', async () => {
|
||||
const engine = new AriaEngine({
|
||||
storageBackend: 'opfs',
|
||||
memtableSizeThreshold: 256 * 1024,
|
||||
checkpointInterval: 2000,
|
||||
walSyncMode: 'full',
|
||||
});
|
||||
await engine.open(uniqueDB(), 1);
|
||||
await engine.createTable(createSchema('big', {
|
||||
id: { type: 'string', primaryKey: true },
|
||||
val: { type: 'number' },
|
||||
tag: { type: 'string', index: true },
|
||||
name: { type: 'string' },
|
||||
}));
|
||||
|
||||
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);
|
||||
|
||||
// 每个 tag 的索引查询都完整(修复前丢 106-771 条)
|
||||
for (let t = 0; t < 10; t++) {
|
||||
const viaIdx = await engine.find('big', { table: 'big', where: { tag: `t${t}` } });
|
||||
expect(viaIdx.length).toBe(5000);
|
||||
}
|
||||
|
||||
// 崩溃恢复后索引仍完整
|
||||
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((engine as any).dbName, 1);
|
||||
for (let t = 0; t < 10; t++) {
|
||||
const viaIdx = await engine2.find('big', { table: 'big', where: { tag: `t${t}` } });
|
||||
expect(viaIdx.length).toBe(5000);
|
||||
}
|
||||
await engine2.close();
|
||||
}, 180000);
|
||||
|
||||
it('小批量高频写入(每批 50 行)触发极端 freeze 竞态', async () => {
|
||||
const engine = new AriaEngine({
|
||||
storageBackend: 'opfs',
|
||||
memtableSizeThreshold: 32 * 1024,
|
||||
checkpointInterval: 5000,
|
||||
walSyncMode: 'full',
|
||||
});
|
||||
await engine.open(uniqueDB(), 1);
|
||||
await engine.createTable(createSchema('items', {
|
||||
id: { type: 'string', primaryKey: true },
|
||||
tag: { type: 'string', index: true },
|
||||
}));
|
||||
|
||||
const TOTAL = 4000;
|
||||
for (let i = 0; i < TOTAL; i++) {
|
||||
await engine.insert('items', [{ id: `k${i}`, tag: i % 3 === 0 ? 'a' : 'b' }]);
|
||||
}
|
||||
expect(await engine.count('items')).toBe(TOTAL);
|
||||
expect(await engine.find('items', { table: 'items', where: { tag: 'a' } })).toHaveLength(Math.ceil(TOTAL / 3));
|
||||
expect(await engine.find('items', { table: 'items', where: { tag: 'b' } })).toHaveLength(TOTAL - Math.ceil(TOTAL / 3));
|
||||
await engine.close();
|
||||
}, 120000);
|
||||
|
||||
it('多索引列同时写入:每列索引都完整', async () => {
|
||||
const engine = new AriaEngine({
|
||||
storageBackend: 'opfs',
|
||||
memtableSizeThreshold: 64 * 1024,
|
||||
checkpointInterval: 5000,
|
||||
walSyncMode: 'full',
|
||||
});
|
||||
await engine.open(uniqueDB(), 1);
|
||||
await engine.createTable(createSchema('multi', {
|
||||
id: { type: 'string', primaryKey: true },
|
||||
cat: { type: 'string', index: true },
|
||||
grp: { type: 'number', index: true },
|
||||
}));
|
||||
|
||||
const TOTAL = 6000;
|
||||
for (let batch = 0; batch < TOTAL / 100; batch++) {
|
||||
const rows = [] as Record<string, unknown>[];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const idx = batch * 100 + i;
|
||||
rows.push({ id: `k${idx}`, cat: idx % 2 === 0 ? 'even' : 'odd', grp: idx % 5 });
|
||||
}
|
||||
await engine.insert('multi', rows);
|
||||
}
|
||||
expect(await engine.find('multi', { table: 'multi', where: { cat: 'even' } })).toHaveLength(TOTAL / 2);
|
||||
expect(await engine.find('multi', { table: 'multi', where: { cat: 'odd' } })).toHaveLength(TOTAL / 2);
|
||||
expect(await engine.find('multi', { table: 'multi', where: { grp: 0 } })).toHaveLength(TOTAL / 5);
|
||||
expect(await engine.find('multi', { table: 'multi', where: { grp: 4 } })).toHaveLength(TOTAL / 5);
|
||||
await engine.close();
|
||||
}, 120000);
|
||||
});
|
||||
Reference in New Issue
Block a user