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
+115
View File
@@ -0,0 +1,115 @@
package memtable
import (
"sync"
"testing"
"github.com/stretchr/testify/require"
)
func TestArenaAllocate(t *testing.T) {
a := NewArena(1024)
off, err := a.Allocate(9)
require.NoError(t, err)
require.Equal(t, uint32(0), off)
copy(a.GetBytes(off, 9), "test data")
require.Equal(t, []byte("test data"), a.GetBytes(off, 9))
off2, err := a.Allocate(200)
require.NoError(t, err)
// 9 bytes aligned to 16, so second allocation starts at 16
require.Equal(t, uint32(16), off2)
}
func TestArenaFull(t *testing.T) {
a := NewArena(64)
off, err := a.Allocate(60)
require.NoError(t, err)
require.Equal(t, uint32(0), off) // 60 aligned to 64
_, err = a.Allocate(10)
require.ErrorIs(t, err, ErrArenaFull)
}
func TestArenaAlignment(t *testing.T) {
a := NewArena(1024)
sizes := []uint32{1, 3, 5, 7, 8, 9, 13, 16, 100}
for _, s := range sizes {
off, err := a.Allocate(s)
require.NoError(t, err, "size=%d", s)
require.Equal(t, uint32(0), off%8, "offset %d not 8-byte aligned for size %d", off, s)
}
}
func TestArenaConcurrent(t *testing.T) {
a := NewArena(65536)
const goroutines = 64
const perGoroutine = 32
var wg sync.WaitGroup
wg.Add(goroutines)
offsets := make([][]uint32, goroutines)
for i := 0; i < goroutines; i++ {
i := i
go func() {
defer wg.Done()
locals := make([]uint32, 0, perGoroutine)
for j := 0; j < perGoroutine; j++ {
off, err := a.Allocate(8)
if err == nil {
// Write unique data to detect corruption
copy(a.GetBytes(off, 8), []byte{byte(i), byte(j), 0, 0, 0, 0, 0, 0})
locals = append(locals, off)
}
}
offsets[i] = locals
}()
}
wg.Wait()
// Verify data integrity — each byte pair must match its goroutine/j index
for i, locals := range offsets {
for j, off := range locals {
data := a.GetBytes(off, 8)
require.Equal(t, byte(i), data[0], "goroutine %d offset %d", i, off)
require.Equal(t, byte(j), data[1], "iteration %d offset %d", j, off)
}
}
}
func TestArenaReserve(t *testing.T) {
a := NewArena(128)
require.NoError(t, a.Reserve(64))
require.NoError(t, a.Reserve(128))
require.ErrorIs(t, a.Reserve(129), ErrArenaFull)
// Allocate some space
_, err := a.Allocate(32)
require.NoError(t, err) // 32 aligned = 32
// 128 - 32 = 96 remaining
require.NoError(t, a.Reserve(96))
require.ErrorIs(t, a.Reserve(97), ErrArenaFull)
}
func TestArenaRemainingAndCapacity(t *testing.T) {
cap := uint32(256)
a := NewArena(cap)
require.Equal(t, cap, a.Capacity())
require.Equal(t, cap, a.Remaining())
_, err := a.Allocate(10)
require.NoError(t, err) // 10 aligned to 16
require.Equal(t, cap-16, a.Remaining())
}