Files
seaweedfs/weed/storage/blockvol/recovery.go
T
pingqiuandClaude Opus 4.6 785a7d7efd feat: wire real pinner into flusher retention + real WAL scan executor (Phase 07 P1)
Pinner wired to real retention:
- NewPinner calls vol.SetV2RetentionFloor(p.MinWALRetentionFloor)
- Flusher.RetentionFloorFn() / SetRetentionFloorFn() exposed
- SetV2RetentionFloor chains with existing shipper retention floor
- Holds actually prevent WAL reclaim (not just tracked state)

Executor uses real WAL scan:
- BlockVol.ScanWALEntries(fromLSN, callback) wraps wal.ScanFrom
  with real fd, walOffset, checkpointLSN
- Executor.StreamWALEntries uses ScanWALEntries (not stub)
- Reads real WAL entries, tracks highest LSN scanned

CommittedLSN mapping:
- Explicitly documented as interim V1 model (committed = checkpointed)
- Will diverge when V2 distributed commit separates from local flush

Carry-forward:
- TransferSnapshot/TransferFullBase/TruncateWAL: stubs (need extent I/O)
- Control intent from confirmed failover: deferred

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 20:01:46 -07:00

186 lines
5.7 KiB
Go

package blockvol
import (
"fmt"
"log"
"os"
)
// RecoveryResult contains the outcome of WAL recovery.
type RecoveryResult struct {
EntriesReplayed int // number of entries replayed into dirty map
HighestLSN uint64 // highest LSN seen during recovery
TornEntries int // entries discarded due to CRC failure
DefensiveScan bool // true if a defensive scan was triggered
}
// RecoverWAL scans the WAL region from tail to head, replaying valid entries
// into the dirty map. Entries with LSN <= checkpointLSN are skipped (already
// in extent).
//
// After scanning the known [tail, head) range, the scanner continues past
// head using CRC validation to discover entries written after the last
// superblock persist. This makes the superblock WALHead advisory (for fast
// recovery) rather than required for correctness. On a clean shutdown the
// first entry past head fails CRC immediately — zero overhead.
//
// The WAL is a circular buffer. If head >= tail, scan [tail, head).
// If head < tail (wrapped), scan [tail, walSize) then [0, head).
func RecoverWAL(fd *os.File, sb *Superblock, dirtyMap *DirtyMap) (RecoveryResult, error) {
result := RecoveryResult{}
logicalHead := sb.WALHead
logicalTail := sb.WALTail
walOffset := sb.WALOffset
walSize := sb.WALSize
checkpointLSN := sb.WALCheckpointLSN
// Build the list of byte ranges to scan.
type scanRange struct {
start, end uint64 // physical positions within WAL
}
var ranges []scanRange
if logicalHead == logicalTail {
// Superblock says WAL is empty. Scan the entire WAL region
// using CRC validation to find any valid entries.
// On a genuinely empty WAL, the first read fails CRC immediately.
ranges = append(ranges, scanRange{0, walSize})
result.DefensiveScan = true
if checkpointLSN == 0 && logicalHead == 0 && logicalTail == 0 {
log.Printf("recovery: defensive scan triggered (WALHead=0 WALTail=0 CheckpointLSN=0)")
} else {
log.Printf("recovery: defensive scan triggered (WALHead==WALTail=%d CheckpointLSN=%d)",
logicalHead, checkpointLSN)
}
} else {
// Normal case: scan the known WAL range, then extend past head.
physHead := logicalHead % walSize
physTail := logicalTail % walSize
if physHead > physTail {
// [tail ... head ... walSize) — scan [tail, head), then extend [head, walSize) + [0, tail)
ranges = append(ranges, scanRange{physTail, physHead})
// Extended scan past head: [head, walSize) then [0, tail)
ranges = append(ranges, scanRange{physHead, walSize})
if physTail > 0 {
ranges = append(ranges, scanRange{0, physTail})
}
} else {
// Wrapped or full: [tail, walSize) + [0, head), then extend [head, tail)
ranges = append(ranges, scanRange{physTail, walSize})
if physHead > 0 {
ranges = append(ranges, scanRange{0, physHead})
}
// Extended scan past head: [head, tail) covers the remaining region
if physHead < physTail {
ranges = append(ranges, scanRange{physHead, physTail})
}
}
}
for _, r := range ranges {
pos := r.start
for pos < r.end {
remaining := r.end - pos
// Need at least a header to proceed.
if remaining < uint64(walEntryHeaderSize) {
break
}
// Read header.
headerBuf := make([]byte, walEntryHeaderSize)
absOff := int64(walOffset + pos)
if _, err := fd.ReadAt(headerBuf, absOff); err != nil {
return result, fmt.Errorf("recovery: read header at WAL+%d: %w", pos, err)
}
// Parse entry type and length field.
entryType := headerBuf[16]
lengthField := parseLength(headerBuf)
// For padding entries, skip forward.
if entryType == EntryTypePadding {
entrySize := uint64(walEntryHeaderSize) + uint64(lengthField)
pos += entrySize
continue
}
// Calculate on-disk entry size. WRITE and PADDING carry data payload;
// TRIM and BARRIER do not (Length is metadata, not data size).
var payloadLen uint64
if entryType == EntryTypeWrite {
payloadLen = uint64(lengthField)
}
entrySize := uint64(walEntryHeaderSize) + payloadLen
if entrySize > remaining {
// Torn write: entry extends past available data.
result.TornEntries++
break
}
// Read full entry.
fullBuf := make([]byte, entrySize)
if _, err := fd.ReadAt(fullBuf, absOff); err != nil {
return result, fmt.Errorf("recovery: read entry at WAL+%d: %w", pos, err)
}
// Decode and validate CRC.
entry, err := DecodeWALEntry(fullBuf)
if err != nil {
// CRC failure or corrupt entry -- stop here (torn write).
result.TornEntries++
break
}
// Skip entries already flushed to extent.
if entry.LSN <= checkpointLSN {
pos += entrySize
continue
}
// Replay entry.
switch entry.Type {
case EntryTypeWrite:
blocks := entry.Length / sb.BlockSize
for i := uint32(0); i < blocks; i++ {
dirtyMap.Put(entry.LBA+uint64(i), pos, entry.LSN, sb.BlockSize)
}
result.EntriesReplayed++
case EntryTypeTrim:
// TRIM carries Length (bytes) covering multiple blocks.
blocks := entry.Length / sb.BlockSize
if blocks == 0 {
blocks = 1 // legacy single-block trim
}
for i := uint32(0); i < blocks; i++ {
dirtyMap.Put(entry.LBA+uint64(i), pos, entry.LSN, sb.BlockSize)
}
result.EntriesReplayed++
case EntryTypeBarrier:
// Barriers don't modify data, just skip.
}
if entry.LSN > result.HighestLSN {
result.HighestLSN = entry.LSN
}
pos += entrySize
}
}
// If we found entries beyond what the superblock recorded, update
// WALHead so the WAL writer starts after the recovered entries.
if result.HighestLSN > sb.WALHead {
log.Printf("recovery: extended scan found entries past WALHead (%d → %d, %d entries replayed)",
sb.WALHead, result.HighestLSN, result.EntriesReplayed)
sb.WALHead = result.HighestLSN
}
return result, nil
}