kevo/pkg/transaction/txbuffer/txbuffer.go
Jeremy Tregunna 6fc3be617d
Some checks failed
Go Tests / Run Tests (1.24.2) (push) Has been cancelled
feat: Initial release of kevo storage engine.
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)
2025-04-20 14:06:50 -06:00

271 lines
5.9 KiB
Go

package txbuffer
import (
"bytes"
"sync"
)
// Operation represents a single transaction operation (put or delete)
type Operation struct {
// Key is the key being operated on
Key []byte
// Value is the value to set (nil for delete operations)
Value []byte
// IsDelete is true for deletion operations
IsDelete bool
}
// TxBuffer maintains a buffer of transaction operations before they are committed
type TxBuffer struct {
// Buffers all operations for the transaction
operations []Operation
// Cache of key -> value for fast lookups without scanning the operation list
// Maps to nil for deletion markers
cache map[string][]byte
// Protects against concurrent access
mu sync.RWMutex
}
// NewTxBuffer creates a new transaction buffer
func NewTxBuffer() *TxBuffer {
return &TxBuffer{
operations: make([]Operation, 0, 16),
cache: make(map[string][]byte),
}
}
// Put adds a key-value pair to the transaction buffer
func (b *TxBuffer) Put(key, value []byte) {
b.mu.Lock()
defer b.mu.Unlock()
// Create a safe copy of key and value to prevent later modifications
keyCopy := make([]byte, len(key))
copy(keyCopy, key)
valueCopy := make([]byte, len(value))
copy(valueCopy, value)
// Add to operations list
b.operations = append(b.operations, Operation{
Key: keyCopy,
Value: valueCopy,
IsDelete: false,
})
// Update cache
b.cache[string(keyCopy)] = valueCopy
}
// Delete marks a key as deleted in the transaction buffer
func (b *TxBuffer) Delete(key []byte) {
b.mu.Lock()
defer b.mu.Unlock()
// Create a safe copy of the key
keyCopy := make([]byte, len(key))
copy(keyCopy, key)
// Add to operations list
b.operations = append(b.operations, Operation{
Key: keyCopy,
Value: nil,
IsDelete: true,
})
// Update cache to mark key as deleted (nil value)
b.cache[string(keyCopy)] = nil
}
// Get retrieves a value from the transaction buffer
// Returns (value, true) if found, (nil, false) if not found
func (b *TxBuffer) Get(key []byte) ([]byte, bool) {
b.mu.RLock()
defer b.mu.RUnlock()
value, found := b.cache[string(key)]
return value, found
}
// Has returns true if the key exists in the buffer, even if it's marked for deletion
func (b *TxBuffer) Has(key []byte) bool {
b.mu.RLock()
defer b.mu.RUnlock()
_, found := b.cache[string(key)]
return found
}
// IsDeleted returns true if the key is marked for deletion in the buffer
func (b *TxBuffer) IsDeleted(key []byte) bool {
b.mu.RLock()
defer b.mu.RUnlock()
value, found := b.cache[string(key)]
return found && value == nil
}
// Operations returns the list of all operations in the transaction
// This is used when committing the transaction
func (b *TxBuffer) Operations() []Operation {
b.mu.RLock()
defer b.mu.RUnlock()
// Return a copy to prevent modification
result := make([]Operation, len(b.operations))
copy(result, b.operations)
return result
}
// Clear empties the transaction buffer
// Used when rolling back a transaction
func (b *TxBuffer) Clear() {
b.mu.Lock()
defer b.mu.Unlock()
b.operations = b.operations[:0]
b.cache = make(map[string][]byte)
}
// Size returns the number of operations in the buffer
func (b *TxBuffer) Size() int {
b.mu.RLock()
defer b.mu.RUnlock()
return len(b.operations)
}
// Iterator returns an iterator over the transaction buffer
type Iterator struct {
// The buffer this iterator is iterating over
buffer *TxBuffer
// The current position in the keys slice
pos int
// Sorted list of keys
keys []string
}
// NewIterator creates a new iterator over the transaction buffer
func (b *TxBuffer) NewIterator() *Iterator {
b.mu.RLock()
defer b.mu.RUnlock()
// Get all keys and sort them
keys := make([]string, 0, len(b.cache))
for k := range b.cache {
keys = append(keys, k)
}
// Sort the keys
keys = sortStrings(keys)
return &Iterator{
buffer: b,
pos: -1, // Start before the first position
keys: keys,
}
}
// SeekToFirst positions the iterator at the first key
func (it *Iterator) SeekToFirst() {
it.pos = 0
}
// SeekToLast positions the iterator at the last key
func (it *Iterator) SeekToLast() {
if len(it.keys) > 0 {
it.pos = len(it.keys) - 1
} else {
it.pos = 0
}
}
// Seek positions the iterator at the first key >= target
func (it *Iterator) Seek(target []byte) bool {
targetStr := string(target)
// Binary search would be more efficient for large sets
for i, key := range it.keys {
if key >= targetStr {
it.pos = i
return true
}
}
// Not found - position past the end
it.pos = len(it.keys)
return false
}
// Next advances the iterator to the next key
func (it *Iterator) Next() bool {
if it.pos < 0 {
it.pos = 0
return it.pos < len(it.keys)
}
it.pos++
return it.pos < len(it.keys)
}
// Key returns the current key
func (it *Iterator) Key() []byte {
if !it.Valid() {
return nil
}
return []byte(it.keys[it.pos])
}
// Value returns the current value
func (it *Iterator) Value() []byte {
if !it.Valid() {
return nil
}
// Get the value from the buffer
it.buffer.mu.RLock()
defer it.buffer.mu.RUnlock()
value := it.buffer.cache[it.keys[it.pos]]
return value // Returns nil for deletion markers
}
// Valid returns true if the iterator is positioned at a valid entry
func (it *Iterator) Valid() bool {
return it.pos >= 0 && it.pos < len(it.keys)
}
// IsTombstone returns true if the current entry is a deletion marker
func (it *Iterator) IsTombstone() bool {
if !it.Valid() {
return false
}
it.buffer.mu.RLock()
defer it.buffer.mu.RUnlock()
// The value is nil for tombstones in our cache implementation
value := it.buffer.cache[it.keys[it.pos]]
return value == nil
}
// Simple implementation of string sorting for the iterator
func sortStrings(strings []string) []string {
// In-place sort
for i := 0; i < len(strings); i++ {
for j := i + 1; j < len(strings); j++ {
if bytes.Compare([]byte(strings[i]), []byte(strings[j])) > 0 {
strings[i], strings[j] = strings[j], strings[i]
}
}
}
return strings
}