- 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
120 lines
3.0 KiB
Go
120 lines
3.0 KiB
Go
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)
|
|
}
|