Files
seaweedfs/weed/storage/blockvol/wal_entry.go
T
Ping QiuandClaude Opus 4.6 6a400f6760 feat: add BlockVol engine and iSCSI target (Phase 1 + Phase 2)
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>
2026-02-28 09:32:01 -08:00

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
}