Files
MetonaSqlark/tests/production-abnormal.test.ts
T

481 lines
19 KiB
TypeScript
Raw 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.
/**
* 生产级异常场景测试 — KVStore / KVStoreEngine 边界与故障
*
* 覆盖:
* 1. KVStore 边界:空 key/value、超大 value、特殊字符 key、未 open、重复 open/close、clear 后写
* 2. KVStoreEngine 异常:未 open 操作、重复 close、事务嵌套、无事务 commit/rollback、
* 级联环(A→B→A)、主键变更+索引、多表事务原子性
* 3. SharedMemoryBackend 全分支:append 已有 key、writeMany/deleteMany、close 后安全
* 4. 日志解析损坏分支(onCorrupt false 提前停止)
*/
import { KVStore } from '../src/engine/kvstore/index';
import { SharedMemoryBackend } from '../src/engine/kvstore/shared_memory_medium';
import { encodeLogRecord, parseLogRecords } from '../src/engine/kvstore/log';
import { KVStoreEngine } from '../src/engine/kvstore_engine';
import { createSchema } from '../src/table/schema';
let counter = 0;
function uniqueDB(): string {
return `abn-${Date.now()}-${++counter}-${Math.random().toString(36).slice(2, 6)}`;
}
const enc = (s: string) => new TextEncoder().encode(s).buffer as ArrayBuffer;
const dec = (b: ArrayBuffer | null) => (b ? new TextDecoder().decode(b) : null);
beforeEach(() => {
SharedMemoryBackend.clearRegistry();
});
// ===================================================================
// KVStore 边界
// ===================================================================
describe('KVStore — 边界与故障', () => {
it('空 value / 空字符串 key 可正常读写', async () => {
const kv = new KVStore(new SharedMemoryBackend(), 0);
await kv.open(uniqueDB());
await kv.put('empty', new ArrayBuffer(0));
expect((await kv.get('empty'))!.byteLength).toBe(0);
await kv.put('', enc('empty-key'));
expect(dec(await kv.get(''))).toBe('empty-key');
await kv.close();
});
it('超大 value1MB)写入恢复完整', async () => {
const dbName = uniqueDB();
const medium = new SharedMemoryBackend();
const kv = new KVStore(medium, 0);
await kv.open(dbName);
const big = new Uint8Array(1024 * 1024);
for (let i = 0; i < big.length; i++) big[i] = i & 0xff;
await kv.put('big', big.buffer as ArrayBuffer);
await kv.close();
const kv2 = new KVStore(medium, 0);
await kv2.open(dbName);
const back = await kv2.get('big');
expect(back!.byteLength).toBe(1024 * 1024);
const backBytes = new Uint8Array(back!);
expect(backBytes[500000]).toBe(500000 & 0xff);
await kv2.close();
});
it('特殊字符 key(中文/emoji/含冒号/超长)读写恢复', async () => {
const dbName = uniqueDB();
const medium = new SharedMemoryBackend();
const kv = new KVStore(medium, 0);
await kv.open(dbName);
const keys = ['中文键', 'emoji-😀-key', 'a:b:c', 'x'.repeat(500)];
for (let i = 0; i < keys.length; i++) {
await kv.put(keys[i], enc(`v${i}`));
}
await kv.checkpoint();
await kv.close();
const kv2 = new KVStore(medium, 0);
await kv2.open(dbName);
for (let i = 0; i < keys.length; i++) {
expect(dec(await kv2.get(keys[i]))).toBe(`v${i}`);
}
await kv2.close();
});
it('未 open 的 KVStore 介质为空(get 返回 null,操作安全)', async () => {
const kv = new KVStore(new SharedMemoryBackend(), 0);
expect(kv.isOpen()).toBe(false);
expect(await kv.get('x')).toBeNull();
expect(kv.size()).toBe(0);
});
it('重复 open / 重复 close 幂等', async () => {
const dbName = uniqueDB();
const kv = new KVStore(new SharedMemoryBackend(), 0);
await kv.open(dbName);
await kv.open(dbName); // 幂等
await kv.put('a', enc('A'));
await kv.close();
await kv.close(); // 幂等
expect(kv.isOpen()).toBe(false);
});
it('clear 后立即写入正常,重开数据只有新写入', async () => {
const dbName = uniqueDB();
const medium = new SharedMemoryBackend();
const kv = new KVStore(medium, 0);
await kv.open(dbName);
await kv.put('old', enc('OLD'));
await kv.clear();
await kv.put('new', enc('NEW'));
await kv.close();
const kv2 = new KVStore(medium, 0);
await kv2.open(dbName);
expect(await kv2.exists('old')).toBe(false);
expect(dec(await kv2.get('new'))).toBe('NEW');
await kv2.close();
});
it('大量小 key1 万)快速写入 + 恢复', async () => {
const dbName = uniqueDB();
const medium = new SharedMemoryBackend();
const kv = new KVStore(medium, 0);
await kv.open(dbName);
const batch: Record<string, ArrayBuffer> = {};
for (let i = 0; i < 10000; i++) batch[`k${i}`] = enc(`v${i}`);
await kv.putMany(batch);
await kv.close();
const kv2 = new KVStore(medium, 0);
await kv2.open(dbName);
expect(kv2.size()).toBe(10000);
expect(dec(await kv2.get('k9999'))).toBe('v9999');
await kv2.close();
}, 60000);
it('写入失败后 KVStore 继续可用(错误不污染后续操作)', async () => {
const dbName = uniqueDB();
const medium = new SharedMemoryBackend();
const kv = new KVStore(medium, 0);
await kv.open(dbName);
// 注入一次失败
const origAppend = medium.append!.bind(medium);
medium.append = async () => { throw new Error('boom'); };
await expect(kv.put('x', enc('X'))).rejects.toMatchObject({ code: 'KV_LOG_ERROR' });
medium.append = origAppend;
// 后续写入正常
await kv.put('y', enc('Y'));
await kv.close();
const kv2 = new KVStore(medium, 0);
await kv2.open(dbName);
expect(dec(await kv2.get('y'))).toBe('Y');
await kv2.close();
});
});
// ===================================================================
// KVStoreEngine 异常场景
// ===================================================================
describe('KVStoreEngine — 异常与一致性', () => {
async function createEngine(): Promise<KVStoreEngine> {
const e = new KVStoreEngine(new SharedMemoryBackend());
await e.open(uniqueDB(), 1);
return e;
}
it('未 open 时所有操作抛 DB_NOT_OPEN', async () => {
const e = new KVStoreEngine(new SharedMemoryBackend());
await expect(e.getTableNames()).rejects.toMatchObject({ code: 'DB_NOT_OPEN' });
await expect(e.insert('t', [{ id: '1' }])).rejects.toMatchObject({ code: 'DB_NOT_OPEN' });
await expect(e.find('t', { table: 't' })).rejects.toMatchObject({ code: 'DB_NOT_OPEN' });
await expect(e.count('t')).rejects.toMatchObject({ code: 'DB_NOT_OPEN' });
await expect(e.beginTransaction()).rejects.toMatchObject({ code: 'DB_NOT_OPEN' });
});
it('重复 close 幂等', async () => {
const e = await createEngine();
await e.close();
await e.close();
expect(e.isOpen()).toBe(false);
});
it('不存在的表操作抛 TABLE_NOT_FOUND', async () => {
const e = await createEngine();
await expect(e.insert('ghost', [{ id: '1' }])).rejects.toMatchObject({ code: 'TABLE_NOT_FOUND' });
await expect(e.update('ghost', { table: 'ghost' }, {})).rejects.toMatchObject({ code: 'TABLE_NOT_FOUND' });
await expect(e.delete('ghost', { table: 'ghost' })).rejects.toMatchObject({ code: 'TABLE_NOT_FOUND' });
await expect(e.count('ghost')).rejects.toMatchObject({ code: 'TABLE_NOT_FOUND' });
await e.close();
});
it('事务嵌套 begin 两次 → TX_ACTIVE', async () => {
const e = await createEngine();
await e.beginTransaction();
await expect(e.beginTransaction()).rejects.toMatchObject({ code: 'TX_ACTIVE' });
await e.rollbackTransaction();
await e.close();
});
it('无事务 commit/rollback → TX_NONE', async () => {
const e = await createEngine();
await expect(e.commitTransaction()).rejects.toMatchObject({ code: 'TX_NONE' });
await expect(e.rollbackTransaction()).rejects.toMatchObject({ code: 'TX_NONE' });
await e.close();
});
it('事务中途失败自动回滚(多表原子性:不出现部分提交)', async () => {
const e = await createEngine();
await e.createTable(createSchema('t1', { id: { type: 'string', primaryKey: true } }));
await e.createTable(createSchema('t2', { id: { type: 'string', primaryKey: true } }));
await e.beginTransaction();
await e.insert('t1', [{ id: 'a' }]);
await e.insert('t2', [{ id: 'b' }]);
// 模拟 commit 失败:注入日志追加故障(KVStore 写走日志 append
const medium = (e as any).kv.medium;
const origAppend = medium.append.bind(medium);
medium.append = async () => { throw new Error('disk full'); };
await expect(e.commitTransaction()).rejects.toMatchObject({ code: 'KV_LOG_ERROR' });
medium.append = origAppend;
// commit 失败后事务未完成:手动回滚 → 内存恢复
await e.rollbackTransaction();
expect(await e.count('t1')).toBe(0);
expect(await e.count('t2')).toBe(0);
await e.close();
// 重启:磁盘也不应有部分提交(事务内写入全部未持久化)
const medium2 = new SharedMemoryBackend();
const e2 = new KVStoreEngine(medium2);
await e2.open((e as any).dbName, 1);
expect(await e2.count('t1')).toBe(0);
expect(await e2.count('t2')).toBe(0);
await e2.close();
});
it('多表事务 commit 单次原子:崩溃后要么全提交要么全无', async () => {
const dbName = uniqueDB();
const medium = new SharedMemoryBackend();
const e = new KVStoreEngine(medium);
await e.open(dbName, 1);
await e.createTable(createSchema('t1', { id: { type: 'string', primaryKey: true } }));
await e.createTable(createSchema('t2', { id: { type: 'string', primaryKey: true } }));
await e.beginTransaction();
await e.insert('t1', [{ id: 'a' }]);
await e.insert('t2', [{ id: 'b' }]);
await e.commitTransaction();
// commit 后的数据在日志(未 checkpoint)—— 崩溃模拟
await e.close();
const e2 = new KVStoreEngine(medium);
await e2.open(dbName, 1);
expect(await e2.count('t1')).toBe(1);
expect(await e2.count('t2')).toBe(1);
await e2.close();
});
it('级联环(A→B→A)删除不无限递归', async () => {
const e = await createEngine();
await e.createTable(createSchema('ta', {
id: { type: 'string', primaryKey: true },
ref_b: { type: 'string', references: 'tb.id', onDelete: 'CASCADE' },
}));
await e.createTable(createSchema('tb', {
id: { type: 'string', primaryKey: true },
ref_a: { type: 'string', references: 'ta.id', onDelete: 'CASCADE' },
}));
await e.insert('ta', [{ id: 'a1', ref_b: 'b1' }]);
await e.insert('tb', [{ id: 'b1', ref_a: 'a1' }]);
// 删除不应死循环(visited 保护)
await e.delete('ta', { table: 'ta', where: { id: 'a1' } });
expect(await e.count('ta')).toBe(0);
await e.close();
});
it('主键变更后索引一致(旧值查询无结果,新值查询命中)', async () => {
const e = await createEngine();
await e.createTable(createSchema('items', {
id: { type: 'string', primaryKey: true },
tag: { type: 'string', index: true },
}));
await e.insert('items', [{ id: '1', tag: 'old' }]);
await e.update('items', { table: 'items', where: { id: '1' } }, { id: '10', tag: 'new' });
// 旧主键查询空
expect(await e.find('items', { table: 'items', where: { id: '1' } })).toHaveLength(0);
// 新主键命中
expect(await e.find('items', { table: 'items', where: { id: '10' } })).toHaveLength(1);
// 索引:旧 tag 空,新 tag 命中
expect(await e.find('items', { table: 'items', where: { tag: 'old' } })).toHaveLength(0);
expect(await e.find('items', { table: 'items', where: { tag: 'new' } })).toHaveLength(1);
await e.close();
// 重启后仍一致
const e2 = new KVStoreEngine(new SharedMemoryBackend());
await e2.open((e as any).dbName, 1);
expect(await e2.find('items', { table: 'items', where: { tag: 'new' } })).toHaveLength(1);
expect(await e2.find('items', { table: 'items', where: { id: '10' } })).toHaveLength(1);
await e2.close();
});
it('空表操作(count/find/clear 空表)正常', async () => {
const e = await createEngine();
await e.createTable(createSchema('empty', { id: { type: 'string', primaryKey: true } }));
expect(await e.count('empty')).toBe(0);
expect(await e.find('empty', { table: 'empty' })).toHaveLength(0);
await e.clear('empty');
await e.close();
});
it('大批量 delete5000 行)持久化后重启正确', async () => {
const dbName = uniqueDB();
const medium = new SharedMemoryBackend();
const e = new KVStoreEngine(medium);
await e.open(dbName, 1);
await e.createTable(createSchema('bulk', { id: { type: 'number', primaryKey: true } }));
const rows = [] as Record<string, unknown>[];
for (let i = 0; i < 5000; i++) rows.push({ id: i });
await e.insert('bulk', rows);
await e.delete('bulk', { table: 'bulk', where: { id: { $gte: 4000 } } });
expect(await e.count('bulk')).toBe(4000);
await e.close();
const e2 = new KVStoreEngine(medium);
await e2.open(dbName, 1);
expect(await e2.count('bulk')).toBe(4000);
const remaining = await e2.find('bulk', { table: 'bulk', where: { id: { $gte: 3000 } } });
expect(remaining).toHaveLength(1000);
await e2.close();
}, 60000);
});
// ===================================================================
// SharedMemoryBackend 全分支 + 日志解析损坏分支
// ===================================================================
describe('SharedMemoryBackend — 全分支', () => {
it('append 到已存在 key(拼接)与不存在 key(新建)', async () => {
const medium = new SharedMemoryBackend();
await medium.open('sm-1');
await medium.append('log', enc('AAA'));
await medium.append('log', enc('BBB'));
expect(dec(await medium.read('log'))).toBe('AAABBB');
await medium.close();
});
it('writeMany / deleteMany 原子批量', async () => {
const medium = new SharedMemoryBackend();
await medium.open('sm-2');
await medium.writeMany({ a: enc('1'), b: enc('2'), c: enc('3') });
expect(dec(await medium.read('b'))).toBe('2');
await medium.deleteMany(['a', 'c']);
expect(await medium.exists('a')).toBe(false);
expect(await medium.exists('b')).toBe(true);
// writeMany 覆盖已有 key
await medium.writeMany({ b: enc('B2') });
expect(dec(await medium.read('b'))).toBe('B2');
await medium.close();
});
it('close 后操作安全(不抛错;写入不持久化)', async () => {
const medium = new SharedMemoryBackend();
await medium.open('sm-3');
await medium.write('k', enc('V'));
await medium.close();
expect(await medium.read('k')).toBeNull(); // close 后内存清空
await medium.write('late', enc('X')); // 不抛错(游离内存,不持久化)
await medium.delete('k'); // 不抛错
await medium.clear(); // 不抛错
});
it('跨实例共享:close 后新实例可读(持久化语义)', async () => {
const medium1 = new SharedMemoryBackend();
await medium1.open('sm-4');
await medium1.write('k', enc('PERSIST'));
await medium1.close();
const medium2 = new SharedMemoryBackend();
await medium2.open('sm-4');
expect(dec(await medium2.read('k'))).toBe('PERSIST');
await medium2.close();
});
});
describe('KVStore 日志解析 — 损坏分支', () => {
it('onCorrupt 返回 false → 立即停止扫描', () => {
const good1 = encodeLogRecord(1, { a: enc('1') });
const bad = encodeLogRecord(2, { b: enc('2') });
bad[10] ^= 0xff; // 破坏
const good2 = encodeLogRecord(3, { c: enc('3') });
const combined = new Uint8Array(good1.byteLength + bad.byteLength + good2.byteLength);
combined.set(good1, 0);
combined.set(bad, good1.byteLength);
combined.set(good2, good1.byteLength + bad.byteLength);
const seqs: number[] = [];
let corruptCalls = 0;
const count = parseLogRecords(combined, (r) => seqs.push(r.seq), () => {
corruptCalls++;
return false; // 停止
});
expect(count).toBe(1); // 只有第一条
expect(seqs).toEqual([1]);
expect(corruptCalls).toBe(1);
});
it('损坏条目(keyLen 越界)→ 记录被丢弃', () => {
// 手工构造:entry 的 keyLen 超长导致解析失败
const buf = new ArrayBuffer(4 + 4 + 4 + 1 + 4 + 0xFFFFFF + 4);
const view = new DataView(buf);
let o = 0;
const recLen = buf.byteLength - 4;
view.setUint32(o, recLen, false); o += 4;
view.setUint32(o, 1, false); o += 4; // seq
view.setUint32(o, 1, false); o += 4; // entryCount
view.setUint8(o, 1); o += 1; // op PUT
view.setUint32(o, 0xFFFFFF, false); o += 4; // keyLen 超长
// crc(随便填,解析会在 keyLen 越界处停止)
view.setUint32(o, 0, false);
const data = new Uint8Array(buf);
let corrupt = 0;
const count = parseLogRecords(data, () => {}, () => { corrupt++; return false; });
expect(count).toBe(0);
expect(corrupt).toBe(1);
});
});
// ===================================================================
// AriaEngine 级联环(与 MemoryEngine 对齐)
// ===================================================================
describe('AriaEngine — 级联环保护', () => {
it('A→B→A 级联环删除不无限递归(visited 保护)', async () => {
const { AriaEngine } = require('../src/engine/aria/index');
const engine = new AriaEngine({ storageBackend: 'memory' });
await engine.open(`aria-loop-${Date.now()}-${Math.random()}`, 1);
await engine.createTable(createSchema('ta', {
id: { type: 'string', primaryKey: true },
ref_b: { type: 'string', references: 'tb.id', onDelete: 'CASCADE' },
}));
await engine.createTable(createSchema('tb', {
id: { type: 'string', primaryKey: true },
ref_a: { type: 'string', references: 'ta.id', onDelete: 'CASCADE' },
}));
await engine.insert('ta', [{ id: 'a1', ref_b: 'b1' }]);
await engine.insert('tb', [{ id: 'b1', ref_a: 'a1' }]);
// 不应死循环(RangeError
await engine.delete('ta', { table: 'ta', where: { id: 'a1' } });
expect(await engine.count('ta')).toBe(0);
expect(await engine.count('tb')).toBe(0);
await engine.close();
});
it('更深级联链(A→B→C→D)删除完整', async () => {
const { AriaEngine } = require('../src/engine/aria/index');
const engine = new AriaEngine({ storageBackend: 'memory' });
await engine.open(`aria-chain-${Date.now()}-${Math.random()}`, 1);
await engine.createTable(createSchema('ta', {
id: { type: 'string', primaryKey: true },
ref: { type: 'string', references: 'tb.id', onDelete: 'CASCADE' },
}));
await engine.createTable(createSchema('tb', {
id: { type: 'string', primaryKey: true },
ref: { type: 'string', references: 'tc.id', onDelete: 'CASCADE' },
}));
await engine.createTable(createSchema('tc', {
id: { type: 'string', primaryKey: true },
ref: { type: 'string', references: 'td.id', onDelete: 'CASCADE' },
}));
await engine.createTable(createSchema('td', {
id: { type: 'string', primaryKey: true },
}));
await engine.insert('td', [{ id: 'd1' }]);
await engine.insert('tc', [{ id: 'c1', ref: 'd1' }]);
await engine.insert('tb', [{ id: 'b1', ref: 'c1' }]);
await engine.insert('ta', [{ id: 'a1', ref: 'b1' }]);
// 删除链尾 td.d1 → 级联 tc(引用 td)→ tb(引用 tc)→ ta(引用 tb)
await engine.delete('td', { table: 'td', where: { id: 'd1' } });
expect(await engine.count('ta')).toBe(0);
expect(await engine.count('tb')).toBe(0);
expect(await engine.count('tc')).toBe(0);
expect(await engine.count('td')).toBe(0);
await engine.close();
});
});