mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-26 18:04:33 +00:00
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>
157 lines
3.8 KiB
Go
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
|
|
}
|