fix(P0): 生产矩阵审计暴露 2 个崩溃恢复丢数据 bug — ① FileManager 页面 id 崩溃回退(allocatePageIds 后 saveMeta 前中断 → nextPageId 回退 → 恢复期页面复用被 compaction 误删,kv 2万行+加密崩溃丢75%):init 以现存最大页面 id+1 为准;② LSM.flush 剩余数据与 compaction 并发写 meta(产物 saveMeta 被覆盖成孤儿 → 优雅关闭重开丢75%):剩余落盘挂链串行。新增 13 组合矩阵审计(后端×特性×功能全量验收)
CI / test (20.x) (push) Canceled after 0s
CI / test (22.x) (push) Canceled after 0s
CI / test (24.x) (push) Canceled after 0s
CI / e2e (push) Canceled after 0s
CI / test (18.x) (push) Canceled after 12m46s

This commit is contained in:
thzxx
2026-08-10 21:30:49 +08:00
parent d83910832e
commit 5c6011d487
5 changed files with 380 additions and 7 deletions
+9 -3
View File
@@ -573,15 +573,21 @@ export class LSM {
}
// v0.4.3-fix: 循环等待级联任务(flush 完成可能触发新的 compaction
await this.drainChain();
// 若仍有 frozen 数据未刷盘,在链尾追加
// v0.6.1-fix(P0): 剩余数据 flush 必须挂链串行 —— 此前直接 await 执行,
// 与链上 compaction 并发写 SSTable metacompaction 产物 saveMeta 后,
// memtable flush 的 saveMeta 读到中间态列表(含 compaction 产物)→ 覆盖产物
// 引用 → compaction 产物变孤儿 → 索引/主表数据静默丢失(优雅关闭后重开丢 75%)。
// 挂链后按序执行(memtable flush 在 compaction 之后),meta 无竞态。
if (this.immutableMemtable) {
const frozen = this.immutableMemtable;
await this.flushImmutableAsync(frozen);
this.flushChain = this.enqueueOnChain(() => this.flushImmutableAsync(frozen));
}
if (this.memtable.getEntryCount() > 0) {
this.freezeMemtable();
const frozen = this.immutableMemtable;
if (frozen) await this.flushImmutableAsync(frozen);
if (frozen) {
this.flushChain = this.enqueueOnChain(() => this.flushImmutableAsync(frozen));
}
}
this.frozenMemtables = [];
// 刷盘完成后级联调度可能触发 compaction → 排空到稳定
+17 -4
View File
@@ -28,11 +28,24 @@ export class FileManager implements PageIO {
async init(dbName: string): Promise<void> {
this.dbName = dbName;
const meta = await this.backend.read('__aria_meta');
let nextPageId = 1;
if (meta && meta instanceof ArrayBuffer && meta.byteLength >= 4) {
const view = new DataView(meta);
this.nextPageId = view.getUint32(0, false);
} else {
this.nextPageId = 1;
nextPageId = new DataView(meta).getUint32(0, false);
}
// v0.6.1-fix(P0): 崩溃一致性 —— allocatePageIds 的 saveMeta 可能在分配
// 页面后被崩溃中断(nextPageId 回退)。恢复时若按回退值继续分配,
// 页面 id 复用会覆盖旧页面;随后 compaction 按 meta.pageIds 删除"旧"SSTable
// 时误删被复用的新数据 → 崩溃恢复后静默丢数据(kv 后端 2 万行丢 75%)。
// 修复:以"现存最大页面 id + 1"为准(单调不回退,绝不复用已存在页面)。
const keys = await this.backend.listKeys();
for (const k of keys) {
if (k.startsWith('pg_')) {
const id = Number.parseInt(k.slice(3), 10);
if (!Number.isNaN(id) && id + 1 > nextPageId) nextPageId = id + 1;
}
}
this.nextPageId = nextPageId;
if (!meta || !(meta instanceof ArrayBuffer) || meta.byteLength < 4 || nextPageId !== new DataView(meta as ArrayBuffer).getUint32(0, false)) {
await this.saveMeta();
}
this.metaLoaded = true;