feat(wal): implement segment rotation, recovery scanner, and record parser
- wal/segment_manager.go: segment lifecycle with rotation at batch boundaries - wal/scanner.go: segment discovery, ordering, and continuity validation - wal/record_parser.go: block-level physical record parsing with tail corruption detection - Comprehensive tests for all modules, all pass with -race
This commit is contained in:
+119
@@ -0,0 +1,119 @@
|
||||
package wal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SegmentInfo holds metadata about a WAL segment file.
|
||||
type SegmentInfo struct {
|
||||
FilePath string
|
||||
SegmentID uint64
|
||||
StartSequence uint64
|
||||
}
|
||||
|
||||
// ParseSegmentFilename extracts the segment ID from a filename matching the
|
||||
// pattern "segment-{N}.wal". Returns (N, true) on match, (0, false) otherwise.
|
||||
func ParseSegmentFilename(name string) (segmentID uint64, ok bool) {
|
||||
if !strings.HasPrefix(name, "segment-") || !strings.HasSuffix(name, ".wal") {
|
||||
return 0, false
|
||||
}
|
||||
// Strip "segment-" prefix and ".wal" suffix.
|
||||
middle := name[len("segment-") : len(name)-len(".wal")]
|
||||
if len(middle) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
id, err := strconv.ParseUint(middle, 10, 64)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrSegmentGap indicates missing WAL segment(s) in the expected sequence.
|
||||
ErrSegmentGap = errors.New("wal: gap detected in segment sequence")
|
||||
)
|
||||
|
||||
// ScanSegments discovers WAL segment files in dir, filters those with
|
||||
// SegmentID >= recoverySegmentID, validates file headers, and returns them
|
||||
// sorted by SegmentID ascending. Returns an error if a gap is detected in the
|
||||
// segment ID sequence.
|
||||
func ScanSegments(dir string, recoverySegmentID uint64) ([]*SegmentInfo, error) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wal: scan segments: %w", err)
|
||||
}
|
||||
|
||||
var candidates []*SegmentInfo
|
||||
for _, ent := range entries {
|
||||
if ent.IsDir() {
|
||||
continue
|
||||
}
|
||||
segID, ok := ParseSegmentFilename(ent.Name())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if segID < recoverySegmentID {
|
||||
continue
|
||||
}
|
||||
candidates = append(candidates, &SegmentInfo{
|
||||
FilePath: filepath.Join(dir, ent.Name()),
|
||||
SegmentID: segID,
|
||||
})
|
||||
}
|
||||
|
||||
if len(candidates) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Sort by SegmentID ascending.
|
||||
sort.Slice(candidates, func(i, j int) bool {
|
||||
return candidates[i].SegmentID < candidates[j].SegmentID
|
||||
})
|
||||
|
||||
// Decode each file header to populate StartSequence and validate.
|
||||
for _, si := range candidates {
|
||||
hdr, err := readFileHeader(si.FilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wal: segment %d: %w", si.SegmentID, err)
|
||||
}
|
||||
si.StartSequence = hdr.StartSequence
|
||||
}
|
||||
|
||||
// Verify continuity: segment IDs must form a consecutive sequence
|
||||
// starting from recoverySegmentID.
|
||||
for i, si := range candidates {
|
||||
expected := recoverySegmentID + uint64(i)
|
||||
if si.SegmentID != expected {
|
||||
return nil, fmt.Errorf("%w: expected segment %d, found %d",
|
||||
ErrSegmentGap, expected, si.SegmentID)
|
||||
}
|
||||
}
|
||||
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
// readFileHeader opens the file, reads the header portion, and decodes it.
|
||||
func readFileHeader(path string) (*WalFileHeader, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
hdrBuf := make([]byte, WalFileHeaderSize)
|
||||
n, err := f.Read(hdrBuf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read header: %w", err)
|
||||
}
|
||||
if n < WalFileHeaderSize {
|
||||
return nil, fmt.Errorf("read header: got %d bytes, need %d", n, WalFileHeaderSize)
|
||||
}
|
||||
return DecodeWalHeader(hdrBuf)
|
||||
}
|
||||
Reference in New Issue
Block a user