mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-09 01:26:13 +00:00
Phase 1: Extent-mapped block storage engine with WAL, crash recovery, dirty map, flusher, and group commit. 174 tests, zero SeaweedFS imports. Phase 2: Pure Go iSCSI target (RFC 7143) with PDU codec, login negotiation, SendTargets discovery, 12 SCSI opcodes, Data-In/Out/R2T sequencing, session management, and standalone iscsi-target binary. 164 tests. IQN->BlockDevice binding via DeviceLookup interface. Total: 338 tests, 14.6K lines. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
154 lines
4.1 KiB
Go
154 lines
4.1 KiB
Go
package blockvol
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"hash/crc32"
|
|
)
|
|
|
|
const (
|
|
EntryTypeWrite = 0x01
|
|
EntryTypeTrim = 0x02
|
|
EntryTypeBarrier = 0x03
|
|
EntryTypePadding = 0xFF
|
|
|
|
// walEntryHeaderSize is the fixed portion: LSN(8) + Epoch(8) + Type(1) +
|
|
// Flags(1) + LBA(8) + Length(4) + CRC32(4) + EntrySize(4) = 38 bytes.
|
|
walEntryHeaderSize = 38
|
|
)
|
|
|
|
var (
|
|
ErrCRCMismatch = errors.New("blockvol: CRC mismatch")
|
|
ErrInvalidEntry = errors.New("blockvol: invalid WAL entry")
|
|
ErrEntryTruncated = errors.New("blockvol: WAL entry truncated")
|
|
)
|
|
|
|
// WALEntry is a variable-size record in the WAL region.
|
|
type WALEntry struct {
|
|
LSN uint64
|
|
Epoch uint64 // writer's epoch (Phase 1: always 0)
|
|
Type uint8 // EntryTypeWrite, EntryTypeTrim, EntryTypeBarrier
|
|
Flags uint8
|
|
LBA uint64 // in blocks
|
|
Length uint32 // data length in bytes (WRITE: data size, TRIM: trim size, BARRIER: 0)
|
|
Data []byte // present only for WRITE
|
|
CRC32 uint32 // covers LSN through Data
|
|
EntrySize uint32 // total serialized size
|
|
}
|
|
|
|
// Encode serializes the entry into a byte slice, computing CRC over all fields.
|
|
func (e *WALEntry) Encode() ([]byte, error) {
|
|
switch e.Type {
|
|
case EntryTypeWrite:
|
|
if len(e.Data) == 0 {
|
|
return nil, fmt.Errorf("%w: WRITE entry with no data", ErrInvalidEntry)
|
|
}
|
|
if uint32(len(e.Data)) != e.Length {
|
|
return nil, fmt.Errorf("%w: data length %d != Length field %d", ErrInvalidEntry, len(e.Data), e.Length)
|
|
}
|
|
case EntryTypeTrim:
|
|
if len(e.Data) != 0 {
|
|
return nil, fmt.Errorf("%w: TRIM entry must have no data payload", ErrInvalidEntry)
|
|
}
|
|
// TRIM carries Length (trim size in bytes) but no Data payload.
|
|
case EntryTypeBarrier:
|
|
if e.Length != 0 || len(e.Data) != 0 {
|
|
return nil, fmt.Errorf("%w: BARRIER entry must have no data", ErrInvalidEntry)
|
|
}
|
|
}
|
|
|
|
totalSize := uint32(walEntryHeaderSize + len(e.Data))
|
|
buf := make([]byte, totalSize)
|
|
|
|
le := binary.LittleEndian
|
|
off := 0
|
|
le.PutUint64(buf[off:], e.LSN)
|
|
off += 8
|
|
le.PutUint64(buf[off:], e.Epoch)
|
|
off += 8
|
|
buf[off] = e.Type
|
|
off++
|
|
buf[off] = e.Flags
|
|
off++
|
|
le.PutUint64(buf[off:], e.LBA)
|
|
off += 8
|
|
le.PutUint32(buf[off:], e.Length)
|
|
off += 4
|
|
|
|
if len(e.Data) > 0 {
|
|
copy(buf[off:], e.Data)
|
|
off += len(e.Data)
|
|
}
|
|
|
|
// CRC covers everything from start through Data (offset 0 to off).
|
|
checksum := crc32.ChecksumIEEE(buf[:off])
|
|
le.PutUint32(buf[off:], checksum)
|
|
off += 4
|
|
le.PutUint32(buf[off:], totalSize)
|
|
|
|
e.CRC32 = checksum
|
|
e.EntrySize = totalSize
|
|
return buf, nil
|
|
}
|
|
|
|
// DecodeWALEntry deserializes a WAL entry from buf, validating CRC.
|
|
func DecodeWALEntry(buf []byte) (WALEntry, error) {
|
|
if len(buf) < walEntryHeaderSize {
|
|
return WALEntry{}, fmt.Errorf("%w: need %d bytes, have %d", ErrEntryTruncated, walEntryHeaderSize, len(buf))
|
|
}
|
|
|
|
le := binary.LittleEndian
|
|
var e WALEntry
|
|
off := 0
|
|
e.LSN = le.Uint64(buf[off:])
|
|
off += 8
|
|
e.Epoch = le.Uint64(buf[off:])
|
|
off += 8
|
|
e.Type = buf[off]
|
|
off++
|
|
e.Flags = buf[off]
|
|
off++
|
|
e.LBA = le.Uint64(buf[off:])
|
|
off += 8
|
|
e.Length = le.Uint32(buf[off:])
|
|
off += 4
|
|
|
|
// For WRITE entries, Length is the data payload size.
|
|
// For TRIM entries, Length is the trim extent in bytes (no data payload).
|
|
// For BARRIER/PADDING, Length is 0 (or padding size).
|
|
var dataLen int
|
|
if e.Type == EntryTypeWrite || e.Type == EntryTypePadding {
|
|
dataLen = int(e.Length)
|
|
}
|
|
|
|
dataEnd := off + dataLen
|
|
if dataEnd+8 > len(buf) { // +8 for CRC32 + EntrySize
|
|
return WALEntry{}, fmt.Errorf("%w: need %d bytes for data+footer, have %d", ErrEntryTruncated, dataEnd+8, len(buf))
|
|
}
|
|
|
|
if dataLen > 0 {
|
|
e.Data = make([]byte, dataLen)
|
|
copy(e.Data, buf[off:dataEnd])
|
|
}
|
|
off = dataEnd
|
|
|
|
e.CRC32 = le.Uint32(buf[off:])
|
|
off += 4
|
|
e.EntrySize = le.Uint32(buf[off:])
|
|
|
|
// Verify CRC: covers LSN through Data.
|
|
expected := crc32.ChecksumIEEE(buf[:dataEnd])
|
|
if e.CRC32 != expected {
|
|
return WALEntry{}, fmt.Errorf("%w: stored=%08x computed=%08x", ErrCRCMismatch, e.CRC32, expected)
|
|
}
|
|
|
|
// Verify EntrySize matches actual layout.
|
|
expectedSize := uint32(walEntryHeaderSize) + uint32(dataLen)
|
|
if e.EntrySize != expectedSize {
|
|
return WALEntry{}, fmt.Errorf("%w: EntrySize=%d, expected=%d", ErrInvalidEntry, e.EntrySize, expectedSize)
|
|
}
|
|
|
|
return e, nil
|
|
}
|