mirror of
https://source.quilibrium.com/quilibrium/ceremonyclient.git
synced 2024-12-26 00:25:17 +00:00
1970 lines
54 KiB
Go
1970 lines
54 KiB
Go
|
// Copyright 2012 The LevelDB-Go and Pebble Authors. All rights reserved. Use
|
||
|
// of this source code is governed by a BSD-style license that can be found in
|
||
|
// the LICENSE file.
|
||
|
|
||
|
package pebble
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"context"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"path/filepath"
|
||
|
"slices"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
"testing"
|
||
|
"time"
|
||
|
|
||
|
"github.com/cockroachdb/errors"
|
||
|
"github.com/cockroachdb/pebble/internal/base"
|
||
|
"github.com/cockroachdb/pebble/internal/cache"
|
||
|
"github.com/cockroachdb/pebble/internal/invariants"
|
||
|
"github.com/cockroachdb/pebble/objstorage/objstorageprovider"
|
||
|
"github.com/cockroachdb/pebble/sstable"
|
||
|
"github.com/cockroachdb/pebble/vfs"
|
||
|
"github.com/stretchr/testify/require"
|
||
|
"golang.org/x/exp/rand"
|
||
|
)
|
||
|
|
||
|
// try repeatedly calls f, sleeping between calls with exponential back-off,
|
||
|
// until f returns a nil error or the total sleep time is greater than or equal
|
||
|
// to maxTotalSleep. It always calls f at least once.
|
||
|
func try(initialSleep, maxTotalSleep time.Duration, f func() error) error {
|
||
|
totalSleep := time.Duration(0)
|
||
|
for d := initialSleep; ; d *= 2 {
|
||
|
time.Sleep(d)
|
||
|
totalSleep += d
|
||
|
if err := f(); err == nil || totalSleep >= maxTotalSleep {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestTry(t *testing.T) {
|
||
|
c := make(chan struct{})
|
||
|
go func() {
|
||
|
time.Sleep(1 * time.Millisecond)
|
||
|
close(c)
|
||
|
}()
|
||
|
|
||
|
attemptsMu := sync.Mutex{}
|
||
|
attempts := 0
|
||
|
|
||
|
err := try(100*time.Microsecond, 20*time.Second, func() error {
|
||
|
attemptsMu.Lock()
|
||
|
attempts++
|
||
|
attemptsMu.Unlock()
|
||
|
|
||
|
select {
|
||
|
default:
|
||
|
return errors.New("timed out")
|
||
|
case <-c:
|
||
|
return nil
|
||
|
}
|
||
|
})
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
attemptsMu.Lock()
|
||
|
a := attempts
|
||
|
attemptsMu.Unlock()
|
||
|
|
||
|
if a == 0 {
|
||
|
t.Fatalf("attempts: got 0, want > 0")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestBasicReads(t *testing.T) {
|
||
|
testCases := []struct {
|
||
|
dirname string
|
||
|
wantMap map[string]string
|
||
|
}{
|
||
|
{
|
||
|
"db-stage-1",
|
||
|
map[string]string{
|
||
|
"aaa": "",
|
||
|
"bar": "",
|
||
|
"baz": "",
|
||
|
"foo": "",
|
||
|
"quux": "",
|
||
|
"zzz": "",
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
"db-stage-2",
|
||
|
map[string]string{
|
||
|
"aaa": "",
|
||
|
"bar": "",
|
||
|
"baz": "three",
|
||
|
"foo": "four",
|
||
|
"quux": "",
|
||
|
"zzz": "",
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
"db-stage-3",
|
||
|
map[string]string{
|
||
|
"aaa": "",
|
||
|
"bar": "",
|
||
|
"baz": "three",
|
||
|
"foo": "four",
|
||
|
"quux": "",
|
||
|
"zzz": "",
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
"db-stage-4",
|
||
|
map[string]string{
|
||
|
"aaa": "",
|
||
|
"bar": "",
|
||
|
"baz": "",
|
||
|
"foo": "five",
|
||
|
"quux": "six",
|
||
|
"zzz": "",
|
||
|
},
|
||
|
},
|
||
|
}
|
||
|
for _, tc := range testCases {
|
||
|
t.Run(tc.dirname, func(t *testing.T) {
|
||
|
fs := vfs.NewMem()
|
||
|
_, err := vfs.Clone(vfs.Default, fs, filepath.Join("testdata", tc.dirname), tc.dirname)
|
||
|
if err != nil {
|
||
|
t.Fatalf("%s: cloneFileSystem failed: %v", tc.dirname, err)
|
||
|
}
|
||
|
d, err := Open(tc.dirname, testingRandomized(t, &Options{
|
||
|
FS: fs,
|
||
|
}))
|
||
|
if err != nil {
|
||
|
t.Fatalf("%s: Open failed: %v", tc.dirname, err)
|
||
|
}
|
||
|
for key, want := range tc.wantMap {
|
||
|
got, closer, err := d.Get([]byte(key))
|
||
|
if err != nil && err != ErrNotFound {
|
||
|
t.Fatalf("%s: Get(%q) failed: %v", tc.dirname, key, err)
|
||
|
}
|
||
|
if string(got) != string(want) {
|
||
|
t.Fatalf("%s: Get(%q): got %q, want %q", tc.dirname, key, got, want)
|
||
|
}
|
||
|
if closer != nil {
|
||
|
closer.Close()
|
||
|
}
|
||
|
}
|
||
|
err = d.Close()
|
||
|
if err != nil {
|
||
|
t.Fatalf("%s: Close failed: %v", tc.dirname, err)
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestBasicWrites(t *testing.T) {
|
||
|
d, err := Open("", testingRandomized(t, &Options{
|
||
|
FS: vfs.NewMem(),
|
||
|
}))
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
names := []string{
|
||
|
"Alatar",
|
||
|
"Gandalf",
|
||
|
"Pallando",
|
||
|
"Radagast",
|
||
|
"Saruman",
|
||
|
"Joe",
|
||
|
}
|
||
|
wantMap := map[string]string{}
|
||
|
|
||
|
inBatch, batch, pending := false, &Batch{}, [][]string(nil)
|
||
|
set0 := func(k, v string) error {
|
||
|
return d.Set([]byte(k), []byte(v), nil)
|
||
|
}
|
||
|
del0 := func(k string) error {
|
||
|
return d.Delete([]byte(k), nil)
|
||
|
}
|
||
|
set1 := func(k, v string) error {
|
||
|
batch.Set([]byte(k), []byte(v), nil)
|
||
|
return nil
|
||
|
}
|
||
|
del1 := func(k string) error {
|
||
|
batch.Delete([]byte(k), nil)
|
||
|
return nil
|
||
|
}
|
||
|
set, del := set0, del0
|
||
|
|
||
|
testCases := []string{
|
||
|
"set Gandalf Grey",
|
||
|
"set Saruman White",
|
||
|
"set Radagast Brown",
|
||
|
"delete Saruman",
|
||
|
"set Gandalf White",
|
||
|
"batch",
|
||
|
" set Alatar AliceBlue",
|
||
|
"apply",
|
||
|
"delete Pallando",
|
||
|
"set Alatar AntiqueWhite",
|
||
|
"set Pallando PapayaWhip",
|
||
|
"batch",
|
||
|
"apply",
|
||
|
"set Pallando PaleVioletRed",
|
||
|
"batch",
|
||
|
" delete Alatar",
|
||
|
" set Gandalf GhostWhite",
|
||
|
" set Saruman Seashell",
|
||
|
" delete Saruman",
|
||
|
" set Saruman SeaGreen",
|
||
|
" set Radagast RosyBrown",
|
||
|
" delete Pallando",
|
||
|
"apply",
|
||
|
"delete Radagast",
|
||
|
"delete Radagast",
|
||
|
"delete Radagast",
|
||
|
"set Gandalf Goldenrod",
|
||
|
"set Pallando PeachPuff",
|
||
|
"batch",
|
||
|
" delete Joe",
|
||
|
" delete Saruman",
|
||
|
" delete Radagast",
|
||
|
" delete Pallando",
|
||
|
" delete Gandalf",
|
||
|
" delete Alatar",
|
||
|
"apply",
|
||
|
"set Joe Plumber",
|
||
|
}
|
||
|
for i, tc := range testCases {
|
||
|
s := strings.Split(strings.TrimSpace(tc), " ")
|
||
|
switch s[0] {
|
||
|
case "set":
|
||
|
if err := set(s[1], s[2]); err != nil {
|
||
|
t.Fatalf("#%d %s: %v", i, tc, err)
|
||
|
}
|
||
|
if inBatch {
|
||
|
pending = append(pending, s)
|
||
|
} else {
|
||
|
wantMap[s[1]] = s[2]
|
||
|
}
|
||
|
case "delete":
|
||
|
if err := del(s[1]); err != nil {
|
||
|
t.Fatalf("#%d %s: %v", i, tc, err)
|
||
|
}
|
||
|
if inBatch {
|
||
|
pending = append(pending, s)
|
||
|
} else {
|
||
|
delete(wantMap, s[1])
|
||
|
}
|
||
|
case "batch":
|
||
|
inBatch, batch, set, del = true, &Batch{}, set1, del1
|
||
|
case "apply":
|
||
|
if err := d.Apply(batch, nil); err != nil {
|
||
|
t.Fatalf("#%d %s: %v", i, tc, err)
|
||
|
}
|
||
|
for _, p := range pending {
|
||
|
switch p[0] {
|
||
|
case "set":
|
||
|
wantMap[p[1]] = p[2]
|
||
|
case "delete":
|
||
|
delete(wantMap, p[1])
|
||
|
}
|
||
|
}
|
||
|
inBatch, pending, set, del = false, nil, set0, del0
|
||
|
default:
|
||
|
t.Fatalf("#%d %s: bad test case: %q", i, tc, s)
|
||
|
}
|
||
|
|
||
|
fail := false
|
||
|
for _, name := range names {
|
||
|
g, closer, err := d.Get([]byte(name))
|
||
|
if err != nil && err != ErrNotFound {
|
||
|
t.Errorf("#%d %s: Get(%q): %v", i, tc, name, err)
|
||
|
fail = true
|
||
|
}
|
||
|
got, gOK := string(g), err == nil
|
||
|
want, wOK := wantMap[name]
|
||
|
if got != want || gOK != wOK {
|
||
|
t.Errorf("#%d %s: Get(%q): got %q, %t, want %q, %t",
|
||
|
i, tc, name, got, gOK, want, wOK)
|
||
|
fail = true
|
||
|
}
|
||
|
if closer != nil {
|
||
|
closer.Close()
|
||
|
}
|
||
|
}
|
||
|
if fail {
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
|
||
|
require.NoError(t, d.Close())
|
||
|
}
|
||
|
|
||
|
func TestRandomWrites(t *testing.T) {
|
||
|
d, err := Open("", testingRandomized(t, &Options{
|
||
|
FS: vfs.NewMem(),
|
||
|
MemTableSize: 8 * 1024,
|
||
|
}))
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
keys := [64][]byte{}
|
||
|
wants := [64]int{}
|
||
|
for k := range keys {
|
||
|
keys[k] = []byte(strconv.Itoa(k))
|
||
|
wants[k] = -1
|
||
|
}
|
||
|
xxx := bytes.Repeat([]byte("x"), 512)
|
||
|
|
||
|
rng := rand.New(rand.NewSource(123))
|
||
|
const N = 1000
|
||
|
for i := 0; i < N; i++ {
|
||
|
k := rng.Intn(len(keys))
|
||
|
if rng.Intn(20) != 0 {
|
||
|
wants[k] = rng.Intn(len(xxx) + 1)
|
||
|
if err := d.Set(keys[k], xxx[:wants[k]], nil); err != nil {
|
||
|
t.Fatalf("i=%d: Set: %v", i, err)
|
||
|
}
|
||
|
} else {
|
||
|
wants[k] = -1
|
||
|
if err := d.Delete(keys[k], nil); err != nil {
|
||
|
t.Fatalf("i=%d: Delete: %v", i, err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if i != N-1 || rng.Intn(50) != 0 {
|
||
|
continue
|
||
|
}
|
||
|
for k := range keys {
|
||
|
got := -1
|
||
|
if v, closer, err := d.Get(keys[k]); err != nil {
|
||
|
if err != ErrNotFound {
|
||
|
t.Fatalf("Get: %v", err)
|
||
|
}
|
||
|
} else {
|
||
|
got = len(v)
|
||
|
closer.Close()
|
||
|
}
|
||
|
if got != wants[k] {
|
||
|
t.Errorf("i=%d, k=%d: got %d, want %d", i, k, got, wants[k])
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
require.NoError(t, d.Close())
|
||
|
}
|
||
|
|
||
|
func TestLargeBatch(t *testing.T) {
|
||
|
d, err := Open("", testingRandomized(t, &Options{
|
||
|
FS: vfs.NewMem(),
|
||
|
MemTableSize: 1400,
|
||
|
MemTableStopWritesThreshold: 100,
|
||
|
}))
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
verifyLSM := func(expected string) func() error {
|
||
|
return func() error {
|
||
|
d.mu.Lock()
|
||
|
s := d.mu.versions.currentVersion().String()
|
||
|
d.mu.Unlock()
|
||
|
if expected != s {
|
||
|
if testing.Verbose() {
|
||
|
fmt.Println(strings.TrimSpace(s))
|
||
|
}
|
||
|
return errors.Errorf("expected %s, but found %s", expected, s)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
logNum := func() base.DiskFileNum {
|
||
|
d.mu.Lock()
|
||
|
defer d.mu.Unlock()
|
||
|
return d.mu.log.queue[len(d.mu.log.queue)-1].fileNum
|
||
|
}
|
||
|
fileSize := func(fileNum base.DiskFileNum) int64 {
|
||
|
info, err := d.opts.FS.Stat(base.MakeFilepath(d.opts.FS, "", fileTypeLog, fileNum))
|
||
|
require.NoError(t, err)
|
||
|
return info.Size()
|
||
|
}
|
||
|
memTableCreationSeqNum := func() uint64 {
|
||
|
d.mu.Lock()
|
||
|
defer d.mu.Unlock()
|
||
|
return d.mu.mem.mutable.logSeqNum
|
||
|
}
|
||
|
|
||
|
startLogNum := logNum()
|
||
|
startLogStartSize := fileSize(startLogNum)
|
||
|
startSeqNum := d.mu.versions.logSeqNum.Load()
|
||
|
|
||
|
// Write a key with a value larger than the memtable size.
|
||
|
require.NoError(t, d.Set([]byte("a"), bytes.Repeat([]byte("a"), 512), nil))
|
||
|
|
||
|
// Verify that the large batch was written to the WAL that existed before it
|
||
|
// was committed. We verify that WAL rotation occurred, where the large batch
|
||
|
// was written to, and that the new WAL is empty.
|
||
|
endLogNum := logNum()
|
||
|
if startLogNum == endLogNum {
|
||
|
t.Fatal("expected WAL rotation")
|
||
|
}
|
||
|
startLogEndSize := fileSize(startLogNum)
|
||
|
if startLogEndSize == startLogStartSize {
|
||
|
t.Fatalf("expected large batch to be written to %s.log, but file size unchanged at %d",
|
||
|
startLogNum, startLogEndSize)
|
||
|
}
|
||
|
endLogSize := fileSize(endLogNum)
|
||
|
if endLogSize != 0 {
|
||
|
t.Fatalf("expected %s.log to be empty, but found %d", endLogNum, endLogSize)
|
||
|
}
|
||
|
if creationSeqNum := memTableCreationSeqNum(); creationSeqNum <= startSeqNum {
|
||
|
t.Fatalf("expected memTable.logSeqNum=%d > largeBatch.seqNum=%d", creationSeqNum, startSeqNum)
|
||
|
}
|
||
|
|
||
|
// Verify this results in one L0 table being created.
|
||
|
require.NoError(t, try(100*time.Microsecond, 20*time.Second,
|
||
|
verifyLSM("0.0:\n 000005:[a#10,SET-a#10,SET]\n")))
|
||
|
|
||
|
require.NoError(t, d.Set([]byte("b"), bytes.Repeat([]byte("b"), 512), nil))
|
||
|
|
||
|
// Verify this results in a second L0 table being created.
|
||
|
require.NoError(t, try(100*time.Microsecond, 20*time.Second,
|
||
|
verifyLSM("0.0:\n 000005:[a#10,SET-a#10,SET]\n 000007:[b#11,SET-b#11,SET]\n")))
|
||
|
|
||
|
// Allocate a bunch of batches to exhaust the batchPool. None of these
|
||
|
// batches should have a non-zero count.
|
||
|
for i := 0; i < 10; i++ {
|
||
|
b := d.NewBatch()
|
||
|
require.EqualValues(t, 0, b.Count())
|
||
|
}
|
||
|
|
||
|
require.NoError(t, d.Close())
|
||
|
}
|
||
|
|
||
|
func TestGetNoCache(t *testing.T) {
|
||
|
cache := NewCache(0)
|
||
|
defer cache.Unref()
|
||
|
|
||
|
d, err := Open("", testingRandomized(t, &Options{
|
||
|
Cache: cache,
|
||
|
FS: vfs.NewMem(),
|
||
|
}))
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
require.NoError(t, d.Set([]byte("a"), []byte("aa"), nil))
|
||
|
require.NoError(t, d.Flush())
|
||
|
verifyGet(t, d, []byte("a"), []byte("aa"))
|
||
|
|
||
|
require.NoError(t, d.Close())
|
||
|
}
|
||
|
|
||
|
func TestGetMerge(t *testing.T) {
|
||
|
d, err := Open("", testingRandomized(t, &Options{
|
||
|
FS: vfs.NewMem(),
|
||
|
}))
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
key := []byte("a")
|
||
|
verify := func(expected string) {
|
||
|
val, closer, err := d.Get(key)
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
if expected != string(val) {
|
||
|
t.Fatalf("expected %s, but got %s", expected, val)
|
||
|
}
|
||
|
closer.Close()
|
||
|
}
|
||
|
|
||
|
const val = "1"
|
||
|
for i := 1; i <= 3; i++ {
|
||
|
require.NoError(t, d.Merge(key, []byte(val), nil))
|
||
|
|
||
|
expected := strings.Repeat(val, i)
|
||
|
verify(expected)
|
||
|
|
||
|
require.NoError(t, d.Flush())
|
||
|
verify(expected)
|
||
|
}
|
||
|
|
||
|
require.NoError(t, d.Close())
|
||
|
}
|
||
|
|
||
|
func TestMergeOrderSameAfterFlush(t *testing.T) {
|
||
|
// Ensure compaction iterator (used by flush) and user iterator process merge
|
||
|
// operands in the same order
|
||
|
d, err := Open("", testingRandomized(t, &Options{
|
||
|
FS: vfs.NewMem(),
|
||
|
}))
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
key := []byte("a")
|
||
|
verify := func(expected string) {
|
||
|
iter, _ := d.NewIter(nil)
|
||
|
if !iter.SeekGE([]byte("a")) {
|
||
|
t.Fatal("expected one value, but got empty iterator")
|
||
|
}
|
||
|
if expected != string(iter.Value()) {
|
||
|
t.Fatalf("expected %s, but got %s", expected, string(iter.Value()))
|
||
|
}
|
||
|
if !iter.SeekLT([]byte("b")) {
|
||
|
t.Fatal("expected one value, but got empty iterator")
|
||
|
}
|
||
|
if expected != string(iter.Value()) {
|
||
|
t.Fatalf("expected %s, but got %s", expected, string(iter.Value()))
|
||
|
}
|
||
|
require.NoError(t, iter.Close())
|
||
|
}
|
||
|
|
||
|
require.NoError(t, d.Merge(key, []byte("0"), nil))
|
||
|
require.NoError(t, d.Merge(key, []byte("1"), nil))
|
||
|
|
||
|
verify("01")
|
||
|
require.NoError(t, d.Flush())
|
||
|
verify("01")
|
||
|
|
||
|
require.NoError(t, d.Close())
|
||
|
}
|
||
|
|
||
|
type closableMerger struct {
|
||
|
lastBuf []byte
|
||
|
closed bool
|
||
|
}
|
||
|
|
||
|
func (m *closableMerger) MergeNewer(value []byte) error {
|
||
|
m.lastBuf = append(m.lastBuf[:0], value...)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (m *closableMerger) MergeOlder(value []byte) error {
|
||
|
m.lastBuf = append(m.lastBuf[:0], value...)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (m *closableMerger) Finish(includesBase bool) ([]byte, io.Closer, error) {
|
||
|
return m.lastBuf, m, nil
|
||
|
}
|
||
|
|
||
|
func (m *closableMerger) Close() error {
|
||
|
m.closed = true
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func TestMergerClosing(t *testing.T) {
|
||
|
m := &closableMerger{}
|
||
|
|
||
|
d, err := Open("", testingRandomized(t, &Options{
|
||
|
FS: vfs.NewMem(),
|
||
|
Merger: &Merger{
|
||
|
Merge: func(key, value []byte) (base.ValueMerger, error) {
|
||
|
return m, m.MergeNewer(value)
|
||
|
},
|
||
|
},
|
||
|
}))
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
defer func() {
|
||
|
require.NoError(t, d.Close())
|
||
|
}()
|
||
|
|
||
|
err = d.Merge([]byte("a"), []byte("b"), nil)
|
||
|
require.NoError(t, err)
|
||
|
require.False(t, m.closed)
|
||
|
|
||
|
val, closer, err := d.Get([]byte("a"))
|
||
|
require.NoError(t, err)
|
||
|
require.Equal(t, []byte("b"), val)
|
||
|
require.NotNil(t, closer)
|
||
|
require.False(t, m.closed)
|
||
|
_ = closer.Close()
|
||
|
require.True(t, m.closed)
|
||
|
}
|
||
|
|
||
|
func TestLogData(t *testing.T) {
|
||
|
d, err := Open("", testingRandomized(t, &Options{
|
||
|
FS: vfs.NewMem(),
|
||
|
}))
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
defer func() {
|
||
|
require.NoError(t, d.Close())
|
||
|
}()
|
||
|
|
||
|
require.NoError(t, d.LogData([]byte("foo"), Sync))
|
||
|
require.NoError(t, d.LogData([]byte("bar"), Sync))
|
||
|
// TODO(itsbilal): Confirm that we wrote some bytes to the WAL.
|
||
|
// For now, LogData proceeding ahead without a panic is good enough.
|
||
|
}
|
||
|
|
||
|
func TestSingleDeleteGet(t *testing.T) {
|
||
|
d, err := Open("", testingRandomized(t, &Options{
|
||
|
FS: vfs.NewMem(),
|
||
|
}))
|
||
|
require.NoError(t, err)
|
||
|
defer func() {
|
||
|
require.NoError(t, d.Close())
|
||
|
}()
|
||
|
|
||
|
key := []byte("key")
|
||
|
val := []byte("val")
|
||
|
|
||
|
require.NoError(t, d.Set(key, val, nil))
|
||
|
verifyGet(t, d, key, val)
|
||
|
|
||
|
key2 := []byte("key2")
|
||
|
val2 := []byte("val2")
|
||
|
|
||
|
require.NoError(t, d.Set(key2, val2, nil))
|
||
|
verifyGet(t, d, key2, val2)
|
||
|
|
||
|
require.NoError(t, d.SingleDelete(key2, nil))
|
||
|
verifyGetNotFound(t, d, key2)
|
||
|
}
|
||
|
|
||
|
func TestSingleDeleteFlush(t *testing.T) {
|
||
|
d, err := Open("", testingRandomized(t, &Options{
|
||
|
FS: vfs.NewMem(),
|
||
|
}))
|
||
|
require.NoError(t, err)
|
||
|
defer func() {
|
||
|
require.NoError(t, d.Close())
|
||
|
}()
|
||
|
|
||
|
key := []byte("key")
|
||
|
valFirst := []byte("first")
|
||
|
valSecond := []byte("second")
|
||
|
key2 := []byte("key2")
|
||
|
val2 := []byte("val2")
|
||
|
|
||
|
require.NoError(t, d.Set(key, valFirst, nil))
|
||
|
require.NoError(t, d.Set(key2, val2, nil))
|
||
|
require.NoError(t, d.Flush())
|
||
|
|
||
|
require.NoError(t, d.SingleDelete(key, nil))
|
||
|
require.NoError(t, d.Set(key, valSecond, nil))
|
||
|
require.NoError(t, d.Delete(key2, nil))
|
||
|
require.NoError(t, d.Set(key2, val2, nil))
|
||
|
require.NoError(t, d.Flush())
|
||
|
|
||
|
require.NoError(t, d.SingleDelete(key, nil))
|
||
|
require.NoError(t, d.Delete(key2, nil))
|
||
|
require.NoError(t, d.Flush())
|
||
|
|
||
|
verifyGetNotFound(t, d, key)
|
||
|
verifyGetNotFound(t, d, key2)
|
||
|
}
|
||
|
|
||
|
func TestUnremovableSingleDelete(t *testing.T) {
|
||
|
d, err := Open("", testingRandomized(t, &Options{
|
||
|
FS: vfs.NewMem(),
|
||
|
L0CompactionThreshold: 8,
|
||
|
}))
|
||
|
require.NoError(t, err)
|
||
|
defer func() {
|
||
|
require.NoError(t, d.Close())
|
||
|
}()
|
||
|
|
||
|
key := []byte("key")
|
||
|
valFirst := []byte("valFirst")
|
||
|
valSecond := []byte("valSecond")
|
||
|
|
||
|
require.NoError(t, d.Set(key, valFirst, nil))
|
||
|
ss := d.NewSnapshot()
|
||
|
defer ss.Close()
|
||
|
require.NoError(t, d.SingleDelete(key, nil))
|
||
|
require.NoError(t, d.Set(key, valSecond, nil))
|
||
|
require.NoError(t, d.Flush())
|
||
|
|
||
|
verifyGet(t, ss, key, valFirst)
|
||
|
verifyGet(t, d, key, valSecond)
|
||
|
|
||
|
require.NoError(t, d.SingleDelete(key, nil))
|
||
|
|
||
|
verifyGet(t, ss, key, valFirst)
|
||
|
verifyGetNotFound(t, d, key)
|
||
|
|
||
|
require.NoError(t, d.Flush())
|
||
|
|
||
|
verifyGet(t, ss, key, valFirst)
|
||
|
verifyGetNotFound(t, d, key)
|
||
|
}
|
||
|
|
||
|
func TestIterLeak(t *testing.T) {
|
||
|
for _, leak := range []bool{true, false} {
|
||
|
t.Run(fmt.Sprintf("leak=%t", leak), func(t *testing.T) {
|
||
|
for _, flush := range []bool{true, false} {
|
||
|
t.Run(fmt.Sprintf("flush=%t", flush), func(t *testing.T) {
|
||
|
d, err := Open("", testingRandomized(t, &Options{
|
||
|
FS: vfs.NewMem(),
|
||
|
}))
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
require.NoError(t, d.Set([]byte("a"), []byte("a"), nil))
|
||
|
if flush {
|
||
|
require.NoError(t, d.Flush())
|
||
|
}
|
||
|
iter, _ := d.NewIter(nil)
|
||
|
iter.First()
|
||
|
if !leak {
|
||
|
require.NoError(t, iter.Close())
|
||
|
require.NoError(t, d.Close())
|
||
|
} else {
|
||
|
defer iter.Close()
|
||
|
if err := d.Close(); err == nil {
|
||
|
t.Fatalf("expected failure, but found success")
|
||
|
} else if !strings.HasPrefix(err.Error(), "leaked iterators:") {
|
||
|
t.Fatalf("expected leaked iterators, but found %+v", err)
|
||
|
} else {
|
||
|
t.Log(err.Error())
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Make sure that we detect an iter leak when only one DB closes
|
||
|
// while the second db still holds a reference to the TableCache.
|
||
|
func TestIterLeakSharedCache(t *testing.T) {
|
||
|
for _, leak := range []bool{true, false} {
|
||
|
t.Run(fmt.Sprintf("leak=%t", leak), func(t *testing.T) {
|
||
|
for _, flush := range []bool{true, false} {
|
||
|
t.Run(fmt.Sprintf("flush=%t", flush), func(t *testing.T) {
|
||
|
d1, err := Open("", &Options{
|
||
|
FS: vfs.NewMem(),
|
||
|
})
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
d2, err := Open("", &Options{
|
||
|
FS: vfs.NewMem(),
|
||
|
})
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
require.NoError(t, d1.Set([]byte("a"), []byte("a"), nil))
|
||
|
if flush {
|
||
|
require.NoError(t, d1.Flush())
|
||
|
}
|
||
|
|
||
|
require.NoError(t, d2.Set([]byte("a"), []byte("a"), nil))
|
||
|
if flush {
|
||
|
require.NoError(t, d2.Flush())
|
||
|
}
|
||
|
|
||
|
// Check if leak detection works with only one db closing.
|
||
|
{
|
||
|
iter1, _ := d1.NewIter(nil)
|
||
|
iter1.First()
|
||
|
if !leak {
|
||
|
require.NoError(t, iter1.Close())
|
||
|
require.NoError(t, d1.Close())
|
||
|
} else {
|
||
|
defer iter1.Close()
|
||
|
if err := d1.Close(); err == nil {
|
||
|
t.Fatalf("expected failure, but found success")
|
||
|
} else if !strings.HasPrefix(err.Error(), "leaked iterators:") {
|
||
|
t.Fatalf("expected leaked iterators, but found %+v", err)
|
||
|
} else {
|
||
|
t.Log(err.Error())
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
{
|
||
|
iter2, _ := d2.NewIter(nil)
|
||
|
iter2.First()
|
||
|
if !leak {
|
||
|
require.NoError(t, iter2.Close())
|
||
|
require.NoError(t, d2.Close())
|
||
|
} else {
|
||
|
defer iter2.Close()
|
||
|
if err := d2.Close(); err == nil {
|
||
|
t.Fatalf("expected failure, but found success")
|
||
|
} else if !strings.HasPrefix(err.Error(), "leaked iterators:") {
|
||
|
t.Fatalf("expected leaked iterators, but found %+v", err)
|
||
|
} else {
|
||
|
t.Log(err.Error())
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
})
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestMemTableReservation(t *testing.T) {
|
||
|
opts := &Options{
|
||
|
Cache: NewCache(128 << 10 /* 128 KB */),
|
||
|
MemTableSize: initialMemTableSize,
|
||
|
FS: vfs.NewMem(),
|
||
|
}
|
||
|
defer opts.Cache.Unref()
|
||
|
opts.testingRandomized(t)
|
||
|
opts.EnsureDefaults()
|
||
|
// We're going to be looking at and asserting the global memtable reservation
|
||
|
// amount below so we don't want to race with any triggered stats collections.
|
||
|
opts.private.disableTableStats = true
|
||
|
|
||
|
// Add a block to the cache. Note that the memtable size is larger than the
|
||
|
// cache size, so opening the DB should cause this block to be evicted.
|
||
|
tmpID := opts.Cache.NewID()
|
||
|
helloWorld := []byte("hello world")
|
||
|
value := cache.Alloc(len(helloWorld))
|
||
|
copy(value.Buf(), helloWorld)
|
||
|
opts.Cache.Set(tmpID, base.FileNum(0).DiskFileNum(), 0, value).Release()
|
||
|
|
||
|
d, err := Open("", opts)
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
checkReserved := func(expected int64) {
|
||
|
t.Helper()
|
||
|
if reserved := d.memTableReserved.Load(); expected != reserved {
|
||
|
t.Fatalf("expected %d reserved, but found %d", expected, reserved)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
checkReserved(int64(opts.MemTableSize))
|
||
|
if refs := d.mu.mem.queue[len(d.mu.mem.queue)-1].readerRefs.Load(); refs != 2 {
|
||
|
t.Fatalf("expected 2 refs, but found %d", refs)
|
||
|
}
|
||
|
// Verify the memtable reservation has caused our test block to be evicted.
|
||
|
if h := opts.Cache.Get(tmpID, base.FileNum(0).DiskFileNum(), 0); h.Get() != nil {
|
||
|
t.Fatalf("expected failure, but found success: %s", h.Get())
|
||
|
}
|
||
|
|
||
|
// Flush the memtable. The memtable reservation should double because old
|
||
|
// memtable will be recycled, saved for the next memtable allocation.
|
||
|
require.NoError(t, d.Flush())
|
||
|
checkReserved(int64(2 * opts.MemTableSize))
|
||
|
// Flush again. The memtable reservation should be unchanged because at most
|
||
|
// 1 memtable may be preserved for recycling.
|
||
|
|
||
|
// Flush in the presence of an active iterator. The iterator will hold a
|
||
|
// reference to a readState which will in turn hold a reader reference to the
|
||
|
// memtable.
|
||
|
iter, _ := d.NewIter(nil)
|
||
|
require.NoError(t, d.Flush())
|
||
|
// The flush moved the recycled memtable into position as an active mutable
|
||
|
// memtable. There are now two allocated memtables: 1 mutable and 1 pinned
|
||
|
// by the iterator's read state.
|
||
|
checkReserved(2 * int64(opts.MemTableSize))
|
||
|
|
||
|
// Flushing again should increase the reservation total to 3x: 1 active
|
||
|
// mutable, 1 for recycling, 1 pinned by iterator's read state.
|
||
|
require.NoError(t, d.Flush())
|
||
|
checkReserved(3 * int64(opts.MemTableSize))
|
||
|
|
||
|
// Closing the iterator will release the iterator's read state, and the old
|
||
|
// memtable will be moved into position as the next memtable to recycle.
|
||
|
// There was already a memtable ready to be recycled, so that memtable will
|
||
|
// be freed and the overall reservation total is reduced to 2x.
|
||
|
require.NoError(t, iter.Close())
|
||
|
checkReserved(2 * int64(opts.MemTableSize))
|
||
|
|
||
|
require.NoError(t, d.Close())
|
||
|
}
|
||
|
|
||
|
func TestMemTableReservationLeak(t *testing.T) {
|
||
|
d, err := Open("", &Options{FS: vfs.NewMem()})
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
d.mu.Lock()
|
||
|
last := d.mu.mem.queue[len(d.mu.mem.queue)-1]
|
||
|
last.readerRef()
|
||
|
defer func() {
|
||
|
last.readerUnref(true)
|
||
|
}()
|
||
|
d.mu.Unlock()
|
||
|
if err := d.Close(); err == nil {
|
||
|
t.Fatalf("expected failure, but found success")
|
||
|
} else if !strings.HasPrefix(err.Error(), "leaked memtable reservation:") {
|
||
|
t.Fatalf("expected leaked memtable reservation, but found %+v", err)
|
||
|
} else {
|
||
|
t.Log(err.Error())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestCacheEvict(t *testing.T) {
|
||
|
cache := NewCache(10 << 20)
|
||
|
defer cache.Unref()
|
||
|
|
||
|
d, err := Open("", &Options{
|
||
|
Cache: cache,
|
||
|
FS: vfs.NewMem(),
|
||
|
})
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
for i := 0; i < 1000; i++ {
|
||
|
key := []byte(fmt.Sprintf("%04d", i))
|
||
|
require.NoError(t, d.Set(key, key, nil))
|
||
|
}
|
||
|
|
||
|
require.NoError(t, d.Flush())
|
||
|
iter, _ := d.NewIter(nil)
|
||
|
for iter.First(); iter.Valid(); iter.Next() {
|
||
|
}
|
||
|
require.NoError(t, iter.Close())
|
||
|
|
||
|
if size := cache.Size(); size == 0 {
|
||
|
t.Fatalf("expected non-zero cache size")
|
||
|
}
|
||
|
|
||
|
for i := 0; i < 1000; i++ {
|
||
|
key := []byte(fmt.Sprintf("%04d", i))
|
||
|
require.NoError(t, d.Delete(key, nil))
|
||
|
}
|
||
|
|
||
|
require.NoError(t, d.Compact([]byte("0"), []byte("1"), false))
|
||
|
|
||
|
require.NoError(t, d.Close())
|
||
|
|
||
|
if size := cache.Size(); size != 0 {
|
||
|
t.Fatalf("expected empty cache, but found %d", size)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestFlushEmpty(t *testing.T) {
|
||
|
d, err := Open("", testingRandomized(t, &Options{
|
||
|
FS: vfs.NewMem(),
|
||
|
}))
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
// Flushing an empty memtable should not fail.
|
||
|
require.NoError(t, d.Flush())
|
||
|
require.NoError(t, d.Close())
|
||
|
}
|
||
|
|
||
|
func TestRollManifest(t *testing.T) {
|
||
|
toPreserve := rand.Int31n(5) + 1
|
||
|
opts := &Options{
|
||
|
MaxManifestFileSize: 1,
|
||
|
L0CompactionThreshold: 10,
|
||
|
L0StopWritesThreshold: 1000,
|
||
|
FS: vfs.NewMem(),
|
||
|
NumPrevManifest: int(toPreserve),
|
||
|
}
|
||
|
opts.DisableAutomaticCompactions = true
|
||
|
opts.testingRandomized(t)
|
||
|
d, err := Open("", opts)
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
manifestFileNumber := func() base.DiskFileNum {
|
||
|
d.mu.Lock()
|
||
|
defer d.mu.Unlock()
|
||
|
return d.mu.versions.manifestFileNum
|
||
|
}
|
||
|
sizeRolloverState := func() (int64, int64) {
|
||
|
d.mu.Lock()
|
||
|
defer d.mu.Unlock()
|
||
|
return d.mu.versions.rotationHelper.DebugInfo()
|
||
|
}
|
||
|
|
||
|
current := func() string {
|
||
|
desc, err := Peek(d.dirname, d.opts.FS)
|
||
|
require.NoError(t, err)
|
||
|
return desc.ManifestFilename
|
||
|
}
|
||
|
|
||
|
lastManifestNum := manifestFileNumber()
|
||
|
manifestNums := []base.DiskFileNum{lastManifestNum}
|
||
|
for i := 0; i < 5; i++ {
|
||
|
// MaxManifestFileSize is 1, but the rollover logic also counts edits
|
||
|
// since the last snapshot to decide on rollover, so do as many flushes as
|
||
|
// it demands.
|
||
|
lastSnapshotCount, editsSinceSnapshotCount := sizeRolloverState()
|
||
|
var expectedLastSnapshotCount, expectedEditsSinceSnapshotCount int64
|
||
|
switch i {
|
||
|
case 0:
|
||
|
// DB is empty.
|
||
|
expectedLastSnapshotCount, expectedEditsSinceSnapshotCount = 0, 0
|
||
|
case 1:
|
||
|
// First edit that caused rollover is not in the snapshot.
|
||
|
expectedLastSnapshotCount, expectedEditsSinceSnapshotCount = 0, 1
|
||
|
case 2:
|
||
|
// One flush is in the snapshot. One flush in the edit.
|
||
|
expectedLastSnapshotCount, expectedEditsSinceSnapshotCount = 1, 1
|
||
|
case 3:
|
||
|
// Two flushes in the snapshot. One flush in the edit. Will need to do
|
||
|
// two more flushes, the first of which will be in the next snapshot.
|
||
|
expectedLastSnapshotCount, expectedEditsSinceSnapshotCount = 2, 1
|
||
|
case 4:
|
||
|
// Four flushes in the snapshot. One flush in the edit. Will need to do
|
||
|
// four more flushes, three of which will be in the snapshot.
|
||
|
expectedLastSnapshotCount, expectedEditsSinceSnapshotCount = 4, 1
|
||
|
}
|
||
|
require.Equal(t, expectedLastSnapshotCount, lastSnapshotCount)
|
||
|
require.Equal(t, expectedEditsSinceSnapshotCount, editsSinceSnapshotCount)
|
||
|
// Number of flushes to do to trigger the rollover.
|
||
|
steps := int(lastSnapshotCount - editsSinceSnapshotCount + 1)
|
||
|
// Steps can be <= 0, but we need to do at least one edit to trigger the
|
||
|
// rollover logic.
|
||
|
if steps <= 0 {
|
||
|
steps = 1
|
||
|
}
|
||
|
for j := 0; j < steps; j++ {
|
||
|
require.NoError(t, d.Set([]byte("a"), nil, nil))
|
||
|
require.NoError(t, d.Flush())
|
||
|
}
|
||
|
d.TestOnlyWaitForCleaning()
|
||
|
num := manifestFileNumber()
|
||
|
if lastManifestNum == num {
|
||
|
t.Fatalf("manifest failed to roll %d: %d == %d", i, lastManifestNum, num)
|
||
|
}
|
||
|
|
||
|
manifestNums = append(manifestNums, num)
|
||
|
lastManifestNum = num
|
||
|
|
||
|
expectedCurrent := fmt.Sprintf("MANIFEST-%s", lastManifestNum)
|
||
|
if v := current(); expectedCurrent != v {
|
||
|
t.Fatalf("expected %s, but found %s", expectedCurrent, v)
|
||
|
}
|
||
|
}
|
||
|
lastSnapshotCount, editsSinceSnapshotCount := sizeRolloverState()
|
||
|
require.EqualValues(t, 8, lastSnapshotCount)
|
||
|
require.EqualValues(t, 1, editsSinceSnapshotCount)
|
||
|
|
||
|
files, err := d.opts.FS.List("")
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
var manifests []string
|
||
|
for _, filename := range files {
|
||
|
fileType, _, ok := base.ParseFilename(d.opts.FS, filename)
|
||
|
if !ok {
|
||
|
continue
|
||
|
}
|
||
|
if fileType == fileTypeManifest {
|
||
|
manifests = append(manifests, filename)
|
||
|
}
|
||
|
}
|
||
|
slices.Sort(manifests)
|
||
|
|
||
|
var expected []string
|
||
|
for i := len(manifestNums) - int(toPreserve) - 1; i < len(manifestNums); i++ {
|
||
|
expected = append(
|
||
|
expected,
|
||
|
fmt.Sprintf("MANIFEST-%s", manifestNums[i]),
|
||
|
)
|
||
|
}
|
||
|
require.EqualValues(t, expected, manifests)
|
||
|
|
||
|
// Test the logic that uses the future snapshot size to rollover.
|
||
|
// Reminder: we have a snapshot with 8 files and the manifest has 1 edit
|
||
|
// (flush) with 1 file.
|
||
|
// Add 8 more files with a different key.
|
||
|
lastManifestNum = manifestFileNumber()
|
||
|
for j := 0; j < 8; j++ {
|
||
|
require.NoError(t, d.Set([]byte("c"), nil, nil))
|
||
|
require.NoError(t, d.Flush())
|
||
|
}
|
||
|
lastSnapshotCount, editsSinceSnapshotCount = sizeRolloverState()
|
||
|
// Need 16 more files in edits to trigger a rollover.
|
||
|
require.EqualValues(t, 16, lastSnapshotCount)
|
||
|
require.EqualValues(t, 1, editsSinceSnapshotCount)
|
||
|
require.NotEqual(t, manifestFileNumber(), lastManifestNum)
|
||
|
lastManifestNum = manifestFileNumber()
|
||
|
// Do a compaction that moves 8 of the files from L0 to 1 file in L6. This
|
||
|
// adds 9 files in edits. We still need 6 more files in edits based on the
|
||
|
// last snapshot. But the current version has only 9 L0 files and 1 L6 file,
|
||
|
// for a total of 10 files. So 1 flush should push us over that threshold.
|
||
|
d.Compact([]byte("c"), []byte("d"), false)
|
||
|
lastSnapshotCount, editsSinceSnapshotCount = sizeRolloverState()
|
||
|
require.EqualValues(t, 16, lastSnapshotCount)
|
||
|
require.EqualValues(t, 10, editsSinceSnapshotCount)
|
||
|
require.Equal(t, manifestFileNumber(), lastManifestNum)
|
||
|
require.NoError(t, d.Set([]byte("c"), nil, nil))
|
||
|
require.NoError(t, d.Flush())
|
||
|
lastSnapshotCount, editsSinceSnapshotCount = sizeRolloverState()
|
||
|
require.EqualValues(t, 10, lastSnapshotCount)
|
||
|
require.EqualValues(t, 1, editsSinceSnapshotCount)
|
||
|
require.NotEqual(t, manifestFileNumber(), lastManifestNum)
|
||
|
|
||
|
require.NoError(t, d.Close())
|
||
|
}
|
||
|
|
||
|
func TestDBClosed(t *testing.T) {
|
||
|
d, err := Open("", &Options{
|
||
|
FS: vfs.NewMem(),
|
||
|
})
|
||
|
require.NoError(t, err)
|
||
|
require.NoError(t, d.Close())
|
||
|
|
||
|
catch := func(f func()) (err error) {
|
||
|
defer func() {
|
||
|
if r := recover(); r != nil {
|
||
|
err = r.(error)
|
||
|
}
|
||
|
}()
|
||
|
f()
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
require.True(t, errors.Is(catch(func() { _ = d.Close() }), ErrClosed))
|
||
|
|
||
|
require.True(t, errors.Is(catch(func() { _ = d.Compact(nil, nil, false) }), ErrClosed))
|
||
|
require.True(t, errors.Is(catch(func() { _ = d.Flush() }), ErrClosed))
|
||
|
require.True(t, errors.Is(catch(func() { _, _ = d.AsyncFlush() }), ErrClosed))
|
||
|
|
||
|
require.True(t, errors.Is(catch(func() { _, _, _ = d.Get(nil) }), ErrClosed))
|
||
|
require.True(t, errors.Is(catch(func() { _ = d.Delete(nil, nil) }), ErrClosed))
|
||
|
require.True(t, errors.Is(catch(func() { _ = d.DeleteRange(nil, nil, nil) }), ErrClosed))
|
||
|
require.True(t, errors.Is(catch(func() { _ = d.Ingest(nil) }), ErrClosed))
|
||
|
require.True(t, errors.Is(catch(func() { _ = d.LogData(nil, nil) }), ErrClosed))
|
||
|
require.True(t, errors.Is(catch(func() { _ = d.Merge(nil, nil, nil) }), ErrClosed))
|
||
|
require.True(t, errors.Is(catch(func() { _ = d.RatchetFormatMajorVersion(internalFormatNewest) }), ErrClosed))
|
||
|
require.True(t, errors.Is(catch(func() { _ = d.Set(nil, nil, nil) }), ErrClosed))
|
||
|
|
||
|
require.True(t, errors.Is(catch(func() { _ = d.NewSnapshot() }), ErrClosed))
|
||
|
|
||
|
b := d.NewIndexedBatch()
|
||
|
require.True(t, errors.Is(catch(func() { _ = b.Commit(nil) }), ErrClosed))
|
||
|
require.True(t, errors.Is(catch(func() { _ = d.Apply(b, nil) }), ErrClosed))
|
||
|
require.True(t, errors.Is(catch(func() { _, _ = b.NewIter(nil) }), ErrClosed))
|
||
|
}
|
||
|
|
||
|
func TestDBConcurrentCommitCompactFlush(t *testing.T) {
|
||
|
d, err := Open("", testingRandomized(t, &Options{
|
||
|
FS: vfs.NewMem(),
|
||
|
}))
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
// Concurrently commit, compact, and flush in order to stress the locking around
|
||
|
// those operations.
|
||
|
const n = 1000
|
||
|
var wg sync.WaitGroup
|
||
|
wg.Add(n)
|
||
|
for i := 0; i < n; i++ {
|
||
|
go func(i int) {
|
||
|
defer wg.Done()
|
||
|
_ = d.Set([]byte(fmt.Sprint(i)), nil, nil)
|
||
|
var err error
|
||
|
switch i % 3 {
|
||
|
case 0:
|
||
|
err = d.Compact(nil, []byte("\xff"), false)
|
||
|
case 1:
|
||
|
err = d.Flush()
|
||
|
case 2:
|
||
|
_, err = d.AsyncFlush()
|
||
|
}
|
||
|
require.NoError(t, err)
|
||
|
}(i)
|
||
|
}
|
||
|
wg.Wait()
|
||
|
|
||
|
require.NoError(t, d.Close())
|
||
|
}
|
||
|
|
||
|
func TestDBConcurrentCompactClose(t *testing.T) {
|
||
|
// Test closing while a compaction is ongoing. This ensures compaction code
|
||
|
// detects the close and finishes cleanly.
|
||
|
mem := vfs.NewMem()
|
||
|
for i := 0; i < 100; i++ {
|
||
|
opts := &Options{
|
||
|
FS: mem,
|
||
|
MaxConcurrentCompactions: func() int {
|
||
|
return 2
|
||
|
},
|
||
|
}
|
||
|
d, err := Open("", testingRandomized(t, opts))
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
// Ingest a series of files containing a single key each. As the outer
|
||
|
// loop progresses, these ingestions will build up compaction debt
|
||
|
// causing compactions to be running concurrently with the close below.
|
||
|
for j := 0; j < 10; j++ {
|
||
|
path := fmt.Sprintf("ext%d", j)
|
||
|
f, err := mem.Create(path)
|
||
|
require.NoError(t, err)
|
||
|
w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{
|
||
|
TableFormat: d.FormatMajorVersion().MaxTableFormat(),
|
||
|
})
|
||
|
require.NoError(t, w.Set([]byte(fmt.Sprint(j)), nil))
|
||
|
require.NoError(t, w.Close())
|
||
|
require.NoError(t, d.Ingest([]string{path}))
|
||
|
}
|
||
|
|
||
|
require.NoError(t, d.Close())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestDBApplyBatchNilDB(t *testing.T) {
|
||
|
d, err := Open("", &Options{FS: vfs.NewMem()})
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
b1 := &Batch{}
|
||
|
b1.Set([]byte("test"), nil, nil)
|
||
|
|
||
|
b2 := &Batch{}
|
||
|
b2.Apply(b1, nil)
|
||
|
if b2.memTableSize != 0 {
|
||
|
t.Fatalf("expected memTableSize to not be set")
|
||
|
}
|
||
|
require.NoError(t, d.Apply(b2, nil))
|
||
|
if b1.memTableSize != b2.memTableSize {
|
||
|
t.Fatalf("expected memTableSize %d, but found %d", b1.memTableSize, b2.memTableSize)
|
||
|
}
|
||
|
|
||
|
require.NoError(t, d.Close())
|
||
|
}
|
||
|
|
||
|
func TestDBApplyBatchMismatch(t *testing.T) {
|
||
|
srcDB, err := Open("", &Options{FS: vfs.NewMem()})
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
applyDB, err := Open("", &Options{FS: vfs.NewMem()})
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
err = func() (err error) {
|
||
|
defer func() {
|
||
|
if v := recover(); v != nil {
|
||
|
err = errors.Errorf("%v", v)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
b := srcDB.NewBatch()
|
||
|
b.Set([]byte("test"), nil, nil)
|
||
|
return applyDB.Apply(b, nil)
|
||
|
}()
|
||
|
if err == nil || !strings.Contains(err.Error(), "pebble: batch db mismatch:") {
|
||
|
t.Fatalf("expected error, but found %v", err)
|
||
|
}
|
||
|
|
||
|
require.NoError(t, srcDB.Close())
|
||
|
require.NoError(t, applyDB.Close())
|
||
|
}
|
||
|
|
||
|
func TestCloseCleanerRace(t *testing.T) {
|
||
|
mem := vfs.NewMem()
|
||
|
for i := 0; i < 20; i++ {
|
||
|
db, err := Open("", testingRandomized(t, &Options{FS: mem}))
|
||
|
require.NoError(t, err)
|
||
|
require.NoError(t, db.Set([]byte("a"), []byte("something"), Sync))
|
||
|
require.NoError(t, db.Flush())
|
||
|
// Ref the sstables so cannot be deleted.
|
||
|
it, _ := db.NewIter(nil)
|
||
|
require.NotNil(t, it)
|
||
|
require.NoError(t, db.DeleteRange([]byte("a"), []byte("b"), Sync))
|
||
|
require.NoError(t, db.Compact([]byte("a"), []byte("b"), false))
|
||
|
// Only the iterator is keeping the sstables alive.
|
||
|
files, err := mem.List("/")
|
||
|
require.NoError(t, err)
|
||
|
var found bool
|
||
|
for _, f := range files {
|
||
|
if strings.HasSuffix(f, ".sst") {
|
||
|
found = true
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
require.True(t, found)
|
||
|
// Close the iterator and the db in succession so file cleaning races with DB.Close() --
|
||
|
// latter should wait for file cleaning to finish.
|
||
|
require.NoError(t, it.Close())
|
||
|
require.NoError(t, db.Close())
|
||
|
files, err = mem.List("/")
|
||
|
require.NoError(t, err)
|
||
|
for _, f := range files {
|
||
|
if strings.HasSuffix(f, ".sst") {
|
||
|
t.Fatalf("found sst: %s", f)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestSSTablesWithApproximateSpanBytes(t *testing.T) {
|
||
|
d, err := Open("", &Options{
|
||
|
FS: vfs.NewMem(),
|
||
|
})
|
||
|
require.NoError(t, err)
|
||
|
defer func() {
|
||
|
if d != nil {
|
||
|
require.NoError(t, d.Close())
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
// Create two sstables.
|
||
|
// sstable is contained within keyspan (fileNum = 5).
|
||
|
require.NoError(t, d.Set([]byte("c"), nil, nil))
|
||
|
require.NoError(t, d.Set([]byte("d"), nil, nil))
|
||
|
require.NoError(t, d.Flush())
|
||
|
|
||
|
// sstable partially overlaps keyspan (fileNum = 7).
|
||
|
require.NoError(t, d.Set([]byte("d"), nil, nil))
|
||
|
require.NoError(t, d.Set([]byte("g"), nil, nil))
|
||
|
require.NoError(t, d.Flush())
|
||
|
|
||
|
// cannot use WithApproximateSpanBytes without WithProperties.
|
||
|
_, err = d.SSTables(WithKeyRangeFilter([]byte("a"), []byte("e")), WithApproximateSpanBytes())
|
||
|
require.Error(t, err)
|
||
|
|
||
|
// cannot use WithApproximateSpanBytes without WithKeyRangeFilter.
|
||
|
_, err = d.SSTables(WithProperties(), WithApproximateSpanBytes())
|
||
|
require.Error(t, err)
|
||
|
|
||
|
tableInfos, err := d.SSTables(WithProperties(), WithKeyRangeFilter([]byte("a"), []byte("e")), WithApproximateSpanBytes())
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
for _, levelTables := range tableInfos {
|
||
|
for _, table := range levelTables {
|
||
|
approximateSpanBytes, err := strconv.ParseInt(table.Properties.UserProperties["approximate-span-bytes"], 10, 64)
|
||
|
require.NoError(t, err)
|
||
|
if table.FileNum == 5 {
|
||
|
require.Equal(t, uint64(approximateSpanBytes), table.Size)
|
||
|
}
|
||
|
if table.FileNum == 7 {
|
||
|
require.Less(t, uint64(approximateSpanBytes), table.Size)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestFilterSSTablesWithOption(t *testing.T) {
|
||
|
d, err := Open("", &Options{
|
||
|
FS: vfs.NewMem(),
|
||
|
})
|
||
|
require.NoError(t, err)
|
||
|
defer func() {
|
||
|
if d != nil {
|
||
|
require.NoError(t, d.Close())
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
// Create two sstables.
|
||
|
require.NoError(t, d.Set([]byte("/Table/5"), nil, nil))
|
||
|
require.NoError(t, d.Flush())
|
||
|
require.NoError(t, d.Set([]byte("/Table/10"), nil, nil))
|
||
|
require.NoError(t, d.Flush())
|
||
|
|
||
|
tableInfos, err := d.SSTables(WithKeyRangeFilter([]byte("/Table/5"), []byte("/Table/6")))
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
totalTables := 0
|
||
|
for _, levelTables := range tableInfos {
|
||
|
totalTables += len(levelTables)
|
||
|
}
|
||
|
|
||
|
// with filter second sstable should not be returned
|
||
|
require.EqualValues(t, 1, totalTables)
|
||
|
|
||
|
tableInfos, err = d.SSTables()
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
totalTables = 0
|
||
|
for _, levelTables := range tableInfos {
|
||
|
totalTables += len(levelTables)
|
||
|
}
|
||
|
|
||
|
// without filter
|
||
|
require.EqualValues(t, 2, totalTables)
|
||
|
}
|
||
|
|
||
|
func TestSSTables(t *testing.T) {
|
||
|
d, err := Open("", &Options{
|
||
|
FS: vfs.NewMem(),
|
||
|
})
|
||
|
require.NoError(t, err)
|
||
|
defer func() {
|
||
|
if d != nil {
|
||
|
require.NoError(t, d.Close())
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
// Create two sstables.
|
||
|
require.NoError(t, d.Set([]byte("hello"), nil, nil))
|
||
|
require.NoError(t, d.Flush())
|
||
|
require.NoError(t, d.Set([]byte("world"), nil, nil))
|
||
|
require.NoError(t, d.Flush())
|
||
|
|
||
|
// by default returned table infos should not contain Properties
|
||
|
tableInfos, err := d.SSTables()
|
||
|
require.NoError(t, err)
|
||
|
for _, levelTables := range tableInfos {
|
||
|
for _, info := range levelTables {
|
||
|
require.Nil(t, info.Properties)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// with opt `WithProperties()` the `Properties` in table info should not be nil
|
||
|
tableInfos, err = d.SSTables(WithProperties())
|
||
|
require.NoError(t, err)
|
||
|
for _, levelTables := range tableInfos {
|
||
|
for _, info := range levelTables {
|
||
|
require.NotNil(t, info.Properties)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type testTracer struct {
|
||
|
enabledOnlyForNonBackgroundContext bool
|
||
|
buf strings.Builder
|
||
|
}
|
||
|
|
||
|
func (t *testTracer) Infof(format string, args ...interface{}) {}
|
||
|
func (t *testTracer) Errorf(format string, args ...interface{}) {}
|
||
|
func (t *testTracer) Fatalf(format string, args ...interface{}) {}
|
||
|
|
||
|
func (t *testTracer) Eventf(ctx context.Context, format string, args ...interface{}) {
|
||
|
if t.enabledOnlyForNonBackgroundContext && ctx == context.Background() {
|
||
|
return
|
||
|
}
|
||
|
fmt.Fprintf(&t.buf, format, args...)
|
||
|
fmt.Fprint(&t.buf, "\n")
|
||
|
}
|
||
|
|
||
|
func (t *testTracer) IsTracingEnabled(ctx context.Context) bool {
|
||
|
if t.enabledOnlyForNonBackgroundContext && ctx == context.Background() {
|
||
|
return false
|
||
|
}
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
func TestTracing(t *testing.T) {
|
||
|
if !invariants.Enabled {
|
||
|
// The test relies on timing behavior injected when invariants.Enabled.
|
||
|
return
|
||
|
}
|
||
|
var tracer testTracer
|
||
|
c := NewCache(0)
|
||
|
defer c.Unref()
|
||
|
d, err := Open("", &Options{
|
||
|
FS: vfs.NewMem(),
|
||
|
Cache: c,
|
||
|
LoggerAndTracer: &tracer,
|
||
|
})
|
||
|
require.NoError(t, err)
|
||
|
defer func() {
|
||
|
require.NoError(t, d.Close())
|
||
|
}()
|
||
|
|
||
|
// Create a sstable.
|
||
|
require.NoError(t, d.Set([]byte("hello"), nil, nil))
|
||
|
require.NoError(t, d.Flush())
|
||
|
_, closer, err := d.Get([]byte("hello"))
|
||
|
require.NoError(t, err)
|
||
|
closer.Close()
|
||
|
readerInitTraceString := "reading 37 bytes took 5ms\nreading 628 bytes took 5ms\n"
|
||
|
iterTraceString := "reading 27 bytes took 5ms\nreading 29 bytes took 5ms\n"
|
||
|
require.Equal(t, readerInitTraceString+iterTraceString, tracer.buf.String())
|
||
|
|
||
|
// Get again, but since it currently uses context.Background(), no trace
|
||
|
// output is produced.
|
||
|
tracer.buf.Reset()
|
||
|
tracer.enabledOnlyForNonBackgroundContext = true
|
||
|
_, closer, err = d.Get([]byte("hello"))
|
||
|
require.NoError(t, err)
|
||
|
closer.Close()
|
||
|
require.Equal(t, "", tracer.buf.String())
|
||
|
|
||
|
ctx, cancel := context.WithCancel(context.Background())
|
||
|
defer cancel()
|
||
|
iter, _ := d.NewIterWithContext(ctx, nil)
|
||
|
iter.SeekGE([]byte("hello"))
|
||
|
iter.Close()
|
||
|
require.Equal(t, iterTraceString, tracer.buf.String())
|
||
|
|
||
|
tracer.buf.Reset()
|
||
|
snap := d.NewSnapshot()
|
||
|
iter, _ = snap.NewIterWithContext(ctx, nil)
|
||
|
iter.SeekGE([]byte("hello"))
|
||
|
iter.Close()
|
||
|
require.Equal(t, iterTraceString, tracer.buf.String())
|
||
|
snap.Close()
|
||
|
|
||
|
tracer.buf.Reset()
|
||
|
b := d.NewIndexedBatch()
|
||
|
iter, err = b.NewIterWithContext(ctx, nil)
|
||
|
require.NoError(t, err)
|
||
|
iter.SeekGE([]byte("hello"))
|
||
|
iter.Close()
|
||
|
require.Equal(t, iterTraceString, tracer.buf.String())
|
||
|
b.Close()
|
||
|
}
|
||
|
|
||
|
func TestMemtableIngestInversion(t *testing.T) {
|
||
|
memFS := vfs.NewMem()
|
||
|
opts := &Options{
|
||
|
FS: memFS,
|
||
|
MemTableSize: 256 << 10, // 4KB
|
||
|
MemTableStopWritesThreshold: 1000,
|
||
|
L0StopWritesThreshold: 1000,
|
||
|
L0CompactionThreshold: 2,
|
||
|
MaxConcurrentCompactions: func() int {
|
||
|
return 1000
|
||
|
},
|
||
|
}
|
||
|
|
||
|
const channelTimeout = 5 * time.Second
|
||
|
|
||
|
// We induce delay in compactions by passing in an EventListener that stalls on
|
||
|
// the first TableCreated event for a compaction job we want to block.
|
||
|
// FlushBegin and CompactionBegin has info on compaction start/output levels
|
||
|
// which is what we need to identify what compactions to block. However
|
||
|
// FlushBegin and CompactionBegin are called while holding db.mu, so we cannot
|
||
|
// block those events forever. Instead, we grab the job ID from those events
|
||
|
// and store it. Then during TableCreated, we check if we're creating an output
|
||
|
// for a job we have identified earlier as one to block, and then hold on a
|
||
|
// semaphore there until there's a signal from the test code to resume with the
|
||
|
// compaction.
|
||
|
//
|
||
|
// If nextBlockedCompaction is non-zero, we must block the next compaction
|
||
|
// out of the nextBlockedCompaction - 3 start level. 1 means block the next
|
||
|
// intra-L0 compaction and 2 means block the next flush (as flushes have
|
||
|
// a -1 start level).
|
||
|
var nextBlockedCompaction, blockedJobID int
|
||
|
var blockedCompactionsMu sync.Mutex // protects the above two variables.
|
||
|
nextSem := make(chan chan struct{}, 1)
|
||
|
var el EventListener
|
||
|
el.EnsureDefaults(testLogger{t: t})
|
||
|
el.FlushBegin = func(info FlushInfo) {
|
||
|
blockedCompactionsMu.Lock()
|
||
|
defer blockedCompactionsMu.Unlock()
|
||
|
if nextBlockedCompaction == 2 {
|
||
|
nextBlockedCompaction = 0
|
||
|
blockedJobID = info.JobID
|
||
|
}
|
||
|
}
|
||
|
el.CompactionBegin = func(info CompactionInfo) {
|
||
|
// 0 = block nothing, 1 = block intra-L0 compaction, 2 = block flush,
|
||
|
// 3 = block L0 -> LBase compaction, 4 = block compaction out of L1, and so on.
|
||
|
blockedCompactionsMu.Lock()
|
||
|
defer blockedCompactionsMu.Unlock()
|
||
|
blockValue := info.Input[0].Level + 3
|
||
|
if info.Input[0].Level == 0 && info.Output.Level == 0 {
|
||
|
// Intra L0 compaction, denoted by casValue of 1.
|
||
|
blockValue = 1
|
||
|
}
|
||
|
if nextBlockedCompaction == blockValue {
|
||
|
nextBlockedCompaction = 0
|
||
|
blockedJobID = info.JobID
|
||
|
}
|
||
|
}
|
||
|
el.TableCreated = func(info TableCreateInfo) {
|
||
|
blockedCompactionsMu.Lock()
|
||
|
if info.JobID != blockedJobID {
|
||
|
blockedCompactionsMu.Unlock()
|
||
|
return
|
||
|
}
|
||
|
blockedJobID = 0
|
||
|
blockedCompactionsMu.Unlock()
|
||
|
sem := make(chan struct{})
|
||
|
nextSem <- sem
|
||
|
<-sem
|
||
|
}
|
||
|
tel := TeeEventListener(MakeLoggingEventListener(testLogger{t: t}), el)
|
||
|
opts.EventListener = &tel
|
||
|
opts.Experimental.L0CompactionConcurrency = 1
|
||
|
d, err := Open("", opts)
|
||
|
require.NoError(t, err)
|
||
|
defer func() {
|
||
|
if d != nil {
|
||
|
require.NoError(t, d.Close())
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
printLSM := func() {
|
||
|
d.mu.Lock()
|
||
|
s := d.mu.versions.currentVersion().String()
|
||
|
d.mu.Unlock()
|
||
|
t.Logf("%s", s)
|
||
|
}
|
||
|
|
||
|
// Create some sstables. These should go into L6. These are irrelevant for
|
||
|
// the rest of the test.
|
||
|
require.NoError(t, d.Set([]byte("b"), []byte("foo"), nil))
|
||
|
require.NoError(t, d.Flush())
|
||
|
require.NoError(t, d.Set([]byte("d"), []byte("bar"), nil))
|
||
|
require.NoError(t, d.Flush())
|
||
|
require.NoError(t, d.Compact([]byte("a"), []byte("z"), true))
|
||
|
|
||
|
var baseCompactionSem, flushSem, intraL0Sem chan struct{}
|
||
|
// Block an L0 -> LBase compaction. This is necessary to induce intra-L0
|
||
|
// compactions later on.
|
||
|
blockedCompactionsMu.Lock()
|
||
|
nextBlockedCompaction = 3
|
||
|
blockedCompactionsMu.Unlock()
|
||
|
timeoutSem := time.After(channelTimeout)
|
||
|
t.Log("blocking an L0 -> LBase compaction")
|
||
|
// Write an sstable to L0 until we're blocked on an L0 -> LBase compaction.
|
||
|
breakLoop := false
|
||
|
for !breakLoop {
|
||
|
select {
|
||
|
case sem := <-nextSem:
|
||
|
baseCompactionSem = sem
|
||
|
breakLoop = true
|
||
|
case <-timeoutSem:
|
||
|
t.Fatal("did not get blocked on an LBase compaction")
|
||
|
default:
|
||
|
require.NoError(t, d.Set([]byte("b"), []byte("foo"), nil))
|
||
|
require.NoError(t, d.Set([]byte("g"), []byte("bar"), nil))
|
||
|
require.NoError(t, d.Flush())
|
||
|
time.Sleep(100 * time.Millisecond)
|
||
|
}
|
||
|
}
|
||
|
printLSM()
|
||
|
|
||
|
// Do 4 ingests, one with the key cc, one with bb and cc, and two with just bb.
|
||
|
// The purpose of the sstable containing cc is to inflate the L0 sublevel
|
||
|
// count of the interval at cc, as that's where we want the intra-L0 compaction
|
||
|
// to be seeded. However we also need a file left of that interval to have
|
||
|
// the same (or higher) sublevel to trigger the bug in
|
||
|
// cockroachdb/cockroach#101896. That's why we ingest a file after it to
|
||
|
// "bridge" the bb/cc intervals, and then ingest a file at bb. These go
|
||
|
// into sublevels like this:
|
||
|
//
|
||
|
// bb
|
||
|
// bb
|
||
|
// bb-----cc
|
||
|
// cc
|
||
|
//
|
||
|
// Eventually, we'll drop an ingested file containing a range del starting at
|
||
|
// cc around here:
|
||
|
//
|
||
|
// bb
|
||
|
// bb cc---...
|
||
|
// bb-----cc
|
||
|
// cc
|
||
|
{
|
||
|
path := "ingest1.sst"
|
||
|
f, err := memFS.Create(path)
|
||
|
require.NoError(t, err)
|
||
|
w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{
|
||
|
TableFormat: d.FormatMajorVersion().MaxTableFormat(),
|
||
|
})
|
||
|
require.NoError(t, w.Set([]byte("cc"), []byte("foo")))
|
||
|
require.NoError(t, w.Close())
|
||
|
require.NoError(t, d.Ingest([]string{path}))
|
||
|
}
|
||
|
{
|
||
|
path := "ingest2.sst"
|
||
|
f, err := memFS.Create(path)
|
||
|
require.NoError(t, err)
|
||
|
w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{
|
||
|
TableFormat: d.FormatMajorVersion().MaxTableFormat(),
|
||
|
})
|
||
|
require.NoError(t, w.Set([]byte("bb"), []byte("foo2")))
|
||
|
require.NoError(t, w.Set([]byte("cc"), []byte("foo2")))
|
||
|
require.NoError(t, w.Close())
|
||
|
require.NoError(t, d.Ingest([]string{path}))
|
||
|
}
|
||
|
{
|
||
|
path := "ingest3.sst"
|
||
|
f, err := memFS.Create(path)
|
||
|
require.NoError(t, err)
|
||
|
w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{
|
||
|
TableFormat: d.FormatMajorVersion().MaxTableFormat(),
|
||
|
})
|
||
|
require.NoError(t, w.Set([]byte("bb"), []byte("foo3")))
|
||
|
require.NoError(t, w.Close())
|
||
|
require.NoError(t, d.Ingest([]string{path}))
|
||
|
}
|
||
|
{
|
||
|
path := "ingest4.sst"
|
||
|
f, err := memFS.Create(path)
|
||
|
require.NoError(t, err)
|
||
|
w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{
|
||
|
TableFormat: d.FormatMajorVersion().MaxTableFormat(),
|
||
|
})
|
||
|
require.NoError(t, w.Set([]byte("bb"), []byte("foo4")))
|
||
|
require.NoError(t, w.Close())
|
||
|
require.NoError(t, d.Ingest([]string{path}))
|
||
|
}
|
||
|
|
||
|
// We now have a base compaction blocked. Block a memtable flush to cause
|
||
|
// memtables to queue up.
|
||
|
//
|
||
|
// Memtable (stuck):
|
||
|
//
|
||
|
// b-----------------g
|
||
|
//
|
||
|
// Relevant L0 ssstables
|
||
|
//
|
||
|
// bb
|
||
|
// bb
|
||
|
// bb-----cc
|
||
|
// cc
|
||
|
blockedCompactionsMu.Lock()
|
||
|
nextBlockedCompaction = 2
|
||
|
blockedCompactionsMu.Unlock()
|
||
|
t.Log("blocking a flush")
|
||
|
require.NoError(t, d.Set([]byte("b"), []byte("foo2"), nil))
|
||
|
require.NoError(t, d.Set([]byte("g"), []byte("bar2"), nil))
|
||
|
_, _ = d.AsyncFlush()
|
||
|
select {
|
||
|
case sem := <-nextSem:
|
||
|
flushSem = sem
|
||
|
case <-time.After(channelTimeout):
|
||
|
t.Fatal("did not get blocked on a flush")
|
||
|
}
|
||
|
// Add one memtable to flush queue, and finish it off.
|
||
|
//
|
||
|
// Memtables (stuck):
|
||
|
//
|
||
|
// b-----------------g (waiting to flush)
|
||
|
// b-----------------g (flushing, blocked)
|
||
|
//
|
||
|
// Relevant L0 ssstables
|
||
|
//
|
||
|
// bb
|
||
|
// bb
|
||
|
// bb-----cc
|
||
|
// cc
|
||
|
require.NoError(t, d.Set([]byte("b"), []byte("foo3"), nil))
|
||
|
require.NoError(t, d.Set([]byte("g"), []byte("bar3"), nil))
|
||
|
// note: this flush will wait for the earlier, blocked flush, but it closes
|
||
|
// off the memtable which is what we want.
|
||
|
_, _ = d.AsyncFlush()
|
||
|
|
||
|
// Open a new mutable memtable. This gets us an earlier earlierUnflushedSeqNum
|
||
|
// than the ingest below it.
|
||
|
require.NoError(t, d.Set([]byte("c"), []byte("somethingbigishappening"), nil))
|
||
|
// Block an intra-L0 compaction, as one might happen around this time.
|
||
|
blockedCompactionsMu.Lock()
|
||
|
nextBlockedCompaction = 1
|
||
|
blockedCompactionsMu.Unlock()
|
||
|
t.Log("blocking an intra-L0 compaction")
|
||
|
// Ingest a file containing a cc-e rangedel.
|
||
|
//
|
||
|
// Memtables:
|
||
|
//
|
||
|
// c (mutable)
|
||
|
// b-----------------g (waiting to flush)
|
||
|
// b-----------------g (flushing, blocked)
|
||
|
//
|
||
|
// Relevant L0 ssstables
|
||
|
//
|
||
|
// bb
|
||
|
// bb cc-----e (just ingested)
|
||
|
// bb-----cc
|
||
|
// cc
|
||
|
{
|
||
|
path := "ingest5.sst"
|
||
|
f, err := memFS.Create(path)
|
||
|
require.NoError(t, err)
|
||
|
w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{
|
||
|
TableFormat: d.FormatMajorVersion().MaxTableFormat(),
|
||
|
})
|
||
|
require.NoError(t, w.DeleteRange([]byte("cc"), []byte("e")))
|
||
|
require.NoError(t, w.Close())
|
||
|
require.NoError(t, d.Ingest([]string{path}))
|
||
|
}
|
||
|
t.Log("main ingest complete")
|
||
|
printLSM()
|
||
|
t.Logf("%s", d.Metrics().String())
|
||
|
|
||
|
require.NoError(t, d.Set([]byte("d"), []byte("ThisShouldNotBeDeleted"), nil))
|
||
|
|
||
|
// Do another ingest with a seqnum newer than d. The purpose of this is to
|
||
|
// increase the LargestSeqNum of the intra-L0 compaction output *beyond*
|
||
|
// the flush that contains d=ThisShouldNotBeDeleted, therefore causing
|
||
|
// that point key to be deleted (in the buggy code).
|
||
|
//
|
||
|
// Memtables:
|
||
|
//
|
||
|
// c-----d (mutable)
|
||
|
// b-----------------g (waiting to flush)
|
||
|
// b-----------------g (flushing, blocked)
|
||
|
//
|
||
|
// Relevant L0 ssstables
|
||
|
//
|
||
|
// bb cc
|
||
|
// bb cc-----e (just ingested)
|
||
|
// bb-----cc
|
||
|
// cc
|
||
|
{
|
||
|
path := "ingest6.sst"
|
||
|
f, err := memFS.Create(path)
|
||
|
require.NoError(t, err)
|
||
|
w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{
|
||
|
TableFormat: d.FormatMajorVersion().MaxTableFormat(),
|
||
|
})
|
||
|
require.NoError(t, w.Set([]byte("cc"), []byte("doesntmatter")))
|
||
|
require.NoError(t, w.Close())
|
||
|
require.NoError(t, d.Ingest([]string{path}))
|
||
|
}
|
||
|
|
||
|
// Unblock earlier flushes. We will first finish flushing the blocked
|
||
|
// memtable, and end up in this state:
|
||
|
//
|
||
|
// Memtables:
|
||
|
//
|
||
|
// c-----d (mutable)
|
||
|
// b-----------------g (waiting to flush)
|
||
|
//
|
||
|
// Relevant L0 ssstables
|
||
|
//
|
||
|
// b-------------------g (irrelevant, just flushed)
|
||
|
// bb cc (has LargestSeqNum > earliestUnflushedSeqNum)
|
||
|
// bb cc-----e (has a rangedel)
|
||
|
// bb-----cc
|
||
|
// cc
|
||
|
//
|
||
|
// Note that while b----g is relatively old (and so has a low LargestSeqNum),
|
||
|
// it bridges a bunch of intervals. Had we regenerated sublevels from scratch,
|
||
|
// it'd have gone below the cc-e sstable. But due to #101896, we just slapped
|
||
|
// it on top. Now, as long as our seed interval is the one at cc and our seed
|
||
|
// file is the just-flushed L0 sstable, we will go down and include anything
|
||
|
// in that interval even if it has a LargestSeqNum > earliestUnflushedSeqNum.
|
||
|
//
|
||
|
// All asterisked L0 sstables should now get picked in an intra-L0 compaction
|
||
|
// right after the flush finishes, that we then block:
|
||
|
//
|
||
|
// b-------------------g*
|
||
|
// bb* cc*
|
||
|
// bb* cc-----e*
|
||
|
// bb-----cc*
|
||
|
// cc*
|
||
|
t.Log("unblocking flush")
|
||
|
flushSem <- struct{}{}
|
||
|
printLSM()
|
||
|
|
||
|
select {
|
||
|
case sem := <-nextSem:
|
||
|
intraL0Sem = sem
|
||
|
case <-time.After(channelTimeout):
|
||
|
t.Fatal("did not get blocked on an intra L0 compaction")
|
||
|
}
|
||
|
|
||
|
// Ensure all memtables are flushed. This will mean d=ThisShouldNotBeDeleted
|
||
|
// will land in L0 and since that was the last key written to a memtable,
|
||
|
// and the ingestion at cc came after it, the output of the intra-L0
|
||
|
// compaction will elevate the cc-e rangedel above it and delete it
|
||
|
// (if #101896 is not fixed).
|
||
|
ch, _ := d.AsyncFlush()
|
||
|
<-ch
|
||
|
|
||
|
// Unblock earlier intra-L0 compaction.
|
||
|
t.Log("unblocking intraL0")
|
||
|
intraL0Sem <- struct{}{}
|
||
|
printLSM()
|
||
|
|
||
|
// Try reading d a couple times.
|
||
|
for i := 0; i < 2; i++ {
|
||
|
val, closer, err := d.Get([]byte("d"))
|
||
|
require.NoError(t, err)
|
||
|
require.Equal(t, []byte("ThisShouldNotBeDeleted"), val)
|
||
|
if closer != nil {
|
||
|
closer.Close()
|
||
|
}
|
||
|
time.Sleep(100 * time.Millisecond)
|
||
|
}
|
||
|
|
||
|
// Unblock everything.
|
||
|
baseCompactionSem <- struct{}{}
|
||
|
}
|
||
|
|
||
|
func BenchmarkDelete(b *testing.B) {
|
||
|
rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano())))
|
||
|
const keyCount = 10000
|
||
|
var keys [keyCount][]byte
|
||
|
for i := 0; i < keyCount; i++ {
|
||
|
keys[i] = []byte(strconv.Itoa(rng.Int()))
|
||
|
}
|
||
|
val := bytes.Repeat([]byte("x"), 10)
|
||
|
|
||
|
benchmark := func(b *testing.B, useSingleDelete bool) {
|
||
|
d, err := Open(
|
||
|
"",
|
||
|
&Options{
|
||
|
FS: vfs.NewMem(),
|
||
|
})
|
||
|
if err != nil {
|
||
|
b.Fatal(err)
|
||
|
}
|
||
|
defer func() {
|
||
|
if err := d.Close(); err != nil {
|
||
|
b.Fatal(err)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
b.StartTimer()
|
||
|
for _, key := range keys {
|
||
|
_ = d.Set(key, val, nil)
|
||
|
if useSingleDelete {
|
||
|
_ = d.SingleDelete(key, nil)
|
||
|
} else {
|
||
|
_ = d.Delete(key, nil)
|
||
|
}
|
||
|
}
|
||
|
// Manually flush as it is flushing/compaction where SingleDelete
|
||
|
// performance shows up. With SingleDelete, we can elide all of the
|
||
|
// SingleDelete and Set records.
|
||
|
if err := d.Flush(); err != nil {
|
||
|
b.Fatal(err)
|
||
|
}
|
||
|
b.StopTimer()
|
||
|
}
|
||
|
|
||
|
b.Run("delete", func(b *testing.B) {
|
||
|
for i := 0; i < b.N; i++ {
|
||
|
benchmark(b, false)
|
||
|
}
|
||
|
})
|
||
|
|
||
|
b.Run("single-delete", func(b *testing.B) {
|
||
|
for i := 0; i < b.N; i++ {
|
||
|
benchmark(b, true)
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func BenchmarkNewIterReadAmp(b *testing.B) {
|
||
|
for _, readAmp := range []int{10, 100, 1000} {
|
||
|
b.Run(strconv.Itoa(readAmp), func(b *testing.B) {
|
||
|
opts := &Options{
|
||
|
FS: vfs.NewMem(),
|
||
|
L0StopWritesThreshold: 1000,
|
||
|
}
|
||
|
opts.DisableAutomaticCompactions = true
|
||
|
|
||
|
d, err := Open("", opts)
|
||
|
require.NoError(b, err)
|
||
|
|
||
|
for i := 0; i < readAmp; i++ {
|
||
|
require.NoError(b, d.Set([]byte("a"), []byte("b"), NoSync))
|
||
|
require.NoError(b, d.Flush())
|
||
|
}
|
||
|
|
||
|
require.Equal(b, d.Metrics().ReadAmp(), readAmp)
|
||
|
|
||
|
b.StopTimer()
|
||
|
b.ResetTimer()
|
||
|
for i := 0; i < b.N; i++ {
|
||
|
b.StartTimer()
|
||
|
iter, _ := d.NewIter(nil)
|
||
|
b.StopTimer()
|
||
|
require.NoError(b, iter.Close())
|
||
|
}
|
||
|
|
||
|
require.NoError(b, d.Close())
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func verifyGet(t *testing.T, r Reader, key, expected []byte) {
|
||
|
val, closer, err := r.Get(key)
|
||
|
require.NoError(t, err)
|
||
|
if !bytes.Equal(expected, val) {
|
||
|
t.Fatalf("expected %s, but got %s", expected, val)
|
||
|
}
|
||
|
closer.Close()
|
||
|
}
|
||
|
|
||
|
func verifyGetNotFound(t *testing.T, r Reader, key []byte) {
|
||
|
val, _, err := r.Get(key)
|
||
|
if err != base.ErrNotFound {
|
||
|
t.Fatalf("expected nil, but got %s", val)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func BenchmarkRotateMemtables(b *testing.B) {
|
||
|
o := &Options{FS: vfs.NewMem(), MemTableSize: 64 << 20 /* 64 MB */}
|
||
|
d, err := Open("", o)
|
||
|
require.NoError(b, err)
|
||
|
|
||
|
// We want to jump to full-sized memtables.
|
||
|
d.mu.Lock()
|
||
|
d.mu.mem.nextSize = o.MemTableSize
|
||
|
d.mu.Unlock()
|
||
|
require.NoError(b, d.Flush())
|
||
|
|
||
|
b.ResetTimer()
|
||
|
for i := 0; i < b.N; i++ {
|
||
|
if err := d.Flush(); err != nil {
|
||
|
b.Fatal(err)
|
||
|
}
|
||
|
}
|
||
|
}
|