feat(wal): implement WAL writer with group commit and recovery batch replay
- wal/commit_queue.go: bounded buffered channel for write requests - wal/writer.go: single-goroutine main loop implementing 11-step write flow with group commit, sequence allocation, MemTable publish/abort, write-stopped - wal/recovery.go: BatchReplayer interface, ReplayBatch, ReplaySegmentFile, RecoverFromSegments with fragment reassembly and tail corruption handling - Comprehensive tests for all modules, all pass with -race
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
package wal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/dailz/go-kv/config"
|
||||
)
|
||||
|
||||
func newTestWalWriter(t *testing.T) *WalWriter {
|
||||
t.Helper()
|
||||
|
||||
cfg := config.Defaults()
|
||||
ww, err := NewWalWriter(&cfg, t.TempDir(), 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("NewWalWriter: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := ww.Close(); err != nil {
|
||||
t.Fatalf("Close: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
return ww
|
||||
}
|
||||
|
||||
func TestWalWriterSinglePut(t *testing.T) {
|
||||
ww := newTestWalWriter(t)
|
||||
|
||||
if err := ww.Put([]byte("k1"), []byte("v1")); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
|
||||
got := ww.Get([]byte("k1"))
|
||||
if !got.Found {
|
||||
t.Fatal("Get(k1) not found")
|
||||
}
|
||||
if string(got.Value) != "v1" {
|
||||
t.Fatalf("Get(k1) value = %q, want %q", got.Value, "v1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalWriterMultiplePuts(t *testing.T) {
|
||||
ww := newTestWalWriter(t)
|
||||
|
||||
for i := range 10 {
|
||||
key := fmt.Appendf(nil, "k%d", i)
|
||||
value := fmt.Appendf(nil, "v%d", i)
|
||||
if err := ww.Put(key, value); err != nil {
|
||||
t.Fatalf("Put(%q): %v", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range 10 {
|
||||
key := fmt.Appendf(nil, "k%d", i)
|
||||
want := fmt.Sprintf("v%d", i)
|
||||
got := ww.Get(key)
|
||||
if !got.Found {
|
||||
t.Fatalf("Get(%q) not found", key)
|
||||
}
|
||||
if string(got.Value) != want {
|
||||
t.Fatalf("Get(%q) value = %q, want %q", key, got.Value, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalWriterGroupCommit(t *testing.T) {
|
||||
ww := newTestWalWriter(t)
|
||||
|
||||
const writers = 5
|
||||
var wg sync.WaitGroup
|
||||
errCh := make(chan error, writers)
|
||||
|
||||
for i := range writers {
|
||||
wg.Go(func() {
|
||||
key := fmt.Appendf(nil, "group-k%d", i)
|
||||
value := fmt.Appendf(nil, "group-v%d", i)
|
||||
if err := ww.Put(key, value); err != nil {
|
||||
errCh <- fmt.Errorf("put %d: %w", i, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errCh)
|
||||
for err := range errCh {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range writers {
|
||||
key := fmt.Appendf(nil, "group-k%d", i)
|
||||
want := fmt.Sprintf("group-v%d", i)
|
||||
got := ww.Get(key)
|
||||
if !got.Found {
|
||||
t.Fatalf("Get(%q) not found", key)
|
||||
}
|
||||
if string(got.Value) != want {
|
||||
t.Fatalf("Get(%q) value = %q, want %q", key, got.Value, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalWriterDelete(t *testing.T) {
|
||||
ww := newTestWalWriter(t)
|
||||
|
||||
if err := ww.Put([]byte("k"), []byte("v")); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
if err := ww.Delete([]byte("k")); err != nil {
|
||||
t.Fatalf("Delete: %v", err)
|
||||
}
|
||||
|
||||
got := ww.Get([]byte("k"))
|
||||
if got.Found {
|
||||
t.Fatalf("Get(k) found deleted key with value %q", got.Value)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user