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 }