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:
+126
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user