feat: initialize project structure and error types

- 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
This commit is contained in:
dailz
2026-06-12 13:23:27 +08:00
parent 59de99ca0b
commit cf913b1d52
52 changed files with 4538 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
package wal
import (
"encoding/binary"
"errors"
"hash/crc32"
)
// PhysicalRecord represents a single physical record in the WAL.
type PhysicalRecord struct {
CRC uint32
Length uint16
Type uint8
Payload []byte
}
// EncodePhysicalRecord encodes a physical record with the given type and payload.
// Format: [crc32 u32 LE][length u16 LE][type u8][payload bytes]
// CRC covers length + type + payload.
func EncodePhysicalRecord(recType uint8, payload []byte) []byte {
length := uint16(len(payload))
buf := make([]byte, PhysicalRecordHeaderSize+len(payload))
// Write length and type first so we can compute CRC.
binary.LittleEndian.PutUint16(buf[4:6], length)
buf[6] = recType
copy(buf[7:], payload)
// CRC covers bytes [4:] = length + type + payload.
crc := crc32.ChecksumIEEE(buf[4:])
binary.LittleEndian.PutUint32(buf[0:4], crc)
return buf
}
// DecodePhysicalRecord decodes a physical record from data.
// Returns the record, number of bytes consumed, and any error.
func DecodePhysicalRecord(data []byte) (rec *PhysicalRecord, consumed int, err error) {
if len(data) < PhysicalRecordHeaderSize {
return nil, 0, errors.New("record: data too short for header")
}
crc := binary.LittleEndian.Uint32(data[0:4])
length := binary.LittleEndian.Uint16(data[4:6])
recType := data[6]
if int(length) > len(data)-PhysicalRecordHeaderSize {
return nil, 0, errors.New("record: data too short for payload")
}
payload := make([]byte, length)
copy(payload, data[7:7+length])
// Verify CRC: covers length + type + payload.
expectedCRC := crc32.ChecksumIEEE(data[4 : 7+length])
if crc != expectedCRC {
return nil, 0, errors.New("record: CRC mismatch")
}
consumed = PhysicalRecordHeaderSize + int(length)
return &PhysicalRecord{
CRC: crc,
Length: length,
Type: recType,
Payload: payload,
}, consumed, nil
}
// PaddingNeeded returns the number of padding bytes needed at blockOffset.
// If the remaining space in the current block is <= PhysicalRecordHeaderSize (7),
// that remaining space must be zero-padded.
func PaddingNeeded(blockOffset uint32) int {
remaining := WalBlockSize - (blockOffset % WalBlockSize)
if remaining <= PhysicalRecordHeaderSize {
return int(remaining)
}
return 0
}
// CanFitRecord reports whether a physical record with the given payload length
// can fit in the current block starting at blockOffset.
func CanFitRecord(blockOffset uint32, payloadLen uint32) bool {
remaining := WalBlockSize - (blockOffset % WalBlockSize)
return int(remaining) >= PhysicalRecordHeaderSize+int(payloadLen)
}
// SplitIntoRecords splits an encoded WAL batch into physical record payloads
// respecting 32 KB block boundaries.
// Each returned byte slice is the full encoded physical record (header + payload).
func SplitIntoRecords(encodedBatch []byte) [][]byte {
maxPayload := WalBlockSize - PhysicalRecordHeaderSize
total := len(encodedBatch)
if total == 0 {
return nil
}
// Single record fits entirely.
if total <= maxPayload {
return [][]byte{EncodePhysicalRecord(RecFull, encodedBatch)}
}
var records [][]byte
offset := 0
for offset < total {
chunkLen := min(total-offset, maxPayload)
var recType uint8
switch {
case offset == 0 && offset+chunkLen == total:
recType = RecFull
case offset == 0:
recType = RecFirst
case offset+chunkLen == total:
recType = RecLast
default:
recType = RecMiddle
}
records = append(records, EncodePhysicalRecord(recType, encodedBatch[offset:offset+chunkLen]))
offset += chunkLen
}
return records
}