feat: v0.2.4 惰性扫描 + 写背压 + 内存预算 + ANALYZE + 加密 + Savepoint + 在线备份
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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>][] {
|
||||
|
||||
@@ -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>[]>>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user