All checks were successful
Go Tests / Run Tests (1.24.2) (push) Successful in 9m48s
311 lines
8.5 KiB
Go
311 lines
8.5 KiB
Go
package transaction
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/KevoDB/kevo/pkg/common/iterator"
|
|
"github.com/KevoDB/kevo/pkg/engine/interfaces"
|
|
"github.com/KevoDB/kevo/pkg/stats"
|
|
"github.com/KevoDB/kevo/pkg/wal"
|
|
)
|
|
|
|
// MockStorageManager is a simple mock for the interfaces.StorageManager
|
|
type MockStorageManager struct {
|
|
data map[string][]byte
|
|
}
|
|
|
|
func NewMockStorageManager() *MockStorageManager {
|
|
return &MockStorageManager{
|
|
data: make(map[string][]byte),
|
|
}
|
|
}
|
|
|
|
func (m *MockStorageManager) Put(key, value []byte) error {
|
|
m.data[string(key)] = value
|
|
return nil
|
|
}
|
|
|
|
func (m *MockStorageManager) Get(key []byte) ([]byte, error) {
|
|
value, ok := m.data[string(key)]
|
|
if !ok {
|
|
return nil, interfaces.ErrKeyNotFound
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func (m *MockStorageManager) Delete(key []byte) error {
|
|
delete(m.data, string(key))
|
|
return nil
|
|
}
|
|
|
|
func (m *MockStorageManager) IsDeleted(key []byte) (bool, error) {
|
|
_, exists := m.data[string(key)]
|
|
return !exists, nil
|
|
}
|
|
|
|
func (m *MockStorageManager) FlushMemTables() error {
|
|
return nil
|
|
}
|
|
|
|
func (m *MockStorageManager) GetIterator() (iterator.Iterator, error) {
|
|
return nil, nil // Not needed for these tests
|
|
}
|
|
|
|
func (m *MockStorageManager) GetRangeIterator(startKey, endKey []byte) (iterator.Iterator, error) {
|
|
return nil, nil // Not needed for these tests
|
|
}
|
|
|
|
func (m *MockStorageManager) ApplyBatch(entries []*wal.Entry) error {
|
|
// Process each entry in the batch
|
|
for _, entry := range entries {
|
|
switch entry.Type {
|
|
case wal.OpTypePut:
|
|
m.data[string(entry.Key)] = entry.Value
|
|
case wal.OpTypeDelete:
|
|
delete(m.data, string(entry.Key))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *MockStorageManager) GetStorageStats() map[string]interface{} {
|
|
return nil // Not needed for these tests
|
|
}
|
|
|
|
func (m *MockStorageManager) Close() error {
|
|
return nil
|
|
}
|
|
|
|
// Additional methods required by the StorageManager interface
|
|
func (m *MockStorageManager) GetMemTableSize() uint64 {
|
|
return 0
|
|
}
|
|
|
|
func (m *MockStorageManager) IsFlushNeeded() bool {
|
|
return false
|
|
}
|
|
|
|
func (m *MockStorageManager) GetSSTables() []string {
|
|
return []string{}
|
|
}
|
|
|
|
func (m *MockStorageManager) ReloadSSTables() error {
|
|
return nil
|
|
}
|
|
|
|
func (m *MockStorageManager) RotateWAL() error {
|
|
return nil
|
|
}
|
|
|
|
func TestTransactionManager_BasicOperations(t *testing.T) {
|
|
// Create dependencies
|
|
storage := NewMockStorageManager()
|
|
collector := stats.NewAtomicCollector()
|
|
|
|
// Create the transaction manager
|
|
manager := NewManager(storage, collector)
|
|
|
|
// Begin a new read-write transaction
|
|
tx, err := manager.BeginTransaction(false)
|
|
if err != nil {
|
|
t.Fatalf("Failed to begin transaction: %v", err)
|
|
}
|
|
|
|
// Put a key-value pair
|
|
err = tx.Put([]byte("test-key"), []byte("test-value"))
|
|
if err != nil {
|
|
t.Fatalf("Failed to put key in transaction: %v", err)
|
|
}
|
|
|
|
// Verify we can get the value within the transaction
|
|
value, err := tx.Get([]byte("test-key"))
|
|
if err != nil {
|
|
t.Fatalf("Failed to get key from transaction: %v", err)
|
|
}
|
|
if string(value) != "test-value" {
|
|
t.Errorf("Got incorrect value in transaction. Expected: test-value, Got: %s", string(value))
|
|
}
|
|
|
|
// The value should not be in the storage yet (not committed)
|
|
_, err = storage.Get([]byte("test-key"))
|
|
if err == nil {
|
|
t.Errorf("Key should not be in storage before commit")
|
|
}
|
|
|
|
// Commit the transaction
|
|
err = tx.Commit()
|
|
if err != nil {
|
|
t.Fatalf("Failed to commit transaction: %v", err)
|
|
}
|
|
|
|
// Now the value should be in the storage
|
|
value, err = storage.Get([]byte("test-key"))
|
|
if err != nil {
|
|
t.Fatalf("Key not found in storage after commit: %v", err)
|
|
}
|
|
if string(value) != "test-value" {
|
|
t.Errorf("Got incorrect value in storage. Expected: test-value, Got: %s", string(value))
|
|
}
|
|
|
|
// Check transaction metrics
|
|
stats := manager.GetTransactionStats()
|
|
if count, ok := stats["tx_started"]; !ok || count.(uint64) != 1 {
|
|
t.Errorf("Incorrect tx_started count. Got: %v", count)
|
|
}
|
|
if count, ok := stats["tx_completed"]; !ok || count.(uint64) != 1 {
|
|
t.Errorf("Incorrect tx_completed count. Got: %v", count)
|
|
}
|
|
}
|
|
|
|
func TestTransactionManager_RollbackAndReadOnly(t *testing.T) {
|
|
// Create dependencies
|
|
storage := NewMockStorageManager()
|
|
collector := stats.NewAtomicCollector()
|
|
|
|
// Create the transaction manager
|
|
manager := NewManager(storage, collector)
|
|
|
|
// Test rollback
|
|
rwTx, err := manager.BeginTransaction(false)
|
|
if err != nil {
|
|
t.Fatalf("Failed to begin read-write transaction: %v", err)
|
|
}
|
|
|
|
// Make some changes
|
|
err = rwTx.Put([]byte("rollback-key"), []byte("rollback-value"))
|
|
if err != nil {
|
|
t.Fatalf("Failed to put key in transaction: %v", err)
|
|
}
|
|
|
|
// Rollback the transaction
|
|
err = rwTx.Rollback()
|
|
if err != nil {
|
|
t.Fatalf("Failed to rollback transaction: %v", err)
|
|
}
|
|
|
|
// Verify the changes were not applied
|
|
_, err = storage.Get([]byte("rollback-key"))
|
|
if err == nil {
|
|
t.Errorf("Key should not be in storage after rollback")
|
|
}
|
|
|
|
// Test read-only transaction
|
|
roTx, err := manager.BeginTransaction(true)
|
|
if err != nil {
|
|
t.Fatalf("Failed to begin read-only transaction: %v", err)
|
|
}
|
|
|
|
// Try to write in a read-only transaction (should fail)
|
|
err = roTx.Put([]byte("readonly-key"), []byte("readonly-value"))
|
|
if err == nil {
|
|
t.Errorf("Put should fail in a read-only transaction")
|
|
}
|
|
|
|
// Add data to storage directly
|
|
storage.Put([]byte("readonly-test"), []byte("readonly-value"))
|
|
|
|
// Read-only transaction should be able to read
|
|
value, err := roTx.Get([]byte("readonly-test"))
|
|
if err != nil {
|
|
t.Fatalf("Failed to get key in read-only transaction: %v", err)
|
|
}
|
|
if string(value) != "readonly-value" {
|
|
t.Errorf("Got incorrect value in read-only transaction. Expected: readonly-value, Got: %s", string(value))
|
|
}
|
|
|
|
// Commit should work for read-only transaction
|
|
err = roTx.Commit()
|
|
if err != nil {
|
|
t.Fatalf("Failed to commit read-only transaction: %v", err)
|
|
}
|
|
|
|
// Check transaction metrics
|
|
stats := manager.GetTransactionStats()
|
|
if count, ok := stats["tx_started"]; !ok || count.(uint64) != 2 {
|
|
t.Errorf("Incorrect tx_started count. Got: %v", count)
|
|
}
|
|
if count, ok := stats["tx_completed"]; !ok || count.(uint64) != 1 {
|
|
t.Errorf("Incorrect tx_completed count. Got: %v", count)
|
|
}
|
|
if count, ok := stats["tx_aborted"]; !ok || count.(uint64) != 1 {
|
|
t.Errorf("Incorrect tx_aborted count. Got: %v", count)
|
|
}
|
|
}
|
|
|
|
func TestTransactionManager_Isolation(t *testing.T) {
|
|
// Create dependencies
|
|
storage := NewMockStorageManager()
|
|
collector := stats.NewAtomicCollector()
|
|
|
|
// Create the transaction manager
|
|
manager := NewManager(storage, collector)
|
|
|
|
// Add initial data
|
|
storage.Put([]byte("isolation-key"), []byte("initial-value"))
|
|
|
|
// In a real scenario with proper locking, we'd test isolation across transactions
|
|
// But for unit testing, we'll simplify to avoid deadlocks
|
|
|
|
// Test part 1: uncommitted changes aren't visible to new transactions
|
|
{
|
|
// Begin a transaction and modify data
|
|
tx1, err := manager.BeginTransaction(false)
|
|
if err != nil {
|
|
t.Fatalf("Failed to begin transaction: %v", err)
|
|
}
|
|
|
|
// Modify the key in the transaction
|
|
err = tx1.Put([]byte("isolation-key"), []byte("tx1-value"))
|
|
if err != nil {
|
|
t.Fatalf("Failed to put key in transaction: %v", err)
|
|
}
|
|
|
|
// Ensure the change is in the transaction buffer but not committed yet
|
|
txValue, err := tx1.Get([]byte("isolation-key"))
|
|
if err != nil || string(txValue) != "tx1-value" {
|
|
t.Fatalf("Transaction doesn't see its own changes. Got: %s, err: %v", txValue, err)
|
|
}
|
|
|
|
// Storage should still have the original value
|
|
storageValue, err := storage.Get([]byte("isolation-key"))
|
|
if err != nil || string(storageValue) != "initial-value" {
|
|
t.Fatalf("Storage changed before commit. Got: %s, err: %v", storageValue, err)
|
|
}
|
|
|
|
// Commit the transaction
|
|
err = tx1.Commit()
|
|
if err != nil {
|
|
t.Fatalf("Failed to commit transaction: %v", err)
|
|
}
|
|
|
|
// Now storage should have the updated value
|
|
storageValue, err = storage.Get([]byte("isolation-key"))
|
|
if err != nil || string(storageValue) != "tx1-value" {
|
|
t.Fatalf("Storage not updated after commit. Got: %s, err: %v", storageValue, err)
|
|
}
|
|
}
|
|
|
|
// Test part 2: reading committed data
|
|
{
|
|
// A new transaction should see the updated value
|
|
tx2, err := manager.BeginTransaction(true)
|
|
if err != nil {
|
|
t.Fatalf("Failed to begin read-only transaction: %v", err)
|
|
}
|
|
|
|
value, err := tx2.Get([]byte("isolation-key"))
|
|
if err != nil {
|
|
t.Fatalf("Failed to get key in transaction: %v", err)
|
|
}
|
|
if string(value) != "tx1-value" {
|
|
t.Errorf("Transaction doesn't see committed changes. Expected: tx1-value, Got: %s", string(value))
|
|
}
|
|
|
|
// Commit the read-only transaction
|
|
err = tx2.Commit()
|
|
if err != nil {
|
|
t.Fatalf("Failed to commit read-only transaction: %v", err)
|
|
}
|
|
}
|
|
}
|