feat(memtable): implement SkipList with lock-free reads
- memtable/skiplist.go: heap-backed SkipList with atomic.Pointer next pointers - Mutex-protected writes, lock-free reads with release/acquire ordering - Copy-on-insert and copy-on-return for key/value safety - Forward iterator that skips pending entries - Comprehensive tests: ordered insert, overwrite, pending hidden, concurrent -race
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
package memtable
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
const (
|
||||
maxLevel = 20
|
||||
probability = 0.25
|
||||
)
|
||||
|
||||
// SkipList is an ordered in-memory key-value index.
|
||||
//
|
||||
// Writers are serialized by mu. Readers never take mu: every next pointer is
|
||||
// published with atomic.Pointer.Store and read with atomic.Pointer.Load, giving
|
||||
// release/acquire ordering for fully initialized nodes.
|
||||
type SkipList struct {
|
||||
arena *Arena
|
||||
mu sync.Mutex
|
||||
head *skipNode
|
||||
|
||||
height atomic.Int32
|
||||
length atomic.Uint64
|
||||
}
|
||||
|
||||
type skipNode struct {
|
||||
next [maxLevel]atomic.Pointer[skipNode]
|
||||
|
||||
key []byte
|
||||
value []byte
|
||||
sequence uint64
|
||||
height int
|
||||
pending bool
|
||||
}
|
||||
|
||||
// NewSkipList creates an empty skip list. The arena is retained for the
|
||||
// memtable API and future arena-backed nodes; Phase 1 stores nodes on the Go
|
||||
// heap so atomic.Pointer can provide correct lock-free reads.
|
||||
func NewSkipList(arena *Arena) *SkipList {
|
||||
head := &skipNode{height: maxLevel}
|
||||
sl := &SkipList{
|
||||
arena: arena,
|
||||
head: head,
|
||||
}
|
||||
sl.height.Store(1)
|
||||
return sl
|
||||
}
|
||||
|
||||
// Put inserts or replaces key with value and sequence.
|
||||
func (sl *SkipList) Put(key []byte, value []byte, sequence uint64, pending bool) error {
|
||||
sl.mu.Lock()
|
||||
defer sl.mu.Unlock()
|
||||
|
||||
var prev [maxLevel]*skipNode
|
||||
found := sl.findGreaterOrEqual(key, &prev)
|
||||
|
||||
if found != nil && bytes.Equal(found.key, key) {
|
||||
sl.replaceLocked(&prev, found, key, value, sequence, pending)
|
||||
return nil
|
||||
}
|
||||
|
||||
height := randomHeight()
|
||||
sl.ensureHeightLocked(height, &prev)
|
||||
node := newSkipNode(key, value, sequence, pending, height)
|
||||
|
||||
for level := range height {
|
||||
node.next[level].Store(prev[level].next[level].Load())
|
||||
}
|
||||
for level := range height {
|
||||
prev[level].next[level].Store(node)
|
||||
}
|
||||
sl.length.Add(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns the latest published value for key. It performs no locking.
|
||||
func (sl *SkipList) Get(key []byte) (found bool, value []byte, sequence uint64) {
|
||||
node := sl.findGreaterOrEqual(key, nil)
|
||||
if node == nil || !bytes.Equal(node.key, key) || node.pending {
|
||||
return false, nil, 0
|
||||
}
|
||||
return true, cloneBytes(node.value), node.sequence
|
||||
}
|
||||
|
||||
// Len returns the number of distinct keys in the skip list.
|
||||
func (sl *SkipList) Len() uint64 {
|
||||
return sl.length.Load()
|
||||
}
|
||||
|
||||
// NewIterator creates a forward iterator over published entries.
|
||||
func (sl *SkipList) NewIterator() *Iterator {
|
||||
it := &Iterator{node: sl.head.next[0].Load()}
|
||||
it.skipPending()
|
||||
return it
|
||||
}
|
||||
|
||||
// Iterator is a lock-free forward iterator over SkipList entries.
|
||||
type Iterator struct {
|
||||
node *skipNode
|
||||
}
|
||||
|
||||
// Valid reports whether the iterator points at an entry.
|
||||
func (it *Iterator) Valid() bool {
|
||||
return it.node != nil
|
||||
}
|
||||
|
||||
// Next advances the iterator to the next published entry.
|
||||
func (it *Iterator) Next() {
|
||||
if it.node == nil {
|
||||
return
|
||||
}
|
||||
it.node = it.node.next[0].Load()
|
||||
it.skipPending()
|
||||
}
|
||||
|
||||
// Key returns a copy of the current key.
|
||||
func (it *Iterator) Key() []byte {
|
||||
if it.node == nil {
|
||||
return nil
|
||||
}
|
||||
return cloneBytes(it.node.key)
|
||||
}
|
||||
|
||||
// Value returns a copy of the current value.
|
||||
func (it *Iterator) Value() []byte {
|
||||
if it.node == nil {
|
||||
return nil
|
||||
}
|
||||
return cloneBytes(it.node.value)
|
||||
}
|
||||
|
||||
// Sequence returns the current entry's sequence number.
|
||||
func (it *Iterator) Sequence() uint64 {
|
||||
if it.node == nil {
|
||||
return 0
|
||||
}
|
||||
return it.node.sequence
|
||||
}
|
||||
|
||||
func (it *Iterator) skipPending() {
|
||||
for it.node != nil && it.node.pending {
|
||||
it.node = it.node.next[0].Load()
|
||||
}
|
||||
}
|
||||
|
||||
func (sl *SkipList) replaceLocked(prev *[maxLevel]*skipNode, old *skipNode, key []byte, value []byte, sequence uint64, pending bool) {
|
||||
height := randomHeight()
|
||||
sl.ensureHeightLocked(height, prev)
|
||||
node := newSkipNode(key, value, sequence, pending, height)
|
||||
|
||||
for level := range maxLevel {
|
||||
oldNext := old.next[level].Load()
|
||||
if level < height {
|
||||
node.next[level].Store(oldNext)
|
||||
prev[level].next[level].Store(node)
|
||||
continue
|
||||
}
|
||||
|
||||
if level < old.height {
|
||||
prev[level].next[level].Store(oldNext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sl *SkipList) ensureHeightLocked(height int, prev *[maxLevel]*skipNode) {
|
||||
currentHeight := int(sl.height.Load())
|
||||
if height <= currentHeight {
|
||||
return
|
||||
}
|
||||
|
||||
for level := currentHeight; level < height; level++ {
|
||||
prev[level] = sl.head
|
||||
}
|
||||
sl.height.Store(int32(height))
|
||||
}
|
||||
|
||||
func (sl *SkipList) findGreaterOrEqual(key []byte, prev *[maxLevel]*skipNode) *skipNode {
|
||||
x := sl.head
|
||||
level := int(sl.height.Load()) - 1
|
||||
for level >= 0 {
|
||||
next := x.next[level].Load()
|
||||
if next != nil && bytes.Compare(next.key, key) < 0 {
|
||||
x = next
|
||||
continue
|
||||
}
|
||||
if prev != nil {
|
||||
prev[level] = x
|
||||
}
|
||||
level--
|
||||
}
|
||||
return x.next[0].Load()
|
||||
}
|
||||
|
||||
func newSkipNode(key []byte, value []byte, sequence uint64, pending bool, height int) *skipNode {
|
||||
return &skipNode{
|
||||
key: cloneBytes(key),
|
||||
value: cloneBytes(value),
|
||||
sequence: sequence,
|
||||
height: height,
|
||||
pending: pending,
|
||||
}
|
||||
}
|
||||
|
||||
func randomHeight() int {
|
||||
height := 1
|
||||
for height < maxLevel && rand.Float64() < probability {
|
||||
height++
|
||||
}
|
||||
return height
|
||||
}
|
||||
|
||||
func cloneBytes(src []byte) []byte {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := make([]byte, len(src))
|
||||
copy(dst, src)
|
||||
return dst
|
||||
}
|
||||
Reference in New Issue
Block a user