mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-09 01:26:13 +00:00
Two-phase prepare/commit/cancel protocol ensures all replicas expand atomically. Standalone volumes use direct-commit (unchanged behavior). Engine: PrepareExpand/CommitExpand/CancelExpand with on-disk PreparedSize+ExpandEpoch in superblock, crash recovery clears stale prepare state on open, v.mu serializes concurrent expand operations. Proto: 3 new RPCs (PrepareExpand/CommitExpand/CancelExpandBlockVolume). Coordinator: expandClean flag pattern — ReleaseExpandInflight only on clean success or full cancel. Partial replica commit failure calls MarkExpandFailed (keeps ExpandInProgress=true, suppresses heartbeat size updates). ClearExpandFailed for manual reconciliation. Registry: AcquireExpandInflight records PendingExpandSize+ExpandEpoch. ExpandFailed state blocks new expands until cleared. Tests: 15 engine + 4 VS + 10 coordinator + heartbeat suppression regression + updated QA CP82/durability tests with prepare/commit mocks. Also includes CP11A-1 remaining: QA storage profile tests, QA io_backend config tests, testrunner perf-baseline scenarios and coordinated-expand actions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
299 lines
8.4 KiB
Go
299 lines
8.4 KiB
Go
package blockvol
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
const (
|
|
SuperblockSize = 4096
|
|
MagicSWBK = "SWBK"
|
|
CurrentVersion = 1
|
|
)
|
|
|
|
var (
|
|
ErrNotBlockVol = errors.New("blockvol: not a blockvol file (bad magic)")
|
|
ErrUnsupportedVersion = errors.New("blockvol: unsupported version")
|
|
ErrInvalidVolumeSize = errors.New("blockvol: volume size must be > 0")
|
|
)
|
|
|
|
// Superblock is the 4KB header at offset 0 of a blockvol file.
|
|
// It identifies the file format, stores volume geometry, and tracks WAL state.
|
|
type Superblock struct {
|
|
Magic [4]byte
|
|
Version uint16
|
|
Flags uint16
|
|
UUID [16]byte
|
|
VolumeSize uint64 // logical size in bytes
|
|
ExtentSize uint32 // default 64KB
|
|
BlockSize uint32 // default 4KB (min I/O unit)
|
|
WALOffset uint64 // byte offset where WAL region starts
|
|
WALSize uint64 // WAL region size in bytes
|
|
WALHead uint64 // logical WAL write position (monotonically increasing)
|
|
WALTail uint64 // logical WAL flush position (monotonically increasing)
|
|
WALCheckpointLSN uint64 // last LSN flushed to extent region
|
|
Replication [4]byte
|
|
CreatedAt uint64 // unix timestamp
|
|
SnapshotCount uint32
|
|
Epoch uint64 // fencing epoch (0 = no fencing, Phase 3 compat)
|
|
DurabilityMode uint8 // CP8-3-1: 0=best_effort, 1=sync_all, 2=sync_quorum
|
|
StorageProfile uint8 // CP11A-1: 0=single, 1=striped (reserved)
|
|
PreparedSize uint64 // CP11A-2: pending expand size (0 = no expand in flight)
|
|
ExpandEpoch uint64 // CP11A-2: expand operation ID (0 = none)
|
|
}
|
|
|
|
// superblockOnDisk is the fixed-size on-disk layout (binary.Write/Read target).
|
|
// Must be <= SuperblockSize bytes. Remaining space is zero-padded.
|
|
type superblockOnDisk struct {
|
|
Magic [4]byte
|
|
Version uint16
|
|
Flags uint16
|
|
UUID [16]byte
|
|
VolumeSize uint64
|
|
ExtentSize uint32
|
|
BlockSize uint32
|
|
WALOffset uint64
|
|
WALSize uint64
|
|
WALHead uint64
|
|
WALTail uint64
|
|
WALCheckpointLSN uint64
|
|
Replication [4]byte
|
|
CreatedAt uint64
|
|
SnapshotCount uint32
|
|
Epoch uint64
|
|
DurabilityMode uint8
|
|
StorageProfile uint8
|
|
PreparedSize uint64
|
|
ExpandEpoch uint64
|
|
}
|
|
|
|
// NewSuperblock creates a superblock with defaults and a fresh UUID.
|
|
func NewSuperblock(volumeSize uint64, opts CreateOptions) (Superblock, error) {
|
|
if volumeSize == 0 {
|
|
return Superblock{}, ErrInvalidVolumeSize
|
|
}
|
|
|
|
extentSize := opts.ExtentSize
|
|
if extentSize == 0 {
|
|
extentSize = 64 * 1024 // 64KB
|
|
}
|
|
blockSize := opts.BlockSize
|
|
if blockSize == 0 {
|
|
blockSize = 4096 // 4KB
|
|
}
|
|
walSize := opts.WALSize
|
|
if walSize == 0 {
|
|
walSize = 64 * 1024 * 1024 // 64MB
|
|
}
|
|
|
|
var repl [4]byte
|
|
if opts.Replication != "" {
|
|
copy(repl[:], opts.Replication)
|
|
} else {
|
|
copy(repl[:], "000")
|
|
}
|
|
|
|
id := uuid.New()
|
|
|
|
sb := Superblock{
|
|
Version: CurrentVersion,
|
|
VolumeSize: volumeSize,
|
|
ExtentSize: extentSize,
|
|
BlockSize: blockSize,
|
|
WALOffset: SuperblockSize,
|
|
WALSize: walSize,
|
|
DurabilityMode: uint8(opts.DurabilityMode),
|
|
StorageProfile: uint8(opts.StorageProfile),
|
|
}
|
|
copy(sb.Magic[:], MagicSWBK)
|
|
sb.UUID = id
|
|
sb.Replication = repl
|
|
|
|
return sb, nil
|
|
}
|
|
|
|
// WriteTo serializes the superblock to w as a 4096-byte block.
|
|
func (sb *Superblock) WriteTo(w io.Writer) (int64, error) {
|
|
buf := make([]byte, SuperblockSize)
|
|
|
|
d := superblockOnDisk{
|
|
Magic: sb.Magic,
|
|
Version: sb.Version,
|
|
Flags: sb.Flags,
|
|
UUID: sb.UUID,
|
|
VolumeSize: sb.VolumeSize,
|
|
ExtentSize: sb.ExtentSize,
|
|
BlockSize: sb.BlockSize,
|
|
WALOffset: sb.WALOffset,
|
|
WALSize: sb.WALSize,
|
|
WALHead: sb.WALHead,
|
|
WALTail: sb.WALTail,
|
|
WALCheckpointLSN: sb.WALCheckpointLSN,
|
|
Replication: sb.Replication,
|
|
CreatedAt: sb.CreatedAt,
|
|
SnapshotCount: sb.SnapshotCount,
|
|
Epoch: sb.Epoch,
|
|
DurabilityMode: sb.DurabilityMode,
|
|
StorageProfile: sb.StorageProfile,
|
|
PreparedSize: sb.PreparedSize,
|
|
ExpandEpoch: sb.ExpandEpoch,
|
|
}
|
|
|
|
// Encode into beginning of buf; rest stays zero (padding).
|
|
endian := binary.LittleEndian
|
|
off := 0
|
|
off += copy(buf[off:], d.Magic[:])
|
|
endian.PutUint16(buf[off:], d.Version)
|
|
off += 2
|
|
endian.PutUint16(buf[off:], d.Flags)
|
|
off += 2
|
|
off += copy(buf[off:], d.UUID[:])
|
|
endian.PutUint64(buf[off:], d.VolumeSize)
|
|
off += 8
|
|
endian.PutUint32(buf[off:], d.ExtentSize)
|
|
off += 4
|
|
endian.PutUint32(buf[off:], d.BlockSize)
|
|
off += 4
|
|
endian.PutUint64(buf[off:], d.WALOffset)
|
|
off += 8
|
|
endian.PutUint64(buf[off:], d.WALSize)
|
|
off += 8
|
|
endian.PutUint64(buf[off:], d.WALHead)
|
|
off += 8
|
|
endian.PutUint64(buf[off:], d.WALTail)
|
|
off += 8
|
|
endian.PutUint64(buf[off:], d.WALCheckpointLSN)
|
|
off += 8
|
|
off += copy(buf[off:], d.Replication[:])
|
|
endian.PutUint64(buf[off:], d.CreatedAt)
|
|
off += 8
|
|
endian.PutUint32(buf[off:], d.SnapshotCount)
|
|
off += 4
|
|
endian.PutUint64(buf[off:], d.Epoch)
|
|
off += 8
|
|
buf[off] = d.DurabilityMode
|
|
off++
|
|
buf[off] = d.StorageProfile
|
|
off++
|
|
endian.PutUint64(buf[off:], d.PreparedSize)
|
|
off += 8
|
|
endian.PutUint64(buf[off:], d.ExpandEpoch)
|
|
|
|
n, err := w.Write(buf)
|
|
return int64(n), err
|
|
}
|
|
|
|
// ReadSuperblock reads and validates a superblock from r.
|
|
func ReadSuperblock(r io.Reader) (Superblock, error) {
|
|
buf := make([]byte, SuperblockSize)
|
|
if _, err := io.ReadFull(r, buf); err != nil {
|
|
return Superblock{}, fmt.Errorf("blockvol: read superblock: %w", err)
|
|
}
|
|
|
|
endian := binary.LittleEndian
|
|
var sb Superblock
|
|
off := 0
|
|
copy(sb.Magic[:], buf[off:off+4])
|
|
off += 4
|
|
|
|
if string(sb.Magic[:]) != MagicSWBK {
|
|
return Superblock{}, ErrNotBlockVol
|
|
}
|
|
|
|
sb.Version = endian.Uint16(buf[off:])
|
|
off += 2
|
|
if sb.Version != CurrentVersion {
|
|
return Superblock{}, fmt.Errorf("%w: got %d, want %d", ErrUnsupportedVersion, sb.Version, CurrentVersion)
|
|
}
|
|
|
|
sb.Flags = endian.Uint16(buf[off:])
|
|
off += 2
|
|
copy(sb.UUID[:], buf[off:off+16])
|
|
off += 16
|
|
sb.VolumeSize = endian.Uint64(buf[off:])
|
|
off += 8
|
|
|
|
if sb.VolumeSize == 0 {
|
|
return Superblock{}, ErrInvalidVolumeSize
|
|
}
|
|
|
|
sb.ExtentSize = endian.Uint32(buf[off:])
|
|
off += 4
|
|
sb.BlockSize = endian.Uint32(buf[off:])
|
|
off += 4
|
|
sb.WALOffset = endian.Uint64(buf[off:])
|
|
off += 8
|
|
sb.WALSize = endian.Uint64(buf[off:])
|
|
off += 8
|
|
sb.WALHead = endian.Uint64(buf[off:])
|
|
off += 8
|
|
sb.WALTail = endian.Uint64(buf[off:])
|
|
off += 8
|
|
sb.WALCheckpointLSN = endian.Uint64(buf[off:])
|
|
off += 8
|
|
copy(sb.Replication[:], buf[off:off+4])
|
|
off += 4
|
|
sb.CreatedAt = endian.Uint64(buf[off:])
|
|
off += 8
|
|
sb.SnapshotCount = endian.Uint32(buf[off:])
|
|
off += 4
|
|
sb.Epoch = endian.Uint64(buf[off:])
|
|
off += 8
|
|
sb.DurabilityMode = buf[off]
|
|
off++
|
|
sb.StorageProfile = buf[off]
|
|
off++
|
|
sb.PreparedSize = endian.Uint64(buf[off:])
|
|
off += 8
|
|
sb.ExpandEpoch = endian.Uint64(buf[off:])
|
|
|
|
return sb, nil
|
|
}
|
|
|
|
var ErrInvalidSuperblock = errors.New("blockvol: invalid superblock")
|
|
|
|
// Validate checks that the superblock fields are internally consistent.
|
|
func (sb *Superblock) Validate() error {
|
|
if string(sb.Magic[:]) != MagicSWBK {
|
|
return ErrNotBlockVol
|
|
}
|
|
if sb.Version != CurrentVersion {
|
|
return fmt.Errorf("%w: got %d", ErrUnsupportedVersion, sb.Version)
|
|
}
|
|
if sb.VolumeSize == 0 {
|
|
return ErrInvalidVolumeSize
|
|
}
|
|
if sb.BlockSize == 0 {
|
|
return fmt.Errorf("%w: BlockSize is 0", ErrInvalidSuperblock)
|
|
}
|
|
if sb.ExtentSize == 0 {
|
|
return fmt.Errorf("%w: ExtentSize is 0", ErrInvalidSuperblock)
|
|
}
|
|
if sb.WALSize == 0 {
|
|
return fmt.Errorf("%w: WALSize is 0", ErrInvalidSuperblock)
|
|
}
|
|
if sb.WALOffset != SuperblockSize {
|
|
return fmt.Errorf("%w: WALOffset=%d, expected %d", ErrInvalidSuperblock, sb.WALOffset, SuperblockSize)
|
|
}
|
|
if sb.VolumeSize%uint64(sb.BlockSize) != 0 {
|
|
return fmt.Errorf("%w: VolumeSize %d not aligned to BlockSize %d", ErrInvalidSuperblock, sb.VolumeSize, sb.BlockSize)
|
|
}
|
|
if sb.DurabilityMode > 2 {
|
|
return fmt.Errorf("%w: invalid DurabilityMode %d", ErrInvalidSuperblock, sb.DurabilityMode)
|
|
}
|
|
if sb.StorageProfile > 1 {
|
|
return fmt.Errorf("%w: invalid StorageProfile %d", ErrInvalidSuperblock, sb.StorageProfile)
|
|
}
|
|
if sb.PreparedSize != 0 && sb.PreparedSize <= sb.VolumeSize {
|
|
return fmt.Errorf("%w: PreparedSize %d must be > VolumeSize %d", ErrInvalidSuperblock, sb.PreparedSize, sb.VolumeSize)
|
|
}
|
|
if sb.PreparedSize == 0 && sb.ExpandEpoch != 0 {
|
|
return fmt.Errorf("%w: ExpandEpoch %d must be 0 when PreparedSize is 0", ErrInvalidSuperblock, sb.ExpandEpoch)
|
|
}
|
|
return nil
|
|
}
|