方法:四个对抗性子代理分头审查(数据正确性 / 文档宣称 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 已重建。
188 lines
6.2 KiB
TypeScript
188 lines
6.2 KiB
TypeScript
/**
|
||
* AriaEngine MVCC — 多版本并发控制
|
||
* @module engine/aria/transaction/mvcc
|
||
*
|
||
* v0.8.0(review 修正文档):本模块提供**行版本链**,用途是"事务内的 undo",
|
||
* 提交即清理。它**不提供快照隔离** —— 引擎的隔离语义是"事务串行"(同一实例
|
||
* 同时只允许一个事务,并发 `beginTransaction` 抛 `TX_ACTIVE`),读取走
|
||
* `txnSnapshot` 未提交快照。旧注释声称 "实现快照隔离 (Snapshot Isolation)"
|
||
* 与实现不符(全量审查发现)。
|
||
*/
|
||
|
||
import type { RowVersion, TxnEntry } from '../types';
|
||
import { TransactionState } from '../types';
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// MVCCManager
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export class MVCCManager {
|
||
/** 所有行版本的存储:tableName.key → 版本链 */
|
||
private versionStore: Map<string, RowVersion[]> = new Map();
|
||
|
||
/** 活跃事务表:txnId → TxnEntry */
|
||
private activeTxns: Map<number, TxnEntry> = new Map();
|
||
|
||
/** 事务 ID 计数器 */
|
||
private nextTxnId = 1;
|
||
|
||
/** 全局提交序列号(用于可见性判断) */
|
||
private globalCommitLsn = 0;
|
||
|
||
/**
|
||
* v0.4.2-fix: 每个事务写入的 tableKey 集合 —
|
||
* commit/rollback 只遍历本事务写过的 key,避免全库版本链扫描(大表事务 O(N) → O(写入数))
|
||
*/
|
||
private txnWriteKeys: Map<number, Set<string>> = new Map();
|
||
|
||
// =======================================================================
|
||
// 事务管理
|
||
// =======================================================================
|
||
|
||
/** 开始一个事务,返回事务 ID */
|
||
beginTransaction(): number {
|
||
const txnId = this.nextTxnId++;
|
||
this.activeTxns.set(txnId, {
|
||
txnId,
|
||
state: TransactionState.ACTIVE,
|
||
snapshotLsn: this.globalCommitLsn,
|
||
startTime: Date.now(),
|
||
});
|
||
this.txnWriteKeys.set(txnId, new Set());
|
||
return txnId;
|
||
}
|
||
|
||
/** 提交事务 */
|
||
commitTransaction(txnId: number): void {
|
||
const txn = this.activeTxns.get(txnId);
|
||
if (!txn) throw new Error(`Transaction ${txnId} not found`);
|
||
|
||
txn.state = TransactionState.COMMITTED;
|
||
this.globalCommitLsn++;
|
||
|
||
// v0.6.3-fix: 已提交版本直接清理 —— 快照读取已移除(v0.5.1),版本链仅作
|
||
// 事务内 undo 记录(rollback/savepoint 用),提交后 LSM 持有权威数据。
|
||
// 此前 commit 仅标记 committed → versionStore 随写入量无限增长(行数据双份常驻)。
|
||
const writeKeys = this.txnWriteKeys.get(txnId);
|
||
if (writeKeys) {
|
||
for (const tableKey of writeKeys) {
|
||
const versions = this.versionStore.get(tableKey);
|
||
if (!versions) continue;
|
||
const filtered = versions.filter((v) => v.txnId !== txnId);
|
||
if (filtered.length === 0) {
|
||
this.versionStore.delete(tableKey);
|
||
} else {
|
||
this.versionStore.set(tableKey, filtered);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 清理已提交事务的记录
|
||
this.activeTxns.delete(txnId);
|
||
this.txnWriteKeys.delete(txnId);
|
||
}
|
||
|
||
/** 回滚事务 */
|
||
rollbackTransaction(txnId: number): void {
|
||
const txn = this.activeTxns.get(txnId);
|
||
if (!txn) throw new Error(`Transaction ${txnId} not found`);
|
||
|
||
txn.state = TransactionState.ABORTED;
|
||
|
||
// v0.4.2-fix: 仅移除本事务写入的版本(此前遍历全库 versionStore)
|
||
const writeKeys = this.txnWriteKeys.get(txnId);
|
||
if (writeKeys) {
|
||
for (const tableKey of writeKeys) {
|
||
const versions = this.versionStore.get(tableKey);
|
||
if (!versions) continue;
|
||
const filtered = versions.filter((v) => v.txnId !== txnId);
|
||
if (filtered.length === 0) {
|
||
this.versionStore.delete(tableKey);
|
||
} else {
|
||
this.versionStore.set(tableKey, filtered);
|
||
}
|
||
}
|
||
}
|
||
|
||
this.activeTxns.delete(txnId);
|
||
this.txnWriteKeys.delete(txnId);
|
||
}
|
||
|
||
// =======================================================================
|
||
// 版本读写
|
||
// =======================================================================
|
||
|
||
/**
|
||
* 写入一行(创建新版本)。
|
||
*/
|
||
writeVersion(
|
||
tableName: string,
|
||
key: string,
|
||
data: Record<string, unknown>,
|
||
txnId: number,
|
||
): void {
|
||
const tableKey = `${tableName}.${key}`;
|
||
const versions = this.versionStore.get(tableKey) ?? [];
|
||
|
||
const newVersion: RowVersion = {
|
||
txnId,
|
||
data,
|
||
prevVersion: versions.length > 0 ? versions[versions.length - 1] : null,
|
||
committed: false,
|
||
};
|
||
|
||
versions.push(newVersion);
|
||
this.versionStore.set(tableKey, versions);
|
||
// v0.4.2-fix: 记录本事务写过的 key(commit/rollback 精准清理)
|
||
this.txnWriteKeys.get(txnId)?.add(tableKey);
|
||
}
|
||
|
||
/**
|
||
* 删除一行(创建墓碑版本)。
|
||
*/
|
||
deleteVersion(tableName: string, key: string, txnId: number): void {
|
||
this.writeVersion(tableName, key, { __mvcc_tombstone: true } as unknown as Record<string, unknown>, txnId);
|
||
}
|
||
|
||
/**
|
||
* v0.3.3: 丢弃指定事务的所有版本记录,但保留事务登记(Savepoint 回滚用)。
|
||
* 快照数据由调用方(引擎 txnSnapshot)负责恢复。
|
||
* v0.4.2-fix: 仅遍历本事务写过的 key(此前全库扫描)。
|
||
*/
|
||
discardVersions(txnId: number): void {
|
||
const writeKeys = this.txnWriteKeys.get(txnId);
|
||
if (!writeKeys) return;
|
||
for (const tableKey of writeKeys) {
|
||
const versions = this.versionStore.get(tableKey);
|
||
if (!versions) continue;
|
||
const filtered = versions.filter((v) => v.txnId !== txnId);
|
||
if (filtered.length === 0) {
|
||
this.versionStore.delete(tableKey);
|
||
} else {
|
||
this.versionStore.set(tableKey, filtered);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清理过旧版本(GC)。
|
||
* 保留每个 key 的最新 N 个已提交版本。
|
||
*/
|
||
gc(maxVersionsPerKey: number = 100): void {
|
||
for (const [tableKey, versions] of this.versionStore) {
|
||
if (versions.length <= maxVersionsPerKey) continue;
|
||
|
||
// 保留最新的 maxVersionsPerKey 个版本
|
||
const pruned = versions.slice(versions.length - maxVersionsPerKey);
|
||
this.versionStore.set(tableKey, pruned);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取全局 LSN。
|
||
*/
|
||
getGlobalLSN(): number {
|
||
return this.globalCommitLsn;
|
||
}
|
||
}
|