- go.mod with github.com/dailz/go-kv, Go 1.26.3, testify - config/config.go with WalConfig, Validate() with checked arithmetic - errors.go with sentinel errors (ErrCommitUnknown, ErrWriteStopped, etc.) - wal/constants.go with all WAL format constants and enums - wal/header.go with WAL File Header encode/decode (CRC32 IEEE) - wal/record.go with Physical Record codec, block boundary, SplitIntoRecords - wal/entry.go with WAL Entry codec (varint keys/values, OpType, ValueKind) - wal/sequence.go with SequenceManager (atomic, CAS, overflow-safe) - manifest/manifest.go with MANIFEST stub (Load/Save atomic) - manifest/current.go with CURRENT file (WriteCurrent/ReadCurrent) - Comprehensive tests for all modules - .golangci.yml configuration
206 lines
6.6 KiB
Go
206 lines
6.6 KiB
Go
// Package config defines configuration types and validation for the go-kv storage engine.
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
)
|
|
|
|
// WAL format constants derived from the binary layout specification.
|
|
const (
|
|
walFileHeaderSize uint64 = 32
|
|
walBlockSize uint32 = 32 * 1024 // 32KB
|
|
physicalRecordHeaderSize uint64 = 7
|
|
walBatchHeaderSize uint64 = 18 // flags(2) + baseSequence(8) + entryCount(4) + entriesSize(4)
|
|
)
|
|
|
|
// WalConfig holds configuration for the Write-Ahead Log subsystem.
|
|
// Zero-value WalConfig is valid and uses defaults; call Validate() to apply
|
|
// defaults and verify invariants.
|
|
type WalConfig struct {
|
|
// MaxSegmentSize is the maximum size of a single WAL segment file in bytes.
|
|
// Default: 64MB. Must be large enough to hold the largest possible WAL Batch.
|
|
MaxSegmentSize uint64
|
|
|
|
// BlockSize is the WAL block size in bytes.
|
|
// Default: 32KB.
|
|
BlockSize uint32
|
|
|
|
// SyncMode controls when WAL writes are flushed to disk.
|
|
// Phase 1 only supports "always".
|
|
SyncMode string
|
|
|
|
// MaxBatchEntries is the maximum number of entries in a single WAL Batch.
|
|
// Default: 10000.
|
|
MaxBatchEntries uint32
|
|
|
|
// MaxBatchSize is the maximum total size of WAL Batch entries in bytes.
|
|
// Default: 4MB.
|
|
MaxBatchSize uint32
|
|
|
|
// MaxKeyBytes is the maximum size of a single key in bytes.
|
|
// Default: 4KB.
|
|
MaxKeyBytes uint32
|
|
|
|
// MaxInlineValue is the maximum size of an inline value in bytes.
|
|
// Values larger than this must use ValueLogPointer.
|
|
// Default: 4KB.
|
|
MaxInlineValue uint32
|
|
|
|
// MemTableSize is the target MemTable size in bytes before triggering flush.
|
|
// Default: 64MB.
|
|
MemTableSize uint32
|
|
|
|
// MaxImmutableCount is the maximum number of immutable MemTables allowed
|
|
// before writes are stalled. Default: 3.
|
|
MaxImmutableCount int
|
|
}
|
|
|
|
// Defaults returns a WalConfig populated with production defaults.
|
|
func Defaults() WalConfig {
|
|
return WalConfig{
|
|
MaxSegmentSize: 64 * 1024 * 1024, // 64MB
|
|
BlockSize: 32 * 1024, // 32KB
|
|
SyncMode: "always",
|
|
MaxBatchEntries: 10000,
|
|
MaxBatchSize: 4 * 1024 * 1024, // 4MB
|
|
MaxKeyBytes: 4 * 1024, // 4KB
|
|
MaxInlineValue: 4 * 1024, // 4KB
|
|
MemTableSize: 64 * 1024 * 1024, // 64MB
|
|
MaxImmutableCount: 3,
|
|
}
|
|
}
|
|
|
|
// applyDefaults fills zero-valued fields with production defaults.
|
|
func (c *WalConfig) applyDefaults() {
|
|
d := Defaults()
|
|
if c.MaxSegmentSize == 0 {
|
|
c.MaxSegmentSize = d.MaxSegmentSize
|
|
}
|
|
if c.BlockSize == 0 {
|
|
c.BlockSize = d.BlockSize
|
|
}
|
|
if c.SyncMode == "" {
|
|
c.SyncMode = d.SyncMode
|
|
}
|
|
if c.MaxBatchEntries == 0 {
|
|
c.MaxBatchEntries = d.MaxBatchEntries
|
|
}
|
|
if c.MaxBatchSize == 0 {
|
|
c.MaxBatchSize = d.MaxBatchSize
|
|
}
|
|
if c.MaxKeyBytes == 0 {
|
|
c.MaxKeyBytes = d.MaxKeyBytes
|
|
}
|
|
if c.MaxInlineValue == 0 {
|
|
c.MaxInlineValue = d.MaxInlineValue
|
|
}
|
|
if c.MemTableSize == 0 {
|
|
c.MemTableSize = d.MemTableSize
|
|
}
|
|
if c.MaxImmutableCount == 0 {
|
|
c.MaxImmutableCount = d.MaxImmutableCount
|
|
}
|
|
}
|
|
|
|
// Validate applies defaults and verifies that all configuration invariants hold.
|
|
// The key invariant ensures that the largest possible WAL Batch can fit into
|
|
// an empty WAL segment:
|
|
//
|
|
// maxWalSegmentPayload >= maxEncodedWalBatchSize + worstCasePhysicalRecordOverhead + worstCaseBlockPadding
|
|
//
|
|
// All arithmetic is checked for overflow.
|
|
func (c *WalConfig) Validate() error {
|
|
c.applyDefaults()
|
|
|
|
if c.SyncMode != "always" {
|
|
return fmt.Errorf("config: SyncMode %q not supported (Phase 1: only \"always\")", c.SyncMode)
|
|
}
|
|
|
|
if c.MaxImmutableCount < 1 {
|
|
return fmt.Errorf("config: MaxImmutableCount must be >= 1, got %d", c.MaxImmutableCount)
|
|
}
|
|
|
|
// --- Checked arithmetic invariant validation ---
|
|
// Mirrors the derivation in docs/design.md § WAL Segment Rotation.
|
|
|
|
blockSize := uint64(c.BlockSize)
|
|
prHeaderSize := physicalRecordHeaderSize
|
|
batchHeaderSize := walBatchHeaderSize
|
|
maxBatchEntriesSize := uint64(c.MaxBatchSize)
|
|
|
|
// maxEncodedWalBatchSize = batchHeaderSize + maxBatchEntriesSize
|
|
maxEncodedWalBatchSize, err := safeAdd(batchHeaderSize, maxBatchEntriesSize)
|
|
if err != nil {
|
|
return fmt.Errorf("config: WAL batch size overflow: %w", err)
|
|
}
|
|
|
|
// maxPhysicalRecordPayload = blockSize - prHeaderSize
|
|
if blockSize <= prHeaderSize {
|
|
return fmt.Errorf("config: BlockSize %d must be > physical record header size %d", blockSize, prHeaderSize)
|
|
}
|
|
maxPhysicalRecordPayload := blockSize - prHeaderSize
|
|
|
|
// maxPhysicalRecordCount = ceil(maxEncodedWalBatchSize / maxPhysicalRecordPayload)
|
|
maxPhysicalRecordCount := divCeil(maxEncodedWalBatchSize, maxPhysicalRecordPayload)
|
|
|
|
// worstCasePhysicalRecordOverhead = maxPhysicalRecordCount * prHeaderSize
|
|
worstCasePhysicalRecordOverhead, err := safeMul(maxPhysicalRecordCount, prHeaderSize)
|
|
if err != nil {
|
|
return fmt.Errorf("config: physical record overhead overflow: %w", err)
|
|
}
|
|
|
|
// worstCaseBlockPadding = blockSize - 1 (at most one partial block of padding)
|
|
// From design doc: worstCaseBlockPadding = 7 bytes with default block size.
|
|
// Generalized: blockSize - maxPhysicalRecordPayload = prHeaderSize
|
|
worstCaseBlockPadding := prHeaderSize
|
|
|
|
// minWalSegmentPayload = maxEncodedWalBatchSize + worstCasePhysicalRecordOverhead + worstCaseBlockPadding
|
|
partial, err := safeAdd(maxEncodedWalBatchSize, worstCasePhysicalRecordOverhead)
|
|
if err != nil {
|
|
return fmt.Errorf("config: segment payload calculation overflow: %w", err)
|
|
}
|
|
minWalSegmentPayload, err := safeAdd(partial, worstCaseBlockPadding)
|
|
if err != nil {
|
|
return fmt.Errorf("config: segment payload calculation overflow: %w", err)
|
|
}
|
|
|
|
// maxWalSegmentPayload = MaxSegmentSize - walFileHeaderSize
|
|
if c.MaxSegmentSize <= walFileHeaderSize {
|
|
return fmt.Errorf("config: MaxSegmentSize %d must be > WAL file header size %d",
|
|
c.MaxSegmentSize, walFileHeaderSize)
|
|
}
|
|
maxWalSegmentPayload := c.MaxSegmentSize - walFileHeaderSize
|
|
|
|
if maxWalSegmentPayload < minWalSegmentPayload {
|
|
return fmt.Errorf("config: MaxSegmentSize %d too small: "+
|
|
"segment payload (%d) < minimum required (%d); "+
|
|
"need MaxSegmentSize >= %d",
|
|
c.MaxSegmentSize, maxWalSegmentPayload, minWalSegmentPayload,
|
|
minWalSegmentPayload+walFileHeaderSize)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// safeAdd returns a + b or an error if the result overflows uint64.
|
|
func safeAdd(a, b uint64) (uint64, error) {
|
|
if a > math.MaxUint64-b {
|
|
return 0, fmt.Errorf("uint64 overflow: %d + %d", a, b)
|
|
}
|
|
return a + b, nil
|
|
}
|
|
|
|
// safeMul returns a * b or an error if the result overflows uint64.
|
|
func safeMul(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
|
|
}
|
|
|
|
// divCeil returns ceil(a / b) for b > 0.
|
|
func divCeil(a, b uint64) uint64 {
|
|
return (a + b - 1) / b
|
|
}
|