fix(v0.8.0): 全量回归审查 —— 1 处 P0 数据丢失 + 4 处 P1 + 9 处 P2 根因修复
方法:四个对抗性子代理分头审查(数据正确性 / 文档宣称 vs 实现 / 公共 API 契约 / 测试质量),每条结论要求可复现证据;逐条复核 + 探针确认 + 变异验证(40 项全部 被对应用例拦住)。 P0:事务活跃期间 repair()/close()/周期 checkpoint 推进 WAL 水位 → 已 COMMIT 的 事务整批消失且恢复报告"干净"。根因 hasPendingFlushData()/computeDurableLsn() 不看 txnSnapshot;守卫此前只在 CheckpointManager 两个回调里。修复:守卫下沉到 computeDurableLsn() 与 advanceWalCheckpoint() 入口(唯一实现)。 P1: - WAL 前缀缺失丢弃整段活分片(回退上一代 manifest 时 kept 为空)→ 前缀缺失单独 记录,后缀照常重放;仅 fromLsn === 0 时才算真异常 - 孤儿回收门槛只看引擎层 dataLossSuspected,漏掉 LSM 层被丢的 SSTable → 统一 describeRecoveryDamage() 聚合判定(损坏时绝不删"引用不到"的文件) - vacuum() 逐层压缩绕过维护链 → vacuumLevels() 每层作为维护链任务执行 - reclaimRetiredNow() 无视在途读者(读者把"已退休"读成"文件损坏")→ 有读者时 退化为延迟回收 P2:WAL 记录级 CRC 损坏不计数不上报;旧格式表结构记录形状损坏静默当空库; bloomFilterBitsPerKey 配置被接受却完全不生效(构建器写死默认值,实现缺陷); 幽灵 meta;介质读故障等于文件损坏的语义无用例;manifest 回读校验两条守卫无用例; 文件名≠载荷世代判定无用例;pageIdWatermark 单调性无用例;分片号两条真实不变量 无用例。 覆盖率口径(第二处漏洞):interface.ts 混着三个运行时函数(cloneRow 等)却被 描述为"纯类型、不纳入统计" → 实现搬到 src/engine/row_clone.ts;搬完门禁真的 失败(functions 93.84% < 94%),补测退化路径后通过。 测试质量:3 条空壳用例改值级断言;1 条"全损坏"用例实际只走缓存 → 拆成两条真 用例;5 秒墙钟 race 改门控 + 失败上限;setTimeout 改 whenIdle();<= 收紧为 <。 变异脚本加固:正控(干净基线必须全绿)、编译失败/0 用例单独归类、300s 超时、 逐字节 sha256 恢复校验、O_EXCL 进程锁、锚点唯一性;变异 22 → 40 项。 文档两轮订正(16 + 11 条不成立宣称):MVCC 快照隔离、backup 一致性快照、 "空洞检测截断"、体积(251,109 B / gzip 63,145 B)、测试与覆盖率数字、 "5 种存储引擎"、Tree-shakable、错误码表补 16 个码、恢复报告字段、已知限制 (回退单向 / 多实例依赖 Web Locks / manifest 体积 / 尾部 WAL 分片不可识别)。 验证:常规套件 92 套件 / 1980 用例全绿;覆盖率 90.59 / 82.59 / 94.14 / 93.50 (阈值 90/82/94/93);e2e 14/14(真实 Chromium + OPFS + CDP 崩溃); 重型套件 4 套件 / 27 用例;变异 40/40;lint + 两份 tsc 干净;dist 已重建。
This commit is contained in:
+197
-51
@@ -1,4 +1,4 @@
|
||||
import { cloneRow } from '../interface';
|
||||
import { cloneRow } from '../row_clone';
|
||||
/**
|
||||
* AriaEngine — 自研页面式存储引擎主类
|
||||
* @module engine/aria/index
|
||||
@@ -14,7 +14,7 @@ import { stripUndefinedUpdates } from '../../table/schema';
|
||||
import { compileValidator } from '../../table/validation';
|
||||
|
||||
import type { AriaEngineConfig, SSTableMeta } from './types';
|
||||
import { DEFAULT_ARIA_CONFIG, MAX_LSM_LEVELS } from './types';
|
||||
import { DEFAULT_ARIA_CONFIG } from './types';
|
||||
|
||||
import { LSM } from './index/lsm';
|
||||
import type { SSTableStore } from './index/lsm';
|
||||
@@ -48,6 +48,8 @@ export interface AriaRecoveryReport {
|
||||
dataLossSuspected: boolean;
|
||||
/** WAL 活跃区间内的分片空洞(已提交事务记录缺失) */
|
||||
walGaps: number[];
|
||||
/** v0.8.0:CRC 校验失败被跳过的 WAL 记录数(它们承载的写入已丢失) */
|
||||
droppedWALRecords: number;
|
||||
/** 本次打开是否从旧格式(__aria_lsm_meta/__aria_schemas)迁移而来 */
|
||||
legacyImported: boolean;
|
||||
/** 是否从更早的 manifest 世代回退(最新世代损坏) */
|
||||
@@ -109,6 +111,7 @@ export class AriaEngine implements IStorageEngine {
|
||||
droppedSSTables: [],
|
||||
dataLossSuspected: false,
|
||||
walGaps: [],
|
||||
droppedWALRecords: 0,
|
||||
legacyImported: false,
|
||||
manifestFallback: false,
|
||||
};
|
||||
@@ -301,6 +304,17 @@ export class AriaEngine implements IStorageEngine {
|
||||
allowGaps: true,
|
||||
});
|
||||
const walInfo = this.wal.getLastRecoveryInfo();
|
||||
if (walInfo && walInfo.corruptRecords > 0) {
|
||||
// v0.8.0(review 修复):记录级 CRC 损坏此前只 console.warn —— 恢复完全
|
||||
// 不感知"少了几条记录",与"静默丢数据必须显式化"的目标不符。
|
||||
this.recoveryReport.droppedWALRecords = walInfo.corruptRecords;
|
||||
this.recoveryReport.dataLossSuspected = true;
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`[AriaEngine] WAL: ${walInfo.corruptRecords} record(s) failed CRC and were skipped — ` +
|
||||
'the writes they carried are missing (recovery report records this)',
|
||||
);
|
||||
}
|
||||
if (walInfo && walInfo.gaps.length > 0) {
|
||||
this.recoveryReport.walGaps = [...walInfo.gaps];
|
||||
this.recoveryReport.dataLossSuspected = true;
|
||||
@@ -555,35 +569,46 @@ export class AriaEngine implements IStorageEngine {
|
||||
* 且 manifest 完整可信时才允许回收空间。
|
||||
*/
|
||||
private async cleanupOrphanPages(): Promise<void> {
|
||||
if (this.recoveryReport.dataLossSuspected || this.recoveryReport.manifestFallback) {
|
||||
const damage = this.describeRecoveryDamage();
|
||||
if (damage.length > 0) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'[AriaEngine] repair: skipping orphan-page reclamation — recovery report shows ' +
|
||||
'damage or manifest fallback (unreferenced pages are kept, never deleted blindly)',
|
||||
`[AriaEngine] repair: skipping orphan reclamation — recovery shows damage (${damage.join('; ')}); ` +
|
||||
'unreferenced data is kept, never deleted blindly',
|
||||
);
|
||||
return;
|
||||
}
|
||||
const keys = await this.backend.listKeys();
|
||||
const pgKeys = keys.filter((k) => /^pg_\d+$/.test(k));
|
||||
if (pgKeys.length === 0) return;
|
||||
// v0.8.0(review 修复):**有在途读者时一律不回收**。
|
||||
// 读者的快照可能持有已被 compaction 取代("退休")的文件;那些文件的页面
|
||||
// 既不在 manifest、也不在 levels 里 —— 按"没人引用"删掉它们会让正在进行中的
|
||||
// 扫描静默少数据。回收只能在没有读者时做。
|
||||
if (this.allLsms().some((lsm) => lsm.hasActiveReaders())) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[AriaEngine] repair: skipping orphan reclamation — readers are active');
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = await this.backend.listKeys();
|
||||
|
||||
// ---- 被引用的页面 id:manifest + 各层内存视图 + 退休表 ----
|
||||
const used = new Set<number>();
|
||||
for (const state of Object.values(this.manifest.namespaces)) {
|
||||
for (const m of state.sstables) {
|
||||
if (m.pageIds) {
|
||||
for (const pid of m.pageIds) used.add(pid);
|
||||
}
|
||||
if (m.pageIds) for (const pid of m.pageIds) used.add(pid);
|
||||
}
|
||||
}
|
||||
// 未落盘的页面(正在写入的 SSTable)也不能删
|
||||
for (const lsm of this.allLsms()) {
|
||||
// 未落盘的页面(正在写入的 SSTable)不能删
|
||||
for (const level of this.getLsmLevels(lsm)) {
|
||||
for (const meta of level) {
|
||||
if (meta.pageIds) for (const pid of meta.pageIds) used.add(pid);
|
||||
}
|
||||
}
|
||||
// 退休但尚未物理删除的 SSTable 的页面同样不能删
|
||||
for (const pid of lsm.getRetiredPageIds()) used.add(pid);
|
||||
}
|
||||
|
||||
const pgKeys = keys.filter((k) => /^pg_\d+$/.test(k));
|
||||
const orphanIds = pgKeys
|
||||
.map((k) => Number(k.slice('pg_'.length)))
|
||||
.filter((pid) => !used.has(pid));
|
||||
@@ -592,6 +617,45 @@ export class AriaEngine implements IStorageEngine {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`[AriaEngine] repair: reclaimed ${orphanIds.length} orphan page file(s)`);
|
||||
}
|
||||
|
||||
// ---- 孤儿 SSTable 文件(整 value 路径:sst_<ns>_<id> / sst_<id>)----
|
||||
// 退休 SSTable 的物理删除是"尽力而为":删除失败/崩溃会留下既不被 manifest
|
||||
// 引用、也不在任何层里的文件。页面化路径由上面的 pg_ 回收覆盖;
|
||||
// 整 value 路径(pageStorage:false / 旧库 / 迁移数据)此前**没有任何回收路径**。
|
||||
const usedSstIds = new Set<number>();
|
||||
for (const state of Object.values(this.manifest.namespaces)) {
|
||||
for (const m of state.sstables) usedSstIds.add(m.id);
|
||||
}
|
||||
for (const lsm of this.allLsms()) {
|
||||
for (const level of this.getLsmLevels(lsm)) {
|
||||
for (const meta of level) usedSstIds.add(meta.id);
|
||||
}
|
||||
for (const id of lsm.getRetiredSstableIds()) usedSstIds.add(id);
|
||||
}
|
||||
const orphanSstKeys: string[] = [];
|
||||
for (const [ns, prefix] of this.sstableKeyPrefixes()) {
|
||||
const re = new RegExp(`^${prefix}(\\d+)$`);
|
||||
for (const key of keys) {
|
||||
const m = re.exec(key);
|
||||
if (!m) continue;
|
||||
if (!usedSstIds.has(Number(m[1]))) orphanSstKeys.push(key);
|
||||
}
|
||||
void ns;
|
||||
}
|
||||
if (orphanSstKeys.length > 0) {
|
||||
await this.backend.deleteMany(orphanSstKeys);
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`[AriaEngine] repair: reclaimed ${orphanSstKeys.length} orphan SSTable file(s)`);
|
||||
}
|
||||
}
|
||||
|
||||
/** v0.8.0:命名空间 → SSTable 文件 key 前缀(与 createSSTableStore 保持一致) */
|
||||
private sstableKeyPrefixes(): [string, string][] {
|
||||
const out: [string, string][] = [['main', 'sst_']];
|
||||
for (const ns of Object.keys(this.manifest.namespaces)) {
|
||||
if (ns !== 'main') out.push([ns, `sst_${ns}_`]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** v0.8.0: 读取某个 LSM 当前引用的层结构(诊断/孤儿回收用) */
|
||||
@@ -2064,6 +2128,61 @@ export class AriaEngine implements IStorageEngine {
|
||||
await this.commitManifest();
|
||||
}
|
||||
|
||||
/**
|
||||
* v0.8.0(review 修复):表结构记录的**唯一**解析实现。
|
||||
*
|
||||
* 为什么必须集中且严格:结构记录有两种坏法 —— JSON 本身就坏了,或 JSON 合法但
|
||||
* 形状不对(数组 / null / 表名映射到非对象 / 列定义不是对象)。修复前只有
|
||||
* "JSON 坏"这一种会抛错,形状不对则被**静默忽略** → 打开后看不到任何表,
|
||||
* 表现为"库是空的"(与审计里"静默空库"同一类缺陷)。
|
||||
*
|
||||
* 判定原则:只要记录存在却不可用,就抛 ARIA_LEGACY_META_CORRUPT —— 宁可让
|
||||
* 调用方看到明确的损坏错误,也不假装这是一个没有表的空库。
|
||||
*/
|
||||
private parseSchemaRecord(raw: ArrayBuffer, source: string): Record<string, Record<string, ColumnDef>> {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(new TextDecoder().decode(raw));
|
||||
} catch (error) {
|
||||
throw new DatabaseError(
|
||||
`${source} is corrupt and cannot be loaded: ${(error as Error).message}`,
|
||||
'ARIA_LEGACY_META_CORRUPT',
|
||||
error,
|
||||
);
|
||||
}
|
||||
return this.validateSchemaShape(parsed, source);
|
||||
}
|
||||
|
||||
/** 形状校验(与 parseSchemaRecord 分离:便于直接喂各种形状做单测) */
|
||||
private validateSchemaShape(parsed: unknown, source: string): Record<string, Record<string, ColumnDef>> {
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
throw new DatabaseError(
|
||||
`${source} is not a table-name → columns object (got ${Array.isArray(parsed) ? 'array' : typeof parsed})`,
|
||||
'ARIA_LEGACY_META_CORRUPT',
|
||||
);
|
||||
}
|
||||
const out: Record<string, Record<string, ColumnDef>> = {};
|
||||
for (const [tableName, columns] of Object.entries(parsed as Record<string, unknown>)) {
|
||||
if (!columns || typeof columns !== 'object' || Array.isArray(columns)) {
|
||||
throw new DatabaseError(
|
||||
`${source} entry "${tableName}" is not a column map (got ${Array.isArray(columns) ? 'array' : typeof columns})`,
|
||||
'ARIA_LEGACY_META_CORRUPT',
|
||||
);
|
||||
}
|
||||
for (const [colName, def] of Object.entries(columns as Record<string, unknown>)) {
|
||||
if (!def || typeof def !== 'object' || Array.isArray(def)) {
|
||||
throw new DatabaseError(
|
||||
`${source} entry "${tableName}.${colName}" is not a column definition ` +
|
||||
`(got ${Array.isArray(def) ? 'array' : typeof def})`,
|
||||
'ARIA_LEGACY_META_CORRUPT',
|
||||
);
|
||||
}
|
||||
}
|
||||
out[tableName] = columns as Record<string, ColumnDef>;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private async loadSchemas(): Promise<void> {
|
||||
// 权威来源:manifest(旧格式已在 importLegacyState 阶段导入)
|
||||
let data = this.manifest.schemas ?? {};
|
||||
@@ -2073,20 +2192,9 @@ export class AriaEngine implements IStorageEngine {
|
||||
if (Object.keys(data).length === 0) {
|
||||
const raw = await this.backend.read('__aria_schemas');
|
||||
if (raw) {
|
||||
try {
|
||||
const parsed = JSON.parse(new TextDecoder().decode(raw)) as Record<string, Record<string, ColumnDef>>;
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
data = parsed;
|
||||
this.manifest.schemas = parsed;
|
||||
}
|
||||
} catch (error) {
|
||||
// 修复前这里 `catch {}` 静默忽略 → 坏 schema = 看不到任何表(静默空库)
|
||||
throw new DatabaseError(
|
||||
`Schema record is corrupt and cannot be loaded: ${(error as Error).message}`,
|
||||
'ARIA_LEGACY_META_CORRUPT',
|
||||
error,
|
||||
);
|
||||
}
|
||||
// 修复前这里 `catch {}` 静默忽略 → 坏 schema = 看不到任何表(静默空库)
|
||||
data = this.parseSchemaRecord(raw, 'Schema record "__aria_schemas"');
|
||||
this.manifest.schemas = data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2102,6 +2210,38 @@ export class AriaEngine implements IStorageEngine {
|
||||
// v0.8.0(B-6):单一提交点与命名空间工厂
|
||||
// =======================================================================
|
||||
|
||||
/**
|
||||
* v0.8.0(review 修复): 本次打开/修复过程中出现过的**一切损坏迹象**。
|
||||
*
|
||||
* 为什么需要它而不是只看 `this.recoveryReport.dataLossSuspected`:
|
||||
* SSTable 被丢弃这一事实记录在**各 LSM** 的报告里,引擎层的 `dataLossSuspected`
|
||||
* 只在"WAL 水位已推进、被丢的数据没有 WAL 兜底"时才置位。于是"manifest 已被
|
||||
* 推进 + 某个 SSTable 因文件损坏被丢"这类**真损坏**会在引擎层看不到 —— 孤儿页
|
||||
* 回收就会照常执行,把在途/退休文件按"没人引用"删掉。
|
||||
*
|
||||
* 判定原则:只要有任何"曾经自愈/丢失/回退"的迹象,就一律不回收任何未被引用
|
||||
* 的文件(宁可留空间,也不可逆地删数据)。
|
||||
*/
|
||||
private describeRecoveryDamage(): string[] {
|
||||
const reasons: string[] = [];
|
||||
if (this.recoveryReport.manifestFallback) reasons.push('manifest fallback to previous generation');
|
||||
if (this.recoveryReport.dataLossSuspected) reasons.push('engine-level data loss suspected');
|
||||
if (this.recoveryReport.walGaps.length > 0) {
|
||||
reasons.push(`WAL segment gap(s) ${this.recoveryReport.walGaps.join(',')}`);
|
||||
}
|
||||
if (this.recoveryReport.droppedWALRecords > 0) {
|
||||
reasons.push(`${this.recoveryReport.droppedWALRecords} corrupt WAL record(s)`);
|
||||
}
|
||||
for (const lsm of this.allLsms()) {
|
||||
const r = lsm.getRecoveryReport();
|
||||
if (r.droppedSSTables.length > 0) {
|
||||
reasons.push(`${r.namespace}: ${r.droppedSSTables.length} dropped SSTable(s)`);
|
||||
}
|
||||
if (r.dataLossSuspected) reasons.push(`${r.namespace}: LSM data loss suspected`);
|
||||
}
|
||||
return reasons;
|
||||
}
|
||||
|
||||
/**
|
||||
* v0.8.0: 恢复诊断(打开时被丢弃的 SSTable、WAL 空洞、是否怀疑数据丢失)。
|
||||
*
|
||||
@@ -2123,6 +2263,7 @@ export class AriaEngine implements IStorageEngine {
|
||||
droppedSSTables: dropped,
|
||||
dataLossSuspected: dataLoss,
|
||||
walGaps: [...this.recoveryReport.walGaps],
|
||||
droppedWALRecords: this.recoveryReport.droppedWALRecords,
|
||||
legacyImported: this.recoveryReport.legacyImported,
|
||||
manifestFallback: this.recoveryReport.manifestFallback,
|
||||
};
|
||||
@@ -2236,6 +2377,17 @@ export class AriaEngine implements IStorageEngine {
|
||||
* - 全部落盘 → 推进到当前 LSN(这些记录已存在于已提交的 SSTable 中)。
|
||||
*/
|
||||
private computeDurableLsn(intents: ManifestFrozenIntent[]): number {
|
||||
// v0.8.0(review 修复 P0):**事务进行中一律不得推进水位**。
|
||||
//
|
||||
// 事务内的写入只落在 `txnSnapshot`(内存)+ WAL 里,**不进 LSM memtable**
|
||||
// —— 因此 `hasPendingFlushData()`(只看 memtable/frozen)会说"没有未落盘数据",
|
||||
// 水位就被推到当前 LSN 并按该水位删掉旧分片。而此时事务记录既不在 SSTable
|
||||
// 也不在 memtable:随后 `COMMIT`(返回成功)→ 崩溃 → 重开时那些 INSERT 记录
|
||||
// 因 `lsn <= startLsn` 被跳过、只剩 COMMIT 记录 → **已确认提交的事务整批消失**,
|
||||
// 且恢复报告是"干净"的(实测复现)。
|
||||
//
|
||||
// 水位是"这些 LSN 已存在于已提交 SSTable 中"的断言,而活跃事务的数据不满足它。
|
||||
if (this.currentTxnId !== null) return this.durableLsn;
|
||||
if (intents.length > 0) {
|
||||
return Math.min(this.durableLsn, Math.min(...intents.map((i) => i.lsnAtFreeze)));
|
||||
}
|
||||
@@ -2251,6 +2403,17 @@ export class AriaEngine implements IStorageEngine {
|
||||
* 否则又会出现"同一语义多处实现、只改一处"的老问题。
|
||||
*/
|
||||
private async advanceWalCheckpoint(): Promise<void> {
|
||||
// v0.8.0(review 修复 P0):事务活跃时整条水位推进 + 分片回收都不做。
|
||||
// 与 `CheckpointManager` 的两个回调同一守卫;这里放在入口处,
|
||||
// 使 repair()/close()/周期 checkpoint 三条路径全部覆盖(它们此前只有后两条有守卫)。
|
||||
if (this.currentTxnId !== null) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'[AriaEngine] WAL checkpoint deferred: an active transaction may hold data ' +
|
||||
'that exists only in memory + WAL (advancing the durable watermark would drop it)',
|
||||
);
|
||||
return;
|
||||
}
|
||||
await this.wal.flush();
|
||||
// 1. 先算:以"如果没有未落盘数据,水位会到哪里"为基准
|
||||
const target = this.hasPendingFlushData()
|
||||
@@ -2331,19 +2494,9 @@ export class AriaEngine implements IStorageEngine {
|
||||
// 2. 表结构(__aria_schemas)
|
||||
const schemaRaw = await this.backend.read('__aria_schemas');
|
||||
if (schemaRaw) {
|
||||
try {
|
||||
const parsed = JSON.parse(new TextDecoder().decode(schemaRaw)) as Record<string, Record<string, ColumnDef>>;
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
this.manifest.schemas = parsed;
|
||||
imported = true;
|
||||
}
|
||||
} catch (error) {
|
||||
throw new DatabaseError(
|
||||
`Legacy schema record "__aria_schemas" is corrupt and cannot be migrated: ${(error as Error).message}`,
|
||||
'ARIA_LEGACY_META_CORRUPT',
|
||||
error,
|
||||
);
|
||||
}
|
||||
// 与 loadSchemas 走**同一个**校验实现(形状不对同样抛错,绝不静默空库)
|
||||
this.manifest.schemas = this.parseSchemaRecord(schemaRaw, 'Legacy schema record "__aria_schemas"');
|
||||
imported = true;
|
||||
}
|
||||
|
||||
// 3. 页面水位(__aria_meta,仅作为单调下限)
|
||||
@@ -2379,8 +2532,6 @@ export class AriaEngine implements IStorageEngine {
|
||||
if (intents.length === 0) return;
|
||||
if (replayedRecordCount > 0) return; // WAL 覆盖到了这些数据(重放会重建)
|
||||
if (!this.config.walEnabled) return; // 未启用 WAL:本来就没有日志兜底(配置语义)
|
||||
// 全部 LSM 已落盘(意图来自上一次会话的残留)→ 数据其实已经安全
|
||||
if (!this.hasPendingFlushData() && this.manifest.frozen.length === 0) return;
|
||||
|
||||
const summary = intents.map((i) => `${i.ns}#${i.id}(${i.entryCount} 项)`).join(', ');
|
||||
throw new DatabaseError(
|
||||
@@ -2975,16 +3126,11 @@ export class AriaEngine implements IStorageEngine {
|
||||
// → 墓碑与历史版本在最底层永久累积),且无论是否真的合并过都返回
|
||||
// `compactedLevels: 6`("报告的数字与事实无关",审计 item 52)。
|
||||
// 现在逐层尝试(含底部层的原地合并 —— 它会回收墓碑),只统计真正合并了的层。
|
||||
let compactedLevels = 0;
|
||||
const isBottom = (level: number): boolean => level === MAX_LSM_LEVELS - 1;
|
||||
for (let level = 0; level < MAX_LSM_LEVELS; level++) {
|
||||
const files = this.lsm.getStats().levelCounts[level] ?? 0;
|
||||
// 底部层即使只有 1 个文件也要合并:那正是"墓碑/历史版本回收"的唯一时机
|
||||
//(删除密集场景下底部层通常就是一个大文件)
|
||||
const minFiles = isBottom(level) ? 1 : 2;
|
||||
if (files < minFiles) continue;
|
||||
if (await this.lsm.compactLevel(level, minFiles)) compactedLevels++;
|
||||
}
|
||||
// 逐层压缩交给 LSM:它会把这些任务挂到**维护链**上串行执行 ——
|
||||
// 直接 `await compactLevel()` 会与后台 compaction 并发写同一层的产物,
|
||||
// 而产物一律 unshift 到队首(层内顺序 = 新旧顺序)→ 旧数据可能排到新数据
|
||||
// 之前(读到旧值),底部层还会因丢墓碑让已删除的行复活。
|
||||
const compactedLevels = await this.lsm.vacuumLevels();
|
||||
// GC MVCC 版本(保留最新 10 个)
|
||||
const beforeGC = this.mvcc.getGlobalLSN();
|
||||
this.mvcc.gc(10);
|
||||
|
||||
Reference in New Issue
Block a user