mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-23 22:40:39 +02:00
Phase 3 delivers five checkpoints: CP1 Engine Tuning: BlockVolConfig tunables, 256-shard DirtyMap, adaptive group commit (low-watermark immediate flush), WAL pressure handling with backpressure and ErrWALFull timeout. CP2 iSCSI Session Refactor: RX/TX goroutine split with respCh (cap 64), txLoop for serialized response writes, StatSN assignment modes. Login phase stays single-goroutine; full-duplex after login. CP3 Store Integration: BlockVolAdapter (iscsi.BlockDevice interface), BlockVolumeStore management, BlockService in volume_server_block.go, CLI flags (--block.listen/dir/iqn.prefix), sw-block-attach.sh helper. CP5 Concurrency Hardening: WAL reuse guard (LSN validation in ReadLBA), opsOutstanding counter with beginOp/endOp + Close drain, appendWithRetry shared by WriteLBA and TrimLBA, flusher LSN guard in FlushOnce. Bug fixes (P3-BUG-2–11): unbounded pending queue cap, Data-Out timeout, flusher error logging, GroupCommitter panic recovery, Close vs concurrent ops guard, target shutdown race, WAL-full retry vs Close, WRITE SAME(16) for XFS, MODE SENSE(10) + VPD 0xB0/0xB2 for Linux kernel compatibility. 797 tests passing (517 engine + 280 iSCSI), go vet clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
289 lines
7.9 KiB
Go
289 lines
7.9 KiB
Go
package blockvol
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Flusher copies WAL entries to the extent region and frees WAL space.
|
|
// It runs as a background goroutine and can also be triggered manually.
|
|
type Flusher struct {
|
|
fd *os.File
|
|
super *Superblock
|
|
wal *WALWriter
|
|
dirtyMap *DirtyMap
|
|
walOffset uint64 // absolute file offset of WAL region
|
|
walSize uint64
|
|
blockSize uint32
|
|
extentStart uint64 // absolute file offset of extent region
|
|
|
|
mu sync.Mutex
|
|
checkpointLSN uint64 // last flushed LSN
|
|
checkpointTail uint64 // WAL physical tail after last flush
|
|
|
|
logger *log.Logger
|
|
lastErr bool // true if last FlushOnce returned error
|
|
|
|
interval time.Duration
|
|
notifyCh chan struct{}
|
|
stopCh chan struct{}
|
|
done chan struct{}
|
|
stopOnce sync.Once
|
|
}
|
|
|
|
// FlusherConfig configures the flusher.
|
|
type FlusherConfig struct {
|
|
FD *os.File
|
|
Super *Superblock
|
|
WAL *WALWriter
|
|
DirtyMap *DirtyMap
|
|
Interval time.Duration // default 100ms
|
|
Logger *log.Logger // optional; defaults to log.Default()
|
|
}
|
|
|
|
// NewFlusher creates a flusher. Call Run() in a goroutine.
|
|
func NewFlusher(cfg FlusherConfig) *Flusher {
|
|
if cfg.Interval == 0 {
|
|
cfg.Interval = 100 * time.Millisecond
|
|
}
|
|
if cfg.Logger == nil {
|
|
cfg.Logger = log.Default()
|
|
}
|
|
return &Flusher{
|
|
fd: cfg.FD,
|
|
super: cfg.Super,
|
|
wal: cfg.WAL,
|
|
dirtyMap: cfg.DirtyMap,
|
|
walOffset: cfg.Super.WALOffset,
|
|
walSize: cfg.Super.WALSize,
|
|
blockSize: cfg.Super.BlockSize,
|
|
extentStart: cfg.Super.WALOffset + cfg.Super.WALSize,
|
|
logger: cfg.Logger,
|
|
checkpointLSN: cfg.Super.WALCheckpointLSN,
|
|
checkpointTail: 0,
|
|
interval: cfg.Interval,
|
|
notifyCh: make(chan struct{}, 1),
|
|
stopCh: make(chan struct{}),
|
|
done: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// Run is the flusher main loop. Call in a goroutine.
|
|
func (f *Flusher) Run() {
|
|
defer close(f.done)
|
|
ticker := time.NewTicker(f.interval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-f.stopCh:
|
|
return
|
|
case <-ticker.C:
|
|
if err := f.FlushOnce(); err != nil {
|
|
if !f.lastErr {
|
|
f.logger.Printf("flusher error: %v", err)
|
|
}
|
|
f.lastErr = true
|
|
} else {
|
|
f.lastErr = false
|
|
}
|
|
case <-f.notifyCh:
|
|
if err := f.FlushOnce(); err != nil {
|
|
if !f.lastErr {
|
|
f.logger.Printf("flusher error: %v", err)
|
|
}
|
|
f.lastErr = true
|
|
} else {
|
|
f.lastErr = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Notify wakes up the flusher for an immediate flush cycle.
|
|
func (f *Flusher) Notify() {
|
|
select {
|
|
case f.notifyCh <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
// NotifyUrgent wakes the flusher for an urgent flush (WAL pressure).
|
|
// Phase 3 MVP: delegates to Notify(). Future: may use a priority channel.
|
|
func (f *Flusher) NotifyUrgent() {
|
|
f.Notify()
|
|
}
|
|
|
|
// Stop shuts down the flusher. Safe to call multiple times.
|
|
func (f *Flusher) Stop() {
|
|
f.stopOnce.Do(func() {
|
|
close(f.stopCh)
|
|
})
|
|
<-f.done
|
|
}
|
|
|
|
// FlushOnce performs a single flush cycle: scan dirty map, copy data to
|
|
// extent region, fsync, update checkpoint, advance WAL tail.
|
|
func (f *Flusher) FlushOnce() error {
|
|
// Snapshot dirty entries. We use a full scan (Range over all possible LBAs
|
|
// is impractical), so we collect from the dirty map directly.
|
|
type flushEntry struct {
|
|
lba uint64
|
|
walOff uint64
|
|
lsn uint64
|
|
length uint32
|
|
}
|
|
|
|
entries := f.dirtyMap.Snapshot()
|
|
if len(entries) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Find the max LSN and max WAL offset to know where to advance tail.
|
|
var maxLSN uint64
|
|
var maxWALEnd uint64
|
|
|
|
for _, e := range entries {
|
|
// Read the WAL entry and copy data to extent region.
|
|
headerBuf := make([]byte, walEntryHeaderSize)
|
|
absWALOff := int64(f.walOffset + e.WalOffset)
|
|
if _, err := f.fd.ReadAt(headerBuf, absWALOff); err != nil {
|
|
return fmt.Errorf("flusher: read WAL header at %d: %w", absWALOff, err)
|
|
}
|
|
|
|
// WAL reuse guard: validate LSN before trusting the entry.
|
|
// Between Snapshot() and this read, the WAL slot could have been
|
|
// reused (by a previous flush cycle advancing the tail + new writes).
|
|
entryLSN := binary.LittleEndian.Uint64(headerBuf[0:8])
|
|
if entryLSN != e.Lsn {
|
|
continue // stale — WAL slot reused, skip this entry
|
|
}
|
|
|
|
// Parse entry type and length.
|
|
entryType := headerBuf[16] // Type at LSN(8)+Epoch(8)=16
|
|
dataLen := parseLength(headerBuf)
|
|
|
|
if entryType == EntryTypeWrite && dataLen > 0 {
|
|
// Read full entry.
|
|
entryLen := walEntryHeaderSize + int(dataLen)
|
|
fullBuf := make([]byte, entryLen)
|
|
if _, err := f.fd.ReadAt(fullBuf, absWALOff); err != nil {
|
|
return fmt.Errorf("flusher: read WAL entry at %d: %w", absWALOff, err)
|
|
}
|
|
|
|
entry, err := DecodeWALEntry(fullBuf)
|
|
if err != nil {
|
|
continue // corrupt or partially overwritten — skip
|
|
}
|
|
|
|
// Write only this block's data to extent (not all blocks in the
|
|
// WAL entry). Other blocks may have been overwritten by newer
|
|
// writes and their dirty map entries point elsewhere.
|
|
if e.Lba < entry.LBA {
|
|
continue // LBA mismatch — stale entry
|
|
}
|
|
blockIdx := e.Lba - entry.LBA
|
|
dataStart := blockIdx * uint64(f.blockSize)
|
|
if dataStart+uint64(f.blockSize) <= uint64(len(entry.Data)) {
|
|
extentOff := int64(f.extentStart + e.Lba*uint64(f.blockSize))
|
|
blockData := entry.Data[dataStart : dataStart+uint64(f.blockSize)]
|
|
if _, err := f.fd.WriteAt(blockData, extentOff); err != nil {
|
|
return fmt.Errorf("flusher: write extent at LBA %d: %w", e.Lba, err)
|
|
}
|
|
}
|
|
|
|
// Track WAL end position for tail advance.
|
|
walEnd := e.WalOffset + uint64(entryLen)
|
|
if walEnd > maxWALEnd {
|
|
maxWALEnd = walEnd
|
|
}
|
|
} else if entryType == EntryTypeTrim {
|
|
// TRIM entries: zero the extent region for this LBA.
|
|
// Each dirty map entry represents one trimmed block.
|
|
zeroBlock := make([]byte, f.blockSize)
|
|
extentOff := int64(f.extentStart + e.Lba*uint64(f.blockSize))
|
|
if _, err := f.fd.WriteAt(zeroBlock, extentOff); err != nil {
|
|
return fmt.Errorf("flusher: zero extent at LBA %d: %w", e.Lba, err)
|
|
}
|
|
|
|
// TRIM entry has no data payload, just a header.
|
|
walEnd := e.WalOffset + uint64(walEntryHeaderSize)
|
|
if walEnd > maxWALEnd {
|
|
maxWALEnd = walEnd
|
|
}
|
|
}
|
|
|
|
if e.Lsn > maxLSN {
|
|
maxLSN = e.Lsn
|
|
}
|
|
}
|
|
|
|
// Fsync extent writes.
|
|
if err := f.fd.Sync(); err != nil {
|
|
return fmt.Errorf("flusher: fsync extent: %w", err)
|
|
}
|
|
|
|
// Remove flushed entries from dirty map.
|
|
f.mu.Lock()
|
|
for _, e := range entries {
|
|
// Only remove if the dirty map entry still has the same LSN
|
|
// (a newer write may have updated it).
|
|
_, currentLSN, _, ok := f.dirtyMap.Get(e.Lba)
|
|
if ok && currentLSN == e.Lsn {
|
|
f.dirtyMap.Delete(e.Lba)
|
|
}
|
|
}
|
|
f.checkpointLSN = maxLSN
|
|
f.checkpointTail = maxWALEnd
|
|
f.mu.Unlock()
|
|
|
|
// Advance WAL tail to free space.
|
|
if maxWALEnd > 0 {
|
|
f.wal.AdvanceTail(maxWALEnd)
|
|
}
|
|
|
|
// Update superblock checkpoint.
|
|
f.updateSuperblockCheckpoint(maxLSN, f.wal.Tail())
|
|
|
|
return nil
|
|
}
|
|
|
|
// updateSuperblockCheckpoint writes the updated checkpoint to disk.
|
|
func (f *Flusher) updateSuperblockCheckpoint(checkpointLSN uint64, walTail uint64) error {
|
|
f.super.WALCheckpointLSN = checkpointLSN
|
|
f.super.WALHead = f.wal.LogicalHead()
|
|
f.super.WALTail = f.wal.LogicalTail()
|
|
|
|
if _, err := f.fd.Seek(0, 0); err != nil {
|
|
return fmt.Errorf("flusher: seek to superblock: %w", err)
|
|
}
|
|
if _, err := f.super.WriteTo(f.fd); err != nil {
|
|
return fmt.Errorf("flusher: write superblock: %w", err)
|
|
}
|
|
return f.fd.Sync()
|
|
}
|
|
|
|
// CheckpointLSN returns the last flushed LSN.
|
|
func (f *Flusher) CheckpointLSN() uint64 {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.checkpointLSN
|
|
}
|
|
|
|
// SetFD replaces the file descriptor used for extent writes. Test-only.
|
|
func (f *Flusher) SetFD(fd *os.File) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.fd = fd
|
|
}
|
|
|
|
// parseLength extracts the Length field from a WAL entry header buffer.
|
|
func parseLength(headerBuf []byte) uint32 {
|
|
// Length at LSN(8)+Epoch(8)+Type(1)+Flags(1)+LBA(8) = 26
|
|
return binary.LittleEndian.Uint32(headerBuf[26:])
|
|
}
|