test: add end-to-end tests and benchmark suite

- Add db_e2e_test.go: WAL roundtrip, partial write recovery, multi-segment
  recovery, Get semantics, concurrent read/write, large batch rotation tests
- Add bench_test.go: SinglePut, ConcurrentPut, Get, ConcurrentGet,
  MixedReadWrite, WALRecovery benchmarks with b.ReportAllocs()

Wave 5 complete (T20 + T21). All implementation tasks done.
This commit is contained in:
dailz
2026-06-12 14:20:16 +08:00
parent 0fe1530e25
commit 5905dbc06f
4 changed files with 558 additions and 4 deletions
+225
View File
@@ -0,0 +1,225 @@
package go_kv
import (
"fmt"
"sync"
"testing"
)
// openBenchDB opens a fresh database in b.TempDir() with default config.
// Returns the DB handle; caller must Close().
func openBenchDB(b *testing.B) *DB {
b.Helper()
db, err := Open(b.TempDir(), nil)
if err != nil {
b.Fatalf("Open: %v", err)
}
return db
}
// BenchmarkSinglePut measures single-threaded sequential Put throughput.
func BenchmarkSinglePut(b *testing.B) {
db := openBenchDB(b)
defer func() {
if err := db.Close(); err != nil {
b.Fatalf("Close: %v", err)
}
}()
b.ReportAllocs()
b.ResetTimer()
i := 0
for b.Loop() {
key := fmt.Appendf(nil, "key-%08d", i)
val := fmt.Appendf(nil, "value-%08d", i)
if err := db.Put(key, val); err != nil {
b.Fatalf("Put: %v", err)
}
i++
}
}
// BenchmarkConcurrentPut measures multi-goroutine Put throughput.
// Uses b.N directly because b.Loop() cannot be called from spawned goroutines.
func BenchmarkConcurrentPut(b *testing.B) {
for _, workers := range []int{2, 4, 8} {
b.Run(fmt.Sprintf("workers=%d", workers), func(b *testing.B) {
db := openBenchDB(b)
defer func() {
if err := db.Close(); err != nil {
b.Fatalf("Close: %v", err)
}
}()
b.ReportAllocs()
b.ResetTimer()
var wg sync.WaitGroup
opsPerWorker := b.N / workers
for w := range workers {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for i := range opsPerWorker {
key := fmt.Appendf(nil, "key-%d-%08d", workerID, i)
val := fmt.Appendf(nil, "value-%d-%08d", workerID, i)
if err := db.Put(key, val); err != nil {
b.Errorf("Put: %v", err)
return
}
}
}(w)
}
wg.Wait()
})
}
}
// BenchmarkGet measures Get latency after pre-loading 1000 keys.
func BenchmarkGet(b *testing.B) {
const numKeys = 1000
db := openBenchDB(b)
defer func() {
if err := db.Close(); err != nil {
b.Fatalf("Close: %v", err)
}
}()
// Pre-load keys.
for i := range numKeys {
key := fmt.Appendf(nil, "key-%08d", i)
val := fmt.Appendf(nil, "value-%08d", i)
if err := db.Put(key, val); err != nil {
b.Fatalf("Put preload: %v", err)
}
}
b.ReportAllocs()
b.ResetTimer()
i := 0
for b.Loop() {
key := fmt.Appendf(nil, "key-%08d", i%numKeys)
res := db.Get(key)
if !res.Found {
b.Fatalf("Get key %s: not found", key)
}
i++
}
}
// BenchmarkConcurrentGet measures parallel Get after pre-loading keys.
func BenchmarkConcurrentGet(b *testing.B) {
const numKeys = 1000
db := openBenchDB(b)
defer func() {
if err := db.Close(); err != nil {
b.Fatalf("Close: %v", err)
}
}()
// Pre-load keys.
for i := range numKeys {
key := fmt.Appendf(nil, "key-%08d", i)
val := fmt.Appendf(nil, "value-%08d", i)
if err := db.Put(key, val); err != nil {
b.Fatalf("Put preload: %v", err)
}
}
b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
key := fmt.Appendf(nil, "key-%08d", i%numKeys)
res := db.Get(key)
if !res.Found {
b.Fatalf("Get key %s: not found", key)
}
i++
}
})
}
// BenchmarkMixedReadWrite measures a 50% Put + 50% Get workload mix.
func BenchmarkMixedReadWrite(b *testing.B) {
const numKeys = 1000
db := openBenchDB(b)
defer func() {
if err := db.Close(); err != nil {
b.Fatalf("Close: %v", err)
}
}()
// Pre-load half the keys so Gets are not all misses.
for i := range numKeys / 2 {
key := fmt.Appendf(nil, "key-%08d", i)
val := fmt.Appendf(nil, "value-%08d", i)
if err := db.Put(key, val); err != nil {
b.Fatalf("Put preload: %v", err)
}
}
b.ReportAllocs()
b.ResetTimer()
i := 0
for b.Loop() {
key := fmt.Appendf(nil, "key-%08d", i%numKeys)
val := fmt.Appendf(nil, "value-%08d", i%numKeys)
if i%2 == 0 {
if err := db.Put(key, val); err != nil {
b.Fatalf("Put: %v", err)
}
} else {
_ = db.Get(key)
}
i++
}
}
// BenchmarkWALRecovery measures recovery (Open) time for different data sizes.
func BenchmarkWALRecovery(b *testing.B) {
for _, nKeys := range []int{100, 1000, 10000} {
b.Run(fmt.Sprintf("keys=%d", nKeys), func(b *testing.B) {
dir := b.TempDir()
// Write N keys to populate WAL segments.
func() {
db, err := Open(dir, nil)
if err != nil {
b.Fatalf("Open for write: %v", err)
}
defer db.Close()
for i := range nKeys {
key := fmt.Appendf(nil, "key-%08d", i)
val := fmt.Appendf(nil, "value-%08d", i)
if err := db.Put(key, val); err != nil {
b.Fatalf("Put: %v", err)
}
}
}()
// Now benchmark only the recovery (Open) phase.
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
db, err := Open(dir, nil)
if err != nil {
b.Fatalf("Open recovery: %v", err)
}
if err := db.Close(); err != nil {
b.Fatalf("Close: %v", err)
}
}
})
}
}