/** * AriaEngine File Manager — 页面文件管理 + PageIO 实现 * @module engine/aria/store/file_manager * * 负责管理页面文件的生命周期:分配/释放页面 ID,读写页面。 */ import type { IStorageBackend } from './backend'; import type { PageIO } from '../buffer/pool'; import { PAGE_SIZE } from '../types'; // --------------------------------------------------------------------------- // FileManager (implements PageIO) // --------------------------------------------------------------------------- export class FileManager implements PageIO { private backend: IStorageBackend; private nextPageId = 0; private metaLoaded = false; private dbName = ''; constructor(backend: IStorageBackend) { this.backend = backend; } /** 初始化:从存储中读取元数据 */ async init(dbName: string, watermarkFloor: number = 1): Promise { this.dbName = dbName; const meta = await this.backend.read('__aria_meta'); let nextPageId = 1; if (meta && meta instanceof ArrayBuffer && meta.byteLength >= 4) { 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; } } // v0.8.0(B-6):manifest 的 pageId 水位是**权威下限**(单调推进、永不复用), // 与"现存最大页面 id + 1"、"旧 __aria_meta" 三者取最大 —— 任何单一来源被 // 截断/回退都不会导致页面 id 复用。 if (Number.isFinite(watermarkFloor) && watermarkFloor > nextPageId) { nextPageId = Math.floor(watermarkFloor); } this.nextPageId = nextPageId; this.metaLoaded = true; // v0.8.0(B-6):`__aria_meta` 降级为**兼容/诊断提示**,不再作为提交点: // 页面水位只由 manifest 提交(单一提交点),这里仅在旧值落后时补写一次, // 且写入失败不影响引擎(旧版本读到的只是"落后但单调"的提示值)。 const legacyValue = meta && meta instanceof ArrayBuffer && meta.byteLength >= 4 ? new DataView(meta as ArrayBuffer).getUint32(0, false) : -1; if (legacyValue !== nextPageId) { try { await this.writeLegacyWatermark(nextPageId); } catch { /* 兼容提示写失败不影响正确性 */ } } } /** v0.8.0: 当前页面 id 水位(下一个可分配 id)—— manifest 提交时记录 */ getNextPageId(): number { return this.nextPageId; } // ---- PageIO ---- async readPage(pageId: number): Promise { const key = `pg_${pageId}`; const data = await this.backend.read(key); if (!data) { // v0.4.5: 页面总是先分配(allocatePageId 持久化)后写入 —— 读取缺失页面视为损坏 // (此前返回空页面会静默掩盖页面丢失,页面化 SSTable 依赖 null 触发自愈清理) return null; } // 确保大小正确 if (data.byteLength < PAGE_SIZE) { const padded = new ArrayBuffer(PAGE_SIZE); new Uint8Array(padded).set(new Uint8Array(data)); return padded; } return data; } async writePage(pageId: number, data: ArrayBuffer): Promise { const key = `pg_${pageId}`; await this.backend.write(key, data); } async allocatePageId(): Promise { const id = this.nextPageId++; await this.persistWatermarkHint(); return id; } /** v0.4.5: 批量分配页面 ID(一次提示写,避免页面化 SSTable 保存时逐页写 meta) */ async allocatePageIds(count: number): Promise { if (count <= 0) return []; const ids: number[] = []; const start = this.nextPageId; this.nextPageId += count; for (let i = 0; i < count; i++) ids.push(start + i); await this.persistWatermarkHint(); return ids; } async freePageId(_pageId: number): Promise { // 简化实现:不回收 pageId const key = `pg_${_pageId}`; await this.backend.delete(key); } // ---- 辅助 ---- /** * v0.8.0(B-6):`__aria_meta` 只是**兼容提示**(旧版本/人工诊断用), * 失败不抛错 —— 真正的提交点是 manifest 的 `pageIdWatermark`。 */ private async persistWatermarkHint(): Promise { if (!this.metaLoaded) return; try { await this.writeLegacyWatermark(this.nextPageId); } catch { /* 提示写失败不影响正确性(manifest 才是权威) */ } } private async writeLegacyWatermark(nextPageId: number): Promise { const buf = new ArrayBuffer(8); new DataView(buf).setUint32(0, nextPageId, false); await this.backend.write('__aria_meta', buf); } /** 清空所有数据 */ async clearAll(): Promise { await this.backend.clear(); this.nextPageId = 1; try { await this.writeLegacyWatermark(1); } catch { /* 提示写失败不影响正确性 */ } } }