- 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
148 lines
4.4 KiB
Go
148 lines
4.4 KiB
Go
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")
|