Append-only WAL with CRC32-validated records, automatic segment rotation at 128 MiB, background periodic fsync, and crash recovery by truncation at the first corrupt or incomplete record. Record format: type(1) | len(4) | payload | crc32c(4). Two payload types: series (ref + labels) and samples (batch of ref/t/v). Fixed-width encoding througout. Torn-write harness truncates at every byte offset and asserts recovery produces a valid prefix of the original sequnce, both single-segment and multi-segment. DESIGN.md, NOTICE.md, and README.md.
252 lines
4.9 KiB
Go
252 lines
4.9 KiB
Go
package wal
|
|
|
|
import (
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Options configures WAL behavior.
|
|
type Options struct {
|
|
// SegmentMaxSize is the maximum size of a single segment file in bytes.
|
|
// A new segment is created when the current one would exceed this.
|
|
// Default: 128 MiB.
|
|
SegmentMaxSize int
|
|
|
|
// SyncInterval controls background fsync frequency.
|
|
// Default (zero): 1s. Negative: sync on every Log call.
|
|
SyncInterval time.Duration
|
|
}
|
|
|
|
func (o *Options) segmentMaxSize() int {
|
|
if o.SegmentMaxSize > 0 {
|
|
return o.SegmentMaxSize
|
|
}
|
|
return defaultSegmentMaxSize
|
|
}
|
|
|
|
func (o *Options) syncInterval() time.Duration {
|
|
if o.SyncInterval < 0 {
|
|
return -1 // sync-per-write sentinel
|
|
}
|
|
if o.SyncInterval == 0 {
|
|
return time.Second
|
|
}
|
|
return o.SyncInterval
|
|
}
|
|
|
|
// WAL is a segmented write-ahead log.
|
|
type WAL struct {
|
|
dir string
|
|
opts Options
|
|
|
|
mu sync.Mutex
|
|
segment *os.File
|
|
segmentIdx int
|
|
segmentOff int64
|
|
buf []byte
|
|
|
|
done chan struct{}
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
// Open opens or creates a WAL in dir. If segments already exist, it runs
|
|
// recovery (truncating at the first corrupt record) before returning.
|
|
func Open(dir string, opts Options) (*WAL, error) {
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Recover existing segments.
|
|
if err := recover(dir); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
w := &WAL{
|
|
dir: dir,
|
|
opts: opts,
|
|
done: make(chan struct{}),
|
|
}
|
|
|
|
// Open or create the active segment.
|
|
segs, err := listSegments(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(segs) == 0 {
|
|
// Fresh WAL.
|
|
w.segmentIdx = 1
|
|
f, err := createSegment(dir, 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
w.segment = f
|
|
} else {
|
|
// Append to the last segment.
|
|
idx := segs[len(segs)-1]
|
|
w.segmentIdx = idx
|
|
f, err := os.OpenFile(segmentPath(dir, idx), os.O_WRONLY|os.O_APPEND, 0644)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
info, err := f.Stat()
|
|
if err != nil {
|
|
f.Close()
|
|
return nil, err
|
|
}
|
|
w.segment = f
|
|
w.segmentOff = info.Size()
|
|
}
|
|
|
|
// Start background syncer.
|
|
if interval := opts.syncInterval(); interval > 0 {
|
|
w.wg.Add(1)
|
|
go w.syncLoop(interval)
|
|
}
|
|
|
|
return w, nil
|
|
}
|
|
|
|
// Log writes a framed record to the WAL. The payload is wrapped with
|
|
// the record envelope (type + length + CRC).
|
|
func (w *WAL) Log(typ RecordType, payload []byte) error {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
w.buf = EncodeRecord(w.buf[:0], typ, payload)
|
|
|
|
// Rotate if this write would exceed the segment size limit.
|
|
if w.segmentOff+int64(len(w.buf)) > int64(w.opts.segmentMaxSize()) {
|
|
if err := w.rotate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
n, err := w.segment.Write(w.buf)
|
|
w.segmentOff += int64(n)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Sync-per-write mode.
|
|
if w.opts.syncInterval() < 0 {
|
|
return w.segment.Sync()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LogSeries encodes and writes a series record.
|
|
func (w *WAL) LogSeries(recs []SeriesRecord) error {
|
|
for _, rec := range recs {
|
|
payload := EncodeSeriesRecord(nil, rec)
|
|
if err := w.Log(RecordSeries, payload); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LogSamples encodes and writes a samples record.
|
|
func (w *WAL) LogSamples(samples []RefSample) error {
|
|
payload := EncodeSamplesRecord(nil, samples)
|
|
return w.Log(RecordSamples, payload)
|
|
}
|
|
|
|
// Replay returns a Reader over all WAL segments. The caller must Close
|
|
// the reader when done.
|
|
func (w *WAL) Replay() (*Reader, error) {
|
|
return NewReader(w.dir)
|
|
}
|
|
|
|
// Sync forces an fsync of the current segment.
|
|
func (w *WAL) Sync() error {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
return w.segment.Sync()
|
|
}
|
|
|
|
// Truncate deletes all segments with index less than below.
|
|
func (w *WAL) Truncate(below int) error {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
segs, err := listSegments(w.dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, idx := range segs {
|
|
if idx >= below {
|
|
break
|
|
}
|
|
if err := os.Remove(segmentPath(w.dir, idx)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LastSegment returns the index of the current active segment.
|
|
func (w *WAL) LastSegment() int {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
return w.segmentIdx
|
|
}
|
|
|
|
// Close stops the background syncer, fsyncs, and closes the active segment.
|
|
func (w *WAL) Close() error {
|
|
close(w.done)
|
|
w.wg.Wait()
|
|
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
if w.segment == nil {
|
|
return nil
|
|
}
|
|
if err := w.segment.Sync(); err != nil {
|
|
w.segment.Close()
|
|
return err
|
|
}
|
|
return w.segment.Close()
|
|
}
|
|
|
|
// rotate fsyncs the current segment, closes it, and creates a new one.
|
|
// Caller must hold w.mu.
|
|
func (w *WAL) rotate() error {
|
|
if err := w.segment.Sync(); err != nil {
|
|
return err
|
|
}
|
|
if err := w.segment.Close(); err != nil {
|
|
return err
|
|
}
|
|
|
|
w.segmentIdx++
|
|
f, err := createSegment(w.dir, w.segmentIdx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.segment = f
|
|
w.segmentOff = 0
|
|
return nil
|
|
}
|
|
|
|
func (w *WAL) syncLoop(interval time.Duration) {
|
|
defer w.wg.Done()
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-w.done:
|
|
return
|
|
case <-ticker.C:
|
|
w.mu.Lock()
|
|
w.segment.Sync()
|
|
w.mu.Unlock()
|
|
}
|
|
}
|
|
}
|