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:
@@ -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 可以跨 segment:recovery 必须在 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。
|
||||
```
|
||||
|
||||
方案 B:Recovery 能识别并忽略空 segment(startSequence == 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 写入 MemTable(pending 状态),步骤 ⑧ 通过更新 `publishedSequence` 使 entry 对无锁读可见。
|
||||
|
||||
在 Go 内存模型中:
|
||||
1. **Skiplist 节点发布**:Mutex 保护下的写入对未持锁的并发读者不一定可见。需要 `atomic.Pointer` 或等效发布机制确保节点对读者可见。
|
||||
2. **`publishedSequence` 更新**:作为普通变量写入,无锁读者可能看到过时值或部分写入。必须是 atomic 操作。
|
||||
|
||||
**影响**: 在 ARM 架构(弱内存序)上可能出现读者看到 `publishedSequence` 已更新但对应 skiplist 节点尚未可见的情况,导致读到不一致数据。
|
||||
|
||||
**建议修复**:
|
||||
|
||||
在 Section 3.2 或 3.3 中明确内存序要求:
|
||||
|
||||
```text
|
||||
1. MemTable skiplist 节点必须通过 atomic store(atomic.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。
|
||||
|
||||
方案 B:MemTable 写入失败后按 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 bytes(u64 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 |
|
||||
Reference in New Issue
Block a user