release: v0.5.1 — 存储后端生产级硬化(CRC-32/全库加密/WAL分片/页面化存储/多标签页锁/e2e)+ 深度审查修复(假实现接线/死代码清理)
CI / test (18.x) (push) Successful in 10m10s
CI / test (20.x) (push) Successful in 10m10s
CI / test (22.x) (push) Successful in 10m6s
CI / e2e (push) Successful in 9m51s
CI / test (24.x) (push) Successful in 10m28s

This commit is contained in:
thzxx
2026-08-10 12:07:00 +08:00
parent cff98b0903
commit 334067d89e
88 changed files with 15713 additions and 10626 deletions
+218
View File
@@ -0,0 +1,218 @@
/**
* AriaEngine WAL — CRC-32 校验升级测试
*
* 覆盖:
* 1. 新算法记录往返(append → recover
* 2. 篡改记录字节 → CRC 不匹配跳过
* 3. 旧版弱校验记录兼容(双算法探测)
* 4. appendBatch 组提交记录往返
* 5. 损坏记录跳过不影响后续完整记录
*/
import { WAL } from '../../src/engine/aria/wal/log';
import { WALRecordType } from '../../src/engine/aria/types';
// ===================================================================
// WAL 内存 Store(模拟 IStorageBackend 单记录语义)
// ===================================================================
class MemoryWALStore {
chunks: Uint8Array[] = [];
truncated = false;
async append(data: Uint8Array): Promise<void> {
this.chunks.push(data);
}
async readAll(): Promise<Uint8Array> {
const total = this.chunks.reduce((s, c) => s + c.byteLength, 0);
const combined = new Uint8Array(total);
let off = 0;
for (const c of this.chunks) { combined.set(c, off); off += c.byteLength; }
return combined;
}
async truncate(): Promise<void> {
this.chunks = [];
this.truncated = true;
}
async exists(): Promise<boolean> {
return this.chunks.length > 0;
}
}
function makeRecord(over: Partial<import('../../src/engine/aria/types').WALRecord> = {}) {
return {
type: WALRecordType.INSERT,
txnId: 0,
tableName: 'users',
key: 'u-1',
data: { id: 'u-1', name: 'Alice' },
...over,
};
}
describe('AriaEngine — WAL CRC-32 校验', () => {
it('新算法记录 append → recover 往返完整', async () => {
const store = new MemoryWALStore();
const wal = new WAL(store as never, true, 'full');
await wal.append(makeRecord({ key: 'u-1' }));
await wal.append(makeRecord({ key: 'u-2', data: { id: 'u-2', name: 'Bob' } }));
const records: import('../../src/engine/aria/types').WALRecord[] = [];
await wal.recover((r) => records.push(r));
expect(records).toHaveLength(2);
expect(records[0].key).toBe('u-1');
expect(records[1].data).toEqual({ id: 'u-2', name: 'Bob' });
});
it('appendBatch 组提交记录往返完整', async () => {
const store = new MemoryWALStore();
const wal = new WAL(store as never, true, 'full');
await wal.appendBatch([
makeRecord({ key: 'a' }),
makeRecord({ key: 'b' }),
makeRecord({ key: 'c' }),
]);
const records: import('../../src/engine/aria/types').WALRecord[] = [];
await wal.recover((r) => records.push(r));
expect(records.map((r) => r.key)).toEqual(['a', 'b', 'c']);
expect(records.map((r) => r.lsn)).toEqual([1, 2, 3]);
});
it('篡改记录数据 → CRC 不匹配跳过', async () => {
const store = new MemoryWALStore();
const wal = new WAL(store as never, true, 'full');
await wal.append(makeRecord({ key: 'good-1' }));
await wal.append(makeRecord({ key: 'evil' }));
await wal.append(makeRecord({ key: 'good-2' }));
// 篡改第二条记录的 data 字节(LSN=2 的记录)
const raw = await store.readAll();
const view = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
// 定位第二条记录起始:按记录格式顺序扫描直到 LSN=2
// [lsn:4][type:1][txnId:4][tblLen:2][table][keyLen:2][key][jsonLen:4][json][crc:4]
let offset = 0;
while (offset + 4 <= raw.byteLength && view.getUint32(offset, false) !== 2) {
const tblLen = view.getUint16(offset + 9, false);
const keyLenOff = offset + 11 + tblLen;
const keyLen = view.getUint16(keyLenOff, false);
const jsonLenOff = keyLenOff + 2 + keyLen;
const jsonLen = view.getUint32(jsonLenOff, false);
offset += 4 + 1 + 4 + 2 + tblLen + 2 + keyLen + 4 + jsonLen + 4;
}
expect(view.getUint32(offset, false)).toBe(2);
// 篡改 data 区(json 起始处第 4 字节)
const tblLen2 = view.getUint16(offset + 9, false);
const jsonStart = offset + 4 + 1 + 4 + 2 + tblLen2 + 2 + view.getUint16(offset + 11 + tblLen2, false) + 4;
raw[jsonStart + 4] ^= 0x01;
// 写回 storereadAll 返回的是合并副本)
store.chunks = [raw];
// 恢复:坏记录被跳过,前后好记录保留
const records: import('../../src/engine/aria/types').WALRecord[] = [];
await wal.recover((r) => records.push(r));
expect(records.map((r) => r.key)).toEqual(['good-1', 'good-2']);
});
it('旧版弱校验记录兼容(双算法探测)', async () => {
const store = new MemoryWALStore();
const wal = new WAL(store as never, true, 'full');
// 手工构造一条"旧算法"记录:与 encodeRecord 布局一致但 CRC 用旧算法计算
const record = {
lsn: 1,
type: WALRecordType.INSERT as number,
txnId: 0,
tableName: 'legacy',
key: 'old',
data: { id: 'old', v: 1 },
};
const encoder = new TextEncoder();
const tblB = encoder.encode(record.tableName);
const keyB = encoder.encode(record.key);
const jsonB = encoder.encode(JSON.stringify(record.data));
const size = 4 + 1 + 4 + 2 + tblB.length + 2 + keyB.length + 4 + jsonB.length + 4;
const buf = new ArrayBuffer(size);
const v = new DataView(buf);
let o = 0;
v.setUint32(o, record.lsn, false); o += 4;
v.setUint8(o, record.type); o += 1;
v.setUint32(o, record.txnId, false); o += 4;
v.setUint16(o, tblB.length, false); o += 2;
new Uint8Array(buf).set(tblB, o); o += tblB.length;
v.setUint16(o, keyB.length, false); o += 2;
new Uint8Array(buf).set(keyB, o); o += keyB.length;
v.setUint32(o, jsonB.length, false); o += 4;
new Uint8Array(buf).set(jsonB, o); o += jsonB.length;
// 旧算法 CRC
let crc = 0;
const u8 = new Uint8Array(buf, 0, o);
for (let i = 0; i < u8.length; i++) crc = ((crc << 5) - crc + u8[i]) | 0;
v.setUint32(o, crc >>> 0, false);
store.chunks.push(new Uint8Array(buf));
const records: import('../../src/engine/aria/types').WALRecord[] = [];
await wal.recover((r) => records.push(r));
expect(records).toHaveLength(1);
expect(records[0].tableName).toBe('legacy');
expect(records[0].data).toEqual({ id: 'old', v: 1 });
});
it('旧算法记录 + 新算法记录混合 → 全部正确恢复', async () => {
const store = new MemoryWALStore();
const wal = new WAL(store as never, true, 'full');
// 先放一条旧算法记录
const record = {
lsn: 1,
type: WALRecordType.DELETE as number,
txnId: 0,
tableName: 't',
key: 'old-key',
data: undefined as Record<string, unknown> | undefined,
};
const encoder = new TextEncoder();
const tblB = encoder.encode(record.tableName);
const keyB = encoder.encode(record.key);
const size = 4 + 1 + 4 + 2 + tblB.length + 2 + keyB.length + 4 + 4;
const buf = new ArrayBuffer(size);
const v = new DataView(buf);
let o = 0;
v.setUint32(o, record.lsn, false); o += 4;
v.setUint8(o, record.type); o += 1;
v.setUint32(o, record.txnId, false); o += 4;
v.setUint16(o, tblB.length, false); o += 2;
new Uint8Array(buf).set(tblB, o); o += tblB.length;
v.setUint16(o, keyB.length, false); o += 2;
new Uint8Array(buf).set(keyB, o); o += keyB.length;
v.setUint32(o, 0, false); o += 4; // jsonLen=0
let crc = 0;
const u8 = new Uint8Array(buf, 0, o);
for (let i = 0; i < u8.length; i++) crc = ((crc << 5) - crc + u8[i]) | 0;
v.setUint32(o, crc >>> 0, false);
store.chunks.push(new Uint8Array(buf));
// 再追加一条新算法记录(LSN=2)
await wal.append(makeRecord({ key: 'new-key', lsn: 2 } as never));
const records: import('../../src/engine/aria/types').WALRecord[] = [];
await wal.recover((r) => records.push(r));
expect(records.map((r) => r.key)).toEqual(['old-key', 'new-key']);
expect(records[0].type).toBe(WALRecordType.DELETE);
});
it('checkpoint 截断后 exists 返回 false', async () => {
const store = new MemoryWALStore();
const wal = new WAL(store as never, true, 'full');
await wal.append(makeRecord({ key: 'x' }));
expect(await store.exists()).toBe(true);
await wal.checkpoint();
expect(store.truncated).toBe(true);
expect(await store.exists()).toBe(false);
});
});