fix(P0): SSTable rangeScan 尾块漏读 — 索引块记录块尾 key 但二分按块首语义定位,endKey 落在块尾时下一块被排除导致二级索引查询丢数据(5万行丢106~771条);endBlockIdx 多扫一块 + 4 个 sstable 边界回归 + 重建二级索引大数据量回归(含崩溃恢复)。附带:checkpoint 同步 flush 二级索引 LSM(P1)、kv 后端页面化 + SharedMemoryBackend chunk 化、生产负载验证测试
CI / test (20.x) (push) Successful in 24m55s
CI / test (22.x) (push) Successful in 16m19s
CI / test (24.x) (push) Successful in 20m55s
CI / e2e (push) Successful in 9m54s
CI / test (18.x) (push) Successful in 26m38s

This commit is contained in:
thzxx
2026-08-10 18:18:50 +08:00
parent 25bab0f9aa
commit 34225a86b3
9 changed files with 579 additions and 38 deletions
+75
View File
@@ -83,6 +83,81 @@ describe('AriaEngine — SSTable Builder + Reader', () => {
expect(items).toHaveLength(count);
});
// v0.6.1-fix(P0): 索引键是"块内最后一个 key"locateBlockLE 排除尾 key 越界的
// 下一块会导致范围扫描漏读尾部数据(二级索引 5 万行查询丢 106~219 条)。
it('v0.6.1-fix — 范围扫描跨前缀边界不漏尾部数据', () => {
// 小块强制多块;t5 数据分散在多块中,最后一个 t5 块之后紧跟 t6 块(尾 key 越界)
const builder = new SSTableBuilder(120);
for (let i = 0; i < 30; i++) {
builder.add(`t5:k${String(i).padStart(5, '0')}`, { pk: `k${i}` });
}
for (let i = 0; i < 30; i++) {
builder.add(`t6:k${String(i).padStart(5, '0')}`, { pk: `k${i}` });
}
for (let i = 0; i < 30; i++) {
builder.add(`t7:k${String(i).padStart(5, '0')}`, { pk: `k${i}` });
}
const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData));
const t5: [string, Record<string, unknown>][] = [];
reader.rangeScan('t5', 't5\uffff', (k, v) => t5.push([k, v]));
expect(t5).toHaveLength(30);
expect(t5[0][0]).toBe('t5:k00000');
expect(t5[t5.length - 1][0]).toBe('t5:k00029');
});
it('v0.6.1-fix — 范围扫描 endKey 恰在块尾(不越界不丢、不多扫错数据)', () => {
const builder = new SSTableBuilder(150);
for (let i = 0; i < 40; i++) {
builder.add(`p-${String(i).padStart(3, '0')}`, { v: i });
}
for (let i = 0; i < 20; i++) {
builder.add(`q-${String(i).padStart(3, '0')}`, { v: i });
}
const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData));
// endKey 恰等于某条存在的 key:结果必须包含它且不含 p 之外的 key
const results: [string, Record<string, unknown>][] = [];
reader.rangeScan('p-000', 'p-039', (k, v) => results.push([k, v]));
expect(results).toHaveLength(40);
expect(results[39][0]).toBe('p-039');
});
it('v0.6.1-fix — 范围扫描 endKey 超出全部数据(返回全量)', () => {
const builder = new SSTableBuilder(100);
for (let i = 0; i < 25; i++) {
builder.add(`z-${String(i).padStart(3, '0')}`, { v: i });
}
const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData));
const results: [string, Record<string, unknown>][] = [];
reader.rangeScan('z-000', '\uffff\uffff', (k, v) => results.push([k, v]));
expect(results).toHaveLength(25);
});
it('v0.6.1-fix — 多前缀数据 rangeScan 与 scanAll 结果一致', () => {
// 模拟索引混合写入:t0..t9 交叉 + 尾部跨块(flush 路径先排序再构建)
const items: [string, Record<string, unknown>][] = [];
for (let i = 0; i < 120; i++) {
const tag = `t${i % 10}`;
items.push([`${tag}:k${String(i).padStart(5, '0')}`, { pk: `k${i}` }]);
}
items.sort((a, b) => (a[0] < b[0] ? -1 : 1));
const builder = new SSTableBuilder(200);
for (const [k, v] of items) builder.add(k, v);
const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData));
for (let t = 0; t < 10; t++) {
const keys: string[] = [];
reader.rangeScan(`t${t}`, `t${t}\uffff`, (k) => keys.push(k));
expect(keys).toHaveLength(12);
}
});
it('边界 — 空 SSTable 不抛异常', () => {
const builder = new SSTableBuilder(4096);