# Phase 1: WAL 子系统开发方案

基于 `docs/design.md` §3.2 设计文档。

## 目标

实现完整的 WAL（预写日志）子系统，使其能够支撑单 key autocommit 的写入、崩溃恢复和读可见性语义。

## 开发阶段总览

```
Phase 1A: 项目骨架 + WAL 编码格式层
Phase 1B: WAL 文件写入 + Segment 管理
Phase 1C: WAL Writer（Group Commit）
Phase 1D: WAL Recovery
Phase 1E: MemTable（SkipList + Arena）
Phase 1F: 写入路径集成（WAL → MemTable 完整流水线）
Phase 1G: 读路径 + 嵌入式 API
Phase 1H: MANIFEST + 文件管理
Phase 1I: 集成测试 + Benchmark
```

---

## Phase 1A: 项目骨架 + WAL 编码格式层

**目标**: 建立 Go 项目结构，实现 WAL 物理格式（Block / Physical Record / WAL Batch / Entry）的编码与解码。

### 任务

#### 1A-1: 项目初始化
- `go.mod` 初始化（模块名 `github.com/dailz/go-kv`）
- 目录结构：
  ```
  go-kv/
  ├── go.mod
  ├── wal/           # WAL 子系统
  │   ├── wal.go     # 公共类型、常量、配置
  │   ├── record.go  # Physical Record 编解码
  │   ├── batch.go   # WAL Batch 编解码
  │   ├── entry.go   # Entry 编解码
  │   ├── header.go  # WAL File Header 编解码
  │   └── wal_test.go
  ├── memtable/      # MemTable（Phase 1E）
  ├── config/        # 全局配置
  ├── errors.go      # 公共错误类型
  └── db.go          # DB 入口
  ```
- `.golangci.yml` 配置（参考 golang-lint skill）

#### 1A-2: 公共错误类型 (`errors.go`)
- `ErrCommitUnknown` — maybe committed 语义
- `ErrWriteStopped` — 引擎 write-stopped
- `ErrSequenceExhausted` — sequence 耗尽
- `ErrWALCorrupted` — WAL 损坏
- `ErrInvalidConfig` — 配置不合法

#### 1A-3: WAL 常量与配置 (`wal/wal.go`)
```go
const (
    WalMagic         uint32 = 0x... // 待定
    WalFormatVersion uint16 = 1
    WalFileHeaderSize       = 32
    WalBlockSize            = 32 * 1024 // 32KB
    PhysicalRecordHeaderSize = 7
    WalBatchHeaderSize      = 18
    MaxWalBatchEntryCount   = 10_000
    MaxWalBatchEntriesSize  = 4 * 1024 * 1024 // 4MB
    MaxWalKeyBytes          = 4 * 1024        // 4KB
    MaxWalInlineValueBytes  = 4 * 1024        // 4KB
    MaxWalVarintBytes       = 5
    DefaultMaxWalSegmentSize = 64 * 1024 * 1024 // 64MB
    DefaultImmutableCount   = 2
)

// Fragment types
const (
    RecInvalid uint8 = 0
    RecFull    uint8 = 1
    RecFirst   uint8 = 2
    RecMiddle  uint8 = 3
    RecLast    uint8 = 4
)

// OpType
const (
    OpInvalid uint8 = 0
    OpPut     uint8 = 1
    OpDelete  uint8 = 2
)

// ValueKind
const (
    VKNone            uint8 = 0
    VKInline          uint8 = 1
    VKValueLogPointer uint8 = 2
)
```

WAL 配置结构体：
```go
type WalConfig struct {
    MaxSegmentSize  uint64 // default 64MB
    BlockSize       uint32 // default 32KB
    SyncMode        SyncMode // Always/Periodic/Never
    PeriodicSyncMs  uint32 // Periodic 模式的 fsync 间隔
    MaxBatchEntries uint32 // default 10000
    MaxBatchSize    uint32 // default 4MB
    MaxKeyBytes     uint32 // default 4KB
    MaxInlineValue  uint32 // default 4KB
}
```

配置校验函数 — 必须在 DB 打开时验证不变量：
```text
maxWalSegmentPayload >= maxEncodedWalBatchSize + worstCasePhysicalRecordOverhead + worstCaseBlockPadding
```

#### 1A-4: WAL File Header 编解码 (`wal/header.go`)
- `WalFileHeader` 结构体：magic, formatVersion, headerSize, blockSize, segmentID, startSequence, headerCRC
- `EncodeWalHeader(h *WalFileHeader) [WalFileHeaderSize]byte`
- `DecodeWalHeader(data []byte) (*WalFileHeader, error)` — 校验 magic、formatVersion、headerSize、headerCRC
- CRC 覆盖范围：magic 到 startSequence，不包含 headerCRC 自身
- 字节序：little-endian

#### 1A-5: Physical Record 编解码 (`wal/record.go`)
- `PhysicalRecord` 结构体：CRC, Length, Type, Payload
- `EncodePhysicalRecord(recType uint8, payload []byte) []byte` — 返回编码后的 bytes
- `DecodePhysicalRecord(data []byte) (*PhysicalRecord, error)` — CRC 校验
- Block 边界处理辅助函数：
  - `PaddingNeeded(blockOffset, blockSize uint32) int` — 剩余空间 <= 7 时返回需要 padding 的字节数
  - `CanFitRecord(blockOffset, blockSize, payloadLen uint32) bool`

#### 1A-6: WAL Batch 编解码 (`wal/batch.go`)
- `WalBatch` 结构体：Flags, BaseSequence, EntryCount, EntriesSize, Entries
- `EncodeWalBatch(batch *WalBatch) ([]byte, error)` — 编码 Batch Header + Entries
- `DecodeWalBatch(data []byte) (*WalBatch, error)` — 校验 flags、entryCount、entriesSize
- Batch 分片：`SplitIntoRecords(encodedBatch []byte, blockSize uint32) [][]byte` — 将编码后的 Batch 拆分为 Physical Record payloads
- Batch 重组：`FragmentCollector` — 收集 fragments 并重组成完整 Batch

FragmentCollector 状态机：
```
Idle → 收到 Full → 重放 batch → Idle
Idle → 收到 First → CollectingFragments
CollectingFragments → 收到 Middle → 追加
CollectingFragments → 收到 Last → 重组 → 重放 → Idle
```

#### 1A-7: Entry 编解码 (`wal/entry.go`)
- `WalEntry` 结构体：OpType, ValueKind, Key, Value
- `EncodeEntry(e *WalEntry) ([]byte, error)` — 编码为 varint 长度 + bytes
- `DecodeEntry(data []byte) (*WalEntry, int, error)` — 解码，返回 entry 和 consumed bytes
- 校验规则：
  - keyLen > 0 && keyLen <= maxKeyBytes
  - Put 要求 valueKind ∈ {Inline, ValueLogPointer}
  - Put + Inline: valLen <= maxInlineValueBytes (允许 valLen = 0)
  - Put + ValueLogPointer: valLen > 0
  - Delete: valueKind == None, valLen == 0

#### 1A-8: WAL Batch 资源校验
- `ValidateBatchLimits(entries []*WalEntry) error` — 在 sequence 分配之前检查：
  - entryCount <= maxBatchEntries
  - 每个 keyLen <= maxKeyBytes
  - 每个 inline valLen <= maxInlineValueBytes
  - entries 编码后总大小 <= maxBatchSize
  - 单个 Batch 的最坏 Physical Record overhead 不超过 segment capacity

### 验收标准
- [ ] 所有编解码函数有 table-driven test
- [ ] CRC 校验正确
- [ ] Fragment 分片/重组 round-trip 正确
- [ ] 资源限制校验覆盖所有边界条件
- [ ] `go vet` / `golangci-lint` 通过

---

## Phase 1B: WAL 文件写入 + Segment 管理

**目标**: 实现 WAL segment 文件的写入、轮转和持久化协议。

### 任务

#### 1B-1: Segment 文件格式写入器 (`wal/segment_writer.go`)
- `SegmentWriter` — 封装 WAL segment 文件的追加写入
- 状态：当前 segment fd、当前 block offset、当前 segmentID、payload written bytes
- `NewSegmentWriter(dir string, segmentID uint64, startSequence uint64, cfg *WalConfig) (*SegmentWriter, error)`
  - 创建 segment-N.wal.tmp
  - 写入 WAL File Header
  - fsync
  - rename → segment-N.wal
  - fsync directory
  - 进入 durable-ready 状态
- `AppendBatch(batch *WalBatch) error` — 编码 batch → split into records → 按 block 边界写入
- `Sync() error` — fsync 当前 segment 文件
- `Close() error`
- `RemainingPayload() uint64` — 当前 segment 剩余可用 payload 空间
- `CurrentOffset() uint64` — 当前写入偏移

#### 1B-2: Block 写入缓冲 (`wal/block_writer.go`)
- 管理 32KB block 的填充和 padding
- `BlockWriter` — 封装 block 内的 Physical Record 写入
- 自动处理 block 边界：剩余 <= 7 bytes 时 padding
- 跨 block 的 batch fragment 自动拆分

#### 1B-3: Segment 轮转逻辑
- 写入 batch 前检查 `RemainingPayload()` 是否足够容纳整个 batch
- 不足时：当前 segment 完成（在 batch 边界）、创建新 segment
- 新 segment 的 durable-ready 协议：
  1. create segment-N+1.wal.tmp
  2. write WAL File Header（含 startSequence = nextExpectedSequence）
  3. fsync segment-N+1.wal.tmp
  4. rename → segment-N+1.wal
  5. fsync WAL directory
  6. segment-N+1 进入 durable-ready
- 旧的 active segment 密封

#### 1B-4: CURRENT 文件管理
- best-effort 更新 CURRENT 文件
- temp + rename 模式
- 更新失败不影响已 durable-ready 的 segment

#### 1B-5: WAL 目录管理工具
- 扫描 WAL 目录中的 segment 文件
- 按 segmentID 排序
- 解析文件名中的 segmentID
- 文件名格式：`segment-{id}.wal`

### 验收标准
- [ ] Segment 创建遵循 durable-ready 协议
- [ ] Batch 不跨 segment
- [ ] Block padding 正确
- [ ] Segment 轮转在 batch 边界发生
- [ ] 多 segment 写入后，每个 segment 的 header 可以正确解析
- [ ] 测试覆盖：正常写入、跨 block batch、segment 轮转触发

---

## Phase 1C: WAL Writer（Group Commit）

**目标**: 实现完整的 WAL 写入路径，包括 group commit、sequence 管理、fsync 策略和错误分类。

### 任务

#### 1C-1: Sequence 管理器 (`wal/sequence.go`)
- `SequenceManager` — 管理 WAL 物理 mutation sequence
- `atomic.Uint64` 存储 nextSequence、publishedSequence、durableSequence
- `AllocateBatch(count uint32) (baseSequence uint64, err error)` — checked arithmetic 检查溢出
- `Publish(sequence uint64)` — release 语义 store publishedSequence
- `MarkDurable(snapshot SegmentEndState)` — 推进 durableSequence
- `Published() uint64` — load publishedSequence
- `Durable() uint64` — load durableSequence

#### 1C-2: Commit Queue (`wal/commit_queue.go`)
- 写请求进入的队列
- 每个写请求关联一个 `*sync.Cond` 或 channel 用于等待/唤醒
- `CommitBatch` 结构体：entries、完成 channel、错误结果、baseSequence

#### 1C-3: WAL Writer 主循环 (`wal/writer.go`)
核心写入循环：
```
loop:
  1. 从 commit queue 收集一批写入
  2. 等待触发条件（500µs 或 32KB）或 queue 非空
  3. 组装 WAL Batch
  4. 校验 batch 资源限制
  5. 预留 MemTable Arena 容量
  6. 分配 sequence（baseSequence）
  7. 在私有缓冲区编码 WAL Batch
  8. 检查/触发 segment 轮转
  9. Append WAL Batch 到 segment 文件
  10. 写入 MemTable（pending/unpublished）
  11. fsync（Always 模式）
  12. 发布 publishedSequence
  13. 唤醒所有等待的调用方
```

错误分类逻辑：
- 步骤 4-7 失败（未分配 sequence）→ 普通错误，可继续
- 步骤 6 后失败（sequence 已分配）→ write-stopped
- 步骤 9 后失败（WAL write 已尝试）→ ErrCommitUnknown + write-stopped
- 步骤 11 失败（fsync）→ ErrCommitUnknown + write-stopped

#### 1C-4: Fsync 策略实现 (`wal/fsync.go`)
- `SyncMode` 类型：Always / Periodic / Never
- `Always`: 每次 batch fsync 后再 publish
- `Periodic`: 后台 goroutine 定期 fsync，write 成功即可 publish
- `Never`: 不主动 fsync
- `Periodic` 的 fsync worker：
  - 快照当前 append high-water mark: (segmentID, endOffset, endSequence)
  - fsync 成功后按连续 batch 推进 durableSequence
  - fsync 失败 → write-stopped

#### 1C-5: durableSequence 推进逻辑
- 每个 batch 记录 `(segmentID, endOffset, endSequence)`
- fsync snapshot 后只推进满足条件的最大连续 batch
- 跨 segment 推进需要 segment 已 durable-ready

#### 1C-6: Write-Stopped 状态管理
- `atomic.Bool` 存储 writeStopped
- 进入 write-stopped 后拒绝新写入
- 已存在的 MemTable / Immutable MemTable 可继续后台处理
- 提供 `IsWriteStopped() bool` 查询接口

### 验收标准
- [ ] Group commit 正确合并多个写请求
- [ ] 双触发（时间/大小）工作正常
- [ ] Sequence 分配无溢出
- [ ] Always 模式下 publish 在 fsync 之后
- [ ] 错误分类准确（普通错误 / write-stopped / ErrCommitUnknown）
- [ ] 并发写入正确（多 goroutine 同时 Put）
- [ ] Write-stopped 后新写入被拒绝

---

## Phase 1D: WAL Recovery

**目标**: 实现 WAL 崩溃恢复，包括 segment 扫描、fragment 重组、batch 校验和尾部截断。

### 任务

#### 1D-1: Segment 扫描器 (`wal/scanner.go`)
- 从 WAL 目录扫描 segment 文件
- 按 segmentID 排序
- 从 MANIFEST 指定的 recoverySegmentID 开始
- 过滤掉 segmentID < recoverySegmentID 的旧 segment
- 校验连续性：segmentID 和 startSequence 都必须连续

#### 1D-2: Physical Record 解析器 (`wal/record_parser.go`)
- Block 级别的顺序解析
- 处理 padding（全 0 校验）
- Physical Record header 解析和 CRC 校验
- 错误分类：尾部 vs 中间损坏

#### 1D-3: Fragment 重组器 (`wal/fragment_collector.go`)
- 实现 Idle / CollectingFragments 状态机
- 收集 First / Middle / Last fragments
- Buffer 大小限制（Batch Header 长度 + entriesSize 上限）
- Fragment 顺序合法性检查

#### 1D-4: Batch 校验与重放 (`wal/recovery.go`)
- Batch Header 校验：flags、entryCount、entriesSize
- Batch sequence 连续性：batch.baseSequence == expectedSequence
- Entry 逐条校验：opType、valueKind、keyLen、valLen
- 重放回调：对每个合法 entry 调用 replay 函数
- Sequence 推进：expectedSequence += entryCount

#### 1D-5: 尾部截断持久化 (`wal/truncation.go`)
- 识别最后一个完整 batch 的结束位置
- ftruncate segment 文件
- fsync 被截断的 segment
- 删除不含任何 complete batch 的后续空 segment
- fsync WAL directory
- 任一步失败 → recovery 报错

#### 1D-6: Recovery 主流程 (`wal/recovery.go`)
```
1. 读取 MANIFEST → recoverySegmentID
2. 扫描 WAL 目录 → 过滤出 recovery segments
3. 排序并校验连续性
4. 逐 segment 扫描：
   a. 校验 File Header
   b. 逐 Block 解析 Physical Records
   c. Fragment 重组 → 完整 Batch
   d. Batch 校验 → 重放
   e. 更新 expectedSequence
5. 处理尾部异常
6. 持久化截断（如需要）
7. 返回恢复结果：recoveredSequence, nextSequence, publishedSequence
```

### 验收标准
- [ ] 正常 WAL 完整恢复
- [ ] 尾部 partial write 正确截断
- [ ] 中间损坏正确报错
- [ ] 跨 segment 恢复正确
- [ ] Fragment 重组 round-trip 正确
- [ ] Segment 连续性校验
- [ ] Sequence 溢出检测
- [ ] 资源限制校验（recovery 侧）

---

## Phase 1E: MemTable（SkipList + Arena）

**目标**: 实现基于 Arena 的 SkipList，支持 pending/unpublished/aborted 状态，容量预留，和原子发布。

### 任务

#### 1E-1: Arena 分配器 (`memtable/arena.go`)
- 固定大小 Arena（默认 64MB）
- 线程安全的内存分配
- 对齐分配
- 剩余容量查询
- 支持预留（reserve）操作

#### 1E-2: SkipList (`memtable/skiplist.go`)
- 最大 20 层
- Mutex 写 + 无锁读
- `atomic.Pointer` 发布 next 指针（release 语义）
- 有序遍历（Iterator）
- key 比较（bytes comparison）

#### 1E-3: Entry 状态管理 (`memtable/entry.go`)
- Entry 结构：key、value、sequence、pending/aborted 标记
- 原子发布：`atomic.Pointer` store-release
- 可见性判断：`entry.sequence <= loadedPublishedSequence && !aborted`

#### 1E-4: MemTable (`memtable/memtable.go`)
- 封装 SkipList + Arena
- `Put(key, value, sequence) error` — 写入 pending entry
- `PublishEntries(upToSequence)` — 批量发布 pending entries
- `AbortEntries(fromSequence)` — 标记 aborted
- `Get(key, publishedSequence) (GetResult, error)` — 无锁读，只返回 sequence <= publishedSequence 且非 aborted 的 entry
- `NewIterator(publishedSequence) Iterator` — 无锁有序遍历
- `ApproximateSize() uint64` — 近似内存使用量
- `IsFull() bool`
- `Reserve(entries []ReserveEntry) (uint64, error)` — 容量预留（最坏情况计算）

#### 1E-5: 容量预留计算
- 每个 entry 的预留大小 = key bytes + value bytes + skiplist node overhead + next 指针数组（最大层高）+ arena 对齐 padding
- 批量预留必须覆盖整个 batch
- checked arithmetic 检查单个 batch 是否超过空 MemTable 容量

#### 1E-6: Immutable MemTable 管理
- Freeze 流程：当前 MemTable → Immutable
- Immutable 队列（上限 2）
- 队列满时阻塞

### 验收标准
- [ ] SkipList 正确性：插入、查找、有序遍历
- [ ] Arena 分配无泄漏
- [ ] 并发读写正确（racetest）
- [ ] pending/unpublished entry 对读不可见
- [ ] 发布后 entry 可见
- [ ] aborted entry 对读不可见
- [ ] 容量预留准确
- [ ] 内存序正确（go test -race 通过）

---

## Phase 1F: 写入路径集成

**目标**: 将 WAL Writer 和 MemTable 连通，实现完整的写入流水线。

### 任务

#### 1F-1: DB 写入 API (`db.go`)
```go
type DB struct { ... }

func Open(dir string, opts ...Option) (*DB, error)
func (db *DB) Close() error
func (db *DB) Put(key, value []byte) error
func (db *DB) Delete(key []byte) error
```

#### 1F-2: 写入路径集成
完整写入流程：
```
Put(key, value)
  → commit queue
  → group commit 组装 batch
  → 校验 batch limits
  → 预留 MemTable Arena
  → 分配 sequence
  → 私有缓冲编码
  → 检查 segment 轮转
  → WAL append
  → MemTable pending write
  → fsync（Always 模式）
  → 原子发布 MemTable entries
  → 推进 publishedSequence
  → 唤醒调用方
```

#### 1F-3: MemTable Freeze + Switch
- 写入前检查容量，不足时 freeze + switch
- Immutable 队列满时阻塞等待
- Freeze 时确保当前 MemTable 已完成所有 pending 发布

#### 1F-4: 恢复启动集成
- Open 时执行 recovery
- 恢复的 entries 写入 MemTable 并标记为 published
- 设置 nextSequence、publishedSequence

### 验收标准
- [ ] 单条 Put 写入成功
- [ ] 并发 Put 正确
- [ ] 写入后读取可见（Always 模式）
- [ ] WAL crash recovery 后数据完整
- [ ] MemTable freeze/switch 正确
- [ ] Sequence 连续无间隙
- [ ] `go test -race` 通过

---

## Phase 1G: 读路径 + 嵌入式 API

**目标**: 实现完整的读路径和嵌入式 API。

### 任务

#### 1G-1: Get API
```go
type GetResult struct {
    Value []byte
    Found bool
}

func (db *DB) Get(key []byte) (GetResult, error)
```

#### 1G-2: 读路径实现
- 读取 publishedSequence（atomic load）
- 遍历 MemTable → Immutable MemTables
- 只返回 `sequence <= publishedSequence` 且非 aborted 的 entry
- Delete (tombstone) 返回 `Found=false`

#### 1G-3: 辅助 API
```go
func (db *DB) GetDurableSequence() uint64
func (db *DB) IsWriteStopped() bool
```

### 验收标准
- [ ] Put 后 Get 返回正确值
- [ ] Delete 后 Get 返回 Found=false
- [ ] 空 value 正确区分（Found=true, Value=[]byte{}）
- [ ] 并发读写正确
- [ ] 未发布 entry 对 Get 不可见

---

## Phase 1H: MANIFEST + 文件管理

**目标**: 实现 MANIFEST 持久化和 WAL segment 生命周期管理。

### 任务

#### 1H-1: MANIFEST 格式
- 记录 recovery 起点的 recoverySegmentID
- temp + rename 原子更新
- MANIFEST 只在 checkpoint（MemTable flush）后推进

#### 1H-2: 首次创建 DB 流程
- 创建目录结构
- 创建初始 MANIFEST（recoverySegmentID=0）
- 创建初始 WAL segment

#### 1H-3: WAL Segment 生命周期
- 旧 segment 删除条件：已被 MANIFEST checkpoint 覆盖
- 删除顺序：先删除文件，再 fsync directory

### 验收标准
- [ ] 首次创建 DB 成功
- [ ] 重复打开 DB 正确恢复
- [ ] MANIFEST 原子更新
- [ ] 旧 WAL segment 正确清理

---

## Phase 1I: 集成测试 + Benchmark

**目标**: 端到端测试和性能基准。

### 任务

#### 1I-1: 集成测试
- 正常写入 + 读取 round-trip
- 并发写入 + 读取一致性
- 崩溃恢复（kill -9 模拟）
- WAL 尾部损坏恢复
- Write-stopped 后的行为
- Sequence 耗尽处理
- 配置校验拒绝非法配置
- 空 value 写入/读取
- 大量数据写入（触发 segment 轮转）

#### 1I-2: Benchmark
- 单线程 Put 吞吐
- 多线程 Put 吞吐
- 单线程 Get 延迟
- 多线程 Get 延迟
- WAL Recovery 时间
- 写入放大测量

#### 1I-3: Race Condition 测试
- `go test -race -count=100`
- 并发 Put + Get
- 并发 Put + Close

### 验收标准
- [ ] 所有集成测试通过
- [ ] Benchmark 数字可作为后续优化基线
- [ ] Race test 无 data race

---

## 依赖关系与并行度

```
1A ─────┐
        ├── 1B ─────┐
        │           ├── 1C ─────┐
        │           │           ├── 1F ── 1G ── 1I
        │           │           │
1A ── 1E ──────────┘           │
        │                       │
        ├── 1D ─────────────────┘
        │
        └── 1H ────────────────────────────── 1I
```

可并行开发的模块：
- 1A 完成后，1B/1D/1E/1H 可以并行开发
- 1B 完成后，1C 可以开始
- 1C + 1D + 1E 完成后，1F 可以集成
- 1F + 1H 完成后，1G 可以集成
- 所有完成后，1I 集成测试

## 技术要点备忘

### 内存序（最关键）
- skiplist next 指针：`atomic.Pointer` store-release
- `publishedSequence`：`atomic.Uint64` store（在所有 entry 节点发布后）
- 读者先 load publishedSequence（acquire），再遍历 skiplist

### WAL 副作用边界
- 未调用 `write()` → 普通错误
- 已调用 `write()` → ErrCommitUnknown + write-stopped
- 私有缓冲区编码，不共享 bufio.Writer

### ErrCommitUnknown 语义
- maybe committed，不是 definitely failed
- 不盲目重试
- 第一阶段为弱确认

### WAL Batch 资源前置校验
- sequence 分配之前完成所有可失败校验
- 减少 write-stopped 触发机会
