Files
MetonaSqlark/src/engine/aria/transaction/mvcc.ts
T
thzxx 0b44620721 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 已重建。
2026-09-15 16:33:40 +08:00

188 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* AriaEngine MVCC — 多版本并发控制
* @module engine/aria/transaction/mvcc
*
* v0.8.0review 修正文档):本模块提供**行版本链**,用途是"事务内的 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: 记录本事务写过的 keycommit/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;
}
}