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
+112
View File
@@ -0,0 +1,112 @@
package wal
import (
"fmt"
"math"
"github.com/dailz/go-kv/config"
)
// ValidateBatchLimits checks that a batch of WAL entries satisfies all resource
// limits from the supplied configuration before sequence allocation or WAL append.
// Returns a descriptive error on violation, nil on success.
func ValidateBatchLimits(entries []*WalEntry, cfg *config.WalConfig) error {
entryCount := uint64(len(entries))
if entryCount == 0 {
return fmt.Errorf("wal: batch entry count must be > 0")
}
if entryCount > uint64(cfg.MaxBatchEntries) {
return fmt.Errorf("wal: entry count %d exceeds limit %d", entryCount, cfg.MaxBatchEntries)
}
// Per-entry validation and total encoded size calculation.
// Each entry encoded size = 1(opType) + 1(valueKind) + varint(keyLen) + varint(valLen) + keyLen + valLen
// We use MaxWalVarintBytes (5) as worst-case varint size.
var totalEncodedSize uint64
for i, e := range entries {
keyLen := uint64(len(e.Key))
if keyLen == 0 || keyLen > uint64(cfg.MaxKeyBytes) {
return fmt.Errorf("wal: entry %d: key length %d out of range [1, %d]", i, keyLen, cfg.MaxKeyBytes)
}
valLen := uint64(len(e.Value))
if e.OpType == OpPut && e.ValueKind == VKInline {
if valLen > uint64(cfg.MaxInlineValue) {
return fmt.Errorf("wal: entry %d: inline value length %d exceeds limit %d", i, valLen, cfg.MaxInlineValue)
}
}
// entrySize = 2 + varint(keyLen) + varint(valLen) + keyLen + valLen
// Use worst-case varint size for safety.
entrySize, err := safeAddChecked(2+MaxWalVarintBytes+MaxWalVarintBytes, keyLen)
if err != nil {
return fmt.Errorf("wal: entry %d: size overflow: %w", i, err)
}
entrySize, err = safeAddChecked(entrySize, valLen)
if err != nil {
return fmt.Errorf("wal: entry %d: size overflow: %w", i, err)
}
totalEncodedSize, err = safeAddChecked(totalEncodedSize, entrySize)
if err != nil {
return fmt.Errorf("wal: total encoded size overflow: %w", err)
}
}
// totalWithBatchHeader = WalBatchHeaderSize + totalEncodedSize
totalWithBatchHeader, err := safeAddChecked(WalBatchHeaderSize, totalEncodedSize)
if err != nil {
return fmt.Errorf("wal: batch size overflow: %w", err)
}
if totalWithBatchHeader > uint64(cfg.MaxBatchSize) {
return fmt.Errorf("wal: total batch size %d exceeds limit %d", totalWithBatchHeader, cfg.MaxBatchSize)
}
// Check that the batch fits in a WAL segment with physical record overhead.
blockSize := uint64(cfg.BlockSize)
prHeaderSize := uint64(PhysicalRecordHeaderSize)
if blockSize <= prHeaderSize {
return fmt.Errorf("wal: block size %d must be > physical record header size %d", blockSize, prHeaderSize)
}
maxPayload := blockSize - prHeaderSize
numRecords := divCeilChecked(totalWithBatchHeader, maxPayload)
overhead, err := safeMulChecked(numRecords, prHeaderSize)
if err != nil {
return fmt.Errorf("wal: physical record overhead overflow: %w", err)
}
totalWithOverhead, err := safeAddChecked(totalWithBatchHeader, overhead)
if err != nil {
return fmt.Errorf("wal: total with overhead overflow: %w", err)
}
maxSegmentPayload := cfg.MaxSegmentSize - WalFileHeaderSize
if totalWithOverhead > maxSegmentPayload {
return fmt.Errorf("wal: batch with overhead %d exceeds segment payload %d", totalWithOverhead, maxSegmentPayload)
}
return nil
}
// safeAddChecked returns a + b or an error if the result overflows uint64.
func safeAddChecked(a, b uint64) (uint64, error) {
if a > math.MaxUint64-b {
return 0, fmt.Errorf("uint64 overflow: %d + %d", a, b)
}
return a + b, nil
}
// safeMulChecked returns a * b or an error if the result overflows uint64.
func safeMulChecked(a, b uint64) (uint64, error) {
if a != 0 && b > math.MaxUint64/a {
return 0, fmt.Errorf("uint64 overflow: %d * %d", a, b)
}
return a * b, nil
}
// divCeilChecked returns ceil(a / b) for b > 0.
func divCeilChecked(a, b uint64) uint64 {
return (a + b - 1) / b
}