kevo/pkg/memtable/recovery_test.go
Jeremy Tregunna 6fc3be617d
Some checks failed
Go Tests / Run Tests (1.24.2) (push) Has been cancelled
feat: Initial release of kevo storage engine.
Adds a complete LSM-based storage engine with these features:
- Single-writer based architecture for the storage engine
- WAL for durability, and hey it's configurable
- MemTable with skip list implementation for fast read/writes
- SSTable with block-based structure for on-disk level-based storage
- Background compaction with tiered strategy
- ACID transactions
- Good documentation (I hope)
2025-04-20 14:06:50 -06:00

277 lines
7.0 KiB
Go

package memtable
import (
"os"
"testing"
"github.com/jer/kevo/pkg/config"
"github.com/jer/kevo/pkg/wal"
)
func setupTestWAL(t *testing.T) (string, *wal.WAL, func()) {
// Create temporary directory
tmpDir, err := os.MkdirTemp("", "memtable_recovery_test")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
// Create config
cfg := config.NewDefaultConfig(tmpDir)
// Create WAL
w, err := wal.NewWAL(cfg, tmpDir)
if err != nil {
os.RemoveAll(tmpDir)
t.Fatalf("failed to create WAL: %v", err)
}
// Return cleanup function
cleanup := func() {
w.Close()
os.RemoveAll(tmpDir)
}
return tmpDir, w, cleanup
}
func TestRecoverFromWAL(t *testing.T) {
tmpDir, w, cleanup := setupTestWAL(t)
defer cleanup()
// Add entries to the WAL
entries := []struct {
opType uint8
key string
value string
}{
{wal.OpTypePut, "key1", "value1"},
{wal.OpTypePut, "key2", "value2"},
{wal.OpTypeDelete, "key1", ""},
{wal.OpTypePut, "key3", "value3"},
}
for _, e := range entries {
var seq uint64
var err error
if e.opType == wal.OpTypePut {
seq, err = w.Append(e.opType, []byte(e.key), []byte(e.value))
} else {
seq, err = w.Append(e.opType, []byte(e.key), nil)
}
if err != nil {
t.Fatalf("failed to append to WAL: %v", err)
}
t.Logf("Appended entry with seq %d", seq)
}
// Sync and close WAL
if err := w.Sync(); err != nil {
t.Fatalf("failed to sync WAL: %v", err)
}
if err := w.Close(); err != nil {
t.Fatalf("failed to close WAL: %v", err)
}
// Create config for recovery
cfg := config.NewDefaultConfig(tmpDir)
cfg.WALDir = tmpDir
cfg.MemTableSize = 1024 * 1024 // 1MB
// Recover memtables from WAL
memTables, maxSeq, err := RecoverFromWAL(cfg, nil)
if err != nil {
t.Fatalf("failed to recover from WAL: %v", err)
}
// Validate recovery results
if len(memTables) == 0 {
t.Fatalf("expected at least one memtable from recovery")
}
t.Logf("Recovered %d memtables with max sequence %d", len(memTables), maxSeq)
// The max sequence number should be 4
if maxSeq != 4 {
t.Errorf("expected max sequence number 4, got %d", maxSeq)
}
// Validate content of the recovered memtable
mt := memTables[0]
// key1 should be deleted
value, found := mt.Get([]byte("key1"))
if !found {
t.Errorf("expected key1 to be found (as deleted)")
}
if value != nil {
t.Errorf("expected key1 to have nil value (deleted), got %v", value)
}
// key2 should have "value2"
value, found = mt.Get([]byte("key2"))
if !found {
t.Errorf("expected key2 to be found")
} else if string(value) != "value2" {
t.Errorf("expected key2 to have value 'value2', got '%s'", string(value))
}
// key3 should have "value3"
value, found = mt.Get([]byte("key3"))
if !found {
t.Errorf("expected key3 to be found")
} else if string(value) != "value3" {
t.Errorf("expected key3 to have value 'value3', got '%s'", string(value))
}
}
func TestRecoveryWithMultipleMemTables(t *testing.T) {
tmpDir, w, cleanup := setupTestWAL(t)
defer cleanup()
// Create a lot of large entries to force multiple memtables
largeValue := make([]byte, 1000) // 1KB value
for i := 0; i < 10; i++ {
key := []byte{byte(i + 'a')}
if _, err := w.Append(wal.OpTypePut, key, largeValue); err != nil {
t.Fatalf("failed to append to WAL: %v", err)
}
}
// Sync and close WAL
if err := w.Sync(); err != nil {
t.Fatalf("failed to sync WAL: %v", err)
}
if err := w.Close(); err != nil {
t.Fatalf("failed to close WAL: %v", err)
}
// Create config for recovery with small memtable size
cfg := config.NewDefaultConfig(tmpDir)
cfg.WALDir = tmpDir
cfg.MemTableSize = 5 * 1000 // 5KB - should fit about 5 entries
cfg.MaxMemTables = 3 // Allow up to 3 memtables
// Recover memtables from WAL
memTables, _, err := RecoverFromWAL(cfg, nil)
if err != nil {
t.Fatalf("failed to recover from WAL: %v", err)
}
// Should have created multiple memtables
if len(memTables) <= 1 {
t.Errorf("expected multiple memtables due to size, got %d", len(memTables))
}
t.Logf("Recovered %d memtables", len(memTables))
// All memtables except the last one should be immutable
for i, mt := range memTables[:len(memTables)-1] {
if !mt.IsImmutable() {
t.Errorf("expected memtable %d to be immutable", i)
}
}
// Verify all data was recovered across all memtables
for i := 0; i < 10; i++ {
key := []byte{byte(i + 'a')}
found := false
// Check each memtable for the key
for _, mt := range memTables {
if _, exists := mt.Get(key); exists {
found = true
break
}
}
if !found {
t.Errorf("key %c not found in any memtable", i+'a')
}
}
}
func TestRecoveryWithBatchOperations(t *testing.T) {
tmpDir, w, cleanup := setupTestWAL(t)
defer cleanup()
// Create a batch of operations
batch := wal.NewBatch()
batch.Put([]byte("batch_key1"), []byte("batch_value1"))
batch.Put([]byte("batch_key2"), []byte("batch_value2"))
batch.Delete([]byte("batch_key3"))
// Write the batch to the WAL
if err := batch.Write(w); err != nil {
t.Fatalf("failed to write batch to WAL: %v", err)
}
// Add some individual operations too
if _, err := w.Append(wal.OpTypePut, []byte("key4"), []byte("value4")); err != nil {
t.Fatalf("failed to append to WAL: %v", err)
}
// Sync and close WAL
if err := w.Sync(); err != nil {
t.Fatalf("failed to sync WAL: %v", err)
}
if err := w.Close(); err != nil {
t.Fatalf("failed to close WAL: %v", err)
}
// Create config for recovery
cfg := config.NewDefaultConfig(tmpDir)
cfg.WALDir = tmpDir
// Recover memtables from WAL
memTables, maxSeq, err := RecoverFromWAL(cfg, nil)
if err != nil {
t.Fatalf("failed to recover from WAL: %v", err)
}
if len(memTables) == 0 {
t.Fatalf("expected at least one memtable from recovery")
}
// The max sequence number should account for batch operations
if maxSeq < 3 { // At least 3 from batch + individual op
t.Errorf("expected max sequence number >= 3, got %d", maxSeq)
}
// Validate content of the recovered memtable
mt := memTables[0]
// Check batch keys were recovered
value, found := mt.Get([]byte("batch_key1"))
if !found {
t.Errorf("batch_key1 not found in recovered memtable")
} else if string(value) != "batch_value1" {
t.Errorf("expected batch_key1 to have value 'batch_value1', got '%s'", string(value))
}
value, found = mt.Get([]byte("batch_key2"))
if !found {
t.Errorf("batch_key2 not found in recovered memtable")
} else if string(value) != "batch_value2" {
t.Errorf("expected batch_key2 to have value 'batch_value2', got '%s'", string(value))
}
// batch_key3 should be marked as deleted
value, found = mt.Get([]byte("batch_key3"))
if !found {
t.Errorf("expected batch_key3 to be found as deleted")
}
if value != nil {
t.Errorf("expected batch_key3 to have nil value (deleted), got %v", value)
}
// Check individual operation was recovered
value, found = mt.Get([]byte("key4"))
if !found {
t.Errorf("key4 not found in recovered memtable")
} else if string(value) != "value4" {
t.Errorf("expected key4 to have value 'value4', got '%s'", string(value))
}
}