All checks were successful
Go Tests / Run Tests (1.24.2) (push) Successful in 9m48s
350 lines
9.1 KiB
Go
350 lines
9.1 KiB
Go
package memtable
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/KevoDB/kevo/pkg/wal"
|
|
)
|
|
|
|
func TestMemTableBasicOperations(t *testing.T) {
|
|
mt := NewMemTable()
|
|
|
|
// Test Put and Get
|
|
mt.Put([]byte("key1"), []byte("value1"), 1)
|
|
|
|
value, found := mt.Get([]byte("key1"))
|
|
if !found {
|
|
t.Fatalf("expected to find key1, but got not found")
|
|
}
|
|
if string(value) != "value1" {
|
|
t.Errorf("expected value1, got %s", string(value))
|
|
}
|
|
|
|
// Test not found
|
|
_, found = mt.Get([]byte("nonexistent"))
|
|
if found {
|
|
t.Errorf("expected key 'nonexistent' to not be found")
|
|
}
|
|
|
|
// Test Delete
|
|
mt.Delete([]byte("key1"), 2)
|
|
|
|
value, found = mt.Get([]byte("key1"))
|
|
if !found {
|
|
t.Fatalf("expected tombstone to be found for key1")
|
|
}
|
|
if value != nil {
|
|
t.Errorf("expected nil value for deleted key, got %v", value)
|
|
}
|
|
|
|
// Test Contains
|
|
if !mt.Contains([]byte("key1")) {
|
|
t.Errorf("expected Contains to return true for deleted key")
|
|
}
|
|
if mt.Contains([]byte("nonexistent")) {
|
|
t.Errorf("expected Contains to return false for nonexistent key")
|
|
}
|
|
}
|
|
|
|
func TestMemTableSequenceNumbers(t *testing.T) {
|
|
mt := NewMemTable()
|
|
|
|
// Add entries with sequence numbers
|
|
mt.Put([]byte("key"), []byte("value1"), 1)
|
|
mt.Put([]byte("key"), []byte("value2"), 3)
|
|
mt.Put([]byte("key"), []byte("value3"), 2)
|
|
|
|
// Should get the latest by sequence number (value2)
|
|
value, found := mt.Get([]byte("key"))
|
|
if !found {
|
|
t.Fatalf("expected to find key, but got not found")
|
|
}
|
|
if string(value) != "value2" {
|
|
t.Errorf("expected value2 (highest seq), got %s", string(value))
|
|
}
|
|
|
|
// The next sequence number should be one more than the highest seen
|
|
if nextSeq := mt.GetNextSequenceNumber(); nextSeq != 4 {
|
|
t.Errorf("expected next sequence number to be 4, got %d", nextSeq)
|
|
}
|
|
}
|
|
|
|
// TestConcurrentReadWrite tests that concurrent reads and writes work as expected
|
|
func TestConcurrentReadWrite(t *testing.T) {
|
|
mt := NewMemTable()
|
|
|
|
// Create some initial data
|
|
const initialKeys = 1000
|
|
for i := 0; i < initialKeys; i++ {
|
|
key := []byte(fmt.Sprintf("initial-key-%d", i))
|
|
value := []byte(fmt.Sprintf("initial-value-%d", i))
|
|
mt.Put(key, value, uint64(i))
|
|
}
|
|
|
|
// Perform concurrent reads and writes
|
|
const (
|
|
numReaders = 4
|
|
numWriters = 2
|
|
opsPerGoroutine = 1000
|
|
)
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(numReaders + numWriters)
|
|
|
|
// Start reader goroutines
|
|
for r := 0; r < numReaders; r++ {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
// Each reader has its own random source
|
|
rnd := rand.New(rand.NewSource(int64(id)))
|
|
|
|
for i := 0; i < opsPerGoroutine; i++ {
|
|
// Read an existing key (one we know exists)
|
|
idx := rnd.Intn(initialKeys)
|
|
key := []byte(fmt.Sprintf("initial-key-%d", idx))
|
|
expectedValue := fmt.Sprintf("initial-value-%d", idx)
|
|
|
|
value, found := mt.Get(key)
|
|
if !found {
|
|
t.Errorf("Reader %d: expected to find key %s but it wasn't found", id, string(key))
|
|
continue
|
|
}
|
|
|
|
// Due to concurrent writes, the value might have been updated or deleted
|
|
// but we at least expect to find the key
|
|
if value != nil && string(value) != expectedValue {
|
|
// This is ok - it means a writer updated this key while we were reading
|
|
// Just ensure it follows the expected pattern for a writer
|
|
if !strings.HasPrefix(string(value), "updated-value-") {
|
|
t.Errorf("Reader %d: unexpected value for key %s: %s", id, string(key), string(value))
|
|
}
|
|
}
|
|
}
|
|
}(r)
|
|
}
|
|
|
|
// Start writer goroutines
|
|
for w := 0; w < numWriters; w++ {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
// Each writer has its own random source
|
|
rnd := rand.New(rand.NewSource(int64(id + numReaders)))
|
|
|
|
for i := 0; i < opsPerGoroutine; i++ {
|
|
// Pick an operation: 50% updates, 25% inserts, 25% deletes
|
|
op := rnd.Intn(4)
|
|
var key []byte
|
|
|
|
if op < 2 {
|
|
// Update an existing key
|
|
idx := rnd.Intn(initialKeys)
|
|
key = []byte(fmt.Sprintf("initial-key-%d", idx))
|
|
value := []byte(fmt.Sprintf("updated-value-%d-%d-%d", id, i, idx))
|
|
mt.Put(key, value, uint64(initialKeys+id*opsPerGoroutine+i))
|
|
} else if op == 2 {
|
|
// Insert a new key
|
|
key = []byte(fmt.Sprintf("new-key-%d-%d", id, i))
|
|
value := []byte(fmt.Sprintf("new-value-%d-%d", id, i))
|
|
mt.Put(key, value, uint64(initialKeys+id*opsPerGoroutine+i))
|
|
} else {
|
|
// Delete a key
|
|
idx := rnd.Intn(initialKeys)
|
|
key = []byte(fmt.Sprintf("initial-key-%d", idx))
|
|
mt.Delete(key, uint64(initialKeys+id*opsPerGoroutine+i))
|
|
}
|
|
}
|
|
}(w)
|
|
}
|
|
|
|
// Wait for all goroutines to finish
|
|
wg.Wait()
|
|
|
|
// Verify the memtable is in a consistent state
|
|
verifyInitialKeys := 0
|
|
verifyNewKeys := 0
|
|
verifyUpdatedKeys := 0
|
|
verifyDeletedKeys := 0
|
|
|
|
for i := 0; i < initialKeys; i++ {
|
|
key := []byte(fmt.Sprintf("initial-key-%d", i))
|
|
value, found := mt.Get(key)
|
|
|
|
if !found {
|
|
// This key should always be found, but it might be deleted
|
|
t.Errorf("expected to find key %s, but it wasn't found", string(key))
|
|
continue
|
|
}
|
|
|
|
if value == nil {
|
|
// This key was deleted
|
|
verifyDeletedKeys++
|
|
} else if strings.HasPrefix(string(value), "initial-value-") {
|
|
// This key still has its original value
|
|
verifyInitialKeys++
|
|
} else if strings.HasPrefix(string(value), "updated-value-") {
|
|
// This key was updated
|
|
verifyUpdatedKeys++
|
|
}
|
|
}
|
|
|
|
// Check for new keys that were inserted
|
|
for w := 0; w < numWriters; w++ {
|
|
for i := 0; i < opsPerGoroutine; i++ {
|
|
key := []byte(fmt.Sprintf("new-key-%d-%d", w, i))
|
|
_, found := mt.Get(key)
|
|
if found {
|
|
verifyNewKeys++
|
|
}
|
|
}
|
|
}
|
|
|
|
// Log the statistics of what happened
|
|
t.Logf("Verified keys after concurrent operations:")
|
|
t.Logf(" - Original keys remaining: %d", verifyInitialKeys)
|
|
t.Logf(" - Updated keys: %d", verifyUpdatedKeys)
|
|
t.Logf(" - Deleted keys: %d", verifyDeletedKeys)
|
|
t.Logf(" - New keys inserted: %d", verifyNewKeys)
|
|
|
|
// Make sure the counts add up correctly
|
|
if verifyInitialKeys+verifyUpdatedKeys+verifyDeletedKeys != initialKeys {
|
|
t.Errorf("Key count mismatch: %d + %d + %d != %d",
|
|
verifyInitialKeys, verifyUpdatedKeys, verifyDeletedKeys, initialKeys)
|
|
}
|
|
}
|
|
|
|
func TestMemTableImmutability(t *testing.T) {
|
|
mt := NewMemTable()
|
|
|
|
// Add initial data
|
|
mt.Put([]byte("key"), []byte("value"), 1)
|
|
|
|
// Mark as immutable
|
|
mt.SetImmutable()
|
|
if !mt.IsImmutable() {
|
|
t.Errorf("expected IsImmutable to return true after SetImmutable")
|
|
}
|
|
|
|
// Attempts to modify should have no effect
|
|
mt.Put([]byte("key2"), []byte("value2"), 2)
|
|
mt.Delete([]byte("key"), 3)
|
|
|
|
// Verify no changes occurred
|
|
_, found := mt.Get([]byte("key2"))
|
|
if found {
|
|
t.Errorf("expected key2 to not be added to immutable memtable")
|
|
}
|
|
|
|
value, found := mt.Get([]byte("key"))
|
|
if !found {
|
|
t.Fatalf("expected to still find key after delete on immutable table")
|
|
}
|
|
if string(value) != "value" {
|
|
t.Errorf("expected value to remain unchanged, got %s", string(value))
|
|
}
|
|
}
|
|
|
|
func TestMemTableAge(t *testing.T) {
|
|
mt := NewMemTable()
|
|
|
|
// A new memtable should have a very small age
|
|
if age := mt.Age(); age > 1.0 {
|
|
t.Errorf("expected new memtable to have age < 1.0s, got %.2fs", age)
|
|
}
|
|
|
|
// Sleep to increase age
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
if age := mt.Age(); age <= 0.0 {
|
|
t.Errorf("expected memtable age to be > 0, got %.6fs", age)
|
|
}
|
|
}
|
|
|
|
func TestMemTableWALIntegration(t *testing.T) {
|
|
mt := NewMemTable()
|
|
|
|
// Create WAL entries
|
|
entries := []*wal.Entry{
|
|
{SequenceNumber: 1, Type: wal.OpTypePut, Key: []byte("key1"), Value: []byte("value1")},
|
|
{SequenceNumber: 2, Type: wal.OpTypeDelete, Key: []byte("key2"), Value: nil},
|
|
{SequenceNumber: 3, Type: wal.OpTypePut, Key: []byte("key3"), Value: []byte("value3")},
|
|
}
|
|
|
|
// Process entries
|
|
for _, entry := range entries {
|
|
if err := mt.ProcessWALEntry(entry); err != nil {
|
|
t.Fatalf("failed to process WAL entry: %v", err)
|
|
}
|
|
}
|
|
|
|
// Verify entries were processed correctly
|
|
testCases := []struct {
|
|
key string
|
|
expected string
|
|
found bool
|
|
}{
|
|
{"key1", "value1", true},
|
|
{"key2", "", true}, // Deleted key
|
|
{"key3", "value3", true},
|
|
{"key4", "", false}, // Non-existent key
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
value, found := mt.Get([]byte(tc.key))
|
|
|
|
if found != tc.found {
|
|
t.Errorf("key %s: expected found=%v, got %v", tc.key, tc.found, found)
|
|
continue
|
|
}
|
|
|
|
if found && tc.expected != "" {
|
|
if string(value) != tc.expected {
|
|
t.Errorf("key %s: expected value '%s', got '%s'", tc.key, tc.expected, string(value))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify next sequence number
|
|
if nextSeq := mt.GetNextSequenceNumber(); nextSeq != 4 {
|
|
t.Errorf("expected next sequence number to be 4, got %d", nextSeq)
|
|
}
|
|
}
|
|
|
|
func TestMemTableIterator(t *testing.T) {
|
|
mt := NewMemTable()
|
|
|
|
// Add entries in non-sorted order
|
|
entries := []struct {
|
|
key string
|
|
value string
|
|
seq uint64
|
|
}{
|
|
{"banana", "yellow", 1},
|
|
{"apple", "red", 2},
|
|
{"cherry", "red", 3},
|
|
{"date", "brown", 4},
|
|
}
|
|
|
|
for _, e := range entries {
|
|
mt.Put([]byte(e.key), []byte(e.value), e.seq)
|
|
}
|
|
|
|
// Use iterator to verify keys are returned in sorted order
|
|
it := mt.NewIterator()
|
|
it.SeekToFirst()
|
|
|
|
expected := []string{"apple", "banana", "cherry", "date"}
|
|
|
|
for i := 0; it.Valid() && i < len(expected); i++ {
|
|
key := string(it.Key())
|
|
if key != expected[i] {
|
|
t.Errorf("position %d: expected key %s, got %s", i, expected[i], key)
|
|
}
|
|
it.Next()
|
|
}
|
|
}
|