///|
/// M2 阶段的数据库:WAL + memtable + 若干 SST,全部通过 `Storage` 落盘。
///
/// 写入顺序(**这个顺序是崩溃契约 C1 的全部内容**):
///
/// ```
/// 分配 seq → 追加 WAL 帧 → fsync WAL → 写 memtable → 可能刷盘
/// ```
///
/// 三个关键点:
///
/// 1. **先 WAL 后 memtable**。反过来的话,进程在两者之间死掉,
/// 内存里的数据会消失而 WAL 里没有记录,重启后这条写入凭空消失。
/// 2. **fsync 成功之后才能对外返回 seq**。fsync 失败就抛错,
/// 调用方拿不到 ack,也就不会认为写成功了。
/// 3. **刷盘时先让 SST 落盘,再清 WAL**。中间崩溃只会导致 WAL 被重放一次,
/// 而重放是幂等的(同样的 seq、同样的值),不会产生错误结果。
///
/// 读路径的新旧顺序:`memtable` → `ssts[0]` → `ssts[1]` → …
/// 越靠前越新。注意这只是**短路优化的依据**,正确性依据永远是 `seq` ——
/// 归并时用 `seq` 判断新旧,而不是数组下标。
///
/// 写入 API 分成两组,对应两种 fsync 粒度:
///
/// - `put` / `delete`:写 + fsync,返回即持久。适合单条写。
/// - `put_deferred` / `delete_deferred` + `sync`:批量追加后一次 fsync。
/// 适合 `putmany`,把 N 次刷盘压成 1 次(组提交)。
///
/// 两组都满足 C1:**ack 发出之前 fsync 一定已经成功**。区别只在
/// 「一批里 fsync 几次」,不在「要不要 fsync」。
///
/// 但两组的**失败可见性不同**,这是批量的固有代价:
/// `put` 把 fsync 放在改 memtable 之前,失败时写入完全不可见;
/// `put_deferred` 做不到(写第一条时还不知道最后的 sync 会不会成功),
/// 所以 `sync` 失败时那批数据会留在内存里可读、但没有 ack、断电必丢。
pub struct Db[S] {
storage : S
mem : MemTable
/// 从新到旧。带上编号是因为 compaction 需要知道「要删哪个文件」。
ssts : Array[LoadedSst]
/// memtable 字节阈值,超过就自动刷盘。
mem_limit : Int32
mut seq : UInt64
mut flush_count : Int32
/// 版本快照:每层有哪些 SST、活跃 WAL 编号、下一个可用文件编号。
/// 它是**唯一**的文件编号来源 —— 不要再单独维护计数器。
mut version : Version
/// 当前 MANIFEST 的文件名(如 `MANIFEST-000007`)。空串表示还没写过。
mut current_manifest : String
/// 每次写入是否立刻 fsync。关掉它是为了压测「不落盘能跑多快」,
/// **默认必须是 true**——关掉就等于放弃了 C1。
sync_on_write : Bool
/// `put_deferred` 攒下来的 WAL 帧,等 `sync()` 时一次性写出。
///
/// 为什么要攒:真文件系统上每次 `fopen`/`fclose` 约 5.9 ms(Windows 实测),
/// 逐帧打开写一次的话,200 条的批次要 1.2 秒,其中 99% 是开关文件的开销。
/// 攒起来一次写,同样的批次降到 6 ms。
///
/// **只用于 deferred 路径。** `put`/`delete` 仍然逐帧立即写 WAL ——
/// 单条写的语义是「返回即持久」,把帧留在内存里会让「优雅重启也丢数据」,
/// 那是比慢严重得多的问题。
///
/// 不需要 `mut`:`Array` 是引用类型,`push`/`clear` 改的是它指向的内容,
/// 不是字段本身。
pending : Array[Bytes]
mut wal_bytes : Int32
mut closed : Bool
}
///|
/// 一个已加载的 SST。编号来自文件名,`reader` 是解析后的内容。
///
/// 之所以不只用 `SstReader`:compaction 要把「被归并掉的文件」从内存视图里
/// 摘掉,而 `SstReader` 本身不记得自己叫什么。
pub struct LoadedSst {
num : UInt64
reader : SstReader
}
///|
/// 磁盘布局(`docs/design/format.md` §1):
///
/// ```
/// /
/// ├── CURRENT # 文本,内容是当前 MANIFEST 的文件名 + "\n"
/// ├── MANIFEST-000007 # 版本快照
/// ├── wal/
/// │ └── 000008.wal # 活跃 WAL
/// └── sst/
/// └── 000004.sst
/// ```
///
/// **文件编号全局共享**:WAL、SST、MANIFEST 从同一个单调递增计数器取号
/// (存在 MANIFEST 的 `next_file_number` 里)。这样任何两个文件不会撞名,
/// 恢复时也只需比较编号大小。
///
/// 早期版本用的是根目录扁平的 `WAL-000001` / `SST-000001`,两个独立计数器。
/// 那种布局在引入 WAL 轮转(每次 flush 换一个新 WAL)之后就不成立了:
/// 两个计数器会各自撞上对方已经用过的号。
pub const WAL_DIR : String = "wal"
///|
pub const SST_DIR : String = "sst"
///|
pub const CURRENT_FILE : String = "CURRENT"
///|
const CURRENT_TMP : String = "CURRENT.tmp"
///|
const MANIFEST_PREFIX : String = "MANIFEST-"
///|
/// 打开(或新建)数据库。`mem_limit` 是 memtable 的字节阈值。
///
/// 每次都立刻 fsync。要关掉它(只用于压测)用 `Db::open_with`。
pub fn[S : Storage] Db::open(
storage : S,
mem_limit : Int32,
) -> Db[S] raise LsmError {
Db::open_with(storage, mem_limit, true)
}
///|
/// 同 `Db::open`,但可以关掉每次写入的 fsync。
///
/// `sync_on_write = false` 时**崩溃会丢已确认的数据**,只应该用于
/// 性能对比实验。所有正确性测试都必须用 true。
pub fn[S : Storage] Db::open_with(
storage : S,
mem_limit : Int32,
sync_on_write : Bool,
) -> Db[S] raise LsmError {
Storage::prepare(storage)
let db : Db[S] = {
storage,
mem: MemTable::new(mem_limit),
ssts: [],
mem_limit,
seq: 0UL,
flush_count: 0,
version: Version::new(),
current_manifest: "",
sync_on_write,
pending: [],
wal_bytes: 0,
closed: false,
}
db.recover()
db
}
///|
/// 打开数据库:读 CURRENT → 读 MANIFEST → 加载 SST → 重放 WAL。
///
/// 顺序是 **先 SST 后 WAL**:SST 提供基线,WAL 覆盖在其上。
///
/// 反过来(先 WAL 后 SST)会出错:SST 里的条目 seq 更小,但插到 memtable
/// 前面之后 `get` 会先命中 memtable,表面上「碰巧对了」;一旦 memtable
/// 里没有那个 key,就会读到 SST 的旧值。这种错在测试里极难复现。
fn[S : Storage] Db::recover(self : Db[S]) -> Unit raise LsmError {
self.load_version()
let mut max_seq = self.version.last_seq
// 加载 SST,顺序必须让 `self.ssts[0]` 是最新的文件 —— `get` 靠这个
// 顺序做短路,顺序反了就会读到旧值。
//
// 正确的顺序是「层号升序(L0 最新)+ 层内编号降序」:
// L0 的 SST 来自更晚的 memtable,所以整个 L0 都比 L1 新;
// 同层内编号越大越新。依次 push 正好得到「从新到旧」。
for lv in 0.. 0 {
i = i - 1
let meta = files[i]
let path = sst_path(meta.number)
let raw = match Storage::read_file(self.storage, path) {
Some(b) => b
None =>
raise LsmError::corruption(
"MANIFEST 里登记了 \{path},但文件不存在(元信息与实际不一致)",
)
}
let r = match open_sst(raw) {
Some(r) => r
None =>
raise LsmError::corruption(
"SST 打不开(文件损坏或版本不兼容):\{path}",
)
}
for e in r.entries() {
if e.seq > max_seq {
max_seq = e.seq
}
}
self.ssts.push({ num: meta.number, reader: r, })
}
}
let wpath = self.wal_name()
let wal = match Storage::read_file(self.storage, wpath) {
Some(b) => b
None => {
// MANIFEST 指向的 WAL 不存在。可能是被人删了,也可能是初始化
// 写到一半崩了。建一个空的:WAL 只是「未刷盘写入的暂存区」,
// 而 SST 里的数据已经登记在 MANIFEST 里,不会因此丢。
// 真正会丢的是「还没 flush 的写入」—— 那种情况下本来也没有 ack。
let empty = empty_wal_bytes()
Storage::write_file(self.storage, wpath, empty)
Storage::sync_file(self.storage, wpath)
empty
}
}
self.wal_bytes = wal.length()
let rep = recover_wal(wal)
if !rep.header_ok() {
// 文件头都不对,整份 WAL 没有可用内容。重建一个空的,
// 而不是让数据库起不来 —— WAL 是「未确认写入的暂存区」,
// 丢掉它最坏的情况是丢最后一次未刷盘的写入,比拒绝启动好。
let empty = empty_wal_bytes()
Storage::write_file(self.storage, wpath, empty)
Storage::sync_file(self.storage, wpath)
self.wal_bytes = empty.length()
} else if rep.is_truncated() {
// 尾部有半条记录 —— 这是断电的常态,不是数据损坏。
// 把 WAL 重写成「只有完整好帧」的版本。
//
// 不用 ftruncate:Windows 的 CRT 里没有这个符号(只有 `_chsize`),
// 而重写一个 WAL 的成本只是「文件多大就写多少」,且只在崩溃后发生一次。
let good = take_prefix(wal, rep.good_bytes())
Storage::write_file(self.storage, wpath, good)
Storage::sync_file(self.storage, wpath)
self.wal_bytes = good.length()
}
for rec in rep.records() {
self.mem.apply(rec)
if rec.seq > max_seq {
max_seq = rec.seq
}
}
self.seq = max_seq
// 编号比 log_number 小的 WAL 都是历史遗留:它们的数据已经进了 SST,
// MANIFEST 也不会再引用它们。删掉,否则目录会一直涨。
for name in Storage::list_files(self.storage, WAL_DIR) {
match parse_number_prefix(name) {
Some(n) =>
if n < self.version.log_number {
Storage::remove_file(self.storage, WAL_DIR + "/" + name)
}
None => ()
}
}
}
///|
/// 读 CURRENT → 读 MANIFEST,把版本快照装进 `self.version`。
///
/// 三种情形:
/// - **CURRENT 存在且指向的 MANIFEST 能解码** → 正常恢复
/// - **CURRENT 不存在,且目录里没有任何数据文件** → 全新的库,初始化
/// - **其他** → `Corruption`,拒绝启动
///
/// 最后一条是刻意「不宽容」的:WAL 尾部的半条记录可以截断(那是断电常态),
/// 但「不知道数据布局」时硬猜会**静默丢数据**。宁可拒绝启动让人来看。
fn[S : Storage] Db::load_version(self : Db[S]) -> Unit raise LsmError {
let cur = Storage::read_file(self.storage, CURRENT_FILE)
match cur {
None =>
if self.has_any_data_file() {
raise LsmError::corruption(
"CURRENT 缺失,但目录里已有数据文件。无法推断布局,拒绝启动。",
)
} else {
self.init_fresh()
}
Some(raw) =>
match parse_current(@utf8.decode_lossy(raw)) {
None => raise LsmError::corruption("CURRENT 内容无法解析")
Some(name) => {
self.version = self.read_manifest(name)
self.current_manifest = name
}
}
}
// 目录里可能残留孤儿文件(崩溃在「写完 SST」和「写 MANIFEST」之间)。
// 编号必须跳过它们,否则下一次 flush 会覆盖一个已经存在的文件 ——
// 虽然孤儿没被引用、覆盖它无害,但「文件名和内容不对应」会让
// 排查问题时彻底失去方向。
self.bump_file_number_past_existing()
}
///|
/// 读 CURRENT 指向的那份 MANIFEST。
///
/// **读不懂就拒绝启动,不退回旧版本。**
///
/// 早期版本这里实现了「退回编号次大的 MANIFEST」的兜底。删掉它是因为
/// 想清楚了一件事:退回旧 MANIFEST 会**丢掉已经 ack 的数据**。
///
/// 具体地:`flush` 把数据写进新 SST、把新 SST 登记进新 MANIFEST,
/// 然后删掉旧 WAL(那批写入在返回 ack 时就已 fsync 过)。此时若退回
/// 旧 MANIFEST,它不认识那个新 SST,于是那批**已经承诺不丢**的数据
/// 变得不可见 —— 直接违反契约 C1。
///
/// 拒绝启动则完全不同:数据一个字节都没少,只是需要人来判断。
/// 所有文件都还在目录里,可以人工检查、可以写工具从 SST 重建 MANIFEST。
///
/// 这和 `wal.mbt` 对坏数据的态度差异是刻意的:WAL 尾部的半条记录是
/// **断电常态**,截断它不损失任何已 ack 的数据;而 MANIFEST 读不懂意味着
/// 「不知道哪些数据是有效的」,这时任何自动决策都可能丢数据。
fn[S : Storage] Db::read_manifest(
self : Db[S],
name : String,
) -> Version raise LsmError {
let raw = match Storage::read_file(self.storage, name) {
Some(b) => b
None =>
raise LsmError::corruption(
"CURRENT 指向的 MANIFEST 不存在:\{name}",
)
}
let v = match decode_manifest(raw) {
Some(v) => v
None =>
raise LsmError::corruption(
"MANIFEST 解码失败(magic/version/CRC/结构):\{name}",
)
}
if !v.check_invariants() {
raise LsmError::corruption(
"MANIFEST 语义不合法(层重叠或编号越界):\{name}",
)
}
v
}
///|
/// 目录里是否已经有数据文件(用来区分「全新库」和「元信息丢了」)。
fn[S : Storage] Db::has_any_data_file(self : Db[S]) -> Bool raise LsmError {
if Storage::list_files(self.storage, SST_DIR).length() > 0 {
return true
}
if Storage::list_files(self.storage, WAL_DIR).length() > 0 {
return true
}
for f in Storage::list_files(self.storage, "") {
if has_prefix(f, MANIFEST_PREFIX) {
return true
}
}
false
}
///|
/// 全新的库:建活跃 WAL,写第一份 MANIFEST 和 CURRENT。
fn[S : Storage] Db::init_fresh(self : Db[S]) -> Unit raise LsmError {
// Version::new() 给的是 log_number=1、next_file_number=2。
let empty = empty_wal_bytes()
let wp = wal_path(1UL)
Storage::write_file(self.storage, wp, empty)
Storage::sync_file(self.storage, wp)
self.wal_bytes = empty.length()
self.persist_version()
}
///|
/// 把 `self.version` 落盘,并原子切换 CURRENT。
///
/// 严格按 `docs/design/format.md` §4.2 的顺序,每一步都不能换位:
///
/// ```
/// 1. 写 MANIFEST- 全部内容
/// 2. fsync(MANIFEST-)
/// 3. 写 CURRENT.tmp = "MANIFEST-\n"
/// 4. fsync(CURRENT.tmp)
/// 5. rename(CURRENT.tmp, CURRENT) ← 原子点
/// 6. fsync(目录) ← 在 rename_file 内部完成
/// 7. 删除旧 MANIFEST ← 必须在 6 之后
/// ```
///
/// 任何一步之前崩溃,CURRENT 都还指向旧 MANIFEST,而旧的 SST / WAL 都还在,
/// 所以一定能恢复到一个一致的旧状态。**先写后删**是这条序列的全部意义。
fn[S : Storage] Db::persist_version(self : Db[S]) -> Unit raise LsmError {
let num = self.version.alloc_file_number()
let name = manifest_path(num)
let raw = encode_manifest(self.version)
Storage::write_file(self.storage, name, raw)
Storage::sync_file(self.storage, name)
let body = @utf8.encode(name + "\n")
Storage::write_file(self.storage, CURRENT_TMP, body)
Storage::sync_file(self.storage, CURRENT_TMP)
Storage::rename_file(self.storage, CURRENT_TMP, CURRENT_FILE)
let old = self.current_manifest
self.current_manifest = name
if old != "" && old != name {
// 删失败不算错误:多留一个 MANIFEST 只是浪费几百字节,
// 而「因为删不掉就报错」会让一次成功的 flush 看起来失败了。
Storage::remove_file(self.storage, old) catch {
Io(_) => ()
Corruption(_) => ()
Closed => ()
}
}
}
///|
/// 当前活跃 WAL 的路径(`wal/000008.wal`)。
pub fn[S] Db::wal_name(self : Db[S]) -> String {
wal_path(self.version.log_number)
}
///|
/// 当前 MANIFEST 的名字(`MANIFEST-000007`)。空串表示还没写过。
pub fn[S] Db::manifest_name(self : Db[S]) -> String {
self.current_manifest
}
///|
/// 按编号找已加载的 SST。找不到说明内存视图和 `version` 不一致 ——
/// 这是内部不变量的破坏,不是用户错误,所以直接报 Corruption。
fn[S] Db::find_reader(self : Db[S], num : UInt64) -> SstReader raise LsmError {
for s in self.ssts {
if s.num == num {
return s.reader
}
}
raise LsmError::corruption(
"内部状态不一致:版本里引用了 sst/\{pad_num(num)},但内存里没有加载它",
)
}
///|
/// 版本快照是否满足全部语义不变量。测试用它确认归并之后 L1 仍不重叠。
pub fn[S] Db::check_version_invariants(self : Db[S]) -> Bool {
self.version.check_invariants()
}
///|
/// 版本快照里有几层。
pub fn[S] Db::level_count(self : Db[S]) -> Int32 {
self.version.level_count()
}
///|
/// 某一层有几个 SST。层不存在返回 0。
pub fn[S] Db::level_sst_count(self : Db[S], idx : Int32) -> Int32 {
self.version.level(idx).length()
}
///|
/// 版本快照里的 `next_file_number`。测试用它验证「编号不复用」。
pub fn[S] Db::next_file_number(self : Db[S]) -> UInt64 {
self.version.next_file_number
}
///|
/// 让 `next_file_number` 跳过目录里已经存在的编号。
fn[S : Storage] Db::bump_file_number_past_existing(
self : Db[S],
) -> Unit raise LsmError {
let mut max = 0UL
for dir in [SST_DIR, WAL_DIR] {
for name in Storage::list_files(self.storage, dir) {
match parse_number_prefix(name) {
Some(n) => if n > max { max = n }
None => ()
}
}
}
for f in Storage::list_files(self.storage, "") {
if has_prefix(f, MANIFEST_PREFIX) {
match parse_number_prefix(f[MANIFEST_PREFIX.length():].to_owned()) {
Some(n) => if n > max { max = n }
None => ()
}
}
}
if self.version.next_file_number <= max {
self.version.next_file_number = max + 1UL
}
}
///|
fn[S] Db::ensure_open(self : Db[S]) -> Unit raise LsmError {
if self.closed {
raise LsmError::closed()
}
}
///|
/// 当前最大 seq。每次写入自增,跨 memtable 与 SST 全局单调。
pub fn[S] Db::last_seq(self : Db[S]) -> UInt64 {
self.seq
}
///|
pub fn[S] Db::sst_count(self : Db[S]) -> Int32 {
self.ssts.length()
}
///|
pub fn[S] Db::flush_count(self : Db[S]) -> Int32 {
self.flush_count
}
///|
/// 当前 WAL 文件字节数(含文件头)。`stats` 命令用它。
pub fn[S] Db::wal_bytes(self : Db[S]) -> Int32 {
self.wal_bytes
}
///|
/// memtable 里的条目数(含墓碑)。`stats` 命令用它区分
/// 「内存里有多少条」和「已经刷到 SST 的有多少条」。
pub fn[S] Db::mem_entry_count(self : Db[S]) -> Int32 {
self.mem.to_entries().length()
}
///|
/// 写。返回本次写入的 seq —— 返回即代表**已持久**(`sync_on_write = true` 时)。
///
/// 顺序是 `追加帧 → fsync → 改 memtable`,**fsync 在 memtable 之前**。
/// 这不是随手写的:fsync 失败时这条写入必须「完全不可见」,
/// 否则会出现「读得到、但断电就消失」的脏读 —— 比直接报错严重得多。
pub fn[S : Storage] Db::put(
self : Db[S],
key : Bytes,
value : Bytes,
) -> UInt64 raise LsmError {
self.ensure_open()
self.seq = self.seq + 1UL
let rec = WalRecord::new(self.seq, Op::Put(key, value))
self.append_wal_frame(rec)
if self.sync_on_write {
self.sync()
}
// 只有 WAL 落盘之后才动 memtable。反过来写的话,
// 「memtable 有、WAL 没有」这种状态一旦崩溃,这条写入就凭空消失了。
self.mem.apply(rec)
self.maybe_flush()
self.seq
}
///|
/// 写,但**不 fsync**。返回的 seq 不代表已持久。
///
/// 这是组提交(group commit)的一半。另一半是 `sync()`。
///
/// 为什么需要它:`putmany` 一批 200 条,如果每条都 fsync,
/// 在 Windows 的 `_commit` 上就是 200 次真实刷盘(实测约 2.3 秒一批)。
/// 而契约 C1 只要求「**返回 ack 之前** fsync 成功」,从没要求每条一次。
/// 正确用法:
///
/// ```
/// for item in batch { db.put_deferred(k, v) } // 只追加,不刷盘
/// db.sync() // 一次刷盘
/// // ← 到这里才能返回 ok
/// ```
///
/// 安全性说明:中途如果 memtable 满了触发 flush,flush 会把 SST 落盘
/// 并清空 WAL —— 那批数据已经通过 SST 持久化,不会因为「还没 sync」而丢。
///
/// **与 `put` 的可见性差异(有意为之)**:这里没法在写第一条时预知
/// 后面的 `sync()` 会不会成功,所以条目先进 memtable。万一 `sync()` 失败,
/// 这批数据会**留在内存里可读,但没有 ack、断电必丢**。
/// 调用方拿到错误后应当丢弃这批数据并重新打开数据库,不要继续读。
/// 单条写没有这个问题 —— `put` 把 fsync 放在 memtable 之前。
pub fn[S : Storage] Db::put_deferred(
self : Db[S],
key : Bytes,
value : Bytes,
) -> UInt64 raise LsmError {
self.ensure_open()
self.seq = self.seq + 1UL
let rec = WalRecord::new(self.seq, Op::Put(key, value))
// 只攒帧,不碰文件。真正写盘发生在 sync() 里,一批只打开一次文件。
self.buffer_wal_frame(rec)
self.mem.apply(rec)
self.maybe_flush()
self.seq
}
///|
/// 删。写墓碑,不是移除条目。
///
/// 与 `put` 一样,fsync 在 memtable 之前:删失败时旧值必须仍然可读,
/// 而不是「删了一半」。
pub fn[S : Storage] Db::delete(
self : Db[S],
key : Bytes,
) -> UInt64 raise LsmError {
self.ensure_open()
self.seq = self.seq + 1UL
let rec = WalRecord::new(self.seq, Op::Delete(key))
self.append_wal_frame(rec)
if self.sync_on_write {
self.sync()
}
self.mem.apply(rec)
self.maybe_flush()
self.seq
}
///|
/// 删,但不 fsync。与 `put_deferred` 对称,同样要配合 `sync()` 使用。
pub fn[S : Storage] Db::delete_deferred(
self : Db[S],
key : Bytes,
) -> UInt64 raise LsmError {
self.ensure_open()
self.seq = self.seq + 1UL
let rec = WalRecord::new(self.seq, Op::Delete(key))
self.buffer_wal_frame(rec)
self.mem.apply(rec)
self.maybe_flush()
self.seq
}
///|
/// 把 WAL 刷到稳定存储。**这是 C1 的最后一道闸门。**
///
/// 先把 `put_deferred` 攒下的帧一次性写出去(一次 `fopen`),再 fsync。
/// 失败一律抛出:fsync 没成功就不能让调用方以为写成功了。
///
/// 没有待写帧时调用它是安全的(代价 = 一次 fsync)。
pub fn[S : Storage] Db::sync(self : Db[S]) -> Unit raise LsmError {
self.ensure_open()
self.flush_pending()
Storage::sync_file(self.storage, self.wal_name())
}
///|
/// 把攒下的 WAL 帧一次性追加到文件。
///
/// 记账顺序:**先写盘成功,再更新 `wal_bytes` 和清空 `pending`**。
/// 反过来的话,写盘失败时帧已经从 `pending` 里消失,重试就永远写不进去了。
///
/// 如果 `append_frames` 写到一半失败,`pending` 会被保留,
/// 重试时已写成功的部分会被重复追加。这是可接受的:重放同样的 seq
/// 和同样的值是幂等的,不会产生错误结果。
fn[S : Storage] Db::flush_pending(self : Db[S]) -> Unit raise LsmError {
if self.pending.length() == 0 {
return
}
Storage::append_frames(self.storage, self.wal_name(), self.pending)
let mut n = 0
for f in self.pending {
n = n + f.length()
}
self.wal_bytes = self.wal_bytes + n
self.pending.clear()
}
///|
/// 立即追加一帧到 WAL(单条写路径),**不 fsync**。
///
/// 名字里刻意带 `_frame` 而不是 `append_wal`:后者容易被误读成
/// 「追加并落盘」。持久化只发生在 `sync()` 里,一个函数只做一件事。
fn[S : Storage] Db::append_wal_frame(
self : Db[S],
rec : WalRecord,
) -> Unit raise LsmError {
let frame = encode_frame(rec)
Storage::append_file(self.storage, self.wal_name(), frame)
self.wal_bytes = self.wal_bytes + frame.length()
}
///|
/// 把一帧攒进 `pending`,等 `sync()` 时统一写出(批量写路径)。
fn[S] Db::buffer_wal_frame(self : Db[S], rec : WalRecord) -> Unit {
self.pending.push(encode_frame(rec))
}
///|
fn[S : Storage] Db::maybe_flush(self : Db[S]) -> Unit raise LsmError {
if self.mem.is_full() {
self.flush()
}
}
///|
/// 把 memtable 刷成一个 SST,并推进版本。
///
/// 这是整个引擎里**唯一会改 MANIFEST 的地方**,也是唯一依赖
/// 文件系统 rename 原子性的地方。顺序一步都不能换:
///
/// ```
/// 1. 写 sst/.sst → fsync (数据先变成持久事实)
/// 2. 建 wal/.wal 空文件 → fsync (新 WAL 必须存在才能被 MANIFEST 引用)
/// 3. 更新 version(L0 加一项、log_number=m、last_seq)
/// 4. persist_version() (写 MANIFEST + 原子切换 CURRENT)
/// 5. 删旧 WAL (CURRENT 已经不再指向它)
/// 6. 清 memtable / pending
/// ```
///
/// 崩溃点分析:
/// - 1 之前/之中崩:MANIFEST 仍指旧 WAL,旧 WAL 里有全部记录 → 重放能恢复;
/// 半截的 SST 是孤儿,没人引用
/// - 1 之后、4 之前崩:同上,SST 是孤儿
/// - 4 之后崩:CURRENT 指向新 MANIFEST,它引用的 SST 已 fsync、
/// 新 WAL 也已存在 → 恢复成功;旧 WAL 是垃圾,下次启动会被清掉
/// - 5 之后崩:与 4 之后等价
///
/// **memtable 只能在 4 成功之后清空。** 提前清空的话,
/// 「SST 写好了但 MANIFEST 没写」这种状态下崩溃,重启后 SST 无人引用,
/// 数据就真丢了。
pub fn[S : Storage] Db::flush(self : Db[S]) -> Unit raise LsmError {
self.ensure_open()
let entries = self.mem.to_entries()
if entries.length() == 0 {
return
}
let w = SstWriter::new()
for e in entries {
w.add(e)
}
let raw = w.finish()
// 读回校验是廉价的:`open_sst` 会校验 magic / version / 区域边界,
// 能在几微秒内发现「写出来的东西自己都打不开」这种灾难性 bug。
let reader = match open_sst(raw) {
Some(r) => r
None =>
raise LsmError::corruption(
"flush: 刚写出的 SST 无法打开(写读不一致)",
)
}
// 1. SST 先落盘
let sst_num = self.version.alloc_file_number()
let sp = sst_path(sst_num)
Storage::write_file(self.storage, sp, raw)
Storage::sync_file(self.storage, sp)
// 2. 新 WAL 空文件先建好并落盘。必须在写 MANIFEST **之前** ——
// 否则 MANIFEST 会引用一个还不存在的文件,崩溃后恢复直接失败。
let new_log = self.version.alloc_file_number()
let np = wal_path(new_log)
let empty = empty_wal_bytes()
Storage::write_file(self.storage, np, empty)
Storage::sync_file(self.storage, np)
// 3. 更新内存里的版本
let old_log = self.version.log_number
self.version.last_seq = self.seq
self.version.push_to_level(
0,
SstMeta::new(
sst_num,
raw.length().to_uint64(),
entries[0].key,
entries[entries.length() - 1].key,
),
)
self.version.log_number = new_log
// 4. 落盘并原子切换。失败就抛错,此时 memtable 原封不动
self.persist_version()
// 5. 旧 WAL 可以删了:CURRENT 已经指向新 MANIFEST,它只认 wal/
Storage::remove_file(self.storage, wal_path(old_log)) catch {
Io(_) => ()
Corruption(_) => ()
Closed => ()
}
// 6. 内存状态收尾
self.ssts.insert(0, { num: sst_num, reader, })
self.mem.clear()
// `pending` 里的帧对应的记录全都在 memtable 里,也就全都进了刚落盘的
// 这个 SST。SST 已经 fsync 过,所以这些帧可以安全丢弃 —— 不丢的话,
// 下一次 sync() 会把它们追加到已经换新的 WAL 上,重放时虽然幂等,
// 但 `wal_bytes` 会凭空变大,stats 看起来像有数据没刷盘。
self.pending.clear()
self.wal_bytes = empty.length()
self.flush_count = self.flush_count + 1
// 刷完之后看 L0 是不是该归并了。放在 flush 末尾而不是单独的后台线程:
// v1 没有并发控制,后台 compaction 会和写入抢同一个 version,
// 引入的锁比它能省下的延迟更值得担心。
self.maybe_compact()
}
///|
/// 读。返回 `None` 表示「不存在」或「已删除」——
/// 对使用者来说这两种没有区别,墓碑是实现细节。
pub fn[S] Db::get(self : Db[S], key : Bytes) -> Bytes? {
match self.mem.get(key) {
Some(e) => return if e.deleted { None } else { Some(e.value) }
None => ()
}
// 哈希只算一次,所有 SST 复用。100 万条数据在 L2 上约 77 个文件,
// 让每个文件各算一遍 FNV 是 77 倍的无谓开销。
let (h1, h2) = bloom_hashes(key)
for s in self.ssts {
match s.reader.get_hashed(key, h1, h2) {
Some(e) => return if e.deleted { None } else { Some(e.value) }
None => ()
}
}
None
}
///|
/// 范围扫描。`start` 闭、`end` 开(`[start, end)`),最多 `limit` 条。
///
/// 用左闭右开而不是双闭:双闭无法表达「扫到最大 key 之后」,
/// 调用方得造一个「比所有 key 都大」的哨兵值,而 key 是任意二进制,
/// 没有这样的哨兵。
///
/// **来源是惰性推进的**(ADR-0011):memtable 从 `lower_bound(start)` 起走,
/// 每个 SST 开一个块级游标。`limit` 一到就停,没被用到的块根本没被解出来。
/// 之前这里是「先把每个来源的整个区间读成数组,再归并」,于是
/// 「要 10 条」和「要 10 万条」付同样的解码成本。
pub fn[S] Db::scan(
self : Db[S],
start : Bytes,
end : Bytes,
limit : Int32,
) -> Array[KvPair] raise LsmError {
let sources : Array[Source] = []
// memtable 已经是有序数组,只需把起点挪到第一个 >= start 的位置。
sources.push(FromArray(ArrayCursor::new_from(self.mem.to_entries(), start)))
for s in self.ssts {
// 文件级剪枝要在**进归并器之前**做,不能指望游标自己发现「我不相交」:
// 归并器每取一行都要遍历所有来源,一个永远返回 `None` 的来源
// 也照样要付一次 `peek` 的钱。55 个文件里只有 1 个相交时,
// 另外 54 个就是纯粹的乘数。
if s.reader.overlaps(start, end) {
sources.push(FromSst(s.reader.cursor(start, end)))
}
}
let it = MergeIterator::from_sources(sources)
let out : Array[KvPair] = []
while out.length() < limit {
match it.next() {
Some(e) => {
// 墓碑必须在这里被吃掉,不能出现在结果里
if e.deleted {
continue
}
if compare_bytes(e.key, start) < 0 {
continue
}
if compare_bytes(e.key, end) >= 0 {
continue
}
out.push(e.to_pair())
}
None => break
}
}
// 有来源解码失败时结果是不完整的,不能当作「扫到了这些」返回。
// 正常情况下走不到这里:`Db::open` 已经把所有 SST 的块全解过一遍(ADR-0010),
// 而 `SstReader.data` 是打开时的内存副本,之后不会被改。
if it.failed() {
raise LsmError::corruption(
"sst: 扫描时遇到无法解码的 data block(open 时校验通过,说明文件在运行期被外部改动)",
)
}
out
}
///|
/// 关闭。v1 不保持常开句柄,所以这里只标记状态;
/// 关闭之后任何读写都抛 `Closed`,而不是静默继续用。
pub fn[S] Db::close(self : Db[S]) -> Unit {
self.closed = true
}
///|
/// 截取前缀。`n` 越界时取全长(恢复路径上宁可多留不可少留)。
fn take_prefix(b : Bytes, n : Int32) -> Bytes {
let out = Buffer::Buffer()
let end = if n > b.length() { b.length() } else { n }
out.write_bytes(b[:end])
out.to_bytes()
}
///|
///|
/// 六位零填充的编号。
///
/// 零填充是必须的:`list_files` 按**字节序**排序,不填充的话
/// `0000010.wal` 会排在 `0000009.wal` 前面,恢复时新旧顺序就反了。
/// 六位意味着上限 999999 个文件 —— 按每次 flush 一个算,
/// 单机每秒 flush 一次也要连续跑 11 天才会耗尽,够用。
fn pad_num(n : UInt64) -> String {
let digits = n.to_string()
let mut pad = ""
for _ in 0..<(6 - digits.length()) {
pad = pad + "0"
}
pad + digits
}
///|
/// `sst/000004.sst`
fn sst_path(n : UInt64) -> String {
SST_DIR + "/" + pad_num(n) + ".sst"
}
///|
/// `wal/000008.wal`
fn wal_path(n : UInt64) -> String {
WAL_DIR + "/" + pad_num(n) + ".wal"
}
///|
/// `MANIFEST-000007`
fn manifest_path(n : UInt64) -> String {
MANIFEST_PREFIX + pad_num(n)
}
///|
/// 从 `000004.sst` 这类文件名里解析出开头的数字。
///
/// 只认开头连续的十进制数字,遇到其他字符就停 —— 所以
/// `000004.sst` 得到 4,`MANIFEST-000007` 要先去掉前缀。
/// 一个数字都没有则返回 `None`(调用方应当忽略这种文件,
/// 而不是把它当成编号 0)。
fn parse_number_prefix(name : String) -> UInt64? {
let mut n = 0UL
let mut digits = 0
for c in name {
let d = c.to_int() - '0'.to_int()
if d < 0 || d > 9 {
break
}
n = n * 10UL + d.to_uint64()
digits = digits + 1
}
if digits == 0 {
None
} else {
Some(n)
}
}
///|
/// 解析 CURRENT 的内容:`"MANIFEST-000007\n"` → `"MANIFEST-000007"`。
///
/// 拒绝任何不是 `MANIFEST-<数字>` 的内容。CURRENT 是一个**约定**,
/// 不是「随便一个文件名」—— 放宽它只会让「CURRENT 被写坏」这种事故
/// 变成「打开了一个莫名其妙的文件」。
fn parse_current(text : String) -> String? {
// 去掉尾部换行(Windows 上可能是 \r\n)
let mut body = text
while body.length() > 0 && body.has_suffix("\n") {
body = body[:body.length() - 1].to_owned()
}
while body.length() > 0 && body.has_suffix("\r") {
body = body[:body.length() - 1].to_owned()
}
if !has_prefix(body, MANIFEST_PREFIX) {
return None
}
let rest = body[MANIFEST_PREFIX.length():].to_owned()
match parse_number_prefix(rest) {
Some(_) => Some(body)
None => None
}
}