feat: initialize project structure and error types

- go.mod with github.com/dailz/go-kv, Go 1.26.3, testify
- config/config.go with WalConfig, Validate() with checked arithmetic
- errors.go with sentinel errors (ErrCommitUnknown, ErrWriteStopped, etc.)
- wal/constants.go with all WAL format constants and enums
- wal/header.go with WAL File Header encode/decode (CRC32 IEEE)
- wal/record.go with Physical Record codec, block boundary, SplitIntoRecords
- wal/entry.go with WAL Entry codec (varint keys/values, OpType, ValueKind)
- wal/sequence.go with SequenceManager (atomic, CAS, overflow-safe)
- manifest/manifest.go with MANIFEST stub (Load/Save atomic)
- manifest/current.go with CURRENT file (WriteCurrent/ReadCurrent)
- Comprehensive tests for all modules
- .golangci.yml configuration
This commit is contained in:
dailz
2026-06-12 13:23:27 +08:00
parent 59de99ca0b
commit cf913b1d52
52 changed files with 4538 additions and 0 deletions
+297
View File
@@ -0,0 +1,297 @@
# WAL Section 3.2 Oracle 审核报告 — Issues 清单
> 来源:Oracle 对 `docs/design.md` Section 3.2 WAL 的架构审核
> 日期:2026-06-09
---
## Critical Issues
### C1. WAL write failure 语义过于简化:`write()` 失败后 bytes 可能已落盘
**严重程度**: Critical
**位置**: Section 3.2 "写入流程" 步骤 ⑤ 及后续错误处理段落(~line 117)
**问题描述**:
当前设计将 WAL encode/write 失败统一当作 "definitely failed" 返回普通错误。但实际上存在两种不同情况:
1. **Encode 失败**(未触及 syscall):确实是 definitely failed,可以安全返回普通错误
2. **`write()` 失败**bytes 可能已进入 OS page cache 或部分写入文件):不是 definitely failed。Recovery 后可能发现该 batch CRC 合法并被重放,导致语义矛盾——调用方收到错误认为写入失败,但数据实际被恢复
**影响**: 调用方可能基于"写入失败"做非幂等业务决策(如放弃、走替代路径),但数据实际持久化了。
**建议修复**:
拆分 WAL write failure 处理:
```text
- encode 失败(未调用 write(): 普通错误,write-stopped
- write() 失败(bytes 可能已交给 OS: ErrCommitUnknown + write-stopped
除非实现能证明零 bytes 到达文件(例如 write 返回 0 且无副作用)
```
---
### C2. Segment 边界跨 Batch 行为未定义
**严重程度**: Critical
**位置**: Section 3.2 "Block 边界处理"~line 303)与 "Recovery 扫描流程"~line 436
**问题描述**:
WAL Batch 可拆成多个 Physical Record 跨多个 Block,但未定义 WAL Batch 是否可以跨 segment 文件。Recovery 中 incomplete fragment 的处理取决于是否位于"最后一个需要恢复的 segment"~line 493):
- 如果 Batch 可以跨 segmentrecovery 必须在 segment 间携带 fragment 收集状态(CollectingFragments),这增加了恢复复杂度
- 如果 Batch 不可跨 segment:需要显式约束 segment rotation 时机
当前 recovery 流程按 segment 顺序独立扫描,未定义跨 segment fragment 收集。
**影响**: 可能导致合法的跨 segment batch 被误判为中间损坏,或需要引入复杂的跨 segment 状态管理。
**建议修复**:
在 Section 3.2 明确添加约束:
```text
WAL Batch 不得跨 segment 文件。Segment rotation 只在 WAL Batch 边界发生。
当前 segment 写入完一个完整 WAL Batch 后,如果需要轮转,在下个 Batch 写入前切换到新 segment。
```
---
### C3. 预创建 segment 可破坏尾部截断逻辑
**严重程度**: Critical
**位置**: Section 3.2 "WAL 元数据持久化协议"~line 173)与 "Physical Record 解析规则"~line 491
**问题描述**:
新 segment 创建协议(步骤 1-8)允许 `segment-N+1.wal` 在文件系统上可见(已完成 rename + directory fsync),即使它尚未承载任何 batch。如果此时进程崩溃,`segment-N.wal` 可能有 crash-torn tail。
Recovery 扫描时,因为 `segment-N+1.wal` 存在且 header 合法,`segment-N` 不再被视为"最后一个需要恢复的 segment"。按照当前的尾部/中间损坏分类规则,`segment-N` 的尾部损坏会被升级为"WAL 中间损坏"→ 报错而非截断。
**影响**: 一个本应可截断恢复的尾部 partial write 场景被错误升级为不可恢复的中间损坏,导致整个 DB 无法启动。
**建议修复**:
方案 A(推荐):禁止预创建未来 segment,直到当前 segment 在完整 batch 边界 sealed
```text
新 segment 只在当前 active segment 写完一个完整 WAL Batch 后才创建。
确保 segment-N 永远在完整 batch 边界结束,segment-N+1 的创建不先于该 sealing。
```
方案 BRecovery 能识别并忽略空 segmentstartSequence == expectedSequence 但无任何 batch):
```text
如果 segment header 合法但不含任何 complete batch,且 startSequence == expectedSequence
视为空 segment,跳过或删除,继续扫描下一个 segment。
```
---
### C4. 恢复后未 fsync 确认的 Batch 可能变成已发布
**严重程度**: Critical
**位置**: Section 3.2 "恢复完成状态"~line 575)与 "持久化策略"~line 96
**问题描述**:
Recovery 重放所有 CRC 合法、sequence 连续的 WAL batch,并在恢复完成后将其全部标记为 `published`。但其中可能包含崩溃前从未 fsync 确认(调用方未收到成功)的 batch。
场景:
1. WAL bytes 已通过 `write()` 写入 OS page cache
2. 进程崩溃(非掉电),OS 将 page cache 刷盘
3. 重启后 recovery 发现该 batch 完整、CRC 合法、sequence 连续
4. 该 batch 被重放并标记为 published
5. 但调用方从未收到成功确认
**影响**: `Always` 策略下,调用方收到的语义是 "Put 返回成功 = 已持久化"。但如果进程 crash(非掉电),未确认的写入可能变成已发布。这是一个 API 语义问题而非数据安全问题。
**建议修复**:
在 Section 3.2 "持久化策略" 或 "恢复完成状态" 中显式声明:
```text
进程崩溃(非掉电)后恢复时,OS page cache 中已写入但尚未 fsync 的完整 WAL Batch
可能被恢复并视为已发布。这不是数据丢失,而是数据可见性前移。
调用方必须理解:进程崩溃重启后,比掉电场景可能多恢复一些写入。
如果需要严格区分"调用方已确认"与"未确认但存在于 WAL",需要后续引入
durable commit marker 或 confirmed-sequence 元数据。
```
---
### C5. Go 内存模型:lock-free read 需要原子发布机制
**严重程度**: Critical
**位置**: Section 3.3 MemTable "并发策略"~line 600)与 Section 3.2 步骤 ⑥⑧(~line 109-112
**问题描述**:
Section 3.3 声明 MemTable 使用 "Mutex 写 + 无锁读"Section 3.2 步骤 ⑥ 在 fsync 前将 entry 写入 MemTablepending 状态),步骤 ⑧ 通过更新 `publishedSequence` 使 entry 对无锁读可见。
在 Go 内存模型中:
1. **Skiplist 节点发布**:Mutex 保护下的写入对未持锁的并发读者不一定可见。需要 `atomic.Pointer` 或等效发布机制确保节点对读者可见。
2. **`publishedSequence` 更新**:作为普通变量写入,无锁读者可能看到过时值或部分写入。必须是 atomic 操作。
**影响**: 在 ARM 架构(弱内存序)上可能出现读者看到 `publishedSequence` 已更新但对应 skiplist 节点尚未可见的情况,导致读到不一致数据。
**建议修复**:
在 Section 3.2 或 3.3 中明确内存序要求:
```text
1. MemTable skiplist 节点必须通过 atomic storeatomic.Pointer 或自定义 release 操作)发布,
确保无锁读者看到完整的节点内容。
2. publishedSequence 必须是 atomic 变量(atomic.Uint64),
且其 Store 必须在所有 batch entries 的 skiplist 节点都已原子发布之后执行。
这保证读者先看到节点,再通过 publishedSequence 筛选可见 entry。
3. 读者必须先 atomic Load publishedSequence,再遍历 skiplist。
```
---
## Important Issues
### I1. MemTable 写入失败(Arena 满)在 WAL 写入后未覆盖
**严重程度**: Important
**位置**: Section 3.2 "写入流程" 步骤 ⑥(~line 109
**问题描述**:
写入流程步骤 ⑤(WAL encode/write)成功后,步骤 ⑥ 写入 MemTable 可能因为 Arena 满而失败。此时 WAL bytes 已持久化(或已在 page cache),但 MemTable 中没有对应 entry。设计文档未覆盖此场景。
**建议修复**:
```text
方案 A(推荐):在 WAL write 之前保证 MemTable 有足够容量。
写入前检查 Arena 剩余空间,不足时先冻结 MemTable 并创建新 MemTable。
Arena 预留必须考虑最大可能的 batch size。
方案 BMemTable 写入失败后按 ErrCommitUnknown + write-stopped 处理。
因为 WAL bytes 可能已持久化,不能按普通错误处理。
```
---
### I2. `CURRENT` 文件权威性与实际恢复模型不一致
**严重程度**: Important
**位置**: Section 3.2 "CURRENT / MANIFEST 权威性"~line 420)与 "WAL 元数据持久化协议"~line 181
**问题描述**:
设计明确声明 `CURRENT` 只是写入侧辅助文件,recovery 权威源是 `MANIFEST + 目录扫描`。但 durable-ready 协议要求在 segment 可承载写入前更新 `CURRENT` 并 fsync(步骤 6-7)。这意味着 `CURRENT` 更新是 batch 确认成功的前提之一,但 recovery 又不依赖它。
**建议修复**:
选择一种并保持一致:
```text
方案 A(推荐):简化 durable-ready 协议,移除 CURRENT 更新作为 batch 确认前提。
Recovery 通过 MANIFEST + 目录扫描发现 segment,CURRENT 仅作为写入侧快速定位优化。
新 segment 只需 rename + WAL directory fsync 即可进入 durable-ready。
方案 B:让 CURRENT 成为 recovery 的必要组件。
这样需要处理 CURRENT 损坏/缺失的 fallback,增加恢复复杂度。不推荐。
```
---
### I3. 尾部截断后缺少持久化步骤
**严重程度**: Important
**位置**: Section 3.2 "Physical Record 解析规则" 尾部损坏处理(~line 495
**问题描述**:
Recovery 允许截断最后一个 segment 的尾部损坏。但截断操作本身(`ftruncate` + 删除后续空 segment)需要 fsync 才能在再次崩溃时保持一致性。设计文档未说明截断后的持久化步骤。
**建议修复**:
在 "恢复完成状态" 之后或 "Recovery 扫描流程" 末尾添加:
```text
截断持久化步骤:
1. ftruncate active segment 到 lastCompleteBatchEnd
2. fsync truncated segment
3. 删除 startSequence == expectedSequence 但无 complete batch 的后续空 segment
4. fsync WAL directory
5. 更新 MANIFEST 记录恢复终点
6. fsync metadata directory
以上完成后,引擎才能开始接受新写入。
```
---
### I4. Batch 校验缺少资源上限
**严重程度**: Important
**位置**: Section 3.2 "WAL Batch 校验与重放"~line 530
**问题描述**:
Recovery 校验 `entryCount``entriesSize` 和 entry 边界,但未定义任何资源上限。恶意或损坏的 WAL 可能包含极大的 `entryCount``entriesSize`,导致 recovery OOM 或无限循环。
**建议修复**:
在 Section 3.2 添加硬性限制:
```text
WAL Batch 资源上限(可配置,建议默认值):
- entryCount: 最大 10,000
- entriesSize: 最大 4MB
- 单个 keyLen: 最大 4KB(不含 value
- 单个 valLen (Inline): 最大 4KB(超过走 ValueLogPointer
- fragment buffer: 最大 entriesSize 上限
- varint: 最大 5 bytesu64 varint 上限)
Recovery 解析时,超过任何上限即视为 WAL 损坏。
写入侧也必须遵守这些限制,超出拒绝写入。
```
---
### I5. `publishedSequence` 需要明确的内存序约束
**严重程度**: Important
**位置**: Section 3.2 "可见性语义"~line 148
**问题描述**:
`publishedSequence` 作为普通变量描述其语义,但未说明其在 Go 内存模型中的操作类型。多 goroutine 并发读写需要明确的 happens-before 关系。
**建议修复**:
在 "可见性语义" 小节补充:
```text
publishedSequence 的内存序约束:
1. 类型:atomic.Uint64(或等效原子变量)
2. 写入侧:Store 只在 WAL durability 和所有 MemTable 节点原子发布都完成后执行
3. 读取侧:Load 在遍历 MemTable 前执行,获得可见性 high-water mark
4. Happens-before 关系:
WAL fsync 完成 → MemTable 节点原子发布 → publishedSequence.Store
→ 读者 publishedSequence.Load → 遍历 MemTable 筛选可见 entry
```
---
## 变更追踪
| Issue | 类型 | 优先级 | 状态 |
|-------|------|--------|------|
| C1 | 语义正确性 | Critical | Open |
| C2 | 格式完整性 | Critical | Open |
| C3 | 恢复正确性 | Critical | Open |
| C4 | API 语义 | Critical | Open |
| C5 | 内存安全 | Critical | Open |
| I1 | 错误处理完整性 | Important | Open |
| I2 | 设计一致性 | Important | Open |
| I3 | 持久化完整性 | Important | Open |
| I4 | 安全性/鲁棒性 | Important | Open |
| I5 | 内存序正确性 | Important | Open |