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:
@@ -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")
|
||||
Reference in New Issue
Block a user