- 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
157 lines
3.9 KiB
Go
157 lines
3.9 KiB
Go
package memtable
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestSkipListEmpty(t *testing.T) {
|
|
sl := NewSkipList(NewArena(1024))
|
|
|
|
found, value, sequence := sl.Get([]byte("missing"))
|
|
require.False(t, found)
|
|
require.Nil(t, value)
|
|
require.Zero(t, sequence)
|
|
}
|
|
|
|
func TestSkipListOrdered(t *testing.T) {
|
|
sl := NewSkipList(NewArena(64 << 20))
|
|
|
|
keys := []string{"c", "a", "e", "b", "d"}
|
|
for i, key := range keys {
|
|
require.NoError(t, sl.Put([]byte(key), []byte("value-"+key), uint64(i+1), false))
|
|
}
|
|
|
|
for i, key := range keys {
|
|
found, value, sequence := sl.Get([]byte(key))
|
|
require.True(t, found, "key %q", key)
|
|
require.Equal(t, []byte("value-"+key), value)
|
|
require.Equal(t, uint64(i+1), sequence)
|
|
}
|
|
|
|
it := sl.NewIterator()
|
|
var got []string
|
|
for it.Valid() {
|
|
got = append(got, string(it.Key()))
|
|
it.Next()
|
|
}
|
|
require.Equal(t, []string{"a", "b", "c", "d", "e"}, got)
|
|
}
|
|
|
|
func TestSkipListOverwrite(t *testing.T) {
|
|
sl := NewSkipList(NewArena(1024))
|
|
|
|
require.NoError(t, sl.Put([]byte("key"), []byte("old"), 1, false))
|
|
require.NoError(t, sl.Put([]byte("key"), []byte("new"), 2, false))
|
|
|
|
found, value, sequence := sl.Get([]byte("key"))
|
|
require.True(t, found)
|
|
require.Equal(t, []byte("new"), value)
|
|
require.Equal(t, uint64(2), sequence)
|
|
|
|
it := sl.NewIterator()
|
|
require.True(t, it.Valid())
|
|
require.Equal(t, []byte("key"), it.Key())
|
|
require.Equal(t, []byte("new"), it.Value())
|
|
it.Next()
|
|
require.False(t, it.Valid())
|
|
}
|
|
|
|
func TestSkipListPendingEntriesAreHidden(t *testing.T) {
|
|
sl := NewSkipList(NewArena(1024))
|
|
|
|
require.NoError(t, sl.Put([]byte("a"), []byte("pending"), 1, true))
|
|
require.NoError(t, sl.Put([]byte("b"), []byte("published"), 2, false))
|
|
|
|
found, _, _ := sl.Get([]byte("a"))
|
|
require.False(t, found)
|
|
|
|
it := sl.NewIterator()
|
|
require.True(t, it.Valid())
|
|
require.Equal(t, []byte("b"), it.Key())
|
|
it.Next()
|
|
require.False(t, it.Valid())
|
|
}
|
|
|
|
func TestSkipListConcurrent(t *testing.T) {
|
|
sl := NewSkipList(NewArena(64 << 20))
|
|
|
|
const (
|
|
writers = 4
|
|
readers = 4
|
|
keysPerWriter = 100
|
|
readsPerReader = 1000
|
|
)
|
|
|
|
var wg sync.WaitGroup
|
|
start := make(chan struct{})
|
|
errCh := make(chan error, writers+readers)
|
|
|
|
for writer := range writers {
|
|
wg.Go(func() {
|
|
<-start
|
|
for i := range keysPerWriter {
|
|
key := fmt.Appendf(nil, "writer-%d-key-%03d", writer, i)
|
|
value := fmt.Appendf(nil, "value-%d-%03d", writer, i)
|
|
sequence := uint64(writer*keysPerWriter + i + 1)
|
|
if err := sl.Put(key, value, sequence, false); err != nil {
|
|
errCh <- err
|
|
return
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
for reader := range readers {
|
|
wg.Go(func() {
|
|
<-start
|
|
rng := rand.New(rand.NewSource(int64(reader)))
|
|
for range readsPerReader {
|
|
writer := rng.Intn(writers)
|
|
keyID := rng.Intn(keysPerWriter)
|
|
key := fmt.Appendf(nil, "writer-%d-key-%03d", writer, keyID)
|
|
found, value, sequence := sl.Get(key)
|
|
if found {
|
|
expectedValue := fmt.Appendf(nil, "value-%d-%03d", writer, keyID)
|
|
expectedSequence := uint64(writer*keysPerWriter + keyID + 1)
|
|
if err := compareEntry(value, sequence, expectedValue, expectedSequence); err != nil {
|
|
errCh <- err
|
|
return
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
close(start)
|
|
wg.Wait()
|
|
close(errCh)
|
|
for err := range errCh {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
for writer := range writers {
|
|
for i := range keysPerWriter {
|
|
key := fmt.Appendf(nil, "writer-%d-key-%03d", writer, i)
|
|
found, value, sequence := sl.Get(key)
|
|
require.True(t, found, "key %q", key)
|
|
require.Equal(t, fmt.Appendf(nil, "value-%d-%03d", writer, i), value)
|
|
require.Equal(t, uint64(writer*keysPerWriter+i+1), sequence)
|
|
}
|
|
}
|
|
}
|
|
|
|
func compareEntry(value []byte, sequence uint64, expectedValue []byte, expectedSequence uint64) error {
|
|
if string(value) != string(expectedValue) {
|
|
return fmt.Errorf("value = %q, want %q", value, expectedValue)
|
|
}
|
|
if sequence != expectedSequence {
|
|
return fmt.Errorf("sequence = %d, want %d", sequence, expectedSequence)
|
|
}
|
|
return nil
|
|
}
|