release: v0.4.3 — 关闭时序与后台任务加固(close 排空、失败不吞错、compaction 竞态修复)+ 提交先 WAL
CI / test (18.x) (push) Successful in 10m6s
CI / test (20.x) (push) Successful in 10m11s
CI / test (22.x) (push) Successful in 10m4s
CI / test (24.x) (push) Successful in 10m0s

This commit is contained in:
thzxx
2026-08-09 19:48:06 +08:00
parent 22b0b1fad4
commit d269bdfb75
23 changed files with 964 additions and 360 deletions
+1 -1
View File
@@ -214,4 +214,4 @@ export class DatabaseError extends Error {
// 版本
// ---------------------------------------------------------------------------
export const VERSION = '0.4.2';
export const VERSION = '0.4.3';
+11 -8
View File
@@ -1094,6 +1094,17 @@ export class AriaEngine implements IStorageEngine {
async commitTransaction(): Promise<void> {
if (!this.currentTxnId) throw new DatabaseError('No active transaction', 'TX_NONE');
// v0.4.3-fix: 先持久化 WAL COMMIT,再合并快照到 LSM —
// 崩溃在 WAL 提交后、快照合并前:恢复时 WAL 重放数据,重启一致;
// 崩溃在 WAL 提交前:commitTransaction 尚未返回,事务视为未提交(可回滚)
await this.wal.append({
type: WALRecordType.COMMIT,
txnId: this.currentTxnId,
tableName: '',
key: '',
});
await this.wal.flush();
if (this.txnSnapshot) {
for (const [key, value] of this.txnSnapshot) {
if ((value as unknown as Record<string, unknown>).__txn_deleted) {
@@ -1106,16 +1117,8 @@ export class AriaEngine implements IStorageEngine {
this.mvcc.commitTransaction(this.currentTxnId);
await this.wal.append({
type: WALRecordType.COMMIT,
txnId: this.currentTxnId,
tableName: '',
key: '',
});
this.currentTxnId = null;
this.txnSnapshot = null;
await this.wal.flush();
}
async rollbackTransaction(): Promise<void> {
+60 -20
View File
@@ -12,6 +12,7 @@ import { MemTable } from './memtable';
import { SSTableBuilder } from './sstable_builder';
import { SSTableReader } from './sstable';
import { MergeIterator, ArrayEntrySource } from './merge_iterator';
import { DatabaseError } from '../../../constants';
import type { SSTableMeta } from '../types';
import {
DEFAULT_MEMTABLE_SIZE,
@@ -77,6 +78,11 @@ export class LSM {
private compacting = false; // 防止重复触发 compaction
/** 串行化 flush/compaction 链:保证持久化顺序与 id 分配顺序一致 */
private flushChain: Promise<void> = Promise.resolve();
/**
* v0.4.3-fix: 最近一次后台 flush/compaction 失败。
* 后台失败不卡死链(吞错防死锁),但在显式 flush()/close() 时报告(不静默)。
*/
private lastBackgroundError: unknown = null;
constructor(config: LSMConfig) {
this.memtableSizeThreshold = config.memtableSizeThreshold ?? DEFAULT_MEMTABLE_SIZE;
@@ -189,12 +195,27 @@ export class LSM {
return this.flushChain
.then(task)
.catch((error) => {
// v0.4.3-fix: 记录失败(flush()/close() 时报告),不再完全静默吞错
this.lastBackgroundError = error;
// eslint-disable-next-line no-console
console.warn('[AriaEngine LSM] background flush/compaction failed:', error);
// catch 返回 undefined → 链恢复为 resolved,后续任务继续
});
}
/**
* v0.4.3-fix: 排空后台链 — 循环等待 flushChain 直到稳定。
* 任务完成时可能级联调度新任务(compaction 多级触发),单次 await 等不到。
* close/flush/clear 必须等待全部后台任务完成后才能安全关闭底层存储。
*/
private async drainChain(): Promise<void> {
while (true) {
const chain = this.flushChain;
await chain;
if (this.flushChain === chain) return;
}
}
/** 将指定 Immutable MemTable 刷盘为 SSTableid 由 store 按命名空间分配) */
private async flushImmutableAsync(frozen: MemTable): Promise<void> {
const entries = frozen.getAllEntries();
@@ -239,25 +260,26 @@ export class LSM {
}
}
/** 异步调度 compaction,使用 setTimeout 分片执行 */
/**
* 异步调度 compaction。
* v0.4.3-fix: 去掉 setTimeout 分片 — 此前未触发的定时器在 close 后执行,
* 用已关闭的 backend 写存储(错误被吞),或 close 后 reopen 时旧闭包引用新 backend 交叉污染。
* 现在直接挂在 flushChain 上:串行执行、close 的 drainChain 能等到全部完成。
*/
private scheduleCompact(level: number): void {
if (level >= MAX_LSM_LEVELS - 1 || this.compacting) return;
this.compacting = true;
setTimeout(() => {
try {
this.flushChain = this.enqueueOnChain(() => this.compactLevelAsync(level));
} finally {
this.compacting = false;
// 连续触发:如果 compaction 后仍然超标,继续调度
if (this.levels[level].length >= 4) {
this.scheduleCompact(level);
}
// 检查下一级是否需要 compaction
if (level + 1 < MAX_LSM_LEVELS - 1 && this.levels[level + 1].length >= 4) {
this.scheduleCompact(level + 1);
}
this.flushChain = this.enqueueOnChain(() => this.compactLevelAsync(level).finally(() => {
this.compacting = false;
// 连续触发:如果 compaction 后仍然超标,继续调度
if (this.levels[level].length >= 4) {
this.scheduleCompact(level);
}
}, 0);
// 检查下一级是否需要 compaction
if (level + 1 < MAX_LSM_LEVELS - 1 && this.levels[level + 1].length >= 4) {
this.scheduleCompact(level + 1);
}
}));
}
/** 背压场景下排队 compaction(写入路径调用) */
@@ -272,6 +294,8 @@ export class LSM {
}
}).catch((error) => {
this.compacting = false;
// v0.4.3-fix: 记录失败(flush()/close() 时报告)
this.lastBackgroundError = error;
// eslint-disable-next-line no-console
console.warn('[AriaEngine LSM] background compaction failed:', error);
});
@@ -284,10 +308,11 @@ export class LSM {
/**
* 预加载指定 key 范围内可能命中的所有 SSTable 到缓存。
* 在同步扫描/查找之前调用,保证 loadSSTableReader 不会因缓存未命中而返回 null。
* 先等待 flush 链完成:避免 flush 的缓存裁剪与预加载竞争(驱逐刚加载的 SSTable)。
* v0.4.3-fix: 等待链稳定(drainChain)— 后台 flush/compaction 在链上动态增长,
* 单次 await 后 compaction 仍可能合并 levels,导致扫描时新 meta 缓存未命中而跳块丢数据。
*/
async prefetchRange(startKey: string, endKey: string): Promise<void> {
await this.flushChain;
await this.drainChain();
this.trimCache();
const toLoad: number[] = [];
for (let level = 0; level < MAX_LSM_LEVELS; level++) {
@@ -304,7 +329,8 @@ export class LSM {
/** 预加载包含指定 key 的所有 SSTable 到缓存 */
async prefetchKeys(keys: string[]): Promise<void> {
if (keys.length === 0) return;
await this.flushChain;
// v0.4.3-fix: 等待链稳定(同 prefetchRange,防 compaction 竞态丢数据)
await this.drainChain();
this.trimCache();
const toLoad = new Set<number>();
for (let level = 0; level < MAX_LSM_LEVELS; level++) {
@@ -519,8 +545,18 @@ export class LSM {
/** 等待所有排队的 flush/compaction 完成,并将剩余数据刷盘 */
async flush(): Promise<void> {
// 等待链上已排队的 flush/compaction
await this.flushChain;
// v0.4.3-fix: 报告后台失败(消费一次,不永久吞错)
if (this.lastBackgroundError !== null) {
const error = this.lastBackgroundError;
this.lastBackgroundError = null;
throw new DatabaseError(
'AriaEngine background flush/compaction failed (data may be inconsistent)',
'ARIA_BACKGROUND_ERROR',
error,
);
}
// v0.4.3-fix: 循环等待级联任务(flush 完成可能触发新的 compaction
await this.drainChain();
// 若仍有 frozen 数据未刷盘,在链尾追加
if (this.immutableMemtable) {
const frozen = this.immutableMemtable;
@@ -532,9 +568,13 @@ export class LSM {
if (frozen) await this.flushImmutableAsync(frozen);
}
this.frozenMemtables = [];
// 刷盘完成后级联调度可能触发 compaction → 排空到稳定
await this.drainChain();
}
async clear(): Promise<void> {
// v0.4.3-fix: 清空前排空后台任务(避免 compaction 在清空后写回残留 meta/数据)
await this.drainChain();
this.memtable.clear();
this.immutableMemtable = null;
this.frozenMemtables = [];
+6
View File
@@ -251,6 +251,12 @@ export class IndexedDBEngine implements IStorageEngine {
}
async close(): Promise<void> {
// v0.4.3-fix: 活跃事务时先回滚(避免 commit 对已关闭连接报错)
if (this.txActive) {
try {
await this.rollbackTransaction();
} catch { /* 回滚失败不阻塞关闭 */ }
}
if (this.db) {
this.db.onversionchange = null; // 清理监听器
this.db.close();
+95 -52
View File
@@ -25,6 +25,20 @@ export class OPFSEngine implements IStorageEngine {
// 运行时内存缓存(OPFS 文件读写有延迟)
private memoryCache: MemoryEngine = new MemoryEngine();
/**
* v0.4.3-fix: 写操作串行队列 — 内存写 + 快照 + 文件持久化整体排队执行,
* close() 等待队列排空后再释放目录句柄(避免 close 后挂起写泄漏/读旧数据)。
* 前一个操作失败不阻塞后续(错误仍返回给调用方)。
*/
private opQueue: Promise<unknown> = Promise.resolve();
/** 将写操作加入串行队列(快照在队列内取,始终最新) */
private enqueueOp<T>(fn: () => Promise<T>): Promise<T> {
const run = this.opQueue.then(fn, fn);
this.opQueue = run.then(() => undefined, () => undefined);
return run;
}
// ---- 生命周期 ----
async open(dbName: string, version: number): Promise<void> {
@@ -42,6 +56,10 @@ export class OPFSEngine implements IStorageEngine {
}
async close(): Promise<void> {
// v0.4.3-fix: 等待所有挂起写操作完成(否则 close 后写仍在进行 → 重启读旧数据)
try {
await this.opQueue;
} catch { /* 写失败已返回给调用方 */ }
this.root = null;
this.tablesDir = null;
await this.memoryCache.close();
@@ -55,6 +73,8 @@ export class OPFSEngine implements IStorageEngine {
/** 自愈:重置内存缓存后从 OPFS 重新加载(单文件损坏不影响其他表) */
async repair(): Promise<void> {
// v0.4.3-fix: 先等写队列排空(避免与挂起写竞态)
try { await this.opQueue; } catch { /* ignore */ }
await this.memoryCache.close();
await this.memoryCache.open(this.dbName, 1);
await this.loadExistingTables();
@@ -62,6 +82,8 @@ export class OPFSEngine implements IStorageEngine {
/** 清空全部数据与表结构(删除目录内全部文件) */
async clearAll(): Promise<void> {
// v0.4.3-fix: 先等写队列排空
try { await this.opQueue; } catch { /* ignore */ }
await this.memoryCache.close();
await this.memoryCache.open(this.dbName, 1);
if (this.tablesDir) {
@@ -94,28 +116,32 @@ export class OPFSEngine implements IStorageEngine {
// ---- 表管理 ----
async createTable(schema: TableSchema): Promise<void> {
await this.memoryCache.createTable(schema);
// v0.4.2-fix: schema 持久化(此前仅写空数据文件 → 空表重启后消失、索引标记丢失)
await this.setMeta(`schema_${schema.name}`, JSON.stringify(schema));
// OPFS 中表以空 JSON 数组文件形式存在
await this.writeTableData(schema.name, []);
return this.enqueueOp(async () => {
await this.memoryCache.createTable(schema);
// v0.4.2-fix: schema 持久化(此前仅写空数据文件 → 空表重启后消失、索引标记丢失)
await this.setMeta(`schema_${schema.name}`, JSON.stringify(schema));
// OPFS 中表以空 JSON 数组文件形式存在
await this.writeTableData(schema.name, []);
});
}
async dropTable(tableName: string): Promise<void> {
await this.memoryCache.dropTable(tableName);
// v0.4.2-fix: 清理 schema meta(否则重启恢复幽灵表)
if (this.tablesDir) {
try {
await this.tablesDir.removeEntry(`__metona_schema_${tableName}.meta`);
} catch {
// 文件不存在则忽略
return this.enqueueOp(async () => {
await this.memoryCache.dropTable(tableName);
// v0.4.2-fix: 清理 schema meta(否则重启恢复幽灵表)
if (this.tablesDir) {
try {
await this.tablesDir.removeEntry(`__metona_schema_${tableName}.meta`);
} catch {
// 文件不存在则忽略
}
try {
await this.tablesDir.removeEntry(`${tableName}.json`);
} catch {
// 文件不存在则忽略
}
}
try {
await this.tablesDir.removeEntry(`${tableName}.json`);
} catch {
// 文件不存在则忽略
}
}
});
}
async hasTable(tableName: string): Promise<boolean> {
@@ -149,21 +175,25 @@ export class OPFSEngine implements IStorageEngine {
action: 'ADD' | 'DROP',
column: import('../constants').ColumnDef & { name: string },
): Promise<void> {
await this.memoryCache.alterTable(tableName, action, column);
const schema = await this.memoryCache.getTableSchema(tableName);
if (schema) await this.setMeta(`schema_${tableName}`, JSON.stringify(schema));
const rows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, rows);
return this.enqueueOp(async () => {
await this.memoryCache.alterTable(tableName, action, column);
const schema = await this.memoryCache.getTableSchema(tableName);
if (schema) await this.setMeta(`schema_${tableName}`, JSON.stringify(schema));
const rows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, rows);
});
}
// ---- CRUD ----
async insert(tableName: string, rows: Record<string, unknown>[]): Promise<string[]> {
const pks = await this.memoryCache.insert(tableName, rows);
// 持久化到 OPFS
const allRows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, allRows);
return pks;
return this.enqueueOp(async () => {
const pks = await this.memoryCache.insert(tableName, rows);
// 持久化到 OPFS(快照在队列内取,始终最新)
const allRows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, allRows);
return pks;
});
}
async find(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]> {
@@ -176,17 +206,21 @@ export class OPFSEngine implements IStorageEngine {
}
async update(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<number> {
const count = await this.memoryCache.update(tableName, query, updates);
const allRows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, allRows);
return count;
return this.enqueueOp(async () => {
const count = await this.memoryCache.update(tableName, query, updates);
const allRows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, allRows);
return count;
});
}
async delete(tableName: string, query: QueryPlan): Promise<number> {
const count = await this.memoryCache.delete(tableName, query);
const allRows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, allRows);
return count;
return this.enqueueOp(async () => {
const count = await this.memoryCache.delete(tableName, query);
const allRows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, allRows);
return count;
});
}
async count(tableName: string, query?: QueryPlan): Promise<number> {
@@ -194,23 +228,29 @@ export class OPFSEngine implements IStorageEngine {
}
async clear(tableName: string): Promise<void> {
await this.memoryCache.clear(tableName);
await this.writeTableData(tableName, []);
return this.enqueueOp(async () => {
await this.memoryCache.clear(tableName);
await this.writeTableData(tableName, []);
});
}
// ---- 动态索引(v0.3.0 ----
async createIndex(tableName: string, column: string, unique?: boolean): Promise<void> {
await this.memoryCache.createIndex(tableName, column, unique);
// v0.4.2-fix: 索引标记持久化(重启后索引结构恢复)
const schema = await this.memoryCache.getTableSchema(tableName);
if (schema) await this.setMeta(`schema_${tableName}`, JSON.stringify(schema));
return this.enqueueOp(async () => {
await this.memoryCache.createIndex(tableName, column, unique);
// v0.4.2-fix: 索引标记持久化(重启后索引结构恢复)
const schema = await this.memoryCache.getTableSchema(tableName);
if (schema) await this.setMeta(`schema_${tableName}`, JSON.stringify(schema));
});
}
async dropIndex(tableName: string, column: string, indexName?: string): Promise<void> {
await this.memoryCache.dropIndex(tableName, column, indexName);
const schema = await this.memoryCache.getTableSchema(tableName);
if (schema) await this.setMeta(`schema_${tableName}`, JSON.stringify(schema));
return this.enqueueOp(async () => {
await this.memoryCache.dropIndex(tableName, column, indexName);
const schema = await this.memoryCache.getTableSchema(tableName);
if (schema) await this.setMeta(`schema_${tableName}`, JSON.stringify(schema));
});
}
// ---- 事务 ----
@@ -220,13 +260,15 @@ export class OPFSEngine implements IStorageEngine {
}
async commitTransaction(): Promise<void> {
await this.memoryCache.commitTransaction();
// 将内存数据刷到 OPFS
const tableNames = await this.memoryCache.getTableNames();
for (const tableName of tableNames) {
const rows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, rows);
}
return this.enqueueOp(async () => {
await this.memoryCache.commitTransaction();
// 将内存数据刷到 OPFS
const tableNames = await this.memoryCache.getTableNames();
for (const tableName of tableNames) {
const rows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, rows);
}
});
}
async rollbackTransaction(): Promise<void> {
@@ -243,6 +285,7 @@ export class OPFSEngine implements IStorageEngine {
}
private async writeTableData(tableName: string, data: Record<string, unknown>[]): Promise<void> {
// 由 enqueueOp 串行化调用,此处直接写文件
const dir = this.ensureDir();
const fileName = `${tableName}.json`;
const fileHandle = await dir.getFileHandle(fileName, { create: true });