feat(wal,memtable): implement segment writer, block writer, and MemTable

- wal/block_writer.go: 32KB block buffer with padding and flush
- wal/segment_writer.go: WAL segment file with durable-ready protocol
- memtable/memtable.go: Arena+SkipList wrapper with publish/abort semantics
- Comprehensive tests for all modules, all pass with -race
This commit is contained in:
dailz
2026-06-12 13:43:24 +08:00
parent 3c0e3b14ff
commit 349063968b
8 changed files with 1407 additions and 4 deletions
+147
View File
@@ -0,0 +1,147 @@
package wal
import (
"errors"
"fmt"
"io"
)
// BlockWriter manages a single 32 KB block buffer for writing physical records.
// It handles block boundary padding and flushing complete blocks to an io.Writer.
type BlockWriter struct {
buf [WalBlockSize]byte
offset uint32 // current write position within the block
}
// NewBlockWriter creates a BlockWriter ready to write into a fresh block.
func NewBlockWriter() *BlockWriter {
return &BlockWriter{}
}
// BlockOffset returns the current write offset within the block (0..WalBlockSize).
func (bw *BlockWriter) BlockOffset() uint32 {
return bw.offset
}
// WriteRecord writes a single physical record into the block buffer.
// If the record (header + payload) does not fit in the remaining space,
// the current block is padded with zeros and flushed to w, then the record
// is written at the start of a fresh block.
//
// Precondition: payload length must be ≤ WalBlockSize - PhysicalRecordHeaderSize
// (the caller is responsible for splitting large batches into appropriately-sized chunks).
func (bw *BlockWriter) WriteRecord(recType uint8, payload []byte, w io.Writer) error {
recordSize := PhysicalRecordHeaderSize + len(payload)
if recordSize > WalBlockSize {
return fmt.Errorf("wal: record size %d exceeds block size %d",
recordSize, WalBlockSize)
}
// Check if padding is needed before writing this record.
pad := bw.paddingNeeded()
if pad > 0 {
// Pad remaining bytes with zeros and flush.
if err := bw.flushPadded(w, pad); err != nil {
return fmt.Errorf("wal: flushing padded block: %w", err)
}
}
// Check if the record fits in the current block.
remaining := WalBlockSize - bw.offset
if uint32(recordSize) > remaining {
// Not enough room — pad the rest and flush, then start a new block.
pad = int(remaining)
if err := bw.flushPadded(w, pad); err != nil {
return fmt.Errorf("wal: flushing partial block: %w", err)
}
}
// Encode physical record directly into the block buffer.
encoded := EncodePhysicalRecord(recType, payload)
copy(bw.buf[bw.offset:], encoded)
bw.offset += uint32(len(encoded))
// If the block is exactly full, flush it immediately.
if bw.offset == WalBlockSize {
if _, err := w.Write(bw.buf[:]); err != nil {
return fmt.Errorf("wal: writing full block: %w", err)
}
bw.offset = 0
}
return nil
}
// Flush writes the current block buffer to w, padding unused bytes with zeros.
// If the block is empty (offset == 0), this is a no-op.
func (bw *BlockWriter) Flush(w io.Writer) error {
if bw.offset == 0 {
return nil
}
return bw.flushPadded(w, int(WalBlockSize-bw.offset))
}
// Reset clears the block buffer, returning it to an empty state.
func (bw *BlockWriter) Reset() {
bw.offset = 0
// Zero the buffer so partial blocks are padded with zeros.
for i := range bw.buf {
bw.buf[i] = 0
}
}
// paddingNeeded returns the number of zero-padding bytes required at the current
// block offset. When the remaining space in the block is ≤ PhysicalRecordHeaderSize (7),
// that space cannot hold even a minimal physical record and must be zero-padded.
func (bw *BlockWriter) paddingNeeded() int {
remaining := WalBlockSize - bw.offset
if remaining <= PhysicalRecordHeaderSize {
return int(remaining)
}
return 0
}
// flushPadded pads the remaining bytes with zeros and writes the full block to w.
// pad is the number of trailing bytes to zero-fill (WalBlockSize - offset - pad already zero
// from initial state or previous Reset).
func (bw *BlockWriter) flushPadded(w io.Writer, pad int) error {
if pad <= 0 {
return nil
}
// Zero-fill padding region. The buffer was zeroed at init/reset,
// but we write explicitly for safety after partial record writes.
for i := uint32(0); i < uint32(pad); i++ {
bw.buf[bw.offset+i] = 0
}
if _, err := w.Write(bw.buf[:]); err != nil {
return err
}
bw.offset = 0
for i := range bw.buf {
bw.buf[i] = 0
}
return nil
}
// Bytes returns a copy of the current block contents up to the current offset.
// Useful for testing.
func (bw *BlockWriter) Bytes() []byte {
out := make([]byte, bw.offset)
copy(out, bw.buf[:bw.offset])
return out
}
// FullBlockBytes returns the full block buffer. Only valid when offset == WalBlockSize.
func (bw *BlockWriter) FullBlockBytes() []byte {
out := make([]byte, WalBlockSize)
copy(out, bw.buf[:])
return out
}
// errBlockWriterNil is returned when a nil writer is passed to write operations.
var errBlockWriterNil = errors.New("wal: writer must not be nil")