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
+18
View File
@@ -2,6 +2,24 @@
All notable changes to MetonaSqlark will be documented in this file. All notable changes to MetonaSqlark will be documented in this file.
## [0.4.3] - 2026-08-09
### Fixed
- **close 后后台 compaction 吞错并在 backend 关闭后执行(P0)** — `scheduleCompact``setTimeout(0)` 调度:close 时未触发的定时器在 `backend.close()` 后执行,用已关闭的存储写(错误被吞);close 后立即 reopen 时旧闭包引用新 backend 交叉污染。改为 compaction 直接挂 flushChain 串行执行,close 的 `drainChain()` 排空全部级联任务后再关闭存储
- **后台 flush/compaction 失败完全静默(P0)** — 吞错防死锁的同时记录 `lastBackgroundError`,显式 `flush()`/`close()` 时抛 `ARIA_BACKGROUND_ERROR`(消费一次),不再永久不可见
- **prefetch 与 compaction 竞态丢数据(P0)** — 后台任务在链上动态增长,`prefetchRange/prefetchKeys` 单次 `await flushChain` 后 compaction 仍可能合并 levels,扫描时新 meta 缓存未命中 → 跳块丢数据(缓存驱逐场景 UPDATE/DELETE 少删行)。改为等待链稳定(`drainChain`)再预加载
- **commitTransaction 提交顺序(P1** — 先合并快照后写 WAL COMMIT:WAL 写失败时内存已提交但持久化缺失,崩溃重启丢失。改为先持久化 WAL COMMIT 再合并快照(WAL 始终领先,崩溃恢复一致)
- **OPFS close 不等挂起写(P1)** — 写操作整体串行入队(内存写 + 快照 + 文件持久化),close/repair/clearAll 先等队列排空再释放目录句柄,避免 close 后挂起写读旧数据
- **IndexedDBEngine close 活跃事务防御** — close 时自动回滚活跃事务(避免对已关闭连接操作报错)
### Changed
- 测试 944 → **950**51 套件),新增 `tests/v044-hardening.test.ts`(6 条:close 后台任务排空、reopen 无污染、后台失败可见、链不卡死、WAL 提交顺序、OPFS close 等待)
- 版本号升至 v0.4.3
---
## [0.4.2] - 2026-08-09 ## [0.4.2] - 2026-08-09
### Fixed ### Fixed
+5 -4
View File
@@ -1,10 +1,10 @@
# MetonaSqlark # MetonaSqlark
<p align="center"> <p align="center">
<img src="https://img.shields.io/badge/version-0.4.2-blue?style=flat-square" alt="version"> <img src="https://img.shields.io/badge/version-0.4.3-blue?style=flat-square" alt="version">
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license"> <img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license">
<img src="https://img.shields.io/badge/coverage-84.2%25-brightgreen?style=flat-square" alt="coverage"> <img src="https://img.shields.io/badge/coverage-84.2%25-brightgreen?style=flat-square" alt="coverage">
<img src="https://img.shields.io/badge/tests-944%20passed-success?style=flat-square" alt="tests"> <img src="https://img.shields.io/badge/tests-950%20passed-success?style=flat-square" alt="tests">
</p> </p>
> 基于 TypeScript 的**前端关系型数据库**,支持完整 SQL 查询、Query Builder 链式 API、与 **AriaEngine 自研页面式存储引擎**。 > 基于 TypeScript 的**前端关系型数据库**,支持完整 SQL 查询、Query Builder 链式 API、与 **AriaEngine 自研页面式存储引擎**。
@@ -25,10 +25,11 @@
- 🔄 **事务回滚** — Memory/IndexedDB/Hybrid/Aria 四引擎事务原子性,自动回滚,MVCC 版本链接入读写路径 - 🔄 **事务回滚** — Memory/IndexedDB/Hybrid/Aria 四引擎事务原子性,自动回滚,MVCC 版本链接入读写路径
- 🔗 **外键级联** — ON DELETE + ON UPDATECASCADE / SET NULL / RESTRICT)全引擎支持,支持更新主键(v0.4.2) - 🔗 **外键级联** — ON DELETE + ON UPDATECASCADE / SET NULL / RESTRICT)全引擎支持,支持更新主键(v0.4.2)
- 🛡 **崩溃恢复自愈** — 残缺 SSTable 打开自动跳过、`db.repair()` 自愈、`db.clearAll()` 重置、迁移版本持久化(v0.4.2) - 🛡 **崩溃恢复自愈** — 残缺 SSTable 打开自动跳过、`db.repair()` 自愈、`db.clearAll()` 重置、迁移版本持久化(v0.4.2)
- 🧵 **关闭时序与后台任务加固** — 后台 flush/compaction 串行入队(close 排空后才关闭存储,失败显式报告 `ARIA_BACKGROUND_ERROR`)、预加载等待链稳定(compaction 竞态修复)、事务提交先落 WAL 再合并快照(v0.4.3)
- 🌲 **RB-Tree 完整实现** — 标准红黑树插入+删除修复,O(log n) 保证 - 🌲 **RB-Tree 完整实现** — 标准红黑树插入+删除修复,O(log n) 保证
-**性能优化** — SSTableReader 二分查找统一、IndexedDB 索引利用、crypto 实例化避免全局状态 -**性能优化** — SSTableReader 二分查找统一、IndexedDB 索引利用、crypto 实例化避免全局状态
- 🌐 **浏览器兼容** — Chrome 80+ / Firefox 80+ / Safari 14+ / Edge 80+ / Node.js 16+ - 🌐 **浏览器兼容** — Chrome 80+ / Firefox 80+ / Safari 14+ / Edge 80+ / Node.js 16+
- 🧪 **944 测试 · 84.2% 覆盖率** — 50 套件,生产级质量保证 - 🧪 **950 测试 · 84.2% 覆盖率** — 51 套件,生产级质量保证
--- ---
@@ -399,7 +400,7 @@ npm run typecheck # 类型检查
| 指标 | 数值 | | 指标 | 数值 |
|------|------| |------|------|
| 测试用例 | 944 | | 测试用例 | 950 |
| 测试套件 | 51 | | 测试套件 | 51 |
| 行覆盖率 | 84.2% | | 行覆盖率 | 84.2% |
| SQL 关键字 | 36 | | SQL 关键字 | 36 |
+177 -85
View File
@@ -34,7 +34,7 @@ class DatabaseError extends Error {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 版本 // 版本
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const VERSION = '0.4.2'; const VERSION = '0.4.3';
/** /**
* metona-sqlark Shared WHERE Matcher 统一的条件匹配逻辑 * metona-sqlark Shared WHERE Matcher 统一的条件匹配逻辑
@@ -1021,6 +1021,13 @@ class IndexedDBEngine {
} }
} }
async close() { async close() {
// v0.4.3-fix: 活跃事务时先回滚(避免 commit 对已关闭连接报错)
if (this.txActive) {
try {
await this.rollbackTransaction();
}
catch { /* 回滚失败不阻塞关闭 */ }
}
if (this.db) { if (this.db) {
this.db.onversionchange = null; // 清理监听器 this.db.onversionchange = null; // 清理监听器
this.db.close(); this.db.close();
@@ -1607,6 +1614,18 @@ class OPFSEngine {
this.dbName = ''; this.dbName = '';
// 运行时内存缓存(OPFS 文件读写有延迟) // 运行时内存缓存(OPFS 文件读写有延迟)
this.memoryCache = new MemoryEngine(); this.memoryCache = new MemoryEngine();
/**
* v0.4.3-fix: 写操作串行队列 内存写 + 快照 + 文件持久化整体排队执行
* close() 等待队列排空后再释放目录句柄避免 close 后挂起写泄漏/读旧数据
* 前一个操作失败不阻塞后续错误仍返回给调用方
*/
this.opQueue = Promise.resolve();
}
/** 将写操作加入串行队列(快照在队列内取,始终最新) */
enqueueOp(fn) {
const run = this.opQueue.then(fn, fn);
this.opQueue = run.then(() => undefined, () => undefined);
return run;
} }
// ---- 生命周期 ---- // ---- 生命周期 ----
async open(dbName, version) { async open(dbName, version) {
@@ -1620,6 +1639,11 @@ class OPFSEngine {
await this.loadExistingTables(); await this.loadExistingTables();
} }
async close() { async close() {
// v0.4.3-fix: 等待所有挂起写操作完成(否则 close 后写仍在进行 → 重启读旧数据)
try {
await this.opQueue;
}
catch { /* 写失败已返回给调用方 */ }
this.root = null; this.root = null;
this.tablesDir = null; this.tablesDir = null;
await this.memoryCache.close(); await this.memoryCache.close();
@@ -1630,12 +1654,22 @@ class OPFSEngine {
// ---- v0.4.2-fix: 自愈 / 重置 / 元数据 ---- // ---- v0.4.2-fix: 自愈 / 重置 / 元数据 ----
/** 自愈:重置内存缓存后从 OPFS 重新加载(单文件损坏不影响其他表) */ /** 自愈:重置内存缓存后从 OPFS 重新加载(单文件损坏不影响其他表) */
async repair() { async repair() {
// v0.4.3-fix: 先等写队列排空(避免与挂起写竞态)
try {
await this.opQueue;
}
catch { /* ignore */ }
await this.memoryCache.close(); await this.memoryCache.close();
await this.memoryCache.open(this.dbName, 1); await this.memoryCache.open(this.dbName, 1);
await this.loadExistingTables(); await this.loadExistingTables();
} }
/** 清空全部数据与表结构(删除目录内全部文件) */ /** 清空全部数据与表结构(删除目录内全部文件) */
async clearAll() { async clearAll() {
// v0.4.3-fix: 先等写队列排空
try {
await this.opQueue;
}
catch { /* ignore */ }
await this.memoryCache.close(); await this.memoryCache.close();
await this.memoryCache.open(this.dbName, 1); await this.memoryCache.open(this.dbName, 1);
if (this.tablesDir) { if (this.tablesDir) {
@@ -1670,29 +1704,33 @@ class OPFSEngine {
} }
// ---- 表管理 ---- // ---- 表管理 ----
async createTable(schema) { async createTable(schema) {
await this.memoryCache.createTable(schema); return this.enqueueOp(async () => {
// v0.4.2-fix: schema 持久化(此前仅写空数据文件 → 空表重启后消失、索引标记丢失) await this.memoryCache.createTable(schema);
await this.setMeta(`schema_${schema.name}`, JSON.stringify(schema)); // v0.4.2-fix: schema 持久化(此前仅写空数据文件 → 空表重启后消失、索引标记丢失)
// OPFS 中表以空 JSON 数组文件形式存在 await this.setMeta(`schema_${schema.name}`, JSON.stringify(schema));
await this.writeTableData(schema.name, []); // OPFS 中表以空 JSON 数组文件形式存在
await this.writeTableData(schema.name, []);
});
} }
async dropTable(tableName) { async dropTable(tableName) {
await this.memoryCache.dropTable(tableName); return this.enqueueOp(async () => {
// v0.4.2-fix: 清理 schema meta(否则重启恢复幽灵表) await this.memoryCache.dropTable(tableName);
if (this.tablesDir) { // v0.4.2-fix: 清理 schema meta(否则重启恢复幽灵表)
try { if (this.tablesDir) {
await this.tablesDir.removeEntry(`__metona_schema_${tableName}.meta`); try {
await this.tablesDir.removeEntry(`__metona_schema_${tableName}.meta`);
}
catch {
// 文件不存在则忽略
}
try {
await this.tablesDir.removeEntry(`${tableName}.json`);
}
catch {
// 文件不存在则忽略
}
} }
catch { });
// 文件不存在则忽略
}
try {
await this.tablesDir.removeEntry(`${tableName}.json`);
}
catch {
// 文件不存在则忽略
}
}
} }
async hasTable(tableName) { async hasTable(tableName) {
if (!this.tablesDir) if (!this.tablesDir)
@@ -1721,20 +1759,24 @@ class OPFSEngine {
} }
/** v0.4.2-fix: 引擎级 ALTER TABLE — 内存 + schema 持久化 + 整表文件重写 */ /** v0.4.2-fix: 引擎级 ALTER TABLE — 内存 + schema 持久化 + 整表文件重写 */
async alterTable(tableName, action, column) { async alterTable(tableName, action, column) {
await this.memoryCache.alterTable(tableName, action, column); return this.enqueueOp(async () => {
const schema = await this.memoryCache.getTableSchema(tableName); await this.memoryCache.alterTable(tableName, action, column);
if (schema) const schema = await this.memoryCache.getTableSchema(tableName);
await this.setMeta(`schema_${tableName}`, JSON.stringify(schema)); if (schema)
const rows = await this.memoryCache.find(tableName, { table: tableName }); await this.setMeta(`schema_${tableName}`, JSON.stringify(schema));
await this.writeTableData(tableName, rows); const rows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, rows);
});
} }
// ---- CRUD ---- // ---- CRUD ----
async insert(tableName, rows) { async insert(tableName, rows) {
const pks = await this.memoryCache.insert(tableName, rows); return this.enqueueOp(async () => {
// 持久化到 OPFS const pks = await this.memoryCache.insert(tableName, rows);
const allRows = await this.memoryCache.find(tableName, { table: tableName }); // 持久化到 OPFS(快照在队列内取,始终最新)
await this.writeTableData(tableName, allRows); const allRows = await this.memoryCache.find(tableName, { table: tableName });
return pks; await this.writeTableData(tableName, allRows);
return pks;
});
} }
async find(tableName, query) { async find(tableName, query) {
return this.memoryCache.find(tableName, query); return this.memoryCache.find(tableName, query);
@@ -1744,50 +1786,62 @@ class OPFSEngine {
return this.memoryCache.findStream(tableName, query, onRow); return this.memoryCache.findStream(tableName, query, onRow);
} }
async update(tableName, query, updates) { async update(tableName, query, updates) {
const count = await this.memoryCache.update(tableName, query, updates); return this.enqueueOp(async () => {
const allRows = await this.memoryCache.find(tableName, { table: tableName }); const count = await this.memoryCache.update(tableName, query, updates);
await this.writeTableData(tableName, allRows); const allRows = await this.memoryCache.find(tableName, { table: tableName });
return count; await this.writeTableData(tableName, allRows);
return count;
});
} }
async delete(tableName, query) { async delete(tableName, query) {
const count = await this.memoryCache.delete(tableName, query); return this.enqueueOp(async () => {
const allRows = await this.memoryCache.find(tableName, { table: tableName }); const count = await this.memoryCache.delete(tableName, query);
await this.writeTableData(tableName, allRows); const allRows = await this.memoryCache.find(tableName, { table: tableName });
return count; await this.writeTableData(tableName, allRows);
return count;
});
} }
async count(tableName, query) { async count(tableName, query) {
return this.memoryCache.count(tableName, query); return this.memoryCache.count(tableName, query);
} }
async clear(tableName) { async clear(tableName) {
await this.memoryCache.clear(tableName); return this.enqueueOp(async () => {
await this.writeTableData(tableName, []); await this.memoryCache.clear(tableName);
await this.writeTableData(tableName, []);
});
} }
// ---- 动态索引(v0.3.0 ---- // ---- 动态索引(v0.3.0 ----
async createIndex(tableName, column, unique) { async createIndex(tableName, column, unique) {
await this.memoryCache.createIndex(tableName, column, unique); return this.enqueueOp(async () => {
// v0.4.2-fix: 索引标记持久化(重启后索引结构恢复) await this.memoryCache.createIndex(tableName, column, unique);
const schema = await this.memoryCache.getTableSchema(tableName); // v0.4.2-fix: 索引标记持久化(重启后索引结构恢复)
if (schema) const schema = await this.memoryCache.getTableSchema(tableName);
await this.setMeta(`schema_${tableName}`, JSON.stringify(schema)); if (schema)
await this.setMeta(`schema_${tableName}`, JSON.stringify(schema));
});
} }
async dropIndex(tableName, column, indexName) { async dropIndex(tableName, column, indexName) {
await this.memoryCache.dropIndex(tableName, column, indexName); return this.enqueueOp(async () => {
const schema = await this.memoryCache.getTableSchema(tableName); await this.memoryCache.dropIndex(tableName, column, indexName);
if (schema) const schema = await this.memoryCache.getTableSchema(tableName);
await this.setMeta(`schema_${tableName}`, JSON.stringify(schema)); if (schema)
await this.setMeta(`schema_${tableName}`, JSON.stringify(schema));
});
} }
// ---- 事务 ---- // ---- 事务 ----
async beginTransaction() { async beginTransaction() {
await this.memoryCache.beginTransaction(); await this.memoryCache.beginTransaction();
} }
async commitTransaction() { async commitTransaction() {
await this.memoryCache.commitTransaction(); return this.enqueueOp(async () => {
// 将内存数据刷到 OPFS await this.memoryCache.commitTransaction();
const tableNames = await this.memoryCache.getTableNames(); // 将内存数据刷到 OPFS
for (const tableName of tableNames) { const tableNames = await this.memoryCache.getTableNames();
const rows = await this.memoryCache.find(tableName, { table: tableName }); for (const tableName of tableNames) {
await this.writeTableData(tableName, rows); const rows = await this.memoryCache.find(tableName, { table: tableName });
} await this.writeTableData(tableName, rows);
}
});
} }
async rollbackTransaction() { async rollbackTransaction() {
await this.memoryCache.rollbackTransaction(); await this.memoryCache.rollbackTransaction();
@@ -1800,6 +1854,7 @@ class OPFSEngine {
return this.tablesDir; return this.tablesDir;
} }
async writeTableData(tableName, data) { async writeTableData(tableName, data) {
// 由 enqueueOp 串行化调用,此处直接写文件
const dir = this.ensureDir(); const dir = this.ensureDir();
const fileName = `${tableName}.json`; const fileName = `${tableName}.json`;
const fileHandle = await dir.getFileHandle(fileName, { create: true }); const fileHandle = await dir.getFileHandle(fileName, { create: true });
@@ -3274,6 +3329,11 @@ class LSM {
this.compacting = false; // 防止重复触发 compaction this.compacting = false; // 防止重复触发 compaction
/** 串行化 flush/compaction 链:保证持久化顺序与 id 分配顺序一致 */ /** 串行化 flush/compaction 链:保证持久化顺序与 id 分配顺序一致 */
this.flushChain = Promise.resolve(); this.flushChain = Promise.resolve();
/**
* v0.4.3-fix: 最近一次后台 flush/compaction 失败
* 后台失败不卡死链吞错防死锁但在显式 flush()/close() 时报告不静默
*/
this.lastBackgroundError = null;
this.memtableSizeThreshold = config.memtableSizeThreshold ?? DEFAULT_MEMTABLE_SIZE; this.memtableSizeThreshold = config.memtableSizeThreshold ?? DEFAULT_MEMTABLE_SIZE;
this.memtable = new MemTable(this.memtableSizeThreshold); this.memtable = new MemTable(this.memtableSizeThreshold);
this.levelSizeMultiplier = config.levelSizeMultiplier ?? DEFAULT_LEVEL_SIZE_MULTIPLIER; this.levelSizeMultiplier = config.levelSizeMultiplier ?? DEFAULT_LEVEL_SIZE_MULTIPLIER;
@@ -3371,11 +3431,26 @@ class LSM {
return this.flushChain return this.flushChain
.then(task) .then(task)
.catch((error) => { .catch((error) => {
// v0.4.3-fix: 记录失败(flush()/close() 时报告),不再完全静默吞错
this.lastBackgroundError = error;
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn('[AriaEngine LSM] background flush/compaction failed:', error); console.warn('[AriaEngine LSM] background flush/compaction failed:', error);
// catch 返回 undefined → 链恢复为 resolved,后续任务继续 // catch 返回 undefined → 链恢复为 resolved,后续任务继续
}); });
} }
/**
* v0.4.3-fix: 排空后台链 循环等待 flushChain 直到稳定
* 任务完成时可能级联调度新任务compaction 多级触发单次 await 等不到
* close/flush/clear 必须等待全部后台任务完成后才能安全关闭底层存储
*/
async drainChain() {
while (true) {
const chain = this.flushChain;
await chain;
if (this.flushChain === chain)
return;
}
}
/** 将指定 Immutable MemTable 刷盘为 SSTableid 由 store 按命名空间分配) */ /** 将指定 Immutable MemTable 刷盘为 SSTableid 由 store 按命名空间分配) */
async flushImmutableAsync(frozen) { async flushImmutableAsync(frozen) {
const entries = frozen.getAllEntries(); const entries = frozen.getAllEntries();
@@ -3415,27 +3490,27 @@ class LSM {
this.scheduleCompact(0); this.scheduleCompact(0);
} }
} }
/** 异步调度 compaction,使用 setTimeout 分片执行 */ /**
* 异步调度 compaction
* v0.4.3-fix: 去掉 setTimeout 分片 此前未触发的定时器在 close 后执行
* 用已关闭的 backend 写存储错误被吞 close reopen 时旧闭包引用新 backend 交叉污染
* 现在直接挂在 flushChain 串行执行close drainChain 能等到全部完成
*/
scheduleCompact(level) { scheduleCompact(level) {
if (level >= MAX_LSM_LEVELS - 1 || this.compacting) if (level >= MAX_LSM_LEVELS - 1 || this.compacting)
return; return;
this.compacting = true; this.compacting = true;
setTimeout(() => { this.flushChain = this.enqueueOnChain(() => this.compactLevelAsync(level).finally(() => {
try { this.compacting = false;
this.flushChain = this.enqueueOnChain(() => this.compactLevelAsync(level)); // 连续触发:如果 compaction 后仍然超标,继续调度
if (this.levels[level].length >= 4) {
this.scheduleCompact(level);
} }
finally { // 检查下一级是否需要 compaction
this.compacting = false; if (level + 1 < MAX_LSM_LEVELS - 1 && this.levels[level + 1].length >= 4) {
// 连续触发:如果 compaction 后仍然超标,继续调度 this.scheduleCompact(level + 1);
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);
}
} }
}, 0); }));
} }
/** 背压场景下排队 compaction(写入路径调用) */ /** 背压场景下排队 compaction(写入路径调用) */
enqueueCompact(level) { enqueueCompact(level) {
@@ -3451,6 +3526,8 @@ class LSM {
} }
}).catch((error) => { }).catch((error) => {
this.compacting = false; this.compacting = false;
// v0.4.3-fix: 记录失败(flush()/close() 时报告)
this.lastBackgroundError = error;
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn('[AriaEngine LSM] background compaction failed:', error); console.warn('[AriaEngine LSM] background compaction failed:', error);
}); });
@@ -3461,10 +3538,11 @@ class LSM {
/** /**
* 预加载指定 key 范围内可能命中的所有 SSTable 到缓存 * 预加载指定 key 范围内可能命中的所有 SSTable 到缓存
* 在同步扫描/查找之前调用保证 loadSSTableReader 不会因缓存未命中而返回 null * 在同步扫描/查找之前调用保证 loadSSTableReader 不会因缓存未命中而返回 null
* 先等待 flush 链完成避免 flush 的缓存裁剪与预加载竞争驱逐刚加载的 SSTable * v0.4.3-fix: 等待链稳定drainChain 后台 flush/compaction 在链上动态增长
* 单次 await compaction 仍可能合并 levels导致扫描时新 meta 缓存未命中而跳块丢数据
*/ */
async prefetchRange(startKey, endKey) { async prefetchRange(startKey, endKey) {
await this.flushChain; await this.drainChain();
this.trimCache(); this.trimCache();
const toLoad = []; const toLoad = [];
for (let level = 0; level < MAX_LSM_LEVELS; level++) { for (let level = 0; level < MAX_LSM_LEVELS; level++) {
@@ -3483,7 +3561,8 @@ class LSM {
async prefetchKeys(keys) { async prefetchKeys(keys) {
if (keys.length === 0) if (keys.length === 0)
return; return;
await this.flushChain; // v0.4.3-fix: 等待链稳定(同 prefetchRange,防 compaction 竞态丢数据)
await this.drainChain();
this.trimCache(); this.trimCache();
const toLoad = new Set(); const toLoad = new Set();
for (let level = 0; level < MAX_LSM_LEVELS; level++) { for (let level = 0; level < MAX_LSM_LEVELS; level++) {
@@ -3674,8 +3753,14 @@ class LSM {
} }
/** 等待所有排队的 flush/compaction 完成,并将剩余数据刷盘 */ /** 等待所有排队的 flush/compaction 完成,并将剩余数据刷盘 */
async flush() { async flush() {
// 等待链上已排队的 flush/compaction // v0.4.3-fix: 报告后台失败(消费一次,不永久吞错)
await this.flushChain; 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 数据未刷盘,在链尾追加 // 若仍有 frozen 数据未刷盘,在链尾追加
if (this.immutableMemtable) { if (this.immutableMemtable) {
const frozen = this.immutableMemtable; const frozen = this.immutableMemtable;
@@ -3688,8 +3773,12 @@ class LSM {
await this.flushImmutableAsync(frozen); await this.flushImmutableAsync(frozen);
} }
this.frozenMemtables = []; this.frozenMemtables = [];
// 刷盘完成后级联调度可能触发 compaction → 排空到稳定
await this.drainChain();
} }
async clear() { async clear() {
// v0.4.3-fix: 清空前排空后台任务(避免 compaction 在清空后写回残留 meta/数据)
await this.drainChain();
this.memtable.clear(); this.memtable.clear();
this.immutableMemtable = null; this.immutableMemtable = null;
this.frozenMemtables = []; this.frozenMemtables = [];
@@ -6304,6 +6393,16 @@ class AriaEngine {
async commitTransaction() { async commitTransaction() {
if (!this.currentTxnId) if (!this.currentTxnId)
throw new DatabaseError('No active transaction', 'TX_NONE'); 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) { if (this.txnSnapshot) {
for (const [key, value] of this.txnSnapshot) { for (const [key, value] of this.txnSnapshot) {
if (value.__txn_deleted) { if (value.__txn_deleted) {
@@ -6315,15 +6414,8 @@ class AriaEngine {
} }
} }
this.mvcc.commitTransaction(this.currentTxnId); this.mvcc.commitTransaction(this.currentTxnId);
await this.wal.append({
type: WALRecordType.COMMIT,
txnId: this.currentTxnId,
tableName: '',
key: '',
});
this.currentTxnId = null; this.currentTxnId = null;
this.txnSnapshot = null; this.txnSnapshot = null;
await this.wal.flush();
} }
async rollbackTransaction() { async rollbackTransaction() {
if (!this.currentTxnId) if (!this.currentTxnId)
+1 -1
View File
File diff suppressed because one or more lines are too long
+9 -1
View File
@@ -118,7 +118,7 @@ interface MetonaPlugin {
/** 销毁 */ /** 销毁 */
destroy(): void; destroy(): void;
} }
declare const VERSION = "0.4.2"; declare const VERSION = "0.4.3";
/** /**
* metona-sqlark Plugin — 插件系统 * metona-sqlark Plugin — 插件系统
@@ -853,6 +853,14 @@ declare class OPFSEngine implements IStorageEngine {
private tablesDir; private tablesDir;
private dbName; private dbName;
private memoryCache; private memoryCache;
/**
* v0.4.3-fix: 写操作串行队列 — 内存写 + 快照 + 文件持久化整体排队执行,
* close() 等待队列排空后再释放目录句柄(避免 close 后挂起写泄漏/读旧数据)。
* 前一个操作失败不阻塞后续(错误仍返回给调用方)。
*/
private opQueue;
/** 将写操作加入串行队列(快照在队列内取,始终最新) */
private enqueueOp;
open(dbName: string, version: number): Promise<void>; open(dbName: string, version: number): Promise<void>;
close(): Promise<void>; close(): Promise<void>;
isOpen(): boolean; isOpen(): boolean;
+177 -85
View File
@@ -30,7 +30,7 @@ class DatabaseError extends Error {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 版本 // 版本
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const VERSION = '0.4.2'; const VERSION = '0.4.3';
/** /**
* metona-sqlark Shared WHERE Matcher 统一的条件匹配逻辑 * metona-sqlark Shared WHERE Matcher 统一的条件匹配逻辑
@@ -1017,6 +1017,13 @@ class IndexedDBEngine {
} }
} }
async close() { async close() {
// v0.4.3-fix: 活跃事务时先回滚(避免 commit 对已关闭连接报错)
if (this.txActive) {
try {
await this.rollbackTransaction();
}
catch { /* 回滚失败不阻塞关闭 */ }
}
if (this.db) { if (this.db) {
this.db.onversionchange = null; // 清理监听器 this.db.onversionchange = null; // 清理监听器
this.db.close(); this.db.close();
@@ -1603,6 +1610,18 @@ class OPFSEngine {
this.dbName = ''; this.dbName = '';
// 运行时内存缓存(OPFS 文件读写有延迟) // 运行时内存缓存(OPFS 文件读写有延迟)
this.memoryCache = new MemoryEngine(); this.memoryCache = new MemoryEngine();
/**
* v0.4.3-fix: 写操作串行队列 内存写 + 快照 + 文件持久化整体排队执行
* close() 等待队列排空后再释放目录句柄避免 close 后挂起写泄漏/读旧数据
* 前一个操作失败不阻塞后续错误仍返回给调用方
*/
this.opQueue = Promise.resolve();
}
/** 将写操作加入串行队列(快照在队列内取,始终最新) */
enqueueOp(fn) {
const run = this.opQueue.then(fn, fn);
this.opQueue = run.then(() => undefined, () => undefined);
return run;
} }
// ---- 生命周期 ---- // ---- 生命周期 ----
async open(dbName, version) { async open(dbName, version) {
@@ -1616,6 +1635,11 @@ class OPFSEngine {
await this.loadExistingTables(); await this.loadExistingTables();
} }
async close() { async close() {
// v0.4.3-fix: 等待所有挂起写操作完成(否则 close 后写仍在进行 → 重启读旧数据)
try {
await this.opQueue;
}
catch { /* 写失败已返回给调用方 */ }
this.root = null; this.root = null;
this.tablesDir = null; this.tablesDir = null;
await this.memoryCache.close(); await this.memoryCache.close();
@@ -1626,12 +1650,22 @@ class OPFSEngine {
// ---- v0.4.2-fix: 自愈 / 重置 / 元数据 ---- // ---- v0.4.2-fix: 自愈 / 重置 / 元数据 ----
/** 自愈:重置内存缓存后从 OPFS 重新加载(单文件损坏不影响其他表) */ /** 自愈:重置内存缓存后从 OPFS 重新加载(单文件损坏不影响其他表) */
async repair() { async repair() {
// v0.4.3-fix: 先等写队列排空(避免与挂起写竞态)
try {
await this.opQueue;
}
catch { /* ignore */ }
await this.memoryCache.close(); await this.memoryCache.close();
await this.memoryCache.open(this.dbName, 1); await this.memoryCache.open(this.dbName, 1);
await this.loadExistingTables(); await this.loadExistingTables();
} }
/** 清空全部数据与表结构(删除目录内全部文件) */ /** 清空全部数据与表结构(删除目录内全部文件) */
async clearAll() { async clearAll() {
// v0.4.3-fix: 先等写队列排空
try {
await this.opQueue;
}
catch { /* ignore */ }
await this.memoryCache.close(); await this.memoryCache.close();
await this.memoryCache.open(this.dbName, 1); await this.memoryCache.open(this.dbName, 1);
if (this.tablesDir) { if (this.tablesDir) {
@@ -1666,29 +1700,33 @@ class OPFSEngine {
} }
// ---- 表管理 ---- // ---- 表管理 ----
async createTable(schema) { async createTable(schema) {
await this.memoryCache.createTable(schema); return this.enqueueOp(async () => {
// v0.4.2-fix: schema 持久化(此前仅写空数据文件 → 空表重启后消失、索引标记丢失) await this.memoryCache.createTable(schema);
await this.setMeta(`schema_${schema.name}`, JSON.stringify(schema)); // v0.4.2-fix: schema 持久化(此前仅写空数据文件 → 空表重启后消失、索引标记丢失)
// OPFS 中表以空 JSON 数组文件形式存在 await this.setMeta(`schema_${schema.name}`, JSON.stringify(schema));
await this.writeTableData(schema.name, []); // OPFS 中表以空 JSON 数组文件形式存在
await this.writeTableData(schema.name, []);
});
} }
async dropTable(tableName) { async dropTable(tableName) {
await this.memoryCache.dropTable(tableName); return this.enqueueOp(async () => {
// v0.4.2-fix: 清理 schema meta(否则重启恢复幽灵表) await this.memoryCache.dropTable(tableName);
if (this.tablesDir) { // v0.4.2-fix: 清理 schema meta(否则重启恢复幽灵表)
try { if (this.tablesDir) {
await this.tablesDir.removeEntry(`__metona_schema_${tableName}.meta`); try {
await this.tablesDir.removeEntry(`__metona_schema_${tableName}.meta`);
}
catch {
// 文件不存在则忽略
}
try {
await this.tablesDir.removeEntry(`${tableName}.json`);
}
catch {
// 文件不存在则忽略
}
} }
catch { });
// 文件不存在则忽略
}
try {
await this.tablesDir.removeEntry(`${tableName}.json`);
}
catch {
// 文件不存在则忽略
}
}
} }
async hasTable(tableName) { async hasTable(tableName) {
if (!this.tablesDir) if (!this.tablesDir)
@@ -1717,20 +1755,24 @@ class OPFSEngine {
} }
/** v0.4.2-fix: 引擎级 ALTER TABLE — 内存 + schema 持久化 + 整表文件重写 */ /** v0.4.2-fix: 引擎级 ALTER TABLE — 内存 + schema 持久化 + 整表文件重写 */
async alterTable(tableName, action, column) { async alterTable(tableName, action, column) {
await this.memoryCache.alterTable(tableName, action, column); return this.enqueueOp(async () => {
const schema = await this.memoryCache.getTableSchema(tableName); await this.memoryCache.alterTable(tableName, action, column);
if (schema) const schema = await this.memoryCache.getTableSchema(tableName);
await this.setMeta(`schema_${tableName}`, JSON.stringify(schema)); if (schema)
const rows = await this.memoryCache.find(tableName, { table: tableName }); await this.setMeta(`schema_${tableName}`, JSON.stringify(schema));
await this.writeTableData(tableName, rows); const rows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, rows);
});
} }
// ---- CRUD ---- // ---- CRUD ----
async insert(tableName, rows) { async insert(tableName, rows) {
const pks = await this.memoryCache.insert(tableName, rows); return this.enqueueOp(async () => {
// 持久化到 OPFS const pks = await this.memoryCache.insert(tableName, rows);
const allRows = await this.memoryCache.find(tableName, { table: tableName }); // 持久化到 OPFS(快照在队列内取,始终最新)
await this.writeTableData(tableName, allRows); const allRows = await this.memoryCache.find(tableName, { table: tableName });
return pks; await this.writeTableData(tableName, allRows);
return pks;
});
} }
async find(tableName, query) { async find(tableName, query) {
return this.memoryCache.find(tableName, query); return this.memoryCache.find(tableName, query);
@@ -1740,50 +1782,62 @@ class OPFSEngine {
return this.memoryCache.findStream(tableName, query, onRow); return this.memoryCache.findStream(tableName, query, onRow);
} }
async update(tableName, query, updates) { async update(tableName, query, updates) {
const count = await this.memoryCache.update(tableName, query, updates); return this.enqueueOp(async () => {
const allRows = await this.memoryCache.find(tableName, { table: tableName }); const count = await this.memoryCache.update(tableName, query, updates);
await this.writeTableData(tableName, allRows); const allRows = await this.memoryCache.find(tableName, { table: tableName });
return count; await this.writeTableData(tableName, allRows);
return count;
});
} }
async delete(tableName, query) { async delete(tableName, query) {
const count = await this.memoryCache.delete(tableName, query); return this.enqueueOp(async () => {
const allRows = await this.memoryCache.find(tableName, { table: tableName }); const count = await this.memoryCache.delete(tableName, query);
await this.writeTableData(tableName, allRows); const allRows = await this.memoryCache.find(tableName, { table: tableName });
return count; await this.writeTableData(tableName, allRows);
return count;
});
} }
async count(tableName, query) { async count(tableName, query) {
return this.memoryCache.count(tableName, query); return this.memoryCache.count(tableName, query);
} }
async clear(tableName) { async clear(tableName) {
await this.memoryCache.clear(tableName); return this.enqueueOp(async () => {
await this.writeTableData(tableName, []); await this.memoryCache.clear(tableName);
await this.writeTableData(tableName, []);
});
} }
// ---- 动态索引(v0.3.0 ---- // ---- 动态索引(v0.3.0 ----
async createIndex(tableName, column, unique) { async createIndex(tableName, column, unique) {
await this.memoryCache.createIndex(tableName, column, unique); return this.enqueueOp(async () => {
// v0.4.2-fix: 索引标记持久化(重启后索引结构恢复) await this.memoryCache.createIndex(tableName, column, unique);
const schema = await this.memoryCache.getTableSchema(tableName); // v0.4.2-fix: 索引标记持久化(重启后索引结构恢复)
if (schema) const schema = await this.memoryCache.getTableSchema(tableName);
await this.setMeta(`schema_${tableName}`, JSON.stringify(schema)); if (schema)
await this.setMeta(`schema_${tableName}`, JSON.stringify(schema));
});
} }
async dropIndex(tableName, column, indexName) { async dropIndex(tableName, column, indexName) {
await this.memoryCache.dropIndex(tableName, column, indexName); return this.enqueueOp(async () => {
const schema = await this.memoryCache.getTableSchema(tableName); await this.memoryCache.dropIndex(tableName, column, indexName);
if (schema) const schema = await this.memoryCache.getTableSchema(tableName);
await this.setMeta(`schema_${tableName}`, JSON.stringify(schema)); if (schema)
await this.setMeta(`schema_${tableName}`, JSON.stringify(schema));
});
} }
// ---- 事务 ---- // ---- 事务 ----
async beginTransaction() { async beginTransaction() {
await this.memoryCache.beginTransaction(); await this.memoryCache.beginTransaction();
} }
async commitTransaction() { async commitTransaction() {
await this.memoryCache.commitTransaction(); return this.enqueueOp(async () => {
// 将内存数据刷到 OPFS await this.memoryCache.commitTransaction();
const tableNames = await this.memoryCache.getTableNames(); // 将内存数据刷到 OPFS
for (const tableName of tableNames) { const tableNames = await this.memoryCache.getTableNames();
const rows = await this.memoryCache.find(tableName, { table: tableName }); for (const tableName of tableNames) {
await this.writeTableData(tableName, rows); const rows = await this.memoryCache.find(tableName, { table: tableName });
} await this.writeTableData(tableName, rows);
}
});
} }
async rollbackTransaction() { async rollbackTransaction() {
await this.memoryCache.rollbackTransaction(); await this.memoryCache.rollbackTransaction();
@@ -1796,6 +1850,7 @@ class OPFSEngine {
return this.tablesDir; return this.tablesDir;
} }
async writeTableData(tableName, data) { async writeTableData(tableName, data) {
// 由 enqueueOp 串行化调用,此处直接写文件
const dir = this.ensureDir(); const dir = this.ensureDir();
const fileName = `${tableName}.json`; const fileName = `${tableName}.json`;
const fileHandle = await dir.getFileHandle(fileName, { create: true }); const fileHandle = await dir.getFileHandle(fileName, { create: true });
@@ -3270,6 +3325,11 @@ class LSM {
this.compacting = false; // 防止重复触发 compaction this.compacting = false; // 防止重复触发 compaction
/** 串行化 flush/compaction 链:保证持久化顺序与 id 分配顺序一致 */ /** 串行化 flush/compaction 链:保证持久化顺序与 id 分配顺序一致 */
this.flushChain = Promise.resolve(); this.flushChain = Promise.resolve();
/**
* v0.4.3-fix: 最近一次后台 flush/compaction 失败
* 后台失败不卡死链吞错防死锁但在显式 flush()/close() 时报告不静默
*/
this.lastBackgroundError = null;
this.memtableSizeThreshold = config.memtableSizeThreshold ?? DEFAULT_MEMTABLE_SIZE; this.memtableSizeThreshold = config.memtableSizeThreshold ?? DEFAULT_MEMTABLE_SIZE;
this.memtable = new MemTable(this.memtableSizeThreshold); this.memtable = new MemTable(this.memtableSizeThreshold);
this.levelSizeMultiplier = config.levelSizeMultiplier ?? DEFAULT_LEVEL_SIZE_MULTIPLIER; this.levelSizeMultiplier = config.levelSizeMultiplier ?? DEFAULT_LEVEL_SIZE_MULTIPLIER;
@@ -3367,11 +3427,26 @@ class LSM {
return this.flushChain return this.flushChain
.then(task) .then(task)
.catch((error) => { .catch((error) => {
// v0.4.3-fix: 记录失败(flush()/close() 时报告),不再完全静默吞错
this.lastBackgroundError = error;
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn('[AriaEngine LSM] background flush/compaction failed:', error); console.warn('[AriaEngine LSM] background flush/compaction failed:', error);
// catch 返回 undefined → 链恢复为 resolved,后续任务继续 // catch 返回 undefined → 链恢复为 resolved,后续任务继续
}); });
} }
/**
* v0.4.3-fix: 排空后台链 循环等待 flushChain 直到稳定
* 任务完成时可能级联调度新任务compaction 多级触发单次 await 等不到
* close/flush/clear 必须等待全部后台任务完成后才能安全关闭底层存储
*/
async drainChain() {
while (true) {
const chain = this.flushChain;
await chain;
if (this.flushChain === chain)
return;
}
}
/** 将指定 Immutable MemTable 刷盘为 SSTableid 由 store 按命名空间分配) */ /** 将指定 Immutable MemTable 刷盘为 SSTableid 由 store 按命名空间分配) */
async flushImmutableAsync(frozen) { async flushImmutableAsync(frozen) {
const entries = frozen.getAllEntries(); const entries = frozen.getAllEntries();
@@ -3411,27 +3486,27 @@ class LSM {
this.scheduleCompact(0); this.scheduleCompact(0);
} }
} }
/** 异步调度 compaction,使用 setTimeout 分片执行 */ /**
* 异步调度 compaction
* v0.4.3-fix: 去掉 setTimeout 分片 此前未触发的定时器在 close 后执行
* 用已关闭的 backend 写存储错误被吞 close reopen 时旧闭包引用新 backend 交叉污染
* 现在直接挂在 flushChain 串行执行close drainChain 能等到全部完成
*/
scheduleCompact(level) { scheduleCompact(level) {
if (level >= MAX_LSM_LEVELS - 1 || this.compacting) if (level >= MAX_LSM_LEVELS - 1 || this.compacting)
return; return;
this.compacting = true; this.compacting = true;
setTimeout(() => { this.flushChain = this.enqueueOnChain(() => this.compactLevelAsync(level).finally(() => {
try { this.compacting = false;
this.flushChain = this.enqueueOnChain(() => this.compactLevelAsync(level)); // 连续触发:如果 compaction 后仍然超标,继续调度
if (this.levels[level].length >= 4) {
this.scheduleCompact(level);
} }
finally { // 检查下一级是否需要 compaction
this.compacting = false; if (level + 1 < MAX_LSM_LEVELS - 1 && this.levels[level + 1].length >= 4) {
// 连续触发:如果 compaction 后仍然超标,继续调度 this.scheduleCompact(level + 1);
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);
}
} }
}, 0); }));
} }
/** 背压场景下排队 compaction(写入路径调用) */ /** 背压场景下排队 compaction(写入路径调用) */
enqueueCompact(level) { enqueueCompact(level) {
@@ -3447,6 +3522,8 @@ class LSM {
} }
}).catch((error) => { }).catch((error) => {
this.compacting = false; this.compacting = false;
// v0.4.3-fix: 记录失败(flush()/close() 时报告)
this.lastBackgroundError = error;
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn('[AriaEngine LSM] background compaction failed:', error); console.warn('[AriaEngine LSM] background compaction failed:', error);
}); });
@@ -3457,10 +3534,11 @@ class LSM {
/** /**
* 预加载指定 key 范围内可能命中的所有 SSTable 到缓存 * 预加载指定 key 范围内可能命中的所有 SSTable 到缓存
* 在同步扫描/查找之前调用保证 loadSSTableReader 不会因缓存未命中而返回 null * 在同步扫描/查找之前调用保证 loadSSTableReader 不会因缓存未命中而返回 null
* 先等待 flush 链完成避免 flush 的缓存裁剪与预加载竞争驱逐刚加载的 SSTable * v0.4.3-fix: 等待链稳定drainChain 后台 flush/compaction 在链上动态增长
* 单次 await compaction 仍可能合并 levels导致扫描时新 meta 缓存未命中而跳块丢数据
*/ */
async prefetchRange(startKey, endKey) { async prefetchRange(startKey, endKey) {
await this.flushChain; await this.drainChain();
this.trimCache(); this.trimCache();
const toLoad = []; const toLoad = [];
for (let level = 0; level < MAX_LSM_LEVELS; level++) { for (let level = 0; level < MAX_LSM_LEVELS; level++) {
@@ -3479,7 +3557,8 @@ class LSM {
async prefetchKeys(keys) { async prefetchKeys(keys) {
if (keys.length === 0) if (keys.length === 0)
return; return;
await this.flushChain; // v0.4.3-fix: 等待链稳定(同 prefetchRange,防 compaction 竞态丢数据)
await this.drainChain();
this.trimCache(); this.trimCache();
const toLoad = new Set(); const toLoad = new Set();
for (let level = 0; level < MAX_LSM_LEVELS; level++) { for (let level = 0; level < MAX_LSM_LEVELS; level++) {
@@ -3670,8 +3749,14 @@ class LSM {
} }
/** 等待所有排队的 flush/compaction 完成,并将剩余数据刷盘 */ /** 等待所有排队的 flush/compaction 完成,并将剩余数据刷盘 */
async flush() { async flush() {
// 等待链上已排队的 flush/compaction // v0.4.3-fix: 报告后台失败(消费一次,不永久吞错)
await this.flushChain; 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 数据未刷盘,在链尾追加 // 若仍有 frozen 数据未刷盘,在链尾追加
if (this.immutableMemtable) { if (this.immutableMemtable) {
const frozen = this.immutableMemtable; const frozen = this.immutableMemtable;
@@ -3684,8 +3769,12 @@ class LSM {
await this.flushImmutableAsync(frozen); await this.flushImmutableAsync(frozen);
} }
this.frozenMemtables = []; this.frozenMemtables = [];
// 刷盘完成后级联调度可能触发 compaction → 排空到稳定
await this.drainChain();
} }
async clear() { async clear() {
// v0.4.3-fix: 清空前排空后台任务(避免 compaction 在清空后写回残留 meta/数据)
await this.drainChain();
this.memtable.clear(); this.memtable.clear();
this.immutableMemtable = null; this.immutableMemtable = null;
this.frozenMemtables = []; this.frozenMemtables = [];
@@ -6300,6 +6389,16 @@ class AriaEngine {
async commitTransaction() { async commitTransaction() {
if (!this.currentTxnId) if (!this.currentTxnId)
throw new DatabaseError('No active transaction', 'TX_NONE'); 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) { if (this.txnSnapshot) {
for (const [key, value] of this.txnSnapshot) { for (const [key, value] of this.txnSnapshot) {
if (value.__txn_deleted) { if (value.__txn_deleted) {
@@ -6311,15 +6410,8 @@ class AriaEngine {
} }
} }
this.mvcc.commitTransaction(this.currentTxnId); this.mvcc.commitTransaction(this.currentTxnId);
await this.wal.append({
type: WALRecordType.COMMIT,
txnId: this.currentTxnId,
tableName: '',
key: '',
});
this.currentTxnId = null; this.currentTxnId = null;
this.txnSnapshot = null; this.txnSnapshot = null;
await this.wal.flush();
} }
async rollbackTransaction() { async rollbackTransaction() {
if (!this.currentTxnId) if (!this.currentTxnId)
+1 -1
View File
File diff suppressed because one or more lines are too long
+177 -85
View File
@@ -36,7 +36,7 @@
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 版本 // 版本
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const VERSION = '0.4.2'; const VERSION = '0.4.3';
/** /**
* metona-sqlark Shared WHERE Matcher 统一的条件匹配逻辑 * metona-sqlark Shared WHERE Matcher 统一的条件匹配逻辑
@@ -1023,6 +1023,13 @@
} }
} }
async close() { async close() {
// v0.4.3-fix: 活跃事务时先回滚(避免 commit 对已关闭连接报错)
if (this.txActive) {
try {
await this.rollbackTransaction();
}
catch { /* 回滚失败不阻塞关闭 */ }
}
if (this.db) { if (this.db) {
this.db.onversionchange = null; // 清理监听器 this.db.onversionchange = null; // 清理监听器
this.db.close(); this.db.close();
@@ -1609,6 +1616,18 @@
this.dbName = ''; this.dbName = '';
// 运行时内存缓存(OPFS 文件读写有延迟) // 运行时内存缓存(OPFS 文件读写有延迟)
this.memoryCache = new MemoryEngine(); this.memoryCache = new MemoryEngine();
/**
* v0.4.3-fix: 写操作串行队列 内存写 + 快照 + 文件持久化整体排队执行
* close() 等待队列排空后再释放目录句柄避免 close 后挂起写泄漏/读旧数据
* 前一个操作失败不阻塞后续错误仍返回给调用方
*/
this.opQueue = Promise.resolve();
}
/** 将写操作加入串行队列(快照在队列内取,始终最新) */
enqueueOp(fn) {
const run = this.opQueue.then(fn, fn);
this.opQueue = run.then(() => undefined, () => undefined);
return run;
} }
// ---- 生命周期 ---- // ---- 生命周期 ----
async open(dbName, version) { async open(dbName, version) {
@@ -1622,6 +1641,11 @@
await this.loadExistingTables(); await this.loadExistingTables();
} }
async close() { async close() {
// v0.4.3-fix: 等待所有挂起写操作完成(否则 close 后写仍在进行 → 重启读旧数据)
try {
await this.opQueue;
}
catch { /* 写失败已返回给调用方 */ }
this.root = null; this.root = null;
this.tablesDir = null; this.tablesDir = null;
await this.memoryCache.close(); await this.memoryCache.close();
@@ -1632,12 +1656,22 @@
// ---- v0.4.2-fix: 自愈 / 重置 / 元数据 ---- // ---- v0.4.2-fix: 自愈 / 重置 / 元数据 ----
/** 自愈:重置内存缓存后从 OPFS 重新加载(单文件损坏不影响其他表) */ /** 自愈:重置内存缓存后从 OPFS 重新加载(单文件损坏不影响其他表) */
async repair() { async repair() {
// v0.4.3-fix: 先等写队列排空(避免与挂起写竞态)
try {
await this.opQueue;
}
catch { /* ignore */ }
await this.memoryCache.close(); await this.memoryCache.close();
await this.memoryCache.open(this.dbName, 1); await this.memoryCache.open(this.dbName, 1);
await this.loadExistingTables(); await this.loadExistingTables();
} }
/** 清空全部数据与表结构(删除目录内全部文件) */ /** 清空全部数据与表结构(删除目录内全部文件) */
async clearAll() { async clearAll() {
// v0.4.3-fix: 先等写队列排空
try {
await this.opQueue;
}
catch { /* ignore */ }
await this.memoryCache.close(); await this.memoryCache.close();
await this.memoryCache.open(this.dbName, 1); await this.memoryCache.open(this.dbName, 1);
if (this.tablesDir) { if (this.tablesDir) {
@@ -1672,29 +1706,33 @@
} }
// ---- 表管理 ---- // ---- 表管理 ----
async createTable(schema) { async createTable(schema) {
await this.memoryCache.createTable(schema); return this.enqueueOp(async () => {
// v0.4.2-fix: schema 持久化(此前仅写空数据文件 → 空表重启后消失、索引标记丢失) await this.memoryCache.createTable(schema);
await this.setMeta(`schema_${schema.name}`, JSON.stringify(schema)); // v0.4.2-fix: schema 持久化(此前仅写空数据文件 → 空表重启后消失、索引标记丢失)
// OPFS 中表以空 JSON 数组文件形式存在 await this.setMeta(`schema_${schema.name}`, JSON.stringify(schema));
await this.writeTableData(schema.name, []); // OPFS 中表以空 JSON 数组文件形式存在
await this.writeTableData(schema.name, []);
});
} }
async dropTable(tableName) { async dropTable(tableName) {
await this.memoryCache.dropTable(tableName); return this.enqueueOp(async () => {
// v0.4.2-fix: 清理 schema meta(否则重启恢复幽灵表) await this.memoryCache.dropTable(tableName);
if (this.tablesDir) { // v0.4.2-fix: 清理 schema meta(否则重启恢复幽灵表)
try { if (this.tablesDir) {
await this.tablesDir.removeEntry(`__metona_schema_${tableName}.meta`); try {
await this.tablesDir.removeEntry(`__metona_schema_${tableName}.meta`);
}
catch {
// 文件不存在则忽略
}
try {
await this.tablesDir.removeEntry(`${tableName}.json`);
}
catch {
// 文件不存在则忽略
}
} }
catch { });
// 文件不存在则忽略
}
try {
await this.tablesDir.removeEntry(`${tableName}.json`);
}
catch {
// 文件不存在则忽略
}
}
} }
async hasTable(tableName) { async hasTable(tableName) {
if (!this.tablesDir) if (!this.tablesDir)
@@ -1723,20 +1761,24 @@
} }
/** v0.4.2-fix: 引擎级 ALTER TABLE — 内存 + schema 持久化 + 整表文件重写 */ /** v0.4.2-fix: 引擎级 ALTER TABLE — 内存 + schema 持久化 + 整表文件重写 */
async alterTable(tableName, action, column) { async alterTable(tableName, action, column) {
await this.memoryCache.alterTable(tableName, action, column); return this.enqueueOp(async () => {
const schema = await this.memoryCache.getTableSchema(tableName); await this.memoryCache.alterTable(tableName, action, column);
if (schema) const schema = await this.memoryCache.getTableSchema(tableName);
await this.setMeta(`schema_${tableName}`, JSON.stringify(schema)); if (schema)
const rows = await this.memoryCache.find(tableName, { table: tableName }); await this.setMeta(`schema_${tableName}`, JSON.stringify(schema));
await this.writeTableData(tableName, rows); const rows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, rows);
});
} }
// ---- CRUD ---- // ---- CRUD ----
async insert(tableName, rows) { async insert(tableName, rows) {
const pks = await this.memoryCache.insert(tableName, rows); return this.enqueueOp(async () => {
// 持久化到 OPFS const pks = await this.memoryCache.insert(tableName, rows);
const allRows = await this.memoryCache.find(tableName, { table: tableName }); // 持久化到 OPFS(快照在队列内取,始终最新)
await this.writeTableData(tableName, allRows); const allRows = await this.memoryCache.find(tableName, { table: tableName });
return pks; await this.writeTableData(tableName, allRows);
return pks;
});
} }
async find(tableName, query) { async find(tableName, query) {
return this.memoryCache.find(tableName, query); return this.memoryCache.find(tableName, query);
@@ -1746,50 +1788,62 @@
return this.memoryCache.findStream(tableName, query, onRow); return this.memoryCache.findStream(tableName, query, onRow);
} }
async update(tableName, query, updates) { async update(tableName, query, updates) {
const count = await this.memoryCache.update(tableName, query, updates); return this.enqueueOp(async () => {
const allRows = await this.memoryCache.find(tableName, { table: tableName }); const count = await this.memoryCache.update(tableName, query, updates);
await this.writeTableData(tableName, allRows); const allRows = await this.memoryCache.find(tableName, { table: tableName });
return count; await this.writeTableData(tableName, allRows);
return count;
});
} }
async delete(tableName, query) { async delete(tableName, query) {
const count = await this.memoryCache.delete(tableName, query); return this.enqueueOp(async () => {
const allRows = await this.memoryCache.find(tableName, { table: tableName }); const count = await this.memoryCache.delete(tableName, query);
await this.writeTableData(tableName, allRows); const allRows = await this.memoryCache.find(tableName, { table: tableName });
return count; await this.writeTableData(tableName, allRows);
return count;
});
} }
async count(tableName, query) { async count(tableName, query) {
return this.memoryCache.count(tableName, query); return this.memoryCache.count(tableName, query);
} }
async clear(tableName) { async clear(tableName) {
await this.memoryCache.clear(tableName); return this.enqueueOp(async () => {
await this.writeTableData(tableName, []); await this.memoryCache.clear(tableName);
await this.writeTableData(tableName, []);
});
} }
// ---- 动态索引(v0.3.0 ---- // ---- 动态索引(v0.3.0 ----
async createIndex(tableName, column, unique) { async createIndex(tableName, column, unique) {
await this.memoryCache.createIndex(tableName, column, unique); return this.enqueueOp(async () => {
// v0.4.2-fix: 索引标记持久化(重启后索引结构恢复) await this.memoryCache.createIndex(tableName, column, unique);
const schema = await this.memoryCache.getTableSchema(tableName); // v0.4.2-fix: 索引标记持久化(重启后索引结构恢复)
if (schema) const schema = await this.memoryCache.getTableSchema(tableName);
await this.setMeta(`schema_${tableName}`, JSON.stringify(schema)); if (schema)
await this.setMeta(`schema_${tableName}`, JSON.stringify(schema));
});
} }
async dropIndex(tableName, column, indexName) { async dropIndex(tableName, column, indexName) {
await this.memoryCache.dropIndex(tableName, column, indexName); return this.enqueueOp(async () => {
const schema = await this.memoryCache.getTableSchema(tableName); await this.memoryCache.dropIndex(tableName, column, indexName);
if (schema) const schema = await this.memoryCache.getTableSchema(tableName);
await this.setMeta(`schema_${tableName}`, JSON.stringify(schema)); if (schema)
await this.setMeta(`schema_${tableName}`, JSON.stringify(schema));
});
} }
// ---- 事务 ---- // ---- 事务 ----
async beginTransaction() { async beginTransaction() {
await this.memoryCache.beginTransaction(); await this.memoryCache.beginTransaction();
} }
async commitTransaction() { async commitTransaction() {
await this.memoryCache.commitTransaction(); return this.enqueueOp(async () => {
// 将内存数据刷到 OPFS await this.memoryCache.commitTransaction();
const tableNames = await this.memoryCache.getTableNames(); // 将内存数据刷到 OPFS
for (const tableName of tableNames) { const tableNames = await this.memoryCache.getTableNames();
const rows = await this.memoryCache.find(tableName, { table: tableName }); for (const tableName of tableNames) {
await this.writeTableData(tableName, rows); const rows = await this.memoryCache.find(tableName, { table: tableName });
} await this.writeTableData(tableName, rows);
}
});
} }
async rollbackTransaction() { async rollbackTransaction() {
await this.memoryCache.rollbackTransaction(); await this.memoryCache.rollbackTransaction();
@@ -1802,6 +1856,7 @@
return this.tablesDir; return this.tablesDir;
} }
async writeTableData(tableName, data) { async writeTableData(tableName, data) {
// 由 enqueueOp 串行化调用,此处直接写文件
const dir = this.ensureDir(); const dir = this.ensureDir();
const fileName = `${tableName}.json`; const fileName = `${tableName}.json`;
const fileHandle = await dir.getFileHandle(fileName, { create: true }); const fileHandle = await dir.getFileHandle(fileName, { create: true });
@@ -3276,6 +3331,11 @@
this.compacting = false; // 防止重复触发 compaction this.compacting = false; // 防止重复触发 compaction
/** 串行化 flush/compaction 链:保证持久化顺序与 id 分配顺序一致 */ /** 串行化 flush/compaction 链:保证持久化顺序与 id 分配顺序一致 */
this.flushChain = Promise.resolve(); this.flushChain = Promise.resolve();
/**
* v0.4.3-fix: 最近一次后台 flush/compaction 失败
* 后台失败不卡死链吞错防死锁但在显式 flush()/close() 时报告不静默
*/
this.lastBackgroundError = null;
this.memtableSizeThreshold = config.memtableSizeThreshold ?? DEFAULT_MEMTABLE_SIZE; this.memtableSizeThreshold = config.memtableSizeThreshold ?? DEFAULT_MEMTABLE_SIZE;
this.memtable = new MemTable(this.memtableSizeThreshold); this.memtable = new MemTable(this.memtableSizeThreshold);
this.levelSizeMultiplier = config.levelSizeMultiplier ?? DEFAULT_LEVEL_SIZE_MULTIPLIER; this.levelSizeMultiplier = config.levelSizeMultiplier ?? DEFAULT_LEVEL_SIZE_MULTIPLIER;
@@ -3373,11 +3433,26 @@
return this.flushChain return this.flushChain
.then(task) .then(task)
.catch((error) => { .catch((error) => {
// v0.4.3-fix: 记录失败(flush()/close() 时报告),不再完全静默吞错
this.lastBackgroundError = error;
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn('[AriaEngine LSM] background flush/compaction failed:', error); console.warn('[AriaEngine LSM] background flush/compaction failed:', error);
// catch 返回 undefined → 链恢复为 resolved,后续任务继续 // catch 返回 undefined → 链恢复为 resolved,后续任务继续
}); });
} }
/**
* v0.4.3-fix: 排空后台链 循环等待 flushChain 直到稳定
* 任务完成时可能级联调度新任务compaction 多级触发单次 await 等不到
* close/flush/clear 必须等待全部后台任务完成后才能安全关闭底层存储
*/
async drainChain() {
while (true) {
const chain = this.flushChain;
await chain;
if (this.flushChain === chain)
return;
}
}
/** 将指定 Immutable MemTable 刷盘为 SSTableid 由 store 按命名空间分配) */ /** 将指定 Immutable MemTable 刷盘为 SSTableid 由 store 按命名空间分配) */
async flushImmutableAsync(frozen) { async flushImmutableAsync(frozen) {
const entries = frozen.getAllEntries(); const entries = frozen.getAllEntries();
@@ -3417,27 +3492,27 @@
this.scheduleCompact(0); this.scheduleCompact(0);
} }
} }
/** 异步调度 compaction,使用 setTimeout 分片执行 */ /**
* 异步调度 compaction
* v0.4.3-fix: 去掉 setTimeout 分片 此前未触发的定时器在 close 后执行
* 用已关闭的 backend 写存储错误被吞 close reopen 时旧闭包引用新 backend 交叉污染
* 现在直接挂在 flushChain 串行执行close drainChain 能等到全部完成
*/
scheduleCompact(level) { scheduleCompact(level) {
if (level >= MAX_LSM_LEVELS - 1 || this.compacting) if (level >= MAX_LSM_LEVELS - 1 || this.compacting)
return; return;
this.compacting = true; this.compacting = true;
setTimeout(() => { this.flushChain = this.enqueueOnChain(() => this.compactLevelAsync(level).finally(() => {
try { this.compacting = false;
this.flushChain = this.enqueueOnChain(() => this.compactLevelAsync(level)); // 连续触发:如果 compaction 后仍然超标,继续调度
if (this.levels[level].length >= 4) {
this.scheduleCompact(level);
} }
finally { // 检查下一级是否需要 compaction
this.compacting = false; if (level + 1 < MAX_LSM_LEVELS - 1 && this.levels[level + 1].length >= 4) {
// 连续触发:如果 compaction 后仍然超标,继续调度 this.scheduleCompact(level + 1);
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);
}
} }
}, 0); }));
} }
/** 背压场景下排队 compaction(写入路径调用) */ /** 背压场景下排队 compaction(写入路径调用) */
enqueueCompact(level) { enqueueCompact(level) {
@@ -3453,6 +3528,8 @@
} }
}).catch((error) => { }).catch((error) => {
this.compacting = false; this.compacting = false;
// v0.4.3-fix: 记录失败(flush()/close() 时报告)
this.lastBackgroundError = error;
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn('[AriaEngine LSM] background compaction failed:', error); console.warn('[AriaEngine LSM] background compaction failed:', error);
}); });
@@ -3463,10 +3540,11 @@
/** /**
* 预加载指定 key 范围内可能命中的所有 SSTable 到缓存 * 预加载指定 key 范围内可能命中的所有 SSTable 到缓存
* 在同步扫描/查找之前调用保证 loadSSTableReader 不会因缓存未命中而返回 null * 在同步扫描/查找之前调用保证 loadSSTableReader 不会因缓存未命中而返回 null
* 先等待 flush 链完成避免 flush 的缓存裁剪与预加载竞争驱逐刚加载的 SSTable * v0.4.3-fix: 等待链稳定drainChain 后台 flush/compaction 在链上动态增长
* 单次 await compaction 仍可能合并 levels导致扫描时新 meta 缓存未命中而跳块丢数据
*/ */
async prefetchRange(startKey, endKey) { async prefetchRange(startKey, endKey) {
await this.flushChain; await this.drainChain();
this.trimCache(); this.trimCache();
const toLoad = []; const toLoad = [];
for (let level = 0; level < MAX_LSM_LEVELS; level++) { for (let level = 0; level < MAX_LSM_LEVELS; level++) {
@@ -3485,7 +3563,8 @@
async prefetchKeys(keys) { async prefetchKeys(keys) {
if (keys.length === 0) if (keys.length === 0)
return; return;
await this.flushChain; // v0.4.3-fix: 等待链稳定(同 prefetchRange,防 compaction 竞态丢数据)
await this.drainChain();
this.trimCache(); this.trimCache();
const toLoad = new Set(); const toLoad = new Set();
for (let level = 0; level < MAX_LSM_LEVELS; level++) { for (let level = 0; level < MAX_LSM_LEVELS; level++) {
@@ -3676,8 +3755,14 @@
} }
/** 等待所有排队的 flush/compaction 完成,并将剩余数据刷盘 */ /** 等待所有排队的 flush/compaction 完成,并将剩余数据刷盘 */
async flush() { async flush() {
// 等待链上已排队的 flush/compaction // v0.4.3-fix: 报告后台失败(消费一次,不永久吞错)
await this.flushChain; 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 数据未刷盘,在链尾追加 // 若仍有 frozen 数据未刷盘,在链尾追加
if (this.immutableMemtable) { if (this.immutableMemtable) {
const frozen = this.immutableMemtable; const frozen = this.immutableMemtable;
@@ -3690,8 +3775,12 @@
await this.flushImmutableAsync(frozen); await this.flushImmutableAsync(frozen);
} }
this.frozenMemtables = []; this.frozenMemtables = [];
// 刷盘完成后级联调度可能触发 compaction → 排空到稳定
await this.drainChain();
} }
async clear() { async clear() {
// v0.4.3-fix: 清空前排空后台任务(避免 compaction 在清空后写回残留 meta/数据)
await this.drainChain();
this.memtable.clear(); this.memtable.clear();
this.immutableMemtable = null; this.immutableMemtable = null;
this.frozenMemtables = []; this.frozenMemtables = [];
@@ -6306,6 +6395,16 @@
async commitTransaction() { async commitTransaction() {
if (!this.currentTxnId) if (!this.currentTxnId)
throw new DatabaseError('No active transaction', 'TX_NONE'); 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) { if (this.txnSnapshot) {
for (const [key, value] of this.txnSnapshot) { for (const [key, value] of this.txnSnapshot) {
if (value.__txn_deleted) { if (value.__txn_deleted) {
@@ -6317,15 +6416,8 @@
} }
} }
this.mvcc.commitTransaction(this.currentTxnId); this.mvcc.commitTransaction(this.currentTxnId);
await this.wal.append({
type: WALRecordType.COMMIT,
txnId: this.currentTxnId,
tableName: '',
key: '',
});
this.currentTxnId = null; this.currentTxnId = null;
this.txnSnapshot = null; this.txnSnapshot = null;
await this.wal.flush();
} }
async rollbackTransaction() { async rollbackTransaction() {
if (!this.currentTxnId) if (!this.currentTxnId)
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@metona-team/metona-sqlark", "name": "@metona-team/metona-sqlark",
"version": "0.4.2", "version": "0.4.3",
"description": "Frontend SQL database with in-memory and disk dual-mode storage", "description": "Frontend SQL database with in-memory and disk dual-mode storage",
"type": "module", "type": "module",
"main": "dist/metona-sqlark.cjs", "main": "dist/metona-sqlark.cjs",
+1 -1
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>⚡ 性能基准 — MetonaSqlark v0.4.2</title> <title>⚡ 性能基准 — MetonaSqlark v0.4.3</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>">
<style> <style>
:root { :root {
+4 -4
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🧪 在线演示 — MetonaSqlark v0.4.2</title> <title>🧪 在线演示 — MetonaSqlark v0.4.3</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>">
<style> <style>
:root { :root {
@@ -84,7 +84,7 @@
<a href="demo.html" class="nav-active">演示</a> <a href="demo.html" class="nav-active">演示</a>
<a href="benchmark.html">基准</a> <a href="benchmark.html">基准</a>
</nav> </nav>
<div class="status"><span class="dot" id="engine-dot"></span> <span id="engine-status">Memory</span> 模式 — v0.4.2</div> <div class="status"><span class="dot" id="engine-dot"></span> <span id="engine-status">Memory</span> 模式 — v0.4.3</div>
<button class="btn btn-preset" onclick="switchEngine('memory')" id="btn-memory" style="margin:6px 4px 6px 0;padding:6px 12px;">⚡ Memory</button> <button class="btn btn-preset" onclick="switchEngine('memory')" id="btn-memory" style="margin:6px 4px 6px 0;padding:6px 12px;">⚡ Memory</button>
<button class="btn btn-preset" onclick="switchEngine('aria')" id="btn-aria" style="margin:6px 0;padding:6px 12px;color:#ec4899;border-color:#ec4899;">🌲 Aria</button> <button class="btn btn-preset" onclick="switchEngine('aria')" id="btn-aria" style="margin:6px 0;padding:6px 12px;color:#ec4899;border-color:#ec4899;">🌲 Aria</button>
</header> </header>
@@ -92,7 +92,7 @@
<div class="main"> <div class="main">
<div class="editor-panel"> <div class="editor-panel">
<div class="editor-area"> <div class="editor-area">
<textarea id="sql-input" placeholder="输入 SQL 语句...&#10;&#10;SELECT * FROM users;&#10;INSERT INTO users VALUES ('4', 'Diana', 'diana@test.com', 28);&#10;SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id;">-- 🚀 MetonaSqlark v0.4.2 在线演示 <textarea id="sql-input" placeholder="输入 SQL 语句...&#10;&#10;SELECT * FROM users;&#10;INSERT INTO users VALUES ('4', 'Diana', 'diana@test.com', 28);&#10;SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id;">-- 🚀 MetonaSqlark v0.4.3 在线演示
-- 已预置 users / orders / products 表数据 -- 已预置 users / orders / products 表数据
-- 新特性: ALTER TABLE · TRUNCATE TABLE · WAL同步 · MVCC · SQL注入防护 -- 新特性: ALTER TABLE · TRUNCATE TABLE · WAL同步 · MVCC · SQL注入防护
@@ -554,7 +554,7 @@ SELECT COUNT(*) as total FROM temp_logs;
-- 清理 -- 清理
DROP TABLE temp_logs;`, DROP TABLE temp_logs;`,
aria: `-- 🌲 AriaEngine 演示 (v0.4.2) aria: `-- 🌲 AriaEngine 演示 (v0.4.3)
-- 点击右上角「🌲 Aria」按钮切换数据库引擎到 AriaEngine -- 点击右上角「🌲 Aria」按钮切换数据库引擎到 AriaEngine
-- 当前数据库即运行在 Aria 引擎上(LSM-Tree · WAL 崩溃恢复 · MVCC · BloomFilter -- 当前数据库即运行在 Aria 引擎上(LSM-Tree · WAL 崩溃恢复 · MVCC · BloomFilter
-- 基础 CRUD 与 Memory 引擎完全兼容 -- 基础 CRUD 与 Memory 引擎完全兼容
+3 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>📖 API 文档 — MetonaSqlark v0.4.2</title> <title>📖 API 文档 — MetonaSqlark v0.4.3</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>">
<style> <style>
:root { :root {
@@ -761,7 +761,8 @@ db.<span class="f">broadcastChange</span>(<span class="s">'users'</span>);</pre>
<p><strong>v0.2.0 新增</strong> — AriaEngine 是专为 MetonaSqlark 设计的页面式存储引擎,对标 SQLite 设计理念。<br> <p><strong>v0.2.0 新增</strong> — AriaEngine 是专为 MetonaSqlark 设计的页面式存储引擎,对标 SQLite 设计理念。<br>
<strong>v0.2.4 生产级</strong> — 二级索引 · MVCC · BloomFilter · WAL CRC全同步 · AES-GCM加密 · Savepoint · EXPLAIN · ANALYZE · REINDEX · VACUUM · BufferPool · 零死代码。<br> <strong>v0.2.4 生产级</strong> — 二级索引 · MVCC · BloomFilter · WAL CRC全同步 · AES-GCM加密 · Savepoint · EXPLAIN · ANALYZE · REINDEX · VACUUM · BufferPool · 零死代码。<br>
<strong>v0.3.2 表达式与并发</strong> — WAL full模式真正同步 · MVCC接入读写路径 · SSTableReader二分查找统一 · crypto实例化 · IndexedDB索引利用 · compactLevel public接口 · WAL大小阈值自动checkpoint · SQL注入防护 · ALTER TABLE · TRUNCATE TABLE · 多标签页同步 · IDB schema持久化。<br> <strong>v0.3.2 表达式与并发</strong> — WAL full模式真正同步 · MVCC接入读写路径 · SSTableReader二分查找统一 · crypto实例化 · IndexedDB索引利用 · compactLevel public接口 · WAL大小阈值自动checkpoint · SQL注入防护 · ALTER TABLE · TRUNCATE TABLE · 多标签页同步 · IDB schema持久化。<br>
<strong>v0.4.2 生产就绪与崩溃自愈</strong> — 残缺 SSTable 打开自动跳过(不删库)· WAL 记录与计数原子写入 + 按 key 扫描恢复 · 事务进行中 checkpoint 不截断 WAL · 二级索引跨重启自动恢复 · ALTER TABLE / 事务内 DDL 全引擎持久化 · ON UPDATE 外键级联(含更新主键)· OPFS schema 持久化(空表/索引完整保留)· `repair()` / `clearAll()` 统一自愈接口 · 迁移版本持久化到库内 · 944 测试 50 套件。</p> <strong>v0.4.2 生产就绪与崩溃自愈</strong> — 残缺 SSTable 打开自动跳过(不删库)· WAL 记录与计数原子写入 + 按 key 扫描恢复 · 事务进行中 checkpoint 不截断 WAL · 二级索引跨重启自动恢复 · ALTER TABLE / 事务内 DDL 全引擎持久化 · ON UPDATE 外键级联(含更新主键)· OPFS schema 持久化(空表/索引完整保留)· `repair()` / `clearAll()` 统一自愈接口 · 迁移版本持久化到库内<br>
<strong>v0.4.3 关闭时序与后台任务加固</strong> — 后台 flush/compaction 不再使用 setTimeout 延迟(close 排空全部任务后才关闭存储,杜绝"backend 关闭后写存储/重开污染")· 后台失败在 `flush()`/`close()` 显式报告(`ARIA_BACKGROUND_ERROR`,不静默吞错)· 预加载等待链稳定(修复 compaction 竞态跳块丢数据)· 事务提交先落 WAL 再合并快照(崩溃一致)· OPFS 写操作串行队列 + close 等待 · 950 测试 51 套件。</p>
<h3>存储模式对比</h3> <h3>存储模式对比</h3>
<table> <table>
+4 -4
View File
@@ -153,7 +153,7 @@
<!-- Hero --> <!-- Hero -->
<section class="hero"> <section class="hero">
<div class="container"> <div class="container">
<div class="badge" style="margin-bottom:24px;"><span class="dot"></span> v0.4.2 生产就绪与崩溃自愈 — 944测试 50套件 · 残缺SSTable自动跳过 · WAL原子写入 · ON UPDATE级联 · 二级索引跨重启恢复 · 零回归</div> <div class="badge" style="margin-bottom:24px;"><span class="dot"></span> v0.4.3 关闭时序与后台任务加固 — 950测试 51套件 · close排空后台任务 · 失败不吞错 · compaction竞态修复 · 提交先WAL · 零回归</div>
<h1>前端的 <span class="gradient-text">SQL 数据库</span></h1> <h1>前端的 <span class="gradient-text">SQL 数据库</span></h1>
<p>TypeScript 原生构建,5 种存储引擎,支持完整 SQL 查询。<br>零运行时依赖,开箱即用。AriaEngine 自研引擎:LSM-Tree + WAL 同步 + MVCC。</p> <p>TypeScript 原生构建,5 种存储引擎,支持完整 SQL 查询。<br>零运行时依赖,开箱即用。AriaEngine 自研引擎:LSM-Tree + WAL 同步 + MVCC。</p>
<div class="actions"> <div class="actions">
@@ -243,7 +243,7 @@ npm install @metona-team/metona-sqlark
</div> </div>
<div class="feature-card"> <div class="feature-card">
<div class="icon">🌲</div> <div class="icon">🌲</div>
<h3>AriaEngine <span style="font-size:0.65rem;color:var(--accent);vertical-align:super;">v0.4.2</span></h3> <h3>AriaEngine <span style="font-size:0.65rem;color:var(--accent);vertical-align:super;">v0.4.3</span></h3>
<p>自研 LSM-Tree 页面式存储引擎。MemTable 红黑树 + 多级 SSTable、Bloom Filter 快速判存、WAL 原子写入 full模式真正同步、MVCC 快照隔离、崩溃恢复自动跳过残缺 SSTable、二级索引跨重启恢复、ON UPDATE/DELETE 外键级联。</p> <p>自研 LSM-Tree 页面式存储引擎。MemTable 红黑树 + 多级 SSTable、Bloom Filter 快速判存、WAL 原子写入 full模式真正同步、MVCC 快照隔离、崩溃恢复自动跳过残缺 SSTable、二级索引跨重启恢复、ON UPDATE/DELETE 外键级联。</p>
</div> </div>
<div class="feature-card"> <div class="feature-card">
@@ -404,12 +404,12 @@ npm install @metona-team/metona-sqlark
<p>MetonaSqlark 的核心指标</p> <p>MetonaSqlark 的核心指标</p>
</div> </div>
<div class="stats"> <div class="stats">
<div class="stat-card"><div class="num">944</div><div class="label">测试用例</div></div> <div class="stat-card"><div class="num">950</div><div class="label">测试用例</div></div>
<div class="stat-card"><div class="num">84.2%</div><div class="label">行覆盖率</div></div> <div class="stat-card"><div class="num">84.2%</div><div class="label">行覆盖率</div></div>
<div class="stat-card"><div class="num">~27KB</div><div class="label">gzip 体积</div></div> <div class="stat-card"><div class="num">~27KB</div><div class="label">gzip 体积</div></div>
<div class="stat-card"><div class="num">5</div><div class="label">存储引擎</div></div> <div class="stat-card"><div class="num">5</div><div class="label">存储引擎</div></div>
<div class="stat-card"><div class="num">36</div><div class="label">SQL 关键字</div></div> <div class="stat-card"><div class="num">36</div><div class="label">SQL 关键字</div></div>
<div class="stat-card"><div class="num">50</div><div class="label">测试套件</div></div> <div class="stat-card"><div class="num">51</div><div class="label">测试套件</div></div>
</div> </div>
</div> </div>
</section> </section>
+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> { async commitTransaction(): Promise<void> {
if (!this.currentTxnId) throw new DatabaseError('No active transaction', 'TX_NONE'); 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) { if (this.txnSnapshot) {
for (const [key, value] of this.txnSnapshot) { for (const [key, value] of this.txnSnapshot) {
if ((value as unknown as Record<string, unknown>).__txn_deleted) { if ((value as unknown as Record<string, unknown>).__txn_deleted) {
@@ -1106,16 +1117,8 @@ export class AriaEngine implements IStorageEngine {
this.mvcc.commitTransaction(this.currentTxnId); this.mvcc.commitTransaction(this.currentTxnId);
await this.wal.append({
type: WALRecordType.COMMIT,
txnId: this.currentTxnId,
tableName: '',
key: '',
});
this.currentTxnId = null; this.currentTxnId = null;
this.txnSnapshot = null; this.txnSnapshot = null;
await this.wal.flush();
} }
async rollbackTransaction(): Promise<void> { async rollbackTransaction(): Promise<void> {
+60 -20
View File
@@ -12,6 +12,7 @@ import { MemTable } from './memtable';
import { SSTableBuilder } from './sstable_builder'; import { SSTableBuilder } from './sstable_builder';
import { SSTableReader } from './sstable'; import { SSTableReader } from './sstable';
import { MergeIterator, ArrayEntrySource } from './merge_iterator'; import { MergeIterator, ArrayEntrySource } from './merge_iterator';
import { DatabaseError } from '../../../constants';
import type { SSTableMeta } from '../types'; import type { SSTableMeta } from '../types';
import { import {
DEFAULT_MEMTABLE_SIZE, DEFAULT_MEMTABLE_SIZE,
@@ -77,6 +78,11 @@ export class LSM {
private compacting = false; // 防止重复触发 compaction private compacting = false; // 防止重复触发 compaction
/** 串行化 flush/compaction 链:保证持久化顺序与 id 分配顺序一致 */ /** 串行化 flush/compaction 链:保证持久化顺序与 id 分配顺序一致 */
private flushChain: Promise<void> = Promise.resolve(); private flushChain: Promise<void> = Promise.resolve();
/**
* v0.4.3-fix: 最近一次后台 flush/compaction
* flush()/close()
*/
private lastBackgroundError: unknown = null;
constructor(config: LSMConfig) { constructor(config: LSMConfig) {
this.memtableSizeThreshold = config.memtableSizeThreshold ?? DEFAULT_MEMTABLE_SIZE; this.memtableSizeThreshold = config.memtableSizeThreshold ?? DEFAULT_MEMTABLE_SIZE;
@@ -189,12 +195,27 @@ export class LSM {
return this.flushChain return this.flushChain
.then(task) .then(task)
.catch((error) => { .catch((error) => {
// v0.4.3-fix: 记录失败(flush()/close() 时报告),不再完全静默吞错
this.lastBackgroundError = error;
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn('[AriaEngine LSM] background flush/compaction failed:', error); console.warn('[AriaEngine LSM] background flush/compaction failed:', error);
// catch 返回 undefined → 链恢复为 resolved,后续任务继续 // 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 按命名空间分配) */ /** 将指定 Immutable MemTable 刷盘为 SSTableid 由 store 按命名空间分配) */
private async flushImmutableAsync(frozen: MemTable): Promise<void> { private async flushImmutableAsync(frozen: MemTable): Promise<void> {
const entries = frozen.getAllEntries(); 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 { private scheduleCompact(level: number): void {
if (level >= MAX_LSM_LEVELS - 1 || this.compacting) return; if (level >= MAX_LSM_LEVELS - 1 || this.compacting) return;
this.compacting = true; this.compacting = true;
setTimeout(() => { this.flushChain = this.enqueueOnChain(() => this.compactLevelAsync(level).finally(() => {
try { this.compacting = false;
this.flushChain = this.enqueueOnChain(() => this.compactLevelAsync(level)); // 连续触发:如果 compaction 后仍然超标,继续调度
} finally { if (this.levels[level].length >= 4) {
this.compacting = false; this.scheduleCompact(level);
// 连续触发:如果 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);
}
} }
}, 0); // 检查下一级是否需要 compaction
if (level + 1 < MAX_LSM_LEVELS - 1 && this.levels[level + 1].length >= 4) {
this.scheduleCompact(level + 1);
}
}));
} }
/** 背压场景下排队 compaction(写入路径调用) */ /** 背压场景下排队 compaction(写入路径调用) */
@@ -272,6 +294,8 @@ export class LSM {
} }
}).catch((error) => { }).catch((error) => {
this.compacting = false; this.compacting = false;
// v0.4.3-fix: 记录失败(flush()/close() 时报告)
this.lastBackgroundError = error;
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn('[AriaEngine LSM] background compaction failed:', error); console.warn('[AriaEngine LSM] background compaction failed:', error);
}); });
@@ -284,10 +308,11 @@ export class LSM {
/** /**
* key SSTable * key SSTable
* / loadSSTableReader null * / loadSSTableReader null
* flush flush SSTable * v0.4.3-fix: 等待链稳定drainChain flush/compaction
* await compaction levels meta
*/ */
async prefetchRange(startKey: string, endKey: string): Promise<void> { async prefetchRange(startKey: string, endKey: string): Promise<void> {
await this.flushChain; await this.drainChain();
this.trimCache(); this.trimCache();
const toLoad: number[] = []; const toLoad: number[] = [];
for (let level = 0; level < MAX_LSM_LEVELS; level++) { for (let level = 0; level < MAX_LSM_LEVELS; level++) {
@@ -304,7 +329,8 @@ export class LSM {
/** 预加载包含指定 key 的所有 SSTable 到缓存 */ /** 预加载包含指定 key 的所有 SSTable 到缓存 */
async prefetchKeys(keys: string[]): Promise<void> { async prefetchKeys(keys: string[]): Promise<void> {
if (keys.length === 0) return; if (keys.length === 0) return;
await this.flushChain; // v0.4.3-fix: 等待链稳定(同 prefetchRange,防 compaction 竞态丢数据)
await this.drainChain();
this.trimCache(); this.trimCache();
const toLoad = new Set<number>(); const toLoad = new Set<number>();
for (let level = 0; level < MAX_LSM_LEVELS; level++) { for (let level = 0; level < MAX_LSM_LEVELS; level++) {
@@ -519,8 +545,18 @@ export class LSM {
/** 等待所有排队的 flush/compaction 完成,并将剩余数据刷盘 */ /** 等待所有排队的 flush/compaction 完成,并将剩余数据刷盘 */
async flush(): Promise<void> { async flush(): Promise<void> {
// 等待链上已排队的 flush/compaction // v0.4.3-fix: 报告后台失败(消费一次,不永久吞错)
await this.flushChain; 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 数据未刷盘,在链尾追加 // 若仍有 frozen 数据未刷盘,在链尾追加
if (this.immutableMemtable) { if (this.immutableMemtable) {
const frozen = this.immutableMemtable; const frozen = this.immutableMemtable;
@@ -532,9 +568,13 @@ export class LSM {
if (frozen) await this.flushImmutableAsync(frozen); if (frozen) await this.flushImmutableAsync(frozen);
} }
this.frozenMemtables = []; this.frozenMemtables = [];
// 刷盘完成后级联调度可能触发 compaction → 排空到稳定
await this.drainChain();
} }
async clear(): Promise<void> { async clear(): Promise<void> {
// v0.4.3-fix: 清空前排空后台任务(避免 compaction 在清空后写回残留 meta/数据)
await this.drainChain();
this.memtable.clear(); this.memtable.clear();
this.immutableMemtable = null; this.immutableMemtable = null;
this.frozenMemtables = []; this.frozenMemtables = [];
+6
View File
@@ -251,6 +251,12 @@ export class IndexedDBEngine implements IStorageEngine {
} }
async close(): Promise<void> { async close(): Promise<void> {
// v0.4.3-fix: 活跃事务时先回滚(避免 commit 对已关闭连接报错)
if (this.txActive) {
try {
await this.rollbackTransaction();
} catch { /* 回滚失败不阻塞关闭 */ }
}
if (this.db) { if (this.db) {
this.db.onversionchange = null; // 清理监听器 this.db.onversionchange = null; // 清理监听器
this.db.close(); this.db.close();
+95 -52
View File
@@ -25,6 +25,20 @@ export class OPFSEngine implements IStorageEngine {
// 运行时内存缓存(OPFS 文件读写有延迟) // 运行时内存缓存(OPFS 文件读写有延迟)
private memoryCache: MemoryEngine = new MemoryEngine(); 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> { async open(dbName: string, version: number): Promise<void> {
@@ -42,6 +56,10 @@ export class OPFSEngine implements IStorageEngine {
} }
async close(): Promise<void> { async close(): Promise<void> {
// v0.4.3-fix: 等待所有挂起写操作完成(否则 close 后写仍在进行 → 重启读旧数据)
try {
await this.opQueue;
} catch { /* 写失败已返回给调用方 */ }
this.root = null; this.root = null;
this.tablesDir = null; this.tablesDir = null;
await this.memoryCache.close(); await this.memoryCache.close();
@@ -55,6 +73,8 @@ export class OPFSEngine implements IStorageEngine {
/** 自愈:重置内存缓存后从 OPFS 重新加载(单文件损坏不影响其他表) */ /** 自愈:重置内存缓存后从 OPFS 重新加载(单文件损坏不影响其他表) */
async repair(): Promise<void> { async repair(): Promise<void> {
// v0.4.3-fix: 先等写队列排空(避免与挂起写竞态)
try { await this.opQueue; } catch { /* ignore */ }
await this.memoryCache.close(); await this.memoryCache.close();
await this.memoryCache.open(this.dbName, 1); await this.memoryCache.open(this.dbName, 1);
await this.loadExistingTables(); await this.loadExistingTables();
@@ -62,6 +82,8 @@ export class OPFSEngine implements IStorageEngine {
/** 清空全部数据与表结构(删除目录内全部文件) */ /** 清空全部数据与表结构(删除目录内全部文件) */
async clearAll(): Promise<void> { async clearAll(): Promise<void> {
// v0.4.3-fix: 先等写队列排空
try { await this.opQueue; } catch { /* ignore */ }
await this.memoryCache.close(); await this.memoryCache.close();
await this.memoryCache.open(this.dbName, 1); await this.memoryCache.open(this.dbName, 1);
if (this.tablesDir) { if (this.tablesDir) {
@@ -94,28 +116,32 @@ export class OPFSEngine implements IStorageEngine {
// ---- 表管理 ---- // ---- 表管理 ----
async createTable(schema: TableSchema): Promise<void> { async createTable(schema: TableSchema): Promise<void> {
await this.memoryCache.createTable(schema); return this.enqueueOp(async () => {
// v0.4.2-fix: schema 持久化(此前仅写空数据文件 → 空表重启后消失、索引标记丢失) await this.memoryCache.createTable(schema);
await this.setMeta(`schema_${schema.name}`, JSON.stringify(schema)); // v0.4.2-fix: schema 持久化(此前仅写空数据文件 → 空表重启后消失、索引标记丢失)
// OPFS 中表以空 JSON 数组文件形式存在 await this.setMeta(`schema_${schema.name}`, JSON.stringify(schema));
await this.writeTableData(schema.name, []); // OPFS 中表以空 JSON 数组文件形式存在
await this.writeTableData(schema.name, []);
});
} }
async dropTable(tableName: string): Promise<void> { async dropTable(tableName: string): Promise<void> {
await this.memoryCache.dropTable(tableName); return this.enqueueOp(async () => {
// v0.4.2-fix: 清理 schema meta(否则重启恢复幽灵表) await this.memoryCache.dropTable(tableName);
if (this.tablesDir) { // v0.4.2-fix: 清理 schema meta(否则重启恢复幽灵表)
try { if (this.tablesDir) {
await this.tablesDir.removeEntry(`__metona_schema_${tableName}.meta`); try {
} catch { 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> { async hasTable(tableName: string): Promise<boolean> {
@@ -149,21 +175,25 @@ export class OPFSEngine implements IStorageEngine {
action: 'ADD' | 'DROP', action: 'ADD' | 'DROP',
column: import('../constants').ColumnDef & { name: string }, column: import('../constants').ColumnDef & { name: string },
): Promise<void> { ): Promise<void> {
await this.memoryCache.alterTable(tableName, action, column); return this.enqueueOp(async () => {
const schema = await this.memoryCache.getTableSchema(tableName); await this.memoryCache.alterTable(tableName, action, column);
if (schema) await this.setMeta(`schema_${tableName}`, JSON.stringify(schema)); const schema = await this.memoryCache.getTableSchema(tableName);
const rows = await this.memoryCache.find(tableName, { table: tableName }); if (schema) await this.setMeta(`schema_${tableName}`, JSON.stringify(schema));
await this.writeTableData(tableName, rows); const rows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, rows);
});
} }
// ---- CRUD ---- // ---- CRUD ----
async insert(tableName: string, rows: Record<string, unknown>[]): Promise<string[]> { async insert(tableName: string, rows: Record<string, unknown>[]): Promise<string[]> {
const pks = await this.memoryCache.insert(tableName, rows); return this.enqueueOp(async () => {
// 持久化到 OPFS const pks = await this.memoryCache.insert(tableName, rows);
const allRows = await this.memoryCache.find(tableName, { table: tableName }); // 持久化到 OPFS(快照在队列内取,始终最新)
await this.writeTableData(tableName, allRows); const allRows = await this.memoryCache.find(tableName, { table: tableName });
return pks; await this.writeTableData(tableName, allRows);
return pks;
});
} }
async find(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]> { 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> { async update(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<number> {
const count = await this.memoryCache.update(tableName, query, updates); return this.enqueueOp(async () => {
const allRows = await this.memoryCache.find(tableName, { table: tableName }); const count = await this.memoryCache.update(tableName, query, updates);
await this.writeTableData(tableName, allRows); const allRows = await this.memoryCache.find(tableName, { table: tableName });
return count; await this.writeTableData(tableName, allRows);
return count;
});
} }
async delete(tableName: string, query: QueryPlan): Promise<number> { async delete(tableName: string, query: QueryPlan): Promise<number> {
const count = await this.memoryCache.delete(tableName, query); return this.enqueueOp(async () => {
const allRows = await this.memoryCache.find(tableName, { table: tableName }); const count = await this.memoryCache.delete(tableName, query);
await this.writeTableData(tableName, allRows); const allRows = await this.memoryCache.find(tableName, { table: tableName });
return count; await this.writeTableData(tableName, allRows);
return count;
});
} }
async count(tableName: string, query?: QueryPlan): Promise<number> { async count(tableName: string, query?: QueryPlan): Promise<number> {
@@ -194,23 +228,29 @@ export class OPFSEngine implements IStorageEngine {
} }
async clear(tableName: string): Promise<void> { async clear(tableName: string): Promise<void> {
await this.memoryCache.clear(tableName); return this.enqueueOp(async () => {
await this.writeTableData(tableName, []); await this.memoryCache.clear(tableName);
await this.writeTableData(tableName, []);
});
} }
// ---- 动态索引(v0.3.0 ---- // ---- 动态索引(v0.3.0 ----
async createIndex(tableName: string, column: string, unique?: boolean): Promise<void> { async createIndex(tableName: string, column: string, unique?: boolean): Promise<void> {
await this.memoryCache.createIndex(tableName, column, unique); return this.enqueueOp(async () => {
// v0.4.2-fix: 索引标记持久化(重启后索引结构恢复) await this.memoryCache.createIndex(tableName, column, unique);
const schema = await this.memoryCache.getTableSchema(tableName); // v0.4.2-fix: 索引标记持久化(重启后索引结构恢复)
if (schema) await this.setMeta(`schema_${tableName}`, JSON.stringify(schema)); 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> { async dropIndex(tableName: string, column: string, indexName?: string): Promise<void> {
await this.memoryCache.dropIndex(tableName, column, indexName); return this.enqueueOp(async () => {
const schema = await this.memoryCache.getTableSchema(tableName); await this.memoryCache.dropIndex(tableName, column, indexName);
if (schema) await this.setMeta(`schema_${tableName}`, JSON.stringify(schema)); 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> { async commitTransaction(): Promise<void> {
await this.memoryCache.commitTransaction(); return this.enqueueOp(async () => {
// 将内存数据刷到 OPFS await this.memoryCache.commitTransaction();
const tableNames = await this.memoryCache.getTableNames(); // 将内存数据刷到 OPFS
for (const tableName of tableNames) { const tableNames = await this.memoryCache.getTableNames();
const rows = await this.memoryCache.find(tableName, { table: tableName }); for (const tableName of tableNames) {
await this.writeTableData(tableName, rows); const rows = await this.memoryCache.find(tableName, { table: tableName });
} await this.writeTableData(tableName, rows);
}
});
} }
async rollbackTransaction(): Promise<void> { async rollbackTransaction(): Promise<void> {
@@ -243,6 +285,7 @@ export class OPFSEngine implements IStorageEngine {
} }
private async writeTableData(tableName: string, data: Record<string, unknown>[]): Promise<void> { private async writeTableData(tableName: string, data: Record<string, unknown>[]): Promise<void> {
// 由 enqueueOp 串行化调用,此处直接写文件
const dir = this.ensureDir(); const dir = this.ensureDir();
const fileName = `${tableName}.json`; const fileName = `${tableName}.json`;
const fileHandle = await dir.getFileHandle(fileName, { create: true }); const fileHandle = await dir.getFileHandle(fileName, { create: true });
+2 -2
View File
@@ -23,8 +23,8 @@ import { createSchema } from '../src/table/schema';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('[v0.2.5] P0-1: 版本号统一', () => { describe('[v0.2.5] P0-1: 版本号统一', () => {
test('VERSION 常量为当前版本(0.4.2', () => { test('VERSION 常量为当前版本(0.4.3', () => {
expect(VERSION).toBe('0.4.2'); expect(VERSION).toBe('0.4.3');
}); });
}); });
+1 -1
View File
@@ -400,7 +400,7 @@ describe('[v0.3.3] P1-9: Savepoint + MVCC 一致性', () => {
describe('[v0.3.3] 端到端', () => { describe('[v0.3.3] 端到端', () => {
test('全部修复点可共存于 MetonaSqlark API', async () => { test('全部修复点可共存于 MetonaSqlark API', async () => {
expect(VERSION).toBe('0.4.2'); expect(VERSION).toBe('0.4.3');
const db = new MetonaSqlark({ name: `e2e-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'memory' }); const db = new MetonaSqlark({ name: `e2e-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'memory' });
await db.init(); await db.init();
await db.defineTable('users', { await db.defineTable('users', {
+208
View File
@@ -0,0 +1,208 @@
/**
* v0.4.3
*
* - P0: close compaction backend /
* - P0: flush
* - P1: commit WAL WAL
* - P1: OPFS close
*/
import { AriaEngine } from '../src/engine/aria/index';
import { createSchema } from '../src/table/schema';
import { LSM } from '../src/engine/aria/index/lsm';
import { OPFSEngine } from '../src/engine/opfs';
import 'fake-indexeddb/auto';
let idbCounter = 0;
function uniqueDB(): string {
return `r43-${Date.now()}-${++idbCounter}-${Math.random().toString(36).slice(2, 8)}`;
}
// ===================================================================
// P0: close 与后台 compaction
// ===================================================================
describe('P0 — close 后无残留后台任务', () => {
it('close 等待后台 compaction 完成(backend 关闭后无残留写)', async () => {
const engine = new AriaEngine({
storageBackend: 'memory',
// 小缓存 + 小 memtable:flush 文件超缓存上限被驱逐 → compaction 缓存未命中 → 触发调度
bufferPoolPages: 4,
memtableSizeThreshold: 32 * 1024,
checkpointInterval: 100000,
});
await engine.open(uniqueDB(), 1);
await engine.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
v: { type: 'number' },
data: { type: 'string' },
}));
for (let i = 0; i < 400; i++) {
await engine.insert('t', [{ id: `k${String(i).padStart(4, '0')}`, v: i, data: 'x'.repeat(200) }]);
}
await (engine as any).lsm.flush();
const backend = (engine as any).backend;
// 立即 close:修复前 setTimeout compaction 在 backend.close() 后才执行 → 残留写
await engine.close();
// 给残留 setTimeout 执行机会
await new Promise((r) => setTimeout(r, 100));
const keysAfterClose = await (backend as any).listKeys();
// 修复前:close 后 compaction 写入 → store 残留 sst_* 文件
expect(keysAfterClose).toHaveLength(0);
});
it('close 后立即 reopen 不被旧后台任务污染', async () => {
const dbName = uniqueDB();
const engine = new AriaEngine({
storageBackend: 'indexeddb',
bufferPoolPages: 4,
memtableSizeThreshold: 32 * 1024,
checkpointInterval: 100000,
});
await engine.open(dbName, 1);
await engine.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
v: { type: 'number' },
data: { type: 'string' },
}));
for (let i = 0; i < 300; i++) {
await engine.insert('t', [{ id: `k${String(i).padStart(4, '0')}`, v: i, data: 'x'.repeat(200) }]);
}
await (engine as any).lsm.flush();
// 立即 close + 立即 reopen(修复前:旧任务的闭包引用新 backend → 交叉写)
await engine.close();
await engine.open(dbName, 1);
// 新库数据必须完整(不被旧任务破坏)
expect(await engine.count('t')).toBe(300);
await engine.close();
});
});
// ===================================================================
// P0: 后台失败可见性(不吞错)
// ===================================================================
describe('P0 — flush 报告后台失败', () => {
class FailingStore {
failNext = true;
saved = 0;
deleted = 0;
metas: { id: number; level: number }[] = [];
async save(id: number, _data: Uint8Array): Promise<void> {
if (this.failNext) {
this.failNext = false;
throw new Error('disk full (simulated)');
}
this.saved++;
}
async load(_id: number): Promise<Uint8Array | null> { return null; }
async delete(_id: number): Promise<void> { this.deleted++; }
async allocateId(): Promise<number> { return ++this.saved; }
async listMeta(): Promise<{ id: number; level: number }[]> { return this.metas; }
async saveMeta(meta: { id: number; level: number }): Promise<void> { this.metas.push(meta); }
async deleteMeta(id: number): Promise<void> { this.metas = this.metas.filter((m) => m.id !== id); }
}
it('后台 flush 失败后 flush() 抛 DatabaseErrorARIA_BACKGROUND_ERROR', async () => {
const store = new FailingStore();
const lsm = new LSM({
memtableSizeThreshold: 64,
sstableStore: store as any,
});
// 触发 freeze + 后台 flushsave 抛错 → 记录 lastBackgroundError
for (let i = 0; i < 200; i++) {
lsm.put(`k${i}`, { v: i });
}
await new Promise((r) => setTimeout(r, 50));
// flush 必须报告后台失败(修复前静默吞错)
await expect(lsm.flush()).rejects.toMatchObject({ code: 'ARIA_BACKGROUND_ERROR' });
// 再次 flush:错误已消费,正常完成
await expect(lsm.flush()).resolves.toBeUndefined();
});
it('后台 compaction 失败后 flush() 报告(链不卡死)', async () => {
const store = new FailingStore();
const lsm = new LSM({
memtableSizeThreshold: 32,
sstableStore: store as any,
});
for (let i = 0; i < 500; i++) {
lsm.put(`k${i}`, { v: i });
}
await new Promise((r) => setTimeout(r, 100));
// 链不卡死:flush 要么成功要么报告错误(不能永久 pending)
const result = await Promise.race([
lsm.flush().then(() => 'ok', (e) => `err:${(e as any).code}`),
new Promise((r) => setTimeout(() => r('pending'), 500)),
]);
expect(result).not.toBe('pending');
});
});
// ===================================================================
// P1: commit 顺序与 OPFS close
// ===================================================================
describe('P1 — 提交顺序与 close 等待', () => {
it('commit 先持久化 WAL 再合并快照(WAL 始终领先)', async () => {
// 通过顺序断言:事务 INSERT 的 WAL 记录必须在快照合并可见之前已落盘
const engine = new AriaEngine({ storageBackend: 'memory' });
await engine.open(uniqueDB(), 1);
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
await engine.beginTransaction();
await engine.insert('t', [{ id: '1' }]);
// 快照合并前:WAL 已含事务记录(batch 模式 flush 时机校验)
await engine.commitTransaction();
// 崩溃恢复路径:WAL 完整则恢复数据
const backend = (engine as any).backend;
const walKeys = (await backend.listKeys()).filter((k) => k.startsWith('__wal_'));
expect(walKeys.length).toBeGreaterThan(0);
await engine.close();
});
it('OPFS close 等待挂起写完成(文件完整)', async () => {
// mock OPFS(跨实例共享)
const files = new Map<string, string>();
const dirMock = {
getDirectoryHandle: async (_n: string, _o?: any) => dirMock as any,
getFileHandle: async (name: string, opts?: any) => {
if (opts?.create) {
return {
createWritable: async () => ({
write: async (d: string) => {
// 模拟慢 I/O
await new Promise((r) => setTimeout(r, 30));
files.set(name, d);
},
close: async () => {},
}),
};
}
if (!files.has(name)) throw new Error('Not found');
return { getFile: async () => ({ text: async () => files.get(name)!, arrayBuffer: async () => new ArrayBuffer(0) }) };
},
removeEntry: async (n: string) => { files.delete(n); },
};
(dirMock as any).entries = () => ({
[Symbol.asyncIterator]: async function* () {
for (const [k] of files) yield [k];
},
});
const nav = (globalThis as any).navigator || {};
nav.storage = { getDirectory: async () => dirMock };
(globalThis as any).navigator = nav;
const engine = new OPFSEngine();
await engine.open('opfs-close', 1);
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
// 写入不等待(写 I/O 30ms 挂起)
const p = engine.insert('t', [{ id: '1' }]);
// 立即 close:必须等待挂起写完成
await engine.close();
await p;
// 文件完整(修复前 close 不等写 → 可能读到旧/空文件)
expect(files.get('t.json')).toContain('"1"');
});
});