- wal/commit_queue.go: bounded buffered channel for write requests - wal/writer.go: single-goroutine main loop implementing 11-step write flow with group commit, sequence allocation, MemTable publish/abort, write-stopped - wal/recovery.go: BatchReplayer interface, ReplayBatch, ReplaySegmentFile, RecoverFromSegments with fragment reassembly and tail corruption handling - Comprehensive tests for all modules, all pass with -race
68 KiB
Phase 1: WAL 子系统 — Go KV 存储引擎
TL;DR
Quick Summary: 基于
docs/design.md§3.2 实现 WAL 子系统,配合 MemTable(§3.3)、读路径(§3.4)和最小 MANIFEST,构建可嵌入的单 key autocommit KV 存储引擎。Deliverables:
- WAL 写入路径(group commit、sequence 管理、segment 轮转、fsync)
- WAL 恢复路径(segment 扫描、fragment 重组、batch 校验、尾部截断)
- MemTable(Arena + SkipList + 原子发布)
- 嵌入式 API(Open/Close/Put/Delete/Get)
- MANIFEST 最小 stub + CURRENT 文件
- 完整测试套件(单元 + 集成 + 故障注入)
Estimated Effort: XL(~30-50 个任务) Parallel Execution: YES — 5 waves Critical Path: Task 1 → Task 6 → Task 10 → Task 11 → Task 13 → Task 19 → Task 20/21 → F1-F4
Context
Original Request
用户指向 docs/design.md §3.2(WAL),要求制定第一阶段开发方案。
Interview Summary
Key Discussions:
- 设计文档是唯一权威来源(1189 行,§3.2 WAL 约 730 行)
- 项目为 greenfield — 无任何 Go 源码
- Phase 1 范围:WAL + MemTable + 读路径 + MANIFEST stub + 嵌入式 API
- 不含 SSTable / Compaction / Value Log / MVCC / 网络层
Research Findings:
- 项目完全空白(仅有 README.md、.omo/、docs/)
- 无 go.mod、无依赖
- 设计文档定义了极其精确的崩溃语义和错误分类
Metis Review
Identified Gaps (addressed):
- MANIFEST 完整设计不在文档中 → Phase 1 用最小 stub(仅存 recoverySegmentID)
- MemTable 无 SSTable 可刷 → 冻结为 Immutable 后持有在内存,不刷盘;3×64MB 上限后阻塞写入
- Periodic/Never sync 策略 → Phase 1 只实现 Always
- Group commit 定时器 → time.AfterFunc + reset
- API surface → Put/Delete/Get/Open/Close + GetDurableSequence + IsWriteStopped
- Graceful shutdown → Close() 等待 pending batch、sync WAL、写 MANIFEST
- 文件锁 → Phase 1 不实现,单进程假设
- Varint → Go 标准库 encoding/binary.PutUvarint,key/value 上限 4KB 保证 varint ≤ 5 bytes
Work Objectives
Core Objective
实现一个功能完整的 WAL 子系统,支持 group commit 写入、崩溃恢复、和单 key autocommit 语义,作为后续 SSTable / MVCC 的基础。
Concrete Deliverables
wal/— WAL 编解码、segment 管理、写入器、恢复器memtable/— Arena + SkipList + 原子发布db.go— DB 入口(Open/Close/Put/Delete/Get)errors.go— 公共错误类型(ErrCommitUnknown 等)manifest/— 最小 MANIFEST stub- 测试覆盖所有关键路径
Definition of Done
go build ./...编译通过go test ./... -race全部通过,无 data race- 写入 N 条数据 → Close → Open → Get 全部正确
- WAL 尾部损坏 → 恢复成功,完整 batch 不丢失
- ErrCommitUnknown 后引擎进入 write-stopped
Must Have
- WAL 写入路径严格遵循设计文档步骤 ①-⑪
- Per-batch private encode buffer + direct write(无 bufio.Writer)
atomic.Uint64用于 publishedSequence / durableSequence / nextSequenceatomic.Pointer用于 skiplist next 指针- MemTable Arena 容量预留发生在 WAL write 之前
- Sequence 分配前完成所有可失败校验
- WAL Batch 不跨 segment
- Segment rotation 只在 batch 边界
Get返回GetResult{Found bool, Value []byte}区分 key-not-found 与 empty-value- Config 不变量在 Open 时校验(checked arithmetic)
- Recovery 严格遵循 fragment 状态机
- 错误分类严格遵循设计文档表格
Must NOT Have (Guardrails)
- ❌ SSTable / Compaction / Bloom Filter / Block Cache
- ❌ Value Log / ValueLogPointer 写入(Entry 类型定义可预留,但 Phase 1 不写入 ValueLogPointer)
- ❌ MVCC / SSI 事务
- ❌ Periodic / Never sync 策略(仅 Always)
- ❌ Range scan / Iterator API
- ❌ 网络层
- ❌
bufio.Writer在 WAL append 路径上 - ❌ MemTable 写入失败发生在 WAL write 成功之后
- ❌ CRC 校验跳过
- ❌ 共享 buffered writer 模糊 WAL 副作用边界
Verification Strategy
ZERO HUMAN INTERVENTION — ALL verification is agent-executed. No exceptions.
Test Decision
- Infrastructure exists: NO (greenfield)
- Automated tests: Tests-after(先实现后测试,每个模块实现完即写测试)
- Framework: Go 标准
testing+testify(断言库) - Agent-Executed QA: 每个任务包含
go test命令和go test -race验证
QA Policy
- 每个 task 的 QA 通过
go test和go vet执行 - Evidence: test output saved to
.omo/evidence/task-{N}-{scenario}.txt
Execution Strategy
Parallel Execution Waves
Wave 1a (Foundation — 1 task):
└── 1. Project scaffolding + go.mod + config types [quick]
Wave 1b (Codec + Types — 4 parallel + 2 independent, after Wave 1a):
├── 2. Error types + constants [quick]
├── 3. WAL File Header encode/decode [quick]
├── 4. Physical Record encode/decode + Block boundary [quick]
├── 5. WAL Entry encode/decode [quick]
├── 12. Sequence manager [quick]
└── 18. MANIFEST stub + CURRENT file [quick]
Wave 2a (Batch + Arena + Validation — 3 parallel, after Wave 1b):
├── 6. WAL Batch encode/decode + fragment collector [unspecified-high] (needs 3,4,5)
├── 7. Arena allocator [unspecified-high] (needs 1)
└── 9. WAL Batch resource validation [quick] (needs 1, 5)
Wave 2b (SkipList — after Arena):
└── 8. SkipList (mutex write + lock-free read) [deep] (needs 7)
Wave 3a (Segment writer — after Batch codec):
└── 10. Segment writer (file write + block writer) [unspecified-high] (needs 3,4,6)
Wave 3b (Segment rotation — after Segment writer):
└── 11. Segment rotation + durable-ready protocol [unspecified-high] (needs 10)
Wave 3c (MemTable — after Arena + SkipList):
└── 14. MemTable (wrap skiplist + arena + publish/abort) [unspecified-high] (needs 7,8)
Wave 3d (WAL Writer — after MemTable API available):
└── 13. Commit queue + WAL writer main loop [deep] (needs 2,9,10,11,12,14)
Wave 4a (Recovery scanner — after Segment writer):
└── 15. WAL recovery: scanner + record parser [unspecified-high] (needs 4,10)
Wave 4b (Recovery batch replay — after scanner + Batch codec):
└── 16. WAL recovery: fragment collector + batch replay [deep] (needs 6,15)
Wave 4c (Recovery tail truncation — after batch replay):
└── 17. WAL recovery: tail truncation + main flow [unspecified-high] (needs 15,16)
Wave 4d (DB integration — after all above):
└── 19. DB integration: Open/Close/Put/Delete/Get [deep] (needs 13,14,17,18)
Wave 5 (Tests + Benchmark — after DB integration):
├── 20. End-to-end + crash recovery tests [deep]
└── 21. Benchmark suite [unspecified-high]
Wave FINAL (4 parallel reviews):
├── F1. Plan compliance audit (oracle)
├── F2. Code quality review (unspecified-high)
├── F3. Real manual QA (unspecified-high)
└── F4. Scope fidelity check (deep)
Critical Path: 1 → 6 → 10 → 11 → 13 → 19 → 20/21 → F1-F4
Parallel Speedup: ~50% faster than sequential
Max Concurrent: 6 (Wave 1b)
Dependency Matrix
| Task | Depends On | Blocks | Sub-Wave |
|---|---|---|---|
| 1 | — | 2-5, 7, 9, 12, 18 | 1a |
| 2 | 1 | 13 | 1b |
| 3 | 1 | 6, 10 | 1b |
| 4 | 1 | 6, 10, 15 | 1b |
| 5 | 1 | 6, 9 | 1b |
| 6 | 3, 4, 5 | 10, 16 | 2a |
| 7 | 1 | 8, 14 | 2a |
| 8 | 7 | 14 | 2b |
| 9 | 1, 5 | 13 | 2a |
| 10 | 3, 4, 6 | 11, 15 | 3a |
| 11 | 10 | 13 | 3b |
| 12 | 1 | 13 | 1b |
| 13 | 2, 9, 10, 11, 12, 14 | 19 | 3d |
| 14 | 7, 8 | 19 | 3c |
| 15 | 4, 10 | 16 | 4a |
| 16 | 6, 15 | 17 | 4b |
| 17 | 15, 16 | 19 | 4c |
| 18 | 1 | 19 | 1b |
| 19 | 13, 14, 17, 18 | 20, 21 | 4d |
| 20 | 19 | F1-F4 | 5 |
| 21 | 19 | F1-F4 | 5 |
Agent Dispatch Summary
- 1a: 1 — T1 →
quick - 1b: 6 — T2 →
quick, T3 →quick, T4 →quick, T5 →quick, T12 →quick, T18 →quick - 2a: 3 — T6 →
unspecified-high, T7 →unspecified-high, T9 →quick - 2b: 1 — T8 →
deep - 3a: 1 — T10 →
unspecified-high - 3b: 1 — T11 →
unspecified-high - 3c: 1 — T14 →
unspecified-high - 3d: 1 — T13 →
deep - 4a: 1 — T15 →
unspecified-high - 4b: 1 — T16 →
deep - 4c: 1 — T17 →
unspecified-high - 4d: 1 — T19 →
deep - 5: 2 — T20 →
deep, T21 →unspecified-high - FINAL: 4 — F1 →
oracle, F2 →unspecified-high, F3 →unspecified-high, F4 →deep
TODOs
-
1. Project scaffolding + go.mod + config types
What to do:
- 初始化
go.mod(模块名github.com/dailz/go-kv,Go 1.22+) - 创建目录结构:
wal/、memtable/、manifest/、config/ - 创建
config/config.go:WalConfig结构体(MaxSegmentSize, BlockSize, SyncMode, MaxBatchEntries, MaxBatchSize, MaxKeyBytes, MaxInlineValue, MemTableSize, MaxImmutableCount) - 创建
config/config.go:Validate() error函数,用 checked arithmetic 校验不变量:maxWalSegmentPayload >= maxEncodedWalBatchSize + worstCasePhysicalRecordOverhead + worstCaseBlockPadding - 计算公式参照设计文档 lines 402-453:walFileHeaderSize=32, walBlockSize=32768, physicalRecordHeaderSize=7, walBatchHeaderSize=18, maxWalBatchEntriesSize=4MB
- 创建
.golangci.yml
Must NOT do:
- 不引入外部依赖(仅标准库 + testify)
- 不定义 SSTable / Compaction 相关配置
Recommended Agent Profile:
- Category:
quick - Skills: [
golang-project-layout]golang-project-layout: Go 项目结构最佳实践
- Skills Evaluated but Omitted:
golang-cli: 不是 CLI 工具
Parallelization:
- Can Run In Parallel: NO (foundation task)
- Parallel Group: Sub-Wave 1a (solo — blocks all others)
- Blocks: 2, 3, 4, 5, 7, 9, 12, 18
- Blocked By: None
References: Pattern References:
docs/design.md:286-289— 设计决策表:批量窗口、日志格式、文件管理、segment 大小docs/design.md:402-453— 配置不变量计算公式和最小 segment 大小推导
API/Type References:
docs/design.md:501-513— WAL Batch 资源上限默认值
External References:
- Go project layout: https://go.dev/doc/modules/layout
WHY Each Reference Matters:
- 设计决策表定义了所有默认值,config 结构体必须映射这些值
- 配置不变量公式是 Open 时必须校验的核心约束
Acceptance Criteria:
QA Scenarios (MANDATORY):
Scenario: Config validation accepts valid defaults Tool: Bash Preconditions: Go toolchain installed Steps: 1. cd /home/dailz/workspace/src/go-kv 2. Create a test file config/config_test.go with TestValidateDefaults 3. Test that WalConfig with default values passes Validate() Expected Result: go test ./config/... passes Evidence: .omo/evidence/task-1-config-defaults.txt Scenario: Config validation rejects invalid segment size Tool: Bash Preconditions: go.mod exists Steps: 1. Create TestValidateSegmentTooSmall in config/config_test.go 2. Set MaxSegmentSize = 1024 (far below minimum 4,195,264) 3. Verify Validate() returns error containing "segment" Expected Result: Test fails validation with descriptive error Evidence: .omo/evidence/task-1-config-invalid.txtCommit: YES (group with Task 2)
- Message:
feat: initialize project structure and error types - Files:
go.mod, config/, .golangci.yml
- 初始化
-
2. Error types + constants
What to do:
- 创建
errors.go:定义ErrCommitUnknown、ErrWriteStopped、ErrSequenceExhausted、ErrWALCorrupted、ErrInvalidConfig - 创建
wal/constants.go:所有 WAL 常量(WalMagic, WalFormatVersion, WalFileHeaderSize, WalBlockSize, PhysicalRecordHeaderSize, WalBatchHeaderSize, MaxWalBatchEntryCount, MaxWalBatchEntriesSize, MaxWalKeyBytes, MaxWalInlineValueBytes, MaxWalVarintBytes, DefaultMaxWalSegmentSize) - 创建
wal/constants.go:Fragment types (RecInvalid=0, RecFull=1, RecFirst=2, RecMiddle=3, RecLast=4) - 创建
wal/constants.go:OpType (OpInvalid=0, OpPut=1, OpDelete=2) 和 ValueKind (VKNone=0, VKInline=1, VKValueLogPointer=2)
Must NOT do:
- 不实现具体逻辑,仅类型和常量定义
Recommended Agent Profile:
- Category:
quick - Skills: []
- Skills Evaluated but Omitted:
golang-naming: 常量命名简单,不需要完整命名 skill
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Sub-Wave 1b (with Tasks 3, 4, 5, 12, 18)
- Blocks: 13 (WAL writer references error types)
- Blocked By: 1 (needs go.mod)
References: Pattern References:
docs/design.md:129-135— 错误分类表(5 种失败场景 + ErrCommitUnknown)docs/design.md:155— ErrCommitUnknown 的 maybe committed 语义docs/design.md:496-499— sequence exhausted 终态
API/Type References:
docs/design.md:501-513— 所有资源上限默认值docs/design.md:364-373— Fragment type 枚举docs/design.md:571-577— OpType 枚举docs/design.md:580-584— ValueKind 枚举
WHY Each Reference Matters:
- 错误类型直接映射设计文档的错误分类表
- 常量值必须与设计文档精确匹配(block size=32KB, header=32 bytes 等)
Acceptance Criteria:
QA Scenarios (MANDATORY):
Scenario: Error types are distinguishable via errors.Is Tool: Bash Preconditions: Task 1 complete Steps: 1. Create errors_test.go with TestErrorTypes 2. Verify errors.Is(ErrCommitUnknown, ErrCommitUnknown) == true 3. Verify errors.Is(ErrWriteStopped, ErrCommitUnknown) == false Expected Result: All error type identity checks pass Evidence: .omo/evidence/task-2-error-types.txt Scenario: Constants match design doc values Tool: Bash Preconditions: wal/constants.go exists Steps: 1. Create wal/constants_test.go with TestConstantValues 2. Assert WalBlockSize == 32*1024 3. Assert WalFileHeaderSize == 32 4. Assert MaxWalBatchEntriesSize == 4*1024*1024 5. Assert DefaultMaxWalSegmentSize == 64*1024*1024 Expected Result: All assertions pass Evidence: .omo/evidence/task-2-constants.txtCommit: YES (group with Task 1)
- Message:
feat: initialize project structure and error types - Files:
errors.go, wal/constants.go
- 创建
-
3. WAL File Header encode/decode
What to do:
- 创建
wal/header.go:WalFileHeader结构体(Magic uint32, FormatVersion uint16, HeaderSize uint16, BlockSize uint32, SegmentID uint64, StartSequence uint64, HeaderCRC uint32) EncodeWalHeader(h *WalFileHeader) [WalFileHeaderSize]byte— 编码为 little-endian 32 bytes,headerCRC 覆盖 magic 到 startSequence(不包含自身)DecodeWalHeader(data []byte) (*WalFileHeader, error)— 校验 magic、formatVersion、headerSize、headerCRC- CRC 使用
hash/crc32with IEEE table(或 CRC32C 如果标准库支持) - 创建 table-driven tests
Must NOT do:
- 不使用外部 CRC 库
Recommended Agent Profile:
- Category:
quick - Skills: []
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Sub-Wave 1b (with Tasks 2, 4, 5, 12, 18)
- Blocks: 6 (batch codec), 10 (segment writer)
- Blocked By: 1
References: Pattern References:
docs/design.md:316-341— WAL File Header 格式定义和字段说明
WHY Each Reference Matters:
- Header 格式是 WAL segment 文件的基础,所有后续 segment 操作都依赖正确编码
Acceptance Criteria:
QA Scenarios (MANDATORY):
Scenario: Header encode/decode round-trip Tool: Bash Preconditions: wal/header.go exists Steps: 1. Create TestHeaderRoundtrip in wal/header_test.go 2. Encode a header with SegmentID=5, StartSequence=1000 3. Decode the encoded bytes 4. Assert all fields match original Expected Result: Round-trip preserves all fields Evidence: .omo/evidence/task-3-header-roundtrip.txt Scenario: Header CRC detects corruption Tool: Bash Steps: 1. Encode a valid header 2. Flip one byte in the magic field 3. Attempt DecodeWalHeader 4. Assert error returned containing "crc" or "checksum" Expected Result: Corrupted header rejected Evidence: .omo/evidence/task-3-header-crc.txtCommit: YES (group with Tasks 4, 5)
- Message:
feat(wal): implement WAL entry/record/header codec
- 创建
-
4. Physical Record encode/decode + Block boundary
What to do:
- 创建
wal/record.go:PhysicalRecord结构体(CRC uint32, Length uint16, Type uint8, Payload []byte) EncodePhysicalRecord(recType uint8, payload []byte) []byte— 编码为 7-byte header + payloadDecodePhysicalRecord(data []byte) (rec *PhysicalRecord, consumed int, err error)— 解析并校验 CRC- CRC 覆盖 length + type + payload(不含 CRC 自身)
- Block 边界辅助函数:
PaddingNeeded(blockOffset uint32) int— 剩余 <= 7 bytes 时返回 padding 大小CanFitRecord(blockOffset uint32, payloadLen uint32) bool
SplitIntoRecords(encodedBatch []byte) [][]byte— 将编码后的 batch 按 block 边界拆分为 Physical Record payloads- Table-driven tests 覆盖:单 record、跨 block 拆分、padding
Recommended Agent Profile:
- Category:
quick - Skills: []
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Sub-Wave 1b (with Tasks 2, 3, 5, 12, 18)
- Blocks: 6, 10, 15
- Blocked By: 1
References: Pattern References:
docs/design.md:345-381— Physical Record 格式、fragment 类型、合法组合docs/design.md:383-391— Block 边界处理规则
WHY Each Reference Matters:
- Physical Record 是 WAL I/O 的最小单元,block 边界处理是拆分/重组的关键
Acceptance Criteria:
QA Scenarios (MANDATORY):
Scenario: Physical Record round-trip Tool: Bash Steps: 1. Create TestRecordRoundtrip in wal/record_test.go 2. Encode RecFull with payload "hello world" 3. Decode the encoded bytes 4. Assert Type==RecFull, Payload=="hello world" Expected Result: Round-trip correct Evidence: .omo/evidence/task-4-record-roundtrip.txt Scenario: Batch split into fragments across blocks Tool: Bash Steps: 1. Create TestSplitIntoRecords in wal/record_test.go 2. Create a 40KB payload (larger than 32KB block) 3. Call SplitIntoRecords with blockSize=32KB 4. Assert result has First + Last (or First + Middle + Last) 5. Concatenate all payloads → equals original Expected Result: Split produces valid fragment sequence, reassembly equals original Evidence: .omo/evidence/task-4-split-records.txt Scenario: Block padding when remaining <= 7 bytes Tool: Bash Steps: 1. Create TestBlockPadding in wal/record_test.go 2. blockOffset = 32*1024 - 5 (only 5 bytes remaining) 3. Assert PaddingNeeded(blockOffset) == 5 4. Assert CanFitRecord(blockOffset, 1) == false Expected Result: Padding and fit check correct Evidence: .omo/evidence/task-4-padding.txtCommit: YES (group with Tasks 3, 5)
- 创建
-
5. WAL Entry encode/decode
What to do:
- 创建
wal/entry.go:WalEntry结构体(OpType uint8, ValueKind uint8, Key []byte, Value []byte) EncodeEntry(e *WalEntry) ([]byte, error)— 编码 opType(u8) + valueKind(u8) + keyLen(varint) + valLen(varint) + key + valueDecodeEntry(data []byte) (entry *WalEntry, consumed int, err error)— 解码返回 entry 和 consumed bytes- Varint 使用
encoding/binary.PutUvarint/binary.ReadUvarint - 校验规则(参照 design.md lines 586-594):
- keyLen > 0 && keyLen <= MaxWalKeyBytes
- Put 要求 valueKind ∈ {VKInline, VKValueLogPointer}
- Put + VKInline: valLen <= MaxWalInlineValueBytes (允许 valLen = 0)
- Put + VKValueLogPointer: valLen > 0
- Delete: valueKind == VKNone, valLen == 0
- Table-driven tests 覆盖所有合法/非法组合
Recommended Agent Profile:
- Category:
quick - Skills: []
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Sub-Wave 1b (with Tasks 2, 3, 4, 12, 18)
- Blocks: 6, 9
- Blocked By: 1
References: Pattern References:
docs/design.md:558-594— Entry 格式、OpType、ValueKind、合法性规则
WHY Each Reference Matters:
- Entry 是 WAL 的最小逻辑单元,编解码正确性直接影响数据完整性
Acceptance Criteria:
QA Scenarios (MANDATORY):
Scenario: Entry encode/decode round-trip for all valid types Tool: Bash Steps: 1. Create TestEntryRoundtrip in wal/entry_test.go 2. Test cases: Put+Inline("key1","val1"), Put+Inline("key2",[]byte{}), Delete("key3") 3. Encode → Decode → assert all fields match Expected Result: All round-trips correct Evidence: .omo/evidence/task-5-entry-roundtrip.txt Scenario: Entry validation rejects invalid entries Tool: Bash Steps: 1. Create TestEntryValidation in wal/entry_test.go 2. Test: OpInvalid → error 3. Test: Put+VKNone → error 4. Test: Delete+VKInline → error 5. Test: keyLen=0 → error 6. Test: keyLen > MaxWalKeyBytes → error Expected Result: All invalid entries rejected with descriptive errors Evidence: .omo/evidence/task-5-entry-validation.txtCommit: YES (group with Tasks 3, 4)
- 创建
-
6. WAL Batch encode/decode + fragment collector
What to do:
- 创建
wal/batch.go:WalBatch结构体(Flags uint16, BaseSequence uint64, EntryCount uint32, EntriesSize uint32, Entries []byte) EncodeWalBatch(baseSequence uint64, entries []*WalEntry) ([]byte, error)— 编码 Batch Header + 序列化 EntriesDecodeWalBatch(data []byte) (*WalBatch, error)— 校验 flags、entryCount(0 < N <= max)、entriesSize(与实际 bytes 匹配)FragmentCollector结构体 — 实现 Idle / CollectingFragments 状态机Reset()Append(recType uint8, payload []byte) error— 按 fragment type 追加,校验 buffer 大小上限IsComplete() bool— 是否拼出完整 batchBatchData() []byte— 返回重组后的完整 batch bytesState() FragmentState— 当前状态(Idle / CollectingFragments)
- 状态转移严格遵循 design.md lines 706-731 的状态机
- Buffer 大小限制:Batch Header 长度 + MaxWalBatchEntriesSize
- Varint 长度限制:MaxWalVarintBytes = 5
Must NOT do:
- FragmentCollector 不做 CRC 校验(由 Physical Record 层负责)
Recommended Agent Profile:
- Category:
unspecified-high - Skills: []
Parallelization:
- Can Run In Parallel: YES (with Tasks 7, 9 — all depend only on Wave 1b outputs)
- Parallel Group: Sub-Wave 2a (with Tasks 7, 9)
- Blocks: 10, 16
- Blocked By: 3, 4, 5
References: Pattern References:
docs/design.md:455-530— WAL Batch 格式、Batch Header、Entry sequence 推导docs/design.md:706-731— Fragment 状态机定义
WHY Each Reference Matters:
- Batch 是 WAL 的核心持久化单元,FragmentCollector 是 recovery 的核心组件
Acceptance Criteria:
QA Scenarios (MANDATORY):
Scenario: Batch encode/decode round-trip with multiple entries Tool: Bash Steps: 1. Create TestBatchRoundtrip in wal/batch_test.go 2. Create 10 entries: 5 Put+Inline, 3 Delete, 2 Put+Inline with empty value 3. Encode with baseSequence=42 4. Decode → assert Flags, BaseSequence, EntryCount, EntriesSize match 5. Parse all entries from Entries bytes → assert each matches original Expected Result: Full round-trip preserves all data Evidence: .omo/evidence/task-6-batch-roundtrip.txt Scenario: Fragment collector state machine Tool: Bash Steps: 1. Create TestFragmentCollector in wal/batch_test.go 2. Encode a 100KB batch, split into records (First + Middle* + Last) 3. Feed fragments to collector one by one 4. Assert state transitions: Idle → CollectingFragments → ... → Idle 5. Assert collected batch bytes equal original encoded batch Expected Result: Fragment reassembly produces original batch Evidence: .omo/evidence/task-6-fragment-collector.txt Scenario: Fragment collector rejects illegal transitions Tool: Bash Steps: 1. Feed RecMiddle to Idle state → expect error 2. Feed RecLast to Idle state → expect error 3. Feed RecFull to CollectingFragments state → expect error 4. Feed RecFirst to CollectingFragments state → expect error Expected Result: All illegal transitions rejected Evidence: .omo/evidence/task-6-fragment-illegal.txtCommit: YES (group with Task 9)
- Message:
feat(wal): implement WAL batch codec and resource validation
- 创建
-
7. Arena allocator
What to do:
- 创建
memtable/arena.go:Arena结构体 - 固定大小 byte slice(默认 64MB,由 config.MemTableSize 配置)
Allocate(size uint32) (offset uint32, err error)— 分配对齐的内存块,返回 offset;满时返回 errorGetBytes(offset uint32, size uint32) []byte— 从 offset 获取 bytes sliceRemaining() uint32— 剩余可用 bytesCapacity() uint32— 总容量Reserve(totalSize uint32) error— 检查剩余容量是否足够(不实际分配)- 对齐:8-byte 对齐分配
- 线程安全:分配使用 sync.Mutex(写路径已被 WAL writer 串行化)
- 测试:分配、对齐、满时错误、并发安全
Recommended Agent Profile:
- Category:
unspecified-high - Skills: [
golang-concurrency]golang-concurrency: Arena 需要线程安全分配
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Sub-Wave 2a (with Tasks 6, 9 — all depend only on Task 1 or Wave 1b outputs)
- Blocks: 8, 14
- Blocked By: 1
References: Pattern References:
docs/design.md:864-879— Arena 容量预留规则
WHY Each Reference Matters:
- Arena 是 MemTable 的内存基础,容量预留是 WAL 写入路径的前置约束
Acceptance Criteria:
QA Scenarios (MANDATORY):
Scenario: Arena allocate and read back Tool: Bash Steps: 1. Create TestArenaAllocate in memtable/arena_test.go 2. Create Arena with 1024 bytes 3. Allocate 100 bytes → write "test data" at offset 0 4. GetBytes(offset, 9) → assert equals "test data" 5. Allocate another 200 bytes → assert offset is 8-byte aligned Expected Result: Allocation and readback correct, alignment enforced Evidence: .omo/evidence/task-7-arena-alloc.txt Scenario: Arena full returns error Tool: Bash Steps: 1. Create Arena with 64 bytes 2. Allocate 60 bytes → success 3. Allocate 10 bytes → error (not enough remaining) Expected Result: Full arena rejects allocation Evidence: .omo/evidence/task-7-arena-full.txtCommit: YES (with Task 8)
- Message:
feat(memtable): implement Arena allocator and SkipList
- 创建
-
8. SkipList (mutex write + lock-free read)
What to do:
- 创建
memtable/skiplist.go:SkipList结构体 - 最大 20 层,p=0.25 的概率晋升
Put(key []byte, value []byte, sequence uint64, nodeData interface{}) error— Mutex 保护下的有序插入Get(key []byte) (found bool, value []byte, sequence uint64)— 无锁读NewIterator() *Iterator— 有序遍历- 原子发布:所有 next 指针使用
atomic.Pointer[skipNode],store 使用 release 语义 - 节点包含:key、value、sequence、pending 标记(用 atomic.Bool 或存入 nodeData)
- Key 比较使用 bytes.Compare
- Arena 分配:节点在 Arena 中分配(offset-based 引用,通过 Arena.GetBytes 访问)
- 测试:有序插入、查找、遍历、并发读写(
-race)
Must NOT do:
- 不使用普通指针(必须 atomic.Pointer)
- 不依赖 Mutex 向读者发布内存(必须 atomic store-release)
Recommended Agent Profile:
- Category:
deep - Skills: [
golang-concurrency]golang-concurrency: 原子操作和内存序是核心难点
Parallelization:
- Can Run In Parallel: NO (depends on Task 7)
- Parallel Group: Sub-Wave 2b (solo — after Arena complete)
- Blocks: 14
- Blocked By: 7
References: Pattern References:
docs/design.md:834-840— SkipList 设计决策(20 层、64MB Arena、Mutex 写 + 无锁读)docs/design.md:841-850— 发布与内存序约束(atomic.Pointer, release store)
WHY Each Reference Matters:
- 内存序约束是整个读路径正确性的关键,ARM 等弱内存序平台必须严格遵守
Acceptance Criteria:
QA Scenarios (MANDATORY):
Scenario: SkipList ordered insertion and retrieval Tool: Bash Steps: 1. Create TestSkipListOrdered in memtable/skiplist_test.go 2. Insert keys "c", "a", "e", "b", "d" with values "val-C" etc. 3. Get each key → assert correct value 4. Iterate → assert order is a, b, c, d, e Expected Result: Keys stored and retrieved in correct order Evidence: .omo/evidence/task-8-skiplist-ordered.txt Scenario: SkipList concurrent read/write no data race Tool: Bash Steps: 1. Create TestSkipListConcurrent in memtable/skiplist_test.go 2. Start 4 writer goroutines inserting 100 keys each 3. Start 4 reader goroutines reading random keys 4. Run with go test -race Expected Result: No data race detected Evidence: .omo/evidence/task-8-skiplist-race.txtCommit: YES (with Task 7)
- 创建
-
9. WAL Batch resource validation
What to do:
- 创建
wal/validate.go:ValidateBatchLimits(entries []*WalEntry, cfg *config.WalConfig) error - 在 sequence 分配之前检查所有可失败条件:
- entryCount > 0 && entryCount <= cfg.MaxBatchEntries
- 每个 entry 的 keyLen > 0 && keyLen <= cfg.MaxKeyBytes
- 每个 entry 如果是 Put+Inline 则 valLen <= cfg.MaxInlineValue
- 所有 entries 编码后总大小 <= cfg.MaxBatchSize
- worstCase Physical Record overhead + batch encoded size <= maxWalSegmentPayload
- checked arithmetic:计算中不溢出
- 返回的具体错误类型允许区分不同违规
Recommended Agent Profile:
- Category:
quick - Skills: []
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Sub-Wave 2a (with Tasks 6, 7 — all depend on Task 1/Wave 1b)
- Blocks: 13
- Blocked By: 1, 5
References: Pattern References:
docs/design.md:501-513— WAL Batch 资源上限docs/design.md:514— "写入侧必须在 sequence 分配、MemTable 预留和 WAL append 之前完成这些校验"
WHY Each Reference Matters:
- 这些校验必须在 sequence 分配之前完成,否则失败会触发 write-stopped
Acceptance Criteria:
QA Scenarios (MANDATORY):
Scenario: Valid batch passes validation Tool: Bash Steps: 1. Create TestValidateBatchLimits in wal/validate_test.go 2. Create 100 Put+Inline entries with 10-byte keys and 10-byte values 3. Call ValidateBatchLimits → expect nil error Expected Result: Valid batch accepted Evidence: .omo/evidence/task-9-validate-pass.txt Scenario: Batch exceeding entry count limit rejected Tool: Bash Steps: 1. Create 10,001 entries (exceeds default MaxBatchEntries=10000) 2. Call ValidateBatchLimits → expect error containing "entry count" Expected Result: Over-limit batch rejected before sequence allocation Evidence: .omo/evidence/task-9-validate-entrycount.txt Scenario: Key exceeding size limit rejected Tool: Bash Steps: 1. Create entry with keyLen = 5KB (exceeds MaxWalKeyBytes=4KB) 2. Call ValidateBatchLimits → expect error containing "key" Expected Result: Oversized key rejected Evidence: .omo/evidence/task-9-validate-keysize.txtCommit: YES (group with Task 6)
- 创建
-
10. Segment writer (file write + block writer)
What to do:
- 创建
wal/block_writer.go:BlockWriter— 管理 32KB block 的 Physical Record 写入- 内部维护当前 block 的 byte buffer
- 写入 Physical Record 时检查 block 边界
- 剩余 <= 7 bytes 时自动 padding(全 0)
- 跨 block 的 batch fragment 自动拆分
WriteRecord(recType uint8, payload []byte) errorFlush(io.Writer) error— 将当前 block 写入 fdReset()— 重置为空 block
- 创建
wal/segment_writer.go:SegmentWriter- 封装 WAL segment 文件的追加写入
NewSegmentWriter(dir string, segmentID uint64, startSequence uint64, cfg *config.WalConfig) (*SegmentWriter, error)— 创建 segment-N.wal.tmp → 写 header → fsync → rename → fsync dir → durable-readyAppendBatch(encodedBatch []byte) error— 按 block 边界写入 batch(使用 BlockWriter)Sync() error— fsync 当前文件Close() errorRemainingPayload() uint64— 当前 segment 剩余 payloadCurrentOffset() uint64SegmentID() uint64EndState() SegmentEndState— 返回 (segmentID, endOffset, endSequence)
Recommended Agent Profile:
- Category:
unspecified-high - Skills: []
Parallelization:
- Can Run In Parallel: NO (depends on 3, 4, 6)
- Parallel Group: Sub-Wave 3a (solo — after batch codec complete)
- Blocks: 11, 15
- Blocked By: 3, 4, 6
References: Pattern References:
docs/design.md:247-278— WAL 元数据持久化协议(durable-ready)docs/design.md:383-391— Block 边界处理规则
Acceptance Criteria:
QA Scenarios (MANDATORY):
Scenario: Segment creation follows durable-ready protocol Tool: Bash Steps: 1. Create TestSegmentCreation in wal/segment_writer_test.go 2. Create a temp directory, create SegmentWriter with segmentID=0 3. Verify segment-0.wal exists in directory 4. Read file → verify header is correct (magic, segmentID, startSequence) 5. Verify no .tmp file remains Expected Result: Segment file created with correct header, no temp file Evidence: .omo/evidence/task-10-segment-create.txt Scenario: Batch written to segment correctly Tool: Bash Steps: 1. Create SegmentWriter in temp dir 2. Encode a batch with 5 entries 3. Call AppendBatch(encodedBatch) 4. Sync + Close 5. Read file back → verify batch can be recovered Expected Result: Written batch recoverable from segment file Evidence: .omo/evidence/task-10-segment-write.txtCommit: YES (with Task 11)
- Message:
feat(wal): implement segment writer with rotation
- 创建
-
11. Segment rotation + durable-ready protocol
What to do:
- 创建
wal/segment_manager.go:管理 segment 的轮转和生命周期 - 写入 batch 前检查
RemainingPayload()是否足够容纳整个 batch - 不足时:
- 当前 segment 完成(在 batch 边界)
- 创建新 segment:segment-N+1.wal.tmp → header → fsync → rename → fsync dir
- 新 segment 的 startSequence = 下一个 expected sequence
- 切换 active segment
- 不允许 batch 跨 segment
- CURRENT 文件 best-effort 更新(temp + rename)
- CURRENT 更新失败不影响已 durable-ready 的 segment
Recommended Agent Profile:
- Category:
unspecified-high - Skills: []
Parallelization:
- Can Run In Parallel: NO (depends on Task 10)
- Parallel Group: Sub-Wave 3b (solo — after segment writer)
- Blocks: 13
- Blocked By: 10
References: Pattern References:
docs/design.md:392-453— Segment Rotation 约束docs/design.md:247-278— WAL 元数据持久化协议
Acceptance Criteria:
QA Scenarios (MANDATORY):
Scenario: Segment rotation triggers when payload exceeds remaining space Tool: Bash Steps: 1. Create a test WalConfig with: MaxBatchSize=256, MaxBatchEntries=10, BlockSize=256, MaxSegmentSize=4096 (satisfies invariant: 4096 ≥ maxEncodedWalBatchSize since MaxBatchSize=256 → maxEncoded ≈ 256 + 4*10 + overhead < 512) 2. Create SegmentManager with this config 3. Write small batches until segment-0 is nearly full (~3800 bytes written) 4. Write one more batch that would exceed remaining → triggers rotation 5. Verify segment-1.wal exists with correct header 6. Verify batch was written to segment-1, not segment-0 Expected Result: Rotation creates new segment, batch not split across segments Failure Indicators: segment-1.wal does not exist, or batch data spans both segment files Evidence: .omo/evidence/task-11-rotation.txt Scenario: CURRENT file updated after rotation Tool: Bash Steps: 1. Trigger rotation using same test config as above 2. Read CURRENT file → verify it points to latest segment filename Expected Result: CURRENT reflects latest active segment filename Failure Indicators: CURRENT still points to segment-0, or file content is stale Evidence: .omo/evidence/task-11-current.txtCommit: YES (with Task 10)
- 创建
-
12. Sequence manager
What to do:
- 创建
wal/sequence.go:SequenceManager nextSequence atomic.Uint64— 下一个待分配 sequencepublishedSequence atomic.Uint64— 已发布的连续 high-water markdurableSequence atomic.Uint64— 已满足 durable 条件的 high-water markAllocateBatch(count uint32) (baseSequence uint64, err error)— checked arithmetic:baseSequence + count - 1不溢出 uint64。溢出 → 返回 ErrSequenceExhaustedPublish(seq uint64)— Store publishedSequence(在所有 entry 节点发布后调用)MarkDurable(endState SegmentEndState)— 推进 durableSequencePublished() uint64— Load publishedSequenceDurable() uint64— Load durableSequenceNextSequence() uint64— Load nextSequence- 恢复后初始化:
NewSequenceManager(recoveredSequence uint64)
Recommended Agent Profile:
- Category:
quick - Skills: []
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 1b (with Tasks 2, 3, 4, 5, 18 — all depend only on Task 1)
- Blocks: 13
- Blocked By: 1
References: Pattern References:
docs/design.md:486-499— Sequence 运算和溢出检查docs/design.md:205-223— durableSequence 推进逻辑
Acceptance Criteria:
QA Scenarios (MANDATORY):
Scenario: Sequence allocation returns consecutive bases Tool: Bash Steps: 1. Create TestSequenceAllocation in wal/sequence_test.go 2. NewSequenceManager(0) 3. AllocateBatch(5) → base=0 4. AllocateBatch(3) → base=5 5. AllocateBatch(1) → base=8 Expected Result: Consecutive, no gaps Evidence: .omo/evidence/task-12-seq-alloc.txt Scenario: Sequence overflow returns ErrSequenceExhausted Tool: Bash Steps: 1. NewSequenceManager(math.MaxUint64 - 2) 2. AllocateBatch(5) → err (would overflow) Expected Result: ErrSequenceExhausted returned Evidence: .omo/evidence/task-12-seq-overflow.txtCommit: YES (with Task 13)
- 创建
-
13. Commit queue + WAL writer main loop
What to do:
- 创建
wal/commit_queue.go:CommitQueue— 写请求队列- 每个 request 包含 entries、result channel
Submit(entries []*WalEntry) chan WriteResult— 非阻塞提交Collect(timeout time.Duration) []*CommitRequest— 收集当前队列中的请求
- 创建
wal/writer.go:WalWriter— 核心写入循环- 单 goroutine 运行 main loop
- Group commit:收集 queue 中的请求 → 组装 batch → 触发条件(500µs 或 32KB)
- 严格遵循 design.md steps ①-⑪:
- 从 commit queue 收集写入
- 组装 WAL Batch
- ValidateBatchLimits → 失败则普通错误(不分配 sequence)
- 计算 MemTable Arena 预留量 → 不足则 freeze/switch 或等待
- AllocateBatch → 分配 baseSequence(此步之后任何失败 → write-stopped)
- 私有缓冲编码 WAL Batch
- 检查 segment rotation
- segmentWriter.AppendBatch(encoded) → 失败 → ErrCommitUnknown + write-stopped
- 写入 MemTable pending entries
- segmentWriter.Sync() (Always 模式) → 失败 → ErrCommitUnknown + write-stopped
- 原子发布 MemTable entries (atomic Pointer store-release)
- Publish(baseSequence + entryCount - 1)
- 唤醒所有等待的调用方
writeStopped atomic.Bool— write-stopped 状态Put(key, value []byte) error— 提交到 commit queue 并等待结果Delete(key []byte) error— 同上Close() error— 等待 pending batch 完成
Must NOT do:
- 不使用 bufio.Writer(直接 os.File.Write)
- 不在 sequence 分配后做可失败的校验
Recommended Agent Profile:
- Category:
deep - Skills: [
golang-concurrency]golang-concurrency: commit queue、group commit、goroutine 生命周期管理
Parallelization:
- Can Run In Parallel: NO (depends on MemTable API from Task 14)
- Parallel Group: Sub-Wave 3d (solo — after MemTable complete)
- Blocks: 19
- Blocked By: 2, 9, 10, 11, 12, 14
References: Pattern References:
docs/design.md:103-115— 写入流程 11 步docs/design.md:119— 资源前置校验约束docs/design.md:127-153— ErrCommitUnknown 和 WAL 副作用边界docs/design.md:139-153— per-batch private encode buffer
WHY Each Reference Matters:
- 这是整个 WAL 子系统最复杂的组件,必须严格遵循设计文档的步骤顺序和错误分类
Acceptance Criteria:
QA Scenarios (MANDATORY):
Scenario: Single Put writes and syncs correctly Tool: Bash Steps: 1. Create TestWalWriterSinglePut in wal/writer_test.go 2. Start WalWriter in temp dir 3. Put("key1", "value1") 4. Close writer 5. Read WAL segment → verify batch with 1 entry exists Expected Result: Entry persisted in WAL Evidence: .omo/evidence/task-13-writer-single.txt Scenario: Multiple concurrent writes group-committed Tool: Bash Steps: 1. Create TestWalWriterGroupCommit in wal/writer_test.go 2. Start WalWriter 3. Launch 10 goroutines each doing Put 4. Wait for all to complete 5. Close writer 6. Read WAL → verify entries batched together Expected Result: Entries grouped in batches, all sequences assigned Evidence: .omo/evidence/task-13-writer-group.txt Scenario: Write-stopped after simulated WAL write failure Tool: Bash Steps: 1. Create a WalWriter with a mock segment writer that fails on 3rd AppendBatch 2. Write 5 entries sequentially 3. 3rd write should return ErrCommitUnknown 4. 4th write should be rejected (write-stopped) Expected Result: ErrCommitUnknown returned, subsequent writes rejected Evidence: .omo/evidence/task-13-writer-stopped.txtCommit: YES (with Task 12)
- Message:
feat(wal): implement WAL writer with group commit
- 创建
-
14. MemTable (wrap skiplist + arena + publish/abort)
What to do:
- 创建
memtable/memtable.go:MemTable结构体 - 封装 Arena + SkipList
Reserve(entries []ReserveEntry) (totalSize uint32, err error)— 容量预留(最坏情况:最大层高 next 指针数组 + 对齐 padding)。单个 batch 超过空 MemTable → 返回错误PutPending(key []byte, value []byte, sequence uint64) error— 写入 pending entry(Arena 已预留)DeletePending(key []byte, sequence uint64) error— 写入 pending tombstonePublish(upToSequence uint64)— 批量发布 sequence <= upToSequence 的 pending entries(atomic store-release)Abort(fromSequence uint64)— 标记 abortedGet(key []byte, publishedSequence uint64) *GetResult— 无锁读:先 Load publishedSequence,遍历 skiplist,只返回 sequence <= publishedSequence 且非 aborted 的 entryNewIterator(publishedSequence uint64) *MemTableIterator— 有序遍历(同样过滤 pending/aborted)ApproximateSize() uint64UsableCapacity() uint32— 可用容量(扣除固定元数据)
Must NOT do:
- 不允许 PutPending 在 Reserve 成功后因 Arena 满失败
Recommended Agent Profile:
- Category:
unspecified-high - Skills: [
golang-concurrency]golang-concurrency: 原子发布、无锁读、内存序
Parallelization:
- Can Run In Parallel: NO (solo in sub-wave, Task 13 depends on this)
- Parallel Group: Sub-Wave 3c (solo)
- Blocks: 13, 19
- Blocked By: 7, 8
References: Pattern References:
docs/design.md:864-879— Arena 容量预留协议docs/design.md:881-889— Flush 过滤规则(pending/aborted 不进入 SSTable)docs/design.md:841-850— 发布与内存序
Acceptance Criteria:
QA Scenarios (MANDATORY):
Scenario: Pending entry invisible to readers Tool: Bash Steps: 1. Create TestMemTablePending in memtable/memtable_test.go 2. Create MemTable, write PutPending("key1","val1", seq=10) 3. Get("key1", publishedSequence=5) → Found=false 4. Publish(upToSequence=10) 5. Get("key1", publishedSequence=10) → Found=true, Value="val1" Expected Result: Entry invisible before publish, visible after Evidence: .omo/evidence/task-14-memtable-pending.txt Scenario: Aborted entry invisible to readers Tool: Bash Steps: 1. PutPending("key2","val2", seq=20) 2. Abort(20) 3. Publish(upToSequence=30) — publish others but not aborted 4. Get("key2", publishedSequence=30) → Found=false Expected Result: Aborted entry never visible Evidence: .omo/evidence/task-14-memtable-aborted.txt Scenario: Reserve accuracy Tool: Bash Steps: 1. Create MemTable with 1024 bytes usable 2. Reserve 10 entries with 10-byte keys + 10-byte values 3. Assert reserved size includes node overhead + alignment 4. Attempt to reserve more than remaining → error Expected Result: Reserve accounts for full overhead Evidence: .omo/evidence/task-14-memtable-reserve.txtCommit: YES
- Message:
feat(memtable): integrate arena+skiplist with publish/abort
- 创建
-
15. WAL recovery: scanner + record parser
What to do:
- 创建
wal/scanner.go:SegmentScanner— 从 WAL 目录扫描 segment 文件ScanSegments(dir string, recoverySegmentID uint64) ([]*SegmentInfo, error)— 按 segmentID 排序,过滤 < recoverySegmentIDSegmentInfo结构体:FilePath, SegmentID, StartSequence(从 header 解析)- 连续性校验:segmentID 连续、startSequence 衔接
- 创建
wal/record_parser.go:RecordParser— Block 级别 Physical Record 解析ParseBlock(data []byte) ([]*PhysicalRecord, error)— 解析 block 内所有 records- Padding 校验:剩余 < 7 bytes 时全 0
- Short block 处理:最后 segment 的最后 block 可以短于 32KB
- 错误分类辅助:
IsTailCorruption(err error) bool
Recommended Agent Profile:
- Category:
unspecified-high - Skills: []
Parallelization:
- Can Run In Parallel: NO (depends on 4, 10)
- Parallel Group: Sub-Wave 4a (solo)
- Blocks: 16
- Blocked By: 4, 10
References: Pattern References:
docs/design.md:597-665— Recovery 扫描流程、segment 连续性校验docs/design.md:669-691— Physical Record 解析规则
Acceptance Criteria:
QA Scenarios (MANDATORY):
Scenario: Segment scanner finds and orders segments correctly Tool: Bash Steps: 1. Create temp dir with segment-0.wal, segment-2.wal, segment-1.wal 2. ScanSegments(dir, 0) → returns 3 segments ordered by ID 3. ScanSegments(dir, 1) → returns segments 1,2 only Expected Result: Correct ordering and filtering Evidence: .omo/evidence/task-15-scanner.txt Scenario: Record parser handles valid blocks Tool: Bash Steps: 1. Create a 32KB block with 3 Physical Records 2. ParseBlock → verify 3 records parsed with correct payloads Expected Result: All records extracted Evidence: .omo/evidence/task-15-parser.txtCommit: YES (with Tasks 16, 17)
- 创建
-
16. WAL recovery: fragment collector + batch replay
What to do:
- 创建
wal/recovery.go:恢复核心逻辑 - 使用 Task 6 的
FragmentCollector进行 fragment 重组 BatchReplayer接口:ReplayPut(key, value []byte, sequence uint64)/ReplayDelete(key []byte, sequence uint64)ReplayBatch(batch *WalBatch, expectedSequence uint64, replayer BatchReplayer) (nextSequence uint64, err error)- 校验 Batch Header:flags、entryCount (0 < N <= max)、entriesSize、baseSequence == expectedSequence
- 逐条解析 Entry,校验 opType、valueKind、keyLen、valLen
- 调用 replayer 回调
- 返回 nextSequence = expectedSequence + entryCount
- Sequence 溢出检查:
baseSequence + entryCount - 1不溢出
Recommended Agent Profile:
- Category:
deep - Skills: []
Parallelization:
- Can Run In Parallel: NO (depends on 6, 15)
- Parallel Group: Sub-Wave 4b (solo — after scanner)
- Blocks: 17
- Blocked By: 6, 15
References: Pattern References:
docs/design.md:733-781— WAL Batch 校验与重放规则docs/design.md:486-499— Sequence 溢出检查
Acceptance Criteria:
QA Scenarios (MANDATORY):
Scenario: Valid batch replayed correctly Tool: Bash Steps: 1. Create TestBatchReplay in wal/recovery_test.go 2. Encode a batch: baseSeq=0, 3 entries (Put a, Put b, Delete c) 3. ReplayBatch with expectedSequence=0 and mock replayer 4. Assert replayer received 3 calls in order with correct args Expected Result: All entries replayed with correct sequences Evidence: .omo/evidence/task-16-batch-replay.txt Scenario: Batch with wrong baseSequence rejected Tool: Bash Steps: 1. Encode batch with baseSeq=100 2. ReplayBatch with expectedSequence=50 → error Expected Result: Sequence mismatch error Evidence: .omo/evidence/task-16-seq-mismatch.txtCommit: YES (with Tasks 15, 17)
- 创建
-
17. WAL recovery: tail truncation + main flow
What to do:
- 创建
wal/truncation.go:尾部截断持久化TruncateTail(segmentPath string, truncateOffset int64) error— ftruncate + fsyncDeleteEmptySegments(dir string, fromSegmentID uint64, expectedSequence uint64) error— 删除空 segment + fsync dir
- 复用 Task 16 定义的
BatchReplayer接口和ReplayBatch函数(同在wal/recovery.go) Recover(dir string, manifest *manifest.Manifest, cfg *config.WalConfig, replayer BatchReplayer) (*RecoveryResult, error)BatchReplayer由 Task 16 定义(interface{ ReplayPut/ReplayDelete }),Task 19 传入 MemTable 适配实现- 主流程严格遵循 design.md lines 622-636:
- 读取 MANIFEST → recoverySegmentID
- ScanSegments → 过滤
- 校验连续性
- 逐 segment:校验 header → 逐 block 解析 → fragment 重组 → 调用 Task 16 的
ReplayBatch(batch, expectedSeq, replayer)重放 - 处理尾部异常(最后 segment → truncation,中间 segment → 报错)
- 持久化截断
- 返回
RecoveryResult:RecoveredSequence, NextSequence, PublishedSequence
- 错误分类:
- 尾部(最后 segment):header 半写、length 越界、CRC 失败、非 0 padding → 截断
- 中间(非最后 segment):同样情况 → 报错
- CRC-valid batch 内部错误(entryCount 越界等)→ 报错(不可截断)
Recommended Agent Profile:
- Category:
unspecified-high - Skills: []
Parallelization:
- Can Run In Parallel: NO (depends on 15, 16)
- Parallel Group: Sub-Wave 4c (solo — after batch replay)
- Blocks: 19
- Blocked By: 15, 16
References: Pattern References:
docs/design.md:622-636— Recovery 扫描流程docs/design.md:640-665— Segment 连续性校验docs/design.md:783-800— 尾部截断持久化docs/design.md:694-703— 错误分类(尾部 vs 中间)
Acceptance Criteria:
QA Scenarios (MANDATORY):
Scenario: Normal recovery from clean WAL Tool: Bash Steps: 1. Create temp dir, write 2 segments with valid batches 2. Recover(dir, manifest) → success 3. Assert RecoveredSequence == last batch end sequence 4. Assert all entries replayed via mock replayer Expected Result: Full recovery, no data loss Evidence: .omo/evidence/task-17-recovery-normal.txt Scenario: Recovery with tail corruption truncates Tool: Bash Steps: 1. Write valid segment + partial write at end (corrupt last record) 2. Recover → success (truncated to last complete batch) 3. Verify segment file was truncated Expected Result: Tail corruption handled, complete batches preserved Evidence: .omo/evidence/task-17-recovery-tail.txt Scenario: Recovery with middle corruption reports error Tool: Bash Steps: 1. Write 3 segments, corrupt the middle one (bad CRC) 2. Recover → error containing "corrupt" or "damaged" Expected Result: Middle corruption detected, recovery fails Evidence: .omo/evidence/task-17-recovery-middle.txtCommit: YES (with Tasks 15, 16)
- 创建
-
18. MANIFEST stub + CURRENT file
What to do:
- 创建
manifest/manifest.go:Manifest最小 stub- 仅存储
recoverySegmentID uint64 Load(dir string) (*Manifest, error)— 读取 MANIFEST 文件Save(dir string, recoverySegmentID uint64) error— temp + rename 原子写入- 首次创建:如果 MANIFEST 不存在,创建初始文件(recoverySegmentID=0)
- 仅存储
- 创建
manifest/current.go:CURRENT 文件WriteCurrent(dir string, segmentID uint64) error— temp + rename,best-effortReadCurrent(dir string) (segmentID uint64, ok bool)— 读取,不存在或损坏返回 ok=false
- 格式:简单的文本文件或二进制(Phase 1 选择简单文本)
Recommended Agent Profile:
- Category:
quick - Skills: []
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 1b (with Tasks 2, 3, 4, 5, 12 — all depend only on Task 1)
- Blocks: 19
- Blocked By: 1
References: Pattern References:
docs/design.md:603-619— CURRENT / MANIFEST 权威性
Acceptance Criteria:
QA Scenarios (MANDATORY):
Scenario: MANIFEST save and load round-trip Tool: Bash Steps: 1. Save(dir, 42) 2. Load(dir) → assert recoverySegmentID == 42 Expected Result: Round-trip correct Evidence: .omo/evidence/task-18-manifest.txt Scenario: CURRENT best-effort handles missing file Tool: Bash Steps: 1. ReadCurrent on empty dir → ok=false, no error Expected Result: Missing CURRENT handled gracefully Evidence: .omo/evidence/task-18-current-missing.txtCommit: YES
- Message:
feat: implement MANIFEST stub and CURRENT file
- 创建
-
19. DB integration: Open/Close/Put/Delete/Get
What to do:
- 创建
db.go:DB结构体和核心 API DB字段:walWriter, memTable, immutableTables, seqMgr, manifest, config, dir, writeStoppedOpen(dir string, opts ...Option) (*DB, error):- Load or create MANIFEST
- Recover WAL → 重建 MemTable(恢复的 entries 标记为 published)
- 设置 nextSequence、publishedSequence
- 启动 WAL writer goroutine
Close() error:- 停止 WAL writer(等待 pending batch 完成)
- Sync WAL
- Save MANIFEST(recoverySegmentID stays 0 in Phase 1 — no SSTable flush means all segments must be replayed)
- Close segment writer
Put(key, value []byte) error:- ValidateBatchLimits (key/value size)
- Submit to WAL writer → wait for result
- 返回 nil 或 ErrCommitUnknown 或 write-stopped error
Delete(key []byte) error— 同上,opType=DeleteGet(key []byte) (GetResult, error):- Load publishedSequence
- 查 active MemTable → Immutable #1 → Immutable #2
- 只返回 sequence <= publishedSequence 且非 aborted
- Tombstone → Found=false
- 未找到 → Found=false
GetResult结构体:Value []byte, Found bool(参照 design.md §3.4)GetDurableSequence() uint64IsWriteStopped() bool- MemTable freeze + switch:
- Put 前检查容量,不足时 freeze
- Immutable 队列满时阻塞
- Phase 1:Immutable 不 flush 到 SSTable,仅持有在内存
- Options pattern:
type Option func(*config.WalConfig)
Recommended Agent Profile:
- Category:
deep - Skills: [
golang-design-patterns]golang-design-patterns: Options pattern for configuration
Parallelization:
- Can Run In Parallel: NO (integrates all deps)
- Parallel Group: Sub-Wave 4d (solo — after recovery + WAL writer + MemTable + MANIFEST)
- Blocks: 20, 21
- Blocked By: 13, 14, 17, 18
References: Pattern References:
docs/design.md:901-921— Get API 和 GetResult 定义docs/design.md:928-941— 读路径优先级docs/design.md:852-862— MemTable 生命周期
WHY Each Reference Matters:
- 这是所有模块的集成点,必须正确协调 WAL writer、MemTable、recovery 和 MANIFEST
Acceptance Criteria:
QA Scenarios (MANDATORY):
Scenario: Open creates fresh DB, Put+Get round-trip Tool: Bash Steps: 1. Create temp dir 2. Open(dir) → success 3. Put("hello", "world") → nil 4. Get("hello") → {Found:true, Value:"world"} 5. Close() → nil Expected Result: Full round-trip works Evidence: .omo/evidence/task-19-open-put-get.txt Scenario: Close and reopen preserves data Tool: Bash Steps: 1. Open(dir), Put("k1","v1"), Put("k2","v2"), Close() 2. Open(dir) → recovery runs 3. Get("k1") → {Found:true, Value:"v1"} 4. Get("k2") → {Found:true, Value:"v2"} 5. Close() Expected Result: Data survives close/reopen Evidence: .omo/evidence/task-19-reopen.txt Scenario: Delete makes key not found Tool: Bash Steps: 1. Open, Put("k","v"), Delete("k") 2. Get("k") → {Found:false} Expected Result: Delete works correctly Evidence: .omo/evidence/task-19-delete.txt Scenario: Empty value distinguished from not found Tool: Bash Steps: 1. Put("empty", []byte{}) 2. Get("empty") → {Found:true, Value:[]byte{}} 3. Get("nonexist") → {Found:false} Expected Result: Found distinguishes empty value from missing key Evidence: .omo/evidence/task-19-empty-value.txt Scenario: Concurrent Put and Get work correctly Tool: Bash Steps: 1. Open 2. Launch 10 writers (Put "key-{i}", "val-{i}") 3. Launch 10 readers (Get "key-{i}") 4. go test -race Expected Result: No race conditions Evidence: .omo/evidence/task-19-concurrent.txtCommit: YES
- Message:
feat: integrate DB with Open/Close/Put/Delete/Get
- 创建
-
20. End-to-end + crash recovery tests
What to do:
- 创建
db_test.go:集成测试 TestWALRoundtrip— 写 N 条 → Close → Open → 验证全部TestRecoveryPartialWrite— 模拟崩溃:写入数据 → 在 WAL 写入中途截断文件 → Recover → 验证完整 batch 保留TestRecoveryMultiSegment— 多 segment 恢复TestRecoveryTailCorruption— 尾部损坏修复TestWriteStoppedAfterFailure— 模拟写入失败 → ErrCommitUnknown → write-stoppedTestGetSemantics— Found vs empty value vs not foundTestSequenceExhaustion— 虽然不可达,但验证 sequence 溢出返回正确错误TestConcurrentReadWrite— 多 goroutine 并发读写TestLargeBatchSegmentRotation— 大量写入触发多次 segment 轮转TestMemTableFreezeNoSSTable— 验证 MemTable 满时冻结,不刷盘
Recommended Agent Profile:
- Category:
deep - Skills: [
golang-testing]golang-testing: Go testing patterns
Parallelization:
- Can Run In Parallel: YES (with Task 21)
- Parallel Group: Wave 5 (with Task 21)
- Blocks: F1-F4
- Blocked By: 19
References: Pattern References:
docs/design.md:694-703— 错误分类docs/design.md:783-800— 尾部截断
Acceptance Criteria:
QA Scenarios (MANDATORY):
Scenario: Full WAL roundtrip with crash simulation Tool: Bash Steps: 1. Open DB, write 1000 keys 2. Close DB (simulate clean shutdown) 3. Open DB → verify all 1000 keys readable Expected Result: All 1000 keys recovered Evidence: .omo/evidence/task-20-roundtrip.txt Scenario: Crash recovery with tail truncation Tool: Bash Steps: 1. Open DB, write 500 keys, Close 2. Open DB, write 200 more keys 3. Manually truncate last WAL segment by 100 bytes (simulate crash) 4. Open DB → verify 500 original keys present, some of 200 may be lost Expected Result: Complete batches preserved, tail truncated Evidence: .omo/evidence/task-20-crash.txt Scenario: go test -race passes all integration tests Tool: Bash Steps: 1. go test ./... -race -count=1 Expected Result: Zero race conditions Evidence: .omo/evidence/task-20-race.txtCommit: YES
- Message:
test: add end-to-end and crash recovery tests
- 创建
-
21. Benchmark suite
What to do:
- 创建
bench_test.go:性能基准 BenchmarkSinglePut— 单线程 Put 吞吐BenchmarkConcurrentPut— 多线程 Put 吞吐(2/4/8 goroutines)BenchmarkGet— Get 延迟BenchmarkConcurrentGet— 并发 GetBenchmarkMixedReadWrite— 混合读写BenchmarkWALRecovery— 不同数据量的恢复时间- 使用
b.ReportAllocs()跟踪内存分配
Recommended Agent Profile:
- Category:
unspecified-high - Skills: [
golang-benchmark]golang-benchmark: Go benchmarking best practices
Parallelization:
- Can Run In Parallel: YES (with Task 20)
- Parallel Group: Wave 5 (with Task 20)
- Blocks: F1-F4
- Blocked By: 19
References:
- 无特定设计文档引用(benchmark 是验证手段,不是设计要求)
Acceptance Criteria:
QA Scenarios (MANDATORY):
Scenario: Benchmark runs without errors Tool: Bash Steps: 1. go test -bench=. -benchmem ./... Expected Result: All benchmarks complete, results printed Evidence: .omo/evidence/task-21-bench.txtCommit: YES
- Message:
bench: add WAL performance benchmarks
- 创建
Final Verification Wave
-
F1. Plan Compliance Audit —
oracleRead the plan end-to-end. For each "Must Have": verify implementation exists (grep for atomic.Uint64, atomic.Pointer, no bufio.Writer on WAL path, GetResult with Found field, etc). For each "Must NOT Have": search codebase for forbidden patterns (SSTable, Compaction, ValueLogPointer write, Periodic sync, bufio.Writer in wal/). Check evidence files exist in .omo/evidence/. Compare deliverables against plan. Acceptance Criteria:- All "Must Have" items verified present in codebase
- All "Must NOT Have" items verified absent
- Evidence files exist for tasks 1-21
- Output:
Must Have [N/N] | Must NOT Have [N/N] | Tasks [21/21] | VERDICT: APPROVE/REJECT
-
F2. Code Quality Review —
unspecified-highRungo vet ./...+golangci-lint run+go test ./... -race. Review all .go files for:anytype casts, empty catches, fmt.Println in prod, commented-out code, unused imports. Check AI slop: excessive comments, over-abstraction, generic names. Acceptance Criteria:go vet ./...passes with zero warningsgolangci-lint runpasses with zero warningsgo test ./... -racepasses with zero races- No
fmt.Printlnorlog.Printlnin non-test files - Output:
Build [PASS/FAIL] | Lint [PASS/FAIL] | Tests [N pass/0 fail] | Files [N clean/0 issues] | VERDICT
-
F3. Real Manual QA —
unspecified-highBuild the project. Run ALL QA scenarios from tasks 1-21. Test cross-module integration: write data with Put → Close → Open → Get verifies data. Test edge cases: empty value, large batch (9999 entries), concurrent writes from 10 goroutines, segment rotation after filling segment. Save output to.omo/evidence/final-qa/. Acceptance Criteria:- Every QA scenario from tasks 1-21 executed and passing
- Cross-module integration: Put → Close → Open → Get round-trip
- Edge case: empty value returns Found=true
- Edge case: key not found returns Found=false
- Edge case: concurrent 10-goroutine write/read with -race clean
- Output:
Scenarios [N/N pass] | Integration [3/3] | Edge Cases [4 tested] | VERDICT
-
F4. Scope Fidelity Check —
deepFor each task: read "What to do", read actual diff (git diff). Verify 1:1 — everything in spec was built (no missing), nothing beyond spec was built (no creep). Check "Must NOT do" compliance per task. Detect cross-task contamination: Task N touching Task M's files without justification. Flag unaccounted files. Acceptance Criteria:- Each task's "What to do" matches its actual implementation
- No SSTable/Compaction/ValueLog/BloomFilter code found
- No bufio.Writer import in wal/ package
- No Periodic/Never sync code (only Always)
- No cross-task file contamination
- Output:
Tasks [21/21 compliant] | Contamination [CLEAN] | Unaccounted [CLEAN] | VERDICT
Commit Strategy
| Tasks | Commit Message | Pre-commit Check |
|---|---|---|
| 1-2 | feat: initialize project structure and error types |
go build ./... |
| 3-5 | feat(wal): implement WAL entry/record/header codec |
go test ./wal/... -run TestCodec |
| 6, 9 | feat(wal): implement WAL batch codec and resource validation |
go test ./wal/... -run TestBatch |
| 7-8 | feat(memtable): implement Arena allocator and SkipList |
go test ./memtable/... -race |
| 10-11 | feat(wal): implement segment writer with rotation |
go test ./wal/... -run TestSegment |
| 12-13 | feat(wal): implement WAL writer with group commit |
go test ./wal/... -run TestWriter |
| 14 | feat(memtable): integrate arena+skiplist with publish/abort |
go test ./memtable/... -race |
| 15-17 | feat(wal): implement WAL recovery |
go test ./wal/... -run TestRecovery |
| 18 | feat: implement MANIFEST stub and CURRENT file |
go test ./manifest/... |
| 19 | feat: integrate DB with Open/Close/Put/Delete/Get |
go test ./... -race |
| 20 | test: add end-to-end and crash recovery tests |
go test ./... -race -count=1 |
| 21 | bench: add WAL performance benchmarks |
go test -bench=. ./... |
Success Criteria
Verification Commands
go build ./... # Expected: success, no errors
go vet ./... # Expected: no issues
go test ./... -race -count=1 # Expected: all pass, no races
go test ./... -run TestWALRoundtrip # Expected: write→close→open→read roundtrip
go test ./... -run TestRecovery # Expected: crash recovery correct
go test ./... -run TestGetSemantics # Expected: Found/empty-value distinction
Final Checklist
- All "Must Have" present
- All "Must NOT Have" absent
- All tests pass with
-race - WAL roundtrip test passes
- Crash recovery test passes
- No bufio.Writer on WAL append path
- All atomics are typed (atomic.Uint64, atomic.Pointer)
- Config invariant checked at Open