Files
seaweedfs/weed/storage/blockvol/dirty_map.go
T
Ping QiuandClaude Opus 4.6 b31383e294 feat: Phase 4A CP3 -- promotion, rebuild, split-brain prevention
Add master-driven lifecycle operations: promotion, demotion, rebuild,
and split-brain prevention. All testable on Windows with mock TCP.

New files:
- promotion.go: HandleAssignment (single entry point for role changes),
  promote (Replica/None -> Primary with durable epoch), demote
  (Primary -> Draining -> Stale with drain timeout)
- rebuild.go: RebuildServer (WAL catch-up + full extent streaming),
  StartRebuild client (WAL catch-up with full extent fallback,
  two-phase rebuild with second catch-up for concurrent writes)

Modified:
- wal_writer.go: ScanFrom() method, ErrWALRecycled sentinel
- repl_proto.go: rebuild message types + RebuildRequest encode/decode
- blockvol.go: assignMu, drainTimeout, rebuildServer fields;
  HandleAssignment/StartRebuildServer/StopRebuildServer methods;
  rebuild server stop in Close()
- dirty_map.go: Clear() method for full extent rebuild

32 new tests covering WAL scan, promotion/demotion, rebuild server,
rebuild client, split-brain prevention, and full lifecycle scenarios.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:26:39 -08:00

157 lines
3.8 KiB
Go

package blockvol
import "sync"
// dirtyEntry tracks a single LBA's location in the WAL.
type dirtyEntry struct {
walOffset uint64
lsn uint64
length uint32
}
// dirtyShard is one shard of the sharded dirty map.
type dirtyShard struct {
mu sync.RWMutex
m map[uint64]dirtyEntry
}
// DirtyMap is a concurrency-safe map from LBA to WAL offset.
// It uses sharding to reduce lock contention on concurrent workloads.
type DirtyMap struct {
shards []dirtyShard
mask uint64 // numShards - 1, for fast modulo
}
// NewDirtyMap creates an empty dirty map with the given number of shards.
// numShards must be a power-of-2. Use 1 for a single-shard (unsharded) map.
func NewDirtyMap(numShards int) *DirtyMap {
if numShards <= 0 {
numShards = 1
}
if numShards&(numShards-1) != 0 {
panic("blockvol: NewDirtyMap numShards must be power of 2")
}
shards := make([]dirtyShard, numShards)
for i := range shards {
shards[i].m = make(map[uint64]dirtyEntry)
}
return &DirtyMap{
shards: shards,
mask: uint64(numShards - 1),
}
}
// shard returns the shard for the given LBA.
func (d *DirtyMap) shard(lba uint64) *dirtyShard {
return &d.shards[lba&d.mask]
}
// Put records that the given LBA has dirty data at walOffset.
func (d *DirtyMap) Put(lba uint64, walOffset uint64, lsn uint64, length uint32) {
s := d.shard(lba)
s.mu.Lock()
s.m[lba] = dirtyEntry{walOffset: walOffset, lsn: lsn, length: length}
s.mu.Unlock()
}
// Get returns the dirty entry for lba. ok is false if lba is not dirty.
func (d *DirtyMap) Get(lba uint64) (walOffset uint64, lsn uint64, length uint32, ok bool) {
s := d.shard(lba)
s.mu.RLock()
e, found := s.m[lba]
s.mu.RUnlock()
if !found {
return 0, 0, 0, false
}
return e.walOffset, e.lsn, e.length, true
}
// Delete removes a single LBA from the dirty map.
func (d *DirtyMap) Delete(lba uint64) {
s := d.shard(lba)
s.mu.Lock()
delete(s.m, lba)
s.mu.Unlock()
}
// rangeEntry is a snapshot of one dirty entry for lock-free iteration.
type rangeEntry struct {
lba uint64
walOffset uint64
lsn uint64
length uint32
}
// Range calls fn for each dirty entry with LBA in [start, start+count).
// Entries are copied under lock (one shard at a time), then fn is called
// without holding any lock, so fn may safely call back into DirtyMap.
func (d *DirtyMap) Range(start uint64, count uint32, fn func(lba, walOffset, lsn uint64, length uint32)) {
end := start + uint64(count)
var entries []rangeEntry
for i := range d.shards {
s := &d.shards[i]
s.mu.RLock()
for lba, e := range s.m {
if lba >= start && lba < end {
entries = append(entries, rangeEntry{lba, e.walOffset, e.lsn, e.length})
}
}
s.mu.RUnlock()
}
for _, e := range entries {
fn(e.lba, e.walOffset, e.lsn, e.length)
}
}
// Clear removes all entries from the dirty map.
func (d *DirtyMap) Clear() {
for i := range d.shards {
s := &d.shards[i]
s.mu.Lock()
s.m = make(map[uint64]dirtyEntry)
s.mu.Unlock()
}
}
// Len returns the number of dirty entries across all shards.
func (d *DirtyMap) Len() int {
n := 0
for i := range d.shards {
s := &d.shards[i]
s.mu.RLock()
n += len(s.m)
s.mu.RUnlock()
}
return n
}
// SnapshotEntry is an exported snapshot of one dirty entry.
type SnapshotEntry struct {
Lba uint64
WalOffset uint64
Lsn uint64
Length uint32
}
// Snapshot returns a copy of all dirty entries. Each shard is locked
// individually, and the result is returned without holding any lock.
func (d *DirtyMap) Snapshot() []SnapshotEntry {
var entries []SnapshotEntry
for i := range d.shards {
s := &d.shards[i]
s.mu.RLock()
for lba, e := range s.m {
entries = append(entries, SnapshotEntry{
Lba: lba,
WalOffset: e.walOffset,
Lsn: e.lsn,
Length: e.length,
})
}
s.mu.RUnlock()
}
return entries
}