Some checks failed
Go Tests / Run Tests (1.24.2) (push) Has been cancelled
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)
323 lines
8.3 KiB
Go
323 lines
8.3 KiB
Go
package transaction
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/jer/kevo/pkg/engine"
|
|
)
|
|
|
|
func setupTestEngine(t *testing.T) (*engine.Engine, string) {
|
|
// Create a temporary directory for the test
|
|
tempDir, err := os.MkdirTemp("", "transaction_test_*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp directory: %v", err)
|
|
}
|
|
|
|
// Create a new engine
|
|
eng, err := engine.NewEngine(tempDir)
|
|
if err != nil {
|
|
os.RemoveAll(tempDir)
|
|
t.Fatalf("Failed to create engine: %v", err)
|
|
}
|
|
|
|
return eng, tempDir
|
|
}
|
|
|
|
func TestReadOnlyTransaction(t *testing.T) {
|
|
eng, tempDir := setupTestEngine(t)
|
|
defer os.RemoveAll(tempDir)
|
|
defer eng.Close()
|
|
|
|
// Add some data directly to the engine
|
|
if err := eng.Put([]byte("key1"), []byte("value1")); err != nil {
|
|
t.Fatalf("Failed to put key1: %v", err)
|
|
}
|
|
if err := eng.Put([]byte("key2"), []byte("value2")); err != nil {
|
|
t.Fatalf("Failed to put key2: %v", err)
|
|
}
|
|
|
|
// Create a read-only transaction
|
|
tx, err := NewTransaction(eng, ReadOnly)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create read-only transaction: %v", err)
|
|
}
|
|
|
|
// Test Get functionality
|
|
value, err := tx.Get([]byte("key1"))
|
|
if err != nil {
|
|
t.Fatalf("Failed to get key1: %v", err)
|
|
}
|
|
if !bytes.Equal(value, []byte("value1")) {
|
|
t.Errorf("Expected 'value1' but got '%s'", value)
|
|
}
|
|
|
|
// Test read-only constraints
|
|
err = tx.Put([]byte("key3"), []byte("value3"))
|
|
if err != ErrReadOnlyTransaction {
|
|
t.Errorf("Expected ErrReadOnlyTransaction but got: %v", err)
|
|
}
|
|
|
|
err = tx.Delete([]byte("key1"))
|
|
if err != ErrReadOnlyTransaction {
|
|
t.Errorf("Expected ErrReadOnlyTransaction but got: %v", err)
|
|
}
|
|
|
|
// Test iterator
|
|
iter := tx.NewIterator()
|
|
count := 0
|
|
for iter.SeekToFirst(); iter.Valid(); iter.Next() {
|
|
count++
|
|
}
|
|
if count != 2 {
|
|
t.Errorf("Expected 2 keys but found %d", count)
|
|
}
|
|
|
|
// Test commit (which for read-only just releases resources)
|
|
if err := tx.Commit(); err != nil {
|
|
t.Errorf("Failed to commit read-only transaction: %v", err)
|
|
}
|
|
|
|
// Transaction should be closed now
|
|
_, err = tx.Get([]byte("key1"))
|
|
if err != ErrTransactionClosed {
|
|
t.Errorf("Expected ErrTransactionClosed but got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestReadWriteTransaction(t *testing.T) {
|
|
eng, tempDir := setupTestEngine(t)
|
|
defer os.RemoveAll(tempDir)
|
|
defer eng.Close()
|
|
|
|
// Add initial data
|
|
if err := eng.Put([]byte("key1"), []byte("value1")); err != nil {
|
|
t.Fatalf("Failed to put key1: %v", err)
|
|
}
|
|
|
|
// Create a read-write transaction
|
|
tx, err := NewTransaction(eng, ReadWrite)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create read-write transaction: %v", err)
|
|
}
|
|
|
|
// Add more data through the transaction
|
|
if err := tx.Put([]byte("key2"), []byte("value2")); err != nil {
|
|
t.Fatalf("Failed to put key2: %v", err)
|
|
}
|
|
if err := tx.Put([]byte("key3"), []byte("value3")); err != nil {
|
|
t.Fatalf("Failed to put key3: %v", err)
|
|
}
|
|
|
|
// Delete a key
|
|
if err := tx.Delete([]byte("key1")); err != nil {
|
|
t.Fatalf("Failed to delete key1: %v", err)
|
|
}
|
|
|
|
// Verify the changes are visible in the transaction but not in the engine yet
|
|
// Check via transaction
|
|
value, err := tx.Get([]byte("key2"))
|
|
if err != nil {
|
|
t.Errorf("Failed to get key2 from transaction: %v", err)
|
|
}
|
|
if !bytes.Equal(value, []byte("value2")) {
|
|
t.Errorf("Expected 'value2' but got '%s'", value)
|
|
}
|
|
|
|
// Check deleted key
|
|
_, err = tx.Get([]byte("key1"))
|
|
if err == nil {
|
|
t.Errorf("key1 should be deleted in transaction")
|
|
}
|
|
|
|
// Check directly in engine - changes shouldn't be visible yet
|
|
value, err = eng.Get([]byte("key2"))
|
|
if err == nil {
|
|
t.Errorf("key2 should not be visible in engine yet")
|
|
}
|
|
|
|
value, err = eng.Get([]byte("key1"))
|
|
if err != nil {
|
|
t.Errorf("key1 should still be visible in engine: %v", err)
|
|
}
|
|
|
|
// Commit the transaction
|
|
if err := tx.Commit(); err != nil {
|
|
t.Fatalf("Failed to commit transaction: %v", err)
|
|
}
|
|
|
|
// Now check engine again - changes should be visible
|
|
value, err = eng.Get([]byte("key2"))
|
|
if err != nil {
|
|
t.Errorf("key2 should be visible in engine after commit: %v", err)
|
|
}
|
|
if !bytes.Equal(value, []byte("value2")) {
|
|
t.Errorf("Expected 'value2' but got '%s'", value)
|
|
}
|
|
|
|
// Deleted key should be gone
|
|
value, err = eng.Get([]byte("key1"))
|
|
if err == nil {
|
|
t.Errorf("key1 should be deleted in engine after commit")
|
|
}
|
|
|
|
// Transaction should be closed
|
|
_, err = tx.Get([]byte("key2"))
|
|
if err != ErrTransactionClosed {
|
|
t.Errorf("Expected ErrTransactionClosed but got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTransactionRollback(t *testing.T) {
|
|
eng, tempDir := setupTestEngine(t)
|
|
defer os.RemoveAll(tempDir)
|
|
defer eng.Close()
|
|
|
|
// Add initial data
|
|
if err := eng.Put([]byte("key1"), []byte("value1")); err != nil {
|
|
t.Fatalf("Failed to put key1: %v", err)
|
|
}
|
|
|
|
// Create a read-write transaction
|
|
tx, err := NewTransaction(eng, ReadWrite)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create read-write transaction: %v", err)
|
|
}
|
|
|
|
// Add and modify data
|
|
if err := tx.Put([]byte("key2"), []byte("value2")); err != nil {
|
|
t.Fatalf("Failed to put key2: %v", err)
|
|
}
|
|
if err := tx.Delete([]byte("key1")); err != nil {
|
|
t.Fatalf("Failed to delete key1: %v", err)
|
|
}
|
|
|
|
// Rollback the transaction
|
|
if err := tx.Rollback(); err != nil {
|
|
t.Fatalf("Failed to rollback transaction: %v", err)
|
|
}
|
|
|
|
// Changes should not be visible in the engine
|
|
value, err := eng.Get([]byte("key1"))
|
|
if err != nil {
|
|
t.Errorf("key1 should still exist after rollback: %v", err)
|
|
}
|
|
if !bytes.Equal(value, []byte("value1")) {
|
|
t.Errorf("Expected 'value1' but got '%s'", value)
|
|
}
|
|
|
|
// key2 should not exist
|
|
_, err = eng.Get([]byte("key2"))
|
|
if err == nil {
|
|
t.Errorf("key2 should not exist after rollback")
|
|
}
|
|
|
|
// Transaction should be closed
|
|
_, err = tx.Get([]byte("key1"))
|
|
if err != ErrTransactionClosed {
|
|
t.Errorf("Expected ErrTransactionClosed but got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTransactionIterator(t *testing.T) {
|
|
eng, tempDir := setupTestEngine(t)
|
|
defer os.RemoveAll(tempDir)
|
|
defer eng.Close()
|
|
|
|
// Add initial data
|
|
if err := eng.Put([]byte("key1"), []byte("value1")); err != nil {
|
|
t.Fatalf("Failed to put key1: %v", err)
|
|
}
|
|
if err := eng.Put([]byte("key3"), []byte("value3")); err != nil {
|
|
t.Fatalf("Failed to put key3: %v", err)
|
|
}
|
|
if err := eng.Put([]byte("key5"), []byte("value5")); err != nil {
|
|
t.Fatalf("Failed to put key5: %v", err)
|
|
}
|
|
|
|
// Create a read-write transaction
|
|
tx, err := NewTransaction(eng, ReadWrite)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create read-write transaction: %v", err)
|
|
}
|
|
|
|
// Add and modify data in transaction
|
|
if err := tx.Put([]byte("key2"), []byte("value2")); err != nil {
|
|
t.Fatalf("Failed to put key2: %v", err)
|
|
}
|
|
if err := tx.Put([]byte("key4"), []byte("value4")); err != nil {
|
|
t.Fatalf("Failed to put key4: %v", err)
|
|
}
|
|
if err := tx.Delete([]byte("key3")); err != nil {
|
|
t.Fatalf("Failed to delete key3: %v", err)
|
|
}
|
|
|
|
// Use iterator to check order and content
|
|
iter := tx.NewIterator()
|
|
expected := []struct {
|
|
key string
|
|
value string
|
|
}{
|
|
{"key1", "value1"},
|
|
{"key2", "value2"},
|
|
{"key4", "value4"},
|
|
{"key5", "value5"},
|
|
}
|
|
|
|
i := 0
|
|
for iter.SeekToFirst(); iter.Valid(); iter.Next() {
|
|
if i >= len(expected) {
|
|
t.Errorf("Too many keys in iterator")
|
|
break
|
|
}
|
|
|
|
if !bytes.Equal(iter.Key(), []byte(expected[i].key)) {
|
|
t.Errorf("Expected key '%s' but got '%s'", expected[i].key, string(iter.Key()))
|
|
}
|
|
if !bytes.Equal(iter.Value(), []byte(expected[i].value)) {
|
|
t.Errorf("Expected value '%s' but got '%s'", expected[i].value, string(iter.Value()))
|
|
}
|
|
i++
|
|
}
|
|
|
|
if i != len(expected) {
|
|
t.Errorf("Expected %d keys but found %d", len(expected), i)
|
|
}
|
|
|
|
// Test range iterator
|
|
rangeIter := tx.NewRangeIterator([]byte("key2"), []byte("key5"))
|
|
expected = []struct {
|
|
key string
|
|
value string
|
|
}{
|
|
{"key2", "value2"},
|
|
{"key4", "value4"},
|
|
}
|
|
|
|
i = 0
|
|
for rangeIter.SeekToFirst(); rangeIter.Valid(); rangeIter.Next() {
|
|
if i >= len(expected) {
|
|
t.Errorf("Too many keys in range iterator")
|
|
break
|
|
}
|
|
|
|
if !bytes.Equal(rangeIter.Key(), []byte(expected[i].key)) {
|
|
t.Errorf("Expected key '%s' but got '%s'", expected[i].key, string(rangeIter.Key()))
|
|
}
|
|
if !bytes.Equal(rangeIter.Value(), []byte(expected[i].value)) {
|
|
t.Errorf("Expected value '%s' but got '%s'", expected[i].value, string(rangeIter.Value()))
|
|
}
|
|
i++
|
|
}
|
|
|
|
if i != len(expected) {
|
|
t.Errorf("Expected %d keys in range but found %d", len(expected), i)
|
|
}
|
|
|
|
// Commit and verify results
|
|
if err := tx.Commit(); err != nil {
|
|
t.Fatalf("Failed to commit transaction: %v", err)
|
|
}
|
|
}
|