- 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
135 lines
3.3 KiB
Go
135 lines
3.3 KiB
Go
package wal
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// WalEntry represents a single WAL record.
|
|
type WalEntry struct {
|
|
OpType uint8
|
|
ValueKind uint8
|
|
Key []byte
|
|
Value []byte
|
|
}
|
|
|
|
// Validate checks that the entry fields are consistent with the design rules.
|
|
func (e *WalEntry) Validate() error {
|
|
keyLen := len(e.Key)
|
|
if keyLen == 0 || keyLen > int(MaxWalKeyBytes) {
|
|
return fmt.Errorf("wal: invalid key length %d", keyLen)
|
|
}
|
|
|
|
valLen := len(e.Value)
|
|
|
|
switch e.OpType {
|
|
case OpPut:
|
|
switch e.ValueKind {
|
|
case VKInline:
|
|
if valLen > int(MaxWalInlineValueBytes) {
|
|
return fmt.Errorf("wal: inline value length %d out of range [0, %d]", valLen, MaxWalInlineValueBytes)
|
|
}
|
|
case VKValueLogPointer:
|
|
if valLen == 0 {
|
|
return errors.New("wal: value log pointer requires non-empty value")
|
|
}
|
|
default:
|
|
return fmt.Errorf("wal: put requires valueKind Inline(1) or ValueLogPointer(2), got %d", e.ValueKind)
|
|
}
|
|
|
|
case OpDelete:
|
|
if e.ValueKind != VKNone {
|
|
return fmt.Errorf("wal: delete requires valueKind None(0), got %d", e.ValueKind)
|
|
}
|
|
if valLen != 0 {
|
|
return fmt.Errorf("wal: delete requires empty value, got length %d", valLen)
|
|
}
|
|
|
|
default:
|
|
return fmt.Errorf("wal: invalid opType %d", e.OpType)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// EncodeEntry serializes a WalEntry into a byte slice.
|
|
func EncodeEntry(e *WalEntry) ([]byte, error) {
|
|
if err := e.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
keyLen := uint64(len(e.Key))
|
|
valLen := uint64(len(e.Value))
|
|
|
|
// Size: 1 (opType) + 1 (valueKind) + varint(keyLen) + varint(valLen) + key + value
|
|
size := 2 + MaxWalVarintBytes + MaxWalVarintBytes + len(e.Key) + len(e.Value)
|
|
buf := make([]byte, size)
|
|
|
|
buf[0] = e.OpType
|
|
buf[1] = e.ValueKind
|
|
n := 2
|
|
n += binary.PutUvarint(buf[n:], keyLen)
|
|
n += binary.PutUvarint(buf[n:], valLen)
|
|
n += copy(buf[n:], e.Key)
|
|
n += copy(buf[n:], e.Value)
|
|
|
|
return buf[:n], nil
|
|
}
|
|
|
|
// DecodeEntry deserializes a WalEntry from a byte slice.
|
|
// Returns the decoded entry and the number of bytes consumed.
|
|
func DecodeEntry(data []byte) (entry *WalEntry, consumed int, err error) {
|
|
if len(data) < 2 {
|
|
return nil, 0, errors.New("wal: data too short for entry header")
|
|
}
|
|
|
|
opType := data[0]
|
|
valueKind := data[1]
|
|
r := bytes.NewReader(data[2:])
|
|
|
|
keyLen, err := binary.ReadUvarint(r)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("wal: reading key length: %w", err)
|
|
}
|
|
valLen, err := binary.ReadUvarint(r)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("wal: reading value length: %w", err)
|
|
}
|
|
|
|
// Calculate consumed so far: 2 header bytes + bytes read from reader
|
|
consumed = 2 + (len(data) - 2 - r.Len())
|
|
|
|
// Read key
|
|
remaining := len(data) - consumed
|
|
if uint64(remaining) < keyLen {
|
|
return nil, 0, fmt.Errorf("wal: data truncated: need %d bytes for key, have %d", keyLen, remaining)
|
|
}
|
|
key := make([]byte, keyLen)
|
|
copy(key, data[consumed:consumed+int(keyLen)])
|
|
consumed += int(keyLen)
|
|
|
|
// Read value
|
|
remaining = len(data) - consumed
|
|
if uint64(remaining) < valLen {
|
|
return nil, 0, fmt.Errorf("wal: data truncated: need %d bytes for value, have %d", valLen, remaining)
|
|
}
|
|
value := make([]byte, valLen)
|
|
copy(value, data[consumed:consumed+int(valLen)])
|
|
consumed += int(valLen)
|
|
|
|
e := &WalEntry{
|
|
OpType: opType,
|
|
ValueKind: valueKind,
|
|
Key: key,
|
|
Value: value,
|
|
}
|
|
|
|
if err := e.Validate(); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return e, consumed, nil
|
|
}
|