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) }