// Package config defines configuration types and validation for the go-kv storage engine. package config import ( "fmt" "math" "time" ) // WAL format constants derived from the binary layout specification. const ( walFileHeaderSize uint64 = 32 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 // GroupCommitDelay is the maximum time to collect requests before writing a // WAL group commit batch. Default: 500µs. Must be > 0 and < 10ms. GroupCommitDelay time.Duration // 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 GroupCommitDelay: 500 * time.Microsecond, 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.GroupCommitDelay == 0 { c.GroupCommitDelay = d.GroupCommitDelay } 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) } if c.GroupCommitDelay <= 0 || c.GroupCommitDelay >= 10*time.Millisecond { return fmt.Errorf("config: GroupCommitDelay must be > 0 and < 10ms, got %s", c.GroupCommitDelay) } // --- 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 }