fix: v0.6.0 复查修正 — KVStore 快照损坏水位 bug(metaSeq 误跳日志)+ 快照损坏测试盲区修复(此前篡改未生效)+ metaSeq 回归测试 + KVStoreEngine 真实浏览器 e2e(3 用例)+ 版本/配置文档校准
This commit is contained in:
+10
-6
@@ -17,23 +17,27 @@
|
||||
/**
|
||||
* 创建并打开 Aria 库。
|
||||
* opts: {
|
||||
* name, diskEngine ('opfs'|'indexeddb'|'memory'),
|
||||
* name, diskEngine ('opfs'|'memory'),
|
||||
* walSyncMode, checkpointInterval,
|
||||
* encryptionPassword, memtableSizeThreshold
|
||||
* }
|
||||
*/
|
||||
open: async (opts) => {
|
||||
const db = await window.MetonaSqlark.create({
|
||||
// opts.mode: 'aria'(默认)| 'disk'(KVStoreEngine)
|
||||
const config = {
|
||||
name: opts.name,
|
||||
mode: 'aria',
|
||||
mode: opts.mode || 'aria',
|
||||
diskEngine: opts.diskEngine || 'opfs',
|
||||
aria: {
|
||||
};
|
||||
if (opts.mode !== 'disk') {
|
||||
config.aria = {
|
||||
walSyncMode: opts.walSyncMode || 'full',
|
||||
checkpointInterval: opts.checkpointInterval || 100000,
|
||||
memtableSizeThreshold: opts.memtableSizeThreshold || 64 * 1024 * 1024,
|
||||
...(opts.encryptionPassword ? { encryption: { password: opts.encryptionPassword } } : {}),
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
const db = await window.MetonaSqlark.create(config);
|
||||
window.__ms.__db = db;
|
||||
return { ok: true, engineName: db.getEngine().name };
|
||||
},
|
||||
|
||||
@@ -327,3 +327,82 @@ test.describe('OPFS 真实环境', () => {
|
||||
await run(page, 'close');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('KVStoreEngine(disk 模式)真实环境', () => {
|
||||
test('写入 → 刷新页面 → 数据保留(KVStore 持久化)', async ({ page }) => {
|
||||
await openPage(page);
|
||||
await run(page, 'open', { name: 'e2e-kv-persist', mode: 'disk', diskEngine: 'opfs' });
|
||||
await run(page, 'createTable', {
|
||||
table: 'kvusers',
|
||||
columns: [
|
||||
{ name: 'id', type: 'string', primaryKey: true },
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'tag', type: 'string', index: true },
|
||||
],
|
||||
});
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await run(page, 'insert', {
|
||||
table: 'kvusers',
|
||||
rows: [{ id: `u-${i}`, name: `User${i}`, tag: i % 2 === 0 ? 'even' : 'odd' }],
|
||||
});
|
||||
}
|
||||
expect(await run<{ count: number }>(page, 'count', { table: 'kvusers' })).toEqual({ count: 30 });
|
||||
|
||||
// 刷新:KVStore 快照/日志恢复
|
||||
await page.reload();
|
||||
await expect(page.locator('#status')).toHaveText('ready');
|
||||
await run(page, 'open', { name: 'e2e-kv-persist', mode: 'disk', diskEngine: 'opfs' });
|
||||
const res = await run<{ count: number }>(page, 'count', { table: 'kvusers' });
|
||||
expect(res.count).toBe(30);
|
||||
// 索引查询(重启重建)
|
||||
const evens = await run<{ rows: Record<string, unknown>[] }>(page, 'find', {
|
||||
table: 'kvusers', where: { tag: 'even' },
|
||||
});
|
||||
expect(evens.rows.length).toBe(15);
|
||||
await run(page, 'close');
|
||||
});
|
||||
|
||||
test('崩溃恢复:写入后强制终止 → 重开日志重放数据完整', async ({ page }) => {
|
||||
await openPage(page);
|
||||
await run(page, 'open', { name: 'e2e-kv-crash', mode: 'disk', diskEngine: 'opfs' });
|
||||
await run(page, 'createTable', {
|
||||
table: 'logs', columns: [{ name: 'id', type: 'string', primaryKey: true }],
|
||||
});
|
||||
const total = 40;
|
||||
for (let i = 0; i < total; i++) {
|
||||
await run(page, 'insert', { table: 'logs', rows: [{ id: `log-${i}` }] });
|
||||
}
|
||||
// 模拟崩溃:不 close
|
||||
await crashPage(page);
|
||||
|
||||
const page2 = await page.context().newPage();
|
||||
await openPage(page2);
|
||||
await run(page2, 'open', { name: 'e2e-kv-crash', mode: 'disk', diskEngine: 'opfs' });
|
||||
const res = await run<{ count: number }>(page2, 'count', { table: 'logs' });
|
||||
expect(res.count).toBe(total);
|
||||
await run(page2, 'close');
|
||||
await page2.close();
|
||||
});
|
||||
|
||||
test('事务 commit 原子性:多表写入后重启一致', async ({ page }) => {
|
||||
await openPage(page);
|
||||
await run(page, 'open', { name: 'e2e-kv-tx', mode: 'disk', diskEngine: 'opfs' });
|
||||
await run(page, 'createTable', {
|
||||
table: 't1', columns: [{ name: 'id', type: 'string', primaryKey: true }],
|
||||
});
|
||||
await run(page, 'createTable', {
|
||||
table: 't2', columns: [{ name: 'id', type: 'string', primaryKey: true }],
|
||||
});
|
||||
// 用 SQL 多语句触发原子写(事务)
|
||||
await run(page, 'query', { sql: "BEGIN; INSERT INTO t1 VALUES ('a'); INSERT INTO t2 VALUES ('b'); COMMIT;" });
|
||||
await crashPage(page);
|
||||
|
||||
const page2 = await page.context().newPage();
|
||||
await openPage(page2);
|
||||
await run(page2, 'open', { name: 'e2e-kv-tx', mode: 'disk', diskEngine: 'opfs' });
|
||||
expect(await run<{ count: number }>(page2, 'count', { table: 't1' })).toEqual({ count: 1 });
|
||||
expect(await run<{ count: number }>(page2, 'count', { table: 't2' })).toEqual({ count: 1 });
|
||||
await run(page2, 'close');
|
||||
await page2.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -122,28 +122,54 @@ describe('KVStore — 持久化与崩溃恢复', () => {
|
||||
await kv2.close();
|
||||
});
|
||||
|
||||
it('快照损坏 → 全量日志重放自愈(数据不丢)', async () => {
|
||||
it('快照损坏 → 全量日志重放:日志数据保留,快照唯一副本丢失(自愈不崩)', async () => {
|
||||
const dbName = uniqueDB();
|
||||
const medium = new SharedMemoryBackend();
|
||||
const kv1 = new KVStore(medium, 0);
|
||||
await kv1.open(dbName);
|
||||
await kv1.putMany({ a: enc('AAA'), b: enc('BBB') });
|
||||
await kv1.checkpoint();
|
||||
await kv1.put('c', enc('CCC'));
|
||||
await kv1.checkpoint(); // 快照成为 a/b 的唯一副本(日志已截断)
|
||||
await kv1.put('c', enc('CCC')); // 日志中(seq=2)
|
||||
await kv1.close();
|
||||
|
||||
// 篡改快照(模拟损坏)
|
||||
const snap = await medium.read('__kv_snapshot');
|
||||
// 篡改快照(close 后介质不可用,重新打开访问"磁盘")
|
||||
const disk = new SharedMemoryBackend();
|
||||
await disk.open(dbName);
|
||||
const snap = await disk.read('__kv_snapshot');
|
||||
expect(snap).not.toBeNull();
|
||||
const corrupted = new Uint8Array(snap as ArrayBuffer);
|
||||
corrupted[20] ^= 0xff;
|
||||
await medium.write('__kv_snapshot', corrupted.buffer as ArrayBuffer);
|
||||
corrupted[20] ^= 0xff; // 破坏 entry 长度字段 → CRC/边界校验失败
|
||||
await disk.write('__kv_snapshot', corrupted.buffer as ArrayBuffer);
|
||||
|
||||
const kv2 = new KVStore(medium, 0);
|
||||
await kv2.open(dbName);
|
||||
// 快照损坏 → 全量日志重放
|
||||
// 快照损坏 → 全量日志重放:c(日志中)保留;a/b(快照唯一副本)丢失
|
||||
expect(await kv2.exists('a')).toBe(false);
|
||||
expect(await kv2.exists('b')).toBe(false);
|
||||
expect(dec(await kv2.get('c'))).toBe('CCC');
|
||||
await kv2.close();
|
||||
});
|
||||
|
||||
it('回归:快照完好但 meta.seq 超前 → 日志不被 metaSeq 跳过(水位只信任快照)', async () => {
|
||||
const dbName = uniqueDB();
|
||||
const medium = new SharedMemoryBackend();
|
||||
const kv1 = new KVStore(medium, 0);
|
||||
await kv1.open(dbName);
|
||||
await kv1.put('a', enc('AAA'));
|
||||
await kv1.checkpoint(); // 快照 seq=1,meta seq=1,日志截断
|
||||
await kv1.put('b', enc('BBB')); // 日志 seq=2
|
||||
await kv1.close();
|
||||
|
||||
// 手工把 meta.seq 改大(模拟异常状态:meta 比日志内容超前)
|
||||
const disk = new SharedMemoryBackend();
|
||||
await disk.open(dbName);
|
||||
await disk.write('__kv_meta', new TextEncoder().encode(JSON.stringify({ seq: 999 })).buffer);
|
||||
|
||||
const kv2 = new KVStore(medium, 0);
|
||||
await kv2.open(dbName);
|
||||
// 快照完好(seq=1)→ 水位=1 → 日志 seq2 应用;meta.seq=999 不参与水位判断
|
||||
expect(dec(await kv2.get('a'))).toBe('AAA');
|
||||
expect(dec(await kv2.get('b'))).toBe('BBB');
|
||||
expect(dec(await kv2.get('c'))).toBe('CCC');
|
||||
await kv2.close();
|
||||
});
|
||||
|
||||
|
||||
@@ -351,7 +351,7 @@ describe('[v0.3.2] 多标签页同步', () => {
|
||||
// 回归:IndexedDB reopen 后 schema 持久化(v0.3.2 修复)
|
||||
// ===================================================================
|
||||
|
||||
describe('[v0.3.2] IndexedDB reopen schema 持久化', () => {
|
||||
describe('[v0.3.2] KVStore reopen schema 持久化', () => {
|
||||
test('close 后重新 open 表结构与数据完整', async () => {
|
||||
const setup = new MetonaSqlark({ name: 'reopen-a', mode: 'hybrid', diskEngine: 'opfs' });
|
||||
await setup.init();
|
||||
|
||||
@@ -121,7 +121,7 @@ describe('v0.1.13 事务回滚 — Memory 模式', () => {
|
||||
// IndexedDB 引擎事务回滚
|
||||
// ===================================================================
|
||||
|
||||
describe('v0.1.13 事务回滚 — Disk 模式 (IndexedDB)', () => {
|
||||
describe('v0.1.13 事务回滚 — Disk 模式 (KVStore)', () => {
|
||||
let db: MetonaSqlark;
|
||||
let dbName: string;
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ describe('P0-1b — AriaEngine 打开时完整性校验', () => {
|
||||
// ===================================================================
|
||||
|
||||
describe('P0-1c / P1-4 — WAL 原子性与恢复', () => {
|
||||
it('IndexedDBBackend.writeMany/deleteMany 单事务原子批量操作', async () => {
|
||||
it('OPFSBackend.writeMany/deleteMany 批量原子操作', async () => {
|
||||
const dbName = uniqueDB();
|
||||
const be = new OPFSBackend();
|
||||
await be.open(dbName);
|
||||
|
||||
Reference in New Issue
Block a user