feat(wal): implement batch codec, validation, and Arena allocator

- wal/batch.go: WalBatch encode/decode with FragmentCollector state machine
- wal/validate.go: ValidateBatchLimits with checked arithmetic
- memtable/arena.go: Arena allocator with 8-byte alignment and mutex
- Comprehensive tests for all modules, all pass with -race
This commit is contained in:
dailz
2026-06-12 13:27:17 +08:00
parent cf913b1d52
commit fede839eb6
8 changed files with 1014 additions and 5 deletions
+106
View File
@@ -0,0 +1,106 @@
package wal
import (
"strings"
"testing"
"github.com/dailz/go-kv/config"
)
func makeEntry(opType uint8, valueKind uint8, keySize, valSize int) *WalEntry {
key := make([]byte, keySize)
for i := range key {
key[i] = byte('a' + i%26)
}
val := make([]byte, valSize)
for i := range val {
val[i] = byte('x')
}
return &WalEntry{OpType: opType, ValueKind: valueKind, Key: key, Value: val}
}
func TestValidateBatchLimitsPass(t *testing.T) {
cfg := config.Defaults()
entries := make([]*WalEntry, 100)
for i := range entries {
entries[i] = makeEntry(OpPut, VKInline, 10, 10)
}
if err := ValidateBatchLimits(entries, &cfg); err != nil {
t.Fatalf("expected nil error, got: %v", err)
}
}
func TestValidateEntryCountExceeded(t *testing.T) {
cfg := config.Defaults()
entries := make([]*WalEntry, 10001)
for i := range entries {
entries[i] = makeEntry(OpPut, VKInline, 10, 10)
}
err := ValidateBatchLimits(entries, &cfg)
if err == nil {
t.Fatal("expected error for entry count exceeded")
}
if !strings.Contains(err.Error(), "entry count") {
t.Fatalf("expected error containing 'entry count', got: %v", err)
}
}
func TestValidateKeyTooBig(t *testing.T) {
cfg := config.Defaults()
// 5KB key exceeds 4KB limit
entries := []*WalEntry{makeEntry(OpPut, VKInline, 5*1024, 10)}
err := ValidateBatchLimits(entries, &cfg)
if err == nil {
t.Fatal("expected error for key too big")
}
if !strings.Contains(err.Error(), "key") {
t.Fatalf("expected error containing 'key', got: %v", err)
}
}
func TestValidateValueTooBig(t *testing.T) {
cfg := config.Defaults()
// 5KB inline value exceeds 4KB limit
entries := []*WalEntry{makeEntry(OpPut, VKInline, 10, 5*1024)}
err := ValidateBatchLimits(entries, &cfg)
if err == nil {
t.Fatal("expected error for value too big")
}
if !strings.Contains(err.Error(), "value") {
t.Fatalf("expected error containing 'value', got: %v", err)
}
}
func TestValidateEmptyBatch(t *testing.T) {
cfg := config.Defaults()
err := ValidateBatchLimits(nil, &cfg)
if err == nil {
t.Fatal("expected error for empty batch")
}
if !strings.Contains(err.Error(), "entry count") && !strings.Contains(err.Error(), "> 0") {
t.Fatalf("expected error about empty batch, got: %v", err)
}
err = ValidateBatchLimits([]*WalEntry{}, &cfg)
if err == nil {
t.Fatal("expected error for empty batch")
}
}
func TestValidateTotalSizeExceeded(t *testing.T) {
cfg := config.Defaults()
// Create entries that collectively exceed MaxBatchSize (4MB).
// Each entry with 1000-byte key + 1000-byte value ≈ 2012 bytes encoded.
// 2100 entries × ~2012 ≈ ~4.2MB > 4MB
entries := make([]*WalEntry, 2100)
for i := range entries {
entries[i] = makeEntry(OpPut, VKInline, 1000, 1000)
}
err := ValidateBatchLimits(entries, &cfg)
if err == nil {
t.Fatal("expected error for total size exceeded")
}
if !strings.Contains(err.Error(), "batch size") {
t.Fatalf("expected error containing 'batch size', got: %v", err)
}
}