kevo/pkg/engine/transaction/transaction.go
Jeremy Tregunna 7e226825df
All checks were successful
Go Tests / Run Tests (1.24.2) (push) Successful in 9m48s
fix: engine refactor bugfix fest, go fmt
2025-04-25 23:36:08 -06:00

318 lines
8.0 KiB
Go

package transaction
import (
"errors"
"sync/atomic"
"time"
"github.com/KevoDB/kevo/pkg/common/iterator"
"github.com/KevoDB/kevo/pkg/common/iterator/bounded"
"github.com/KevoDB/kevo/pkg/common/iterator/composite"
"github.com/KevoDB/kevo/pkg/engine/interfaces"
engineIterator "github.com/KevoDB/kevo/pkg/engine/iterator"
"github.com/KevoDB/kevo/pkg/wal"
)
// Common errors for transaction operations
var (
ErrReadOnlyTransaction = errors.New("cannot write to a read-only transaction")
ErrTransactionClosed = errors.New("transaction already committed or rolled back")
ErrKeyNotFound = errors.New("key not found")
)
// Transaction implements the interfaces.Transaction interface
type Transaction struct {
// Reference to the transaction manager
manager interfaces.TransactionManager
// Reference to the storage
storage interfaces.StorageManager
// Read-only flag
readOnly bool
// Buffer for transaction operations
buffer *Buffer
// Transaction state
active atomic.Bool
// For read-only transactions, tracks if we have a read lock
hasReadLock atomic.Bool
// For read-write transactions, tracks if we have the write lock
hasWriteLock atomic.Bool
// Iterator factory
iterFactory *engineIterator.Factory
// Start time for tracking latency
startTime time.Time
}
// NewTransaction creates a new transaction
func NewTransaction(manager interfaces.TransactionManager, storage interfaces.StorageManager, readOnly bool) *Transaction {
tx := &Transaction{
manager: manager,
storage: storage,
readOnly: readOnly,
buffer: NewBuffer(),
iterFactory: engineIterator.NewFactory(),
startTime: time.Now(),
}
// Set active flag
tx.active.Store(true)
// Acquire appropriate lock
lock := manager.GetRWLock()
if readOnly {
lock.RLock()
tx.hasReadLock.Store(true)
} else {
lock.Lock()
tx.hasWriteLock.Store(true)
}
return tx
}
// Get retrieves a value for the given key
func (tx *Transaction) Get(key []byte) ([]byte, error) {
// Check if transaction is still active
if !tx.active.Load() {
return nil, ErrTransactionClosed
}
// First check the transaction buffer for any pending changes
if val, found := tx.buffer.Get(key); found {
if val == nil {
// This is a deletion marker
return nil, ErrKeyNotFound
}
return val, nil
}
// Not in the buffer, get from the underlying storage
val, err := tx.storage.Get(key)
// Debug print on error to help diagnose key encoding issues
if err != nil {
// Log in both ASCII and hex for debugging
if len(key) < 100 {
strKey := string(key)
hexKey := ""
for _, b := range key {
hexKey += string("0123456789abcdef"[b>>4])
hexKey += string("0123456789abcdef"[b&0xF])
}
// Log both representations
println("Transaction key not found:", strKey, "(hex:", hexKey, ")")
}
}
return val, err
}
// Put adds or updates a key-value pair
func (tx *Transaction) Put(key, value []byte) error {
// Check if transaction is still active
if !tx.active.Load() {
return ErrTransactionClosed
}
// Check if transaction is read-only
if tx.readOnly {
return ErrReadOnlyTransaction
}
// Debug print key being stored
if len(key) < 100 {
strKey := string(key)
hexKey := ""
for _, b := range key {
hexKey += string("0123456789abcdef"[b>>4])
hexKey += string("0123456789abcdef"[b&0xF])
}
println("Transaction storing key:", strKey, "(hex:", hexKey, ")")
}
// Buffer the change - it will be applied on commit
tx.buffer.Put(key, value)
return nil
}
// Delete removes a key
func (tx *Transaction) Delete(key []byte) error {
// Check if transaction is still active
if !tx.active.Load() {
return ErrTransactionClosed
}
// Check if transaction is read-only
if tx.readOnly {
return ErrReadOnlyTransaction
}
// Buffer the deletion - it will be applied on commit
tx.buffer.Delete(key)
return nil
}
// NewIterator returns an iterator over the entire keyspace
func (tx *Transaction) NewIterator() iterator.Iterator {
// Check if transaction is still active
if !tx.active.Load() {
// Return an empty iterator from the engine iterator package
return engineIterator.NewFactory().CreateIterator(nil, nil)
}
// Get the storage iterator
storageIter, err := tx.storage.GetIterator()
if err != nil {
// If we can't get a storage iterator, return a buffer-only iterator
return tx.buffer.NewIterator()
}
// If there are no changes in the buffer, just use the storage's iterator
if tx.buffer.Size() == 0 {
return storageIter
}
// Merge buffer and storage iterators
bufferIter := tx.buffer.NewIterator()
// Using composite.NewHierarchicalIterator from common/iterator/composite
// with the transaction buffer having higher priority
return composite.NewHierarchicalIterator([]iterator.Iterator{bufferIter, storageIter})
}
// NewRangeIterator returns an iterator limited to a specific key range
func (tx *Transaction) NewRangeIterator(startKey, endKey []byte) iterator.Iterator {
// Check if transaction is still active
if !tx.active.Load() {
// Return an empty iterator from the engine iterator package
return engineIterator.NewFactory().CreateIterator(nil, nil)
}
// Get the storage iterator for the range
storageIter, err := tx.storage.GetRangeIterator(startKey, endKey)
if err != nil {
// If we can't get a storage iterator, use a bounded buffer iterator
bufferIter := tx.buffer.NewIterator()
return bounded.NewBoundedIterator(bufferIter, startKey, endKey)
}
// If there are no changes in the buffer, just use the storage's range iterator
if tx.buffer.Size() == 0 {
return storageIter
}
// Create a bounded buffer iterator
bufferIter := tx.buffer.NewIterator()
boundedBufferIter := bounded.NewBoundedIterator(bufferIter, startKey, endKey)
// Merge the bounded buffer iterator with the storage range iterator
return composite.NewHierarchicalIterator([]iterator.Iterator{boundedBufferIter, storageIter})
}
// Commit makes all changes permanent
func (tx *Transaction) Commit() error {
// Only proceed if the transaction is still active
if !tx.active.CompareAndSwap(true, false) {
return ErrTransactionClosed
}
var err error
// For read-only transactions, just release the read lock
if tx.readOnly {
tx.releaseReadLock()
// Track transaction completion
tx.manager.IncrementTxCompleted()
return nil
}
// For read-write transactions, apply the changes
if tx.buffer.Size() > 0 {
// Get operations from the buffer
ops := tx.buffer.Operations()
// Create a batch for all operations
walBatch := make([]*wal.Entry, 0, len(ops))
// Build WAL entries for each operation
for _, op := range ops {
if op.IsDelete {
// Create delete entry
walBatch = append(walBatch, &wal.Entry{
Type: wal.OpTypeDelete,
Key: op.Key,
})
} else {
// Create put entry
walBatch = append(walBatch, &wal.Entry{
Type: wal.OpTypePut,
Key: op.Key,
Value: op.Value,
})
}
}
// Apply the batch atomically
err = tx.storage.ApplyBatch(walBatch)
}
// Release the write lock
tx.releaseWriteLock()
// Track transaction completion
tx.manager.IncrementTxCompleted()
return err
}
// Rollback discards all transaction changes
func (tx *Transaction) Rollback() error {
// Only proceed if the transaction is still active
if !tx.active.CompareAndSwap(true, false) {
return ErrTransactionClosed
}
// Clear the buffer
tx.buffer.Clear()
// Release locks based on transaction mode
if tx.readOnly {
tx.releaseReadLock()
} else {
tx.releaseWriteLock()
}
// Track transaction abort
tx.manager.IncrementTxAborted()
return nil
}
// IsReadOnly returns true if this is a read-only transaction
func (tx *Transaction) IsReadOnly() bool {
return tx.readOnly
}
// releaseReadLock safely releases the read lock for read-only transactions
func (tx *Transaction) releaseReadLock() {
if tx.hasReadLock.CompareAndSwap(true, false) {
tx.manager.GetRWLock().RUnlock()
}
}
// releaseWriteLock safely releases the write lock for read-write transactions
func (tx *Transaction) releaseWriteLock() {
if tx.hasWriteLock.CompareAndSwap(true, false) {
tx.manager.GetRWLock().Unlock()
}
}