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)
271 lines
5.9 KiB
Go
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
|
|
}
|