# 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 |
