feat: v0.2.4 惰性扫描 + 写背压 + 内存预算 + ANALYZE + 加密 + Savepoint + 在线备份
CI / test (18.x) (push) Failing after 4m59s
CI / test (20.x) (push) Failing after 5m0s
CI / test (22.x) (push) Failing after 4m59s
CI / test (24.x) (push) Failing after 4m58s

This commit is contained in:
thzxx
2026-07-27 21:55:57 +08:00
parent 82e6baaae9
commit f248e5fb05
4 changed files with 193 additions and 9 deletions
+54
View File
@@ -0,0 +1,54 @@
/**
* AriaEngine Crypto — 页面级 AES-GCM 加密
* @module engine/aria/crypto
*
* 使用 Web Crypto API (SubtleCrypto) 进行 AES-256-GCM 加密。
* 浏览器原生支持,无需额外依赖。
*/
const ALGO = 'AES-GCM';
const KEY_LENGTH = 256;
const IV_LENGTH = 12; // GCM 推荐 96-bit nonce
let cryptoKey: CryptoKey | null = null;
let enabled = false;
/**
* 初始化加密密钥。传入原始密码字符串,通过 PBKDF2 派生 AES 密钥。
*/
export async function initCrypto(password: string, salt?: Uint8Array): Promise<Uint8Array> {
const enc = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw', enc.encode(password), 'PBKDF2', false, ['deriveKey'],
);
const actualSalt = salt || crypto.getRandomValues(new Uint8Array(16));
cryptoKey = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt: actualSalt, iterations: 100000, hash: 'SHA-256' },
keyMaterial, { name: ALGO, length: KEY_LENGTH }, false, ['encrypt', 'decrypt'],
);
enabled = true;
return actualSalt;
}
/** 是否已启用加密 */
export function isCryptoEnabled(): boolean { return enabled; }
/** 加密 ArrayBuffer,返回 { iv, ciphertext } */
export async function encryptPage(data: ArrayBuffer): Promise<{ iv: Uint8Array; data: ArrayBuffer }> {
if (!cryptoKey) throw new Error('Crypto not initialized');
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
const ciphertext = await crypto.subtle.encrypt({ name: ALGO, iv }, cryptoKey, data);
return { iv, data: ciphertext };
}
/** 解密 ArrayBuffer */
export async function decryptPage(iv: Uint8Array, data: ArrayBuffer): Promise<ArrayBuffer> {
if (!cryptoKey) throw new Error('Crypto not initialized');
return crypto.subtle.decrypt({ name: ALGO, iv }, cryptoKey, data);
}
/** 关闭加密,清除密钥 */
export function closeCrypto(): void {
cryptoKey = null;
enabled = false;
}
+88
View File
@@ -302,6 +302,7 @@ export class AriaEngine implements IStorageEngine {
}
this.opCounter += rows.length;
this.checkMemoryBudget();
await this.checkpointManager.tick();
this.tryGC();
return pks;
@@ -519,6 +520,49 @@ export class AriaEngine implements IStorageEngine {
this.currentTxnId = null;
}
// ---- Savepoint 嵌套事务 ----
private savepoints: Map<string, { txnId: number; snapshot: Map<string, Record<string, unknown>> | null }> = new Map();
async savepoint(name: string): Promise<void> {
if (!this.currentTxnId) throw new DatabaseError('No active transaction for savepoint', 'TX_NONE');
if (this.savepoints.has(name)) throw new DatabaseError(`Savepoint "${name}" already exists`, 'SAVEPOINT_EXISTS');
// 保存当前事务快照
this.savepoints.set(name, {
txnId: this.currentTxnId,
snapshot: this.txnSnapshot ? new Map(this.txnSnapshot) : null,
});
}
async rollbackToSavepoint(name: string): Promise<void> {
const sp = this.savepoints.get(name);
if (!sp) throw new DatabaseError(`Savepoint "${name}" not found`, 'SAVEPOINT_NOT_FOUND');
// 恢复到 savepoint 时的快照
this.txnSnapshot = sp.snapshot ? new Map(sp.snapshot) : null;
// 清除此 savepoint 之后的所有 savepoint
let found = false;
for (const [k] of this.savepoints) {
if (k === name) { found = true; continue; }
if (found) this.savepoints.delete(k);
}
}
async releaseSavepoint(name: string): Promise<void> {
if (!this.savepoints.has(name)) throw new DatabaseError(`Savepoint "${name}" not found`, 'SAVEPOINT_NOT_FOUND');
this.savepoints.delete(name);
}
// ---- 在线备份 ----
async backup(): Promise<Record<string, Record<string, unknown>[]>> {
this.ensureOpen();
const result: Record<string, Record<string, unknown>[]> = {};
for (const tableName of this.schemas.keys()) {
result[tableName] = this.getAllRows(tableName);
}
return result;
}
// =======================================================================
// 内部
// =======================================================================
@@ -821,6 +865,50 @@ export class AriaEngine implements IStorageEngine {
}
}
/** 检查内存预算,超出时强制 flush + GC */
private checkMemoryBudget(): void {
const maxBytes = this.config.maxMemoryMB * 1024 * 1024;
const used = this.lsm.getEstimatedMemory();
if (used > maxBytes) {
this.lsm.flush().catch(() => {});
this.mvcc.gc(50);
}
}
/**
* ANALYZE: 收集表统计信息
* 返回行数、平均行大小、索引深度等
*/
async analyzeTable(tableName: string): Promise<Record<string, unknown>> {
this.ensureOpen();
this.ensureTable(tableName);
const rows = this.getAllRows(tableName);
const stats: Record<string, unknown> = {
table: tableName,
rowCount: rows.length,
avgRowSize: rows.length > 0
? Math.round(rows.reduce((s, r) => s + JSON.stringify(r).length, 0) / rows.length)
: 0,
indexDepth: this.lsm.getStats().levelCounts.filter((c: number) => c > 0).length,
sstableCount: this.lsm.getStats().sstableCount,
memtableSize: this.lsm.getStats().memtableSize,
estimatedMemory: this.lsm.getEstimatedMemory(),
};
// 列基数统计
const schema = this.schemas.get(tableName);
if (schema && rows.length > 0) {
const columnStats: Record<string, unknown> = {};
for (const colName of Object.keys(schema.columns)) {
const values = new Set(rows.map((r) => String(r[colName])));
columnStats[colName] = { distinctValues: values.size };
}
stats.columnStats = columnStats;
}
return stats;
}
private ensureOpen(): void {
if (!this.opened) throw new DatabaseError('AriaEngine not opened', 'DB_NOT_OPEN');
}
+35 -9
View File
@@ -123,6 +123,11 @@ export class LSM {
// =======================================================================
put(key: string, value: Record<string, unknown>): void {
// 写背压:level 0 SSTable 过多时等待 compaction
if (this.levels[0].length >= 8) {
// 同步执行一次 compaction 缓解压力
this.compactLevelSync(0);
}
this.memtable.put(key, value);
this.operationCount++;
if (this.memtable.shouldFlush()) {
@@ -131,6 +136,9 @@ export class LSM {
}
delete(key: string): void {
if (this.levels[0].length >= 8) {
this.compactLevelSync(0);
}
this.memtable.put(key, { __tombstone: true } as unknown as Record<string, unknown>);
this.operationCount++;
if (this.memtable.shouldFlush()) {
@@ -138,6 +146,14 @@ export class LSM {
}
}
/** 获取估算内存使用(字节) */
getEstimatedMemory(): number {
let mem = this.memtable.getEstimatedSize();
if (this.immutableMemtable) mem += this.immutableMemtable.getEstimatedSize();
for (const [, buf] of this.sstableCache) mem += buf.byteLength;
return mem;
}
freezeMemtable(): void {
if (this.immutableMemtable) {
this.flushImmutableSync();
@@ -242,9 +258,19 @@ export class LSM {
}
rangeScan(startKey: string, endKey: string): [string, Record<string, unknown>][] {
const result: [string, Record<string, unknown>][] = [];
this.rangeScanLazy(startKey, endKey, (k, v) => result.push([k, v]));
return result;
}
/** 惰性范围扫描:通过回调逐条返回,不一次性物化所有源 */
rangeScanLazy(
startKey: string,
endKey: string,
callback: (key: string, value: Record<string, unknown>) => void,
): void {
const mergeIter = new MergeIterator();
// MemTable(最新优先)
mergeIter.addSource(new ArrayEntrySource(
this.memtable.rangeScan(startKey, endKey),
));
@@ -255,23 +281,23 @@ export class LSM {
));
}
// SSTable
for (let level = 0; level < MAX_LSM_LEVELS; level++) {
for (const meta of this.levels[level]) {
if (endKey < meta.minKey || startKey > meta.maxKey) continue;
const reader = this.loadSSTableReader(meta);
if (!reader) continue;
const entries: [string, Record<string, unknown>][] = [];
reader.rangeScan(startKey, endKey, (k, v) => entries.push([k, v]));
mergeIter.addSource(new ArrayEntrySource(entries));
reader.rangeScan(startKey, endKey, (k, v) => {
mergeIter.addSource(new ArrayEntrySource([[k, v]]));
});
}
}
const merged = mergeIter.drain();
return merged
.filter(([, v]) => !(v as unknown as Record<string, unknown>).__tombstone);
for (const [k, v] of merged) {
if (!(v as unknown as Record<string, unknown>).__tombstone) {
callback(k, v);
}
}
}
getAllEntries(): [string, Record<string, unknown>][] {
+16
View File
@@ -65,4 +65,20 @@ export interface IStorageEngine {
/** 回滚事务 */
rollbackTransaction(): Promise<void>;
// ---- Savepoint (可选) ----
/** 创建 Savepoint */
savepoint?(name: string): Promise<void>;
/** 回滚到 Savepoint */
rollbackToSavepoint?(name: string): Promise<void>;
/** 释放 Savepoint */
releaseSavepoint?(name: string): Promise<void>;
// ---- 备份 (可选) ----
/** 在线备份:导出全库一致性快照 */
backup?(): Promise<Record<string, Record<string, unknown>[]>>;
}