Files
seaweedfs/weed/storage/blockvol/superblock.go
T
Ping QiuandClaude Opus 4.6 1b3edd7856 feat: CP11A-2 coordinated expand protocol for replicated block volumes
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>
2026-03-12 15:06:48 -07:00

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
}