All checks were successful
Go Tests / Run Tests (1.24.2) (push) Successful in 9m48s
318 lines
8.0 KiB
Go
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()
|
|
}
|
|
}
|