// Copyright 2018 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" "fmt" "runtime" "strings" "sync" "testing" "time" "github.com/cockroachdb/datadriven" "github.com/cockroachdb/pebble/internal/base" "github.com/cockroachdb/pebble/internal/testkeys" "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" ) func TestRangeDel(t *testing.T) { var d *DB defer func() { if d != nil { require.NoError(t, closeAllSnapshots(d)) require.NoError(t, d.Close()) } }() opts := &Options{} opts.DisableAutomaticCompactions = true datadriven.RunTest(t, "testdata/range_del", func(t *testing.T, td *datadriven.TestData) string { switch td.Cmd { case "define": if d != nil { if err := closeAllSnapshots(d); err != nil { return err.Error() } if err := d.Close(); err != nil { return err.Error() } } var err error if d, err = runDBDefineCmd(td, opts); err != nil { return err.Error() } d.mu.Lock() // Disable the "dynamic base level" code for this test. d.mu.versions.picker.forceBaseLevel1() s := fmt.Sprintf("mem: %d\n%s", len(d.mu.mem.queue), d.mu.versions.currentVersion().String()) d.mu.Unlock() return s case "wait-pending-table-stats": return runTableStatsCmd(td, d) case "compact": if err := runCompactCmd(td, d); err != nil { return err.Error() } d.mu.Lock() // Disable the "dynamic base level" code for this test. d.mu.versions.picker.forceBaseLevel1() s := d.mu.versions.currentVersion().String() d.mu.Unlock() return s case "get": return runGetCmd(t, td, d) case "iter": snap := Snapshot{ db: d, seqNum: InternalKeySeqNumMax, } td.MaybeScanArgs(t, "seq", &snap.seqNum) iter, _ := snap.NewIter(nil) return runIterCmd(td, iter, true) default: return fmt.Sprintf("unknown command: %s", td.Cmd) } }) } func TestFlushDelay(t *testing.T) { opts := &Options{ FS: vfs.NewMem(), Comparer: testkeys.Comparer, FlushDelayDeleteRange: 10 * time.Millisecond, FlushDelayRangeKey: 10 * time.Millisecond, FormatMajorVersion: internalFormatNewest, } d, err := Open("", opts) require.NoError(t, err) // Ensure that all the various means of writing a rangedel or range key // trigger their respective flush delays. cases := []func(){ func() { require.NoError(t, d.DeleteRange([]byte("a"), []byte("z"), nil)) }, func() { b := d.NewBatch() require.NoError(t, b.DeleteRange([]byte("a"), []byte("z"), nil)) require.NoError(t, b.Commit(nil)) }, func() { b := d.NewBatch() op := b.DeleteRangeDeferred(1, 1) op.Key[0] = 'a' op.Value[0] = 'z' op.Finish() require.NoError(t, b.Commit(nil)) }, func() { b := d.NewBatch() b2 := d.NewBatch() require.NoError(t, b.DeleteRange([]byte("a"), []byte("z"), nil)) require.NoError(t, b2.SetRepr(b.Repr())) require.NoError(t, b2.Commit(nil)) require.NoError(t, b.Close()) }, func() { b := d.NewBatch() b2 := d.NewBatch() require.NoError(t, b.DeleteRange([]byte("a"), []byte("z"), nil)) require.NoError(t, b2.Apply(b, nil)) require.NoError(t, b2.Commit(nil)) require.NoError(t, b.Close()) }, func() { require.NoError(t, d.RangeKeySet([]byte("a"), []byte("z"), nil, nil, nil)) }, func() { require.NoError(t, d.RangeKeyUnset([]byte("a"), []byte("z"), nil, nil)) }, func() { require.NoError(t, d.RangeKeyDelete([]byte("a"), []byte("z"), nil)) }, func() { b := d.NewBatch() require.NoError(t, b.RangeKeySet([]byte("a"), []byte("z"), nil, nil, nil)) require.NoError(t, b.Commit(nil)) }, func() { b := d.NewBatch() require.NoError(t, b.RangeKeyUnset([]byte("a"), []byte("z"), nil, nil)) require.NoError(t, b.Commit(nil)) }, func() { b := d.NewBatch() require.NoError(t, b.RangeKeyDelete([]byte("a"), []byte("z"), nil)) require.NoError(t, b.Commit(nil)) }, func() { b := d.NewBatch() b2 := d.NewBatch() require.NoError(t, b.RangeKeySet([]byte("a"), []byte("z"), nil, nil, nil)) require.NoError(t, b2.SetRepr(b.Repr())) require.NoError(t, b2.Commit(nil)) require.NoError(t, b.Close()) }, func() { b := d.NewBatch() b2 := d.NewBatch() require.NoError(t, b.RangeKeySet([]byte("a"), []byte("z"), nil, nil, nil)) require.NoError(t, b2.Apply(b, nil)) require.NoError(t, b2.Commit(nil)) require.NoError(t, b.Close()) }, } for _, f := range cases { d.mu.Lock() flushed := d.mu.mem.queue[len(d.mu.mem.queue)-1].flushed d.mu.Unlock() f() <-flushed } require.NoError(t, d.Close()) } func TestFlushDelayStress(t *testing.T) { rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) opts := &Options{ FS: vfs.NewMem(), Comparer: testkeys.Comparer, FlushDelayDeleteRange: time.Duration(rng.Intn(10)+1) * time.Millisecond, FlushDelayRangeKey: time.Duration(rng.Intn(10)+1) * time.Millisecond, FormatMajorVersion: internalFormatNewest, MemTableSize: 8192, } const runs = 100 for run := 0; run < runs; run++ { d, err := Open("", opts) require.NoError(t, err) now := time.Now().UnixNano() writers := runtime.GOMAXPROCS(0) var wg sync.WaitGroup wg.Add(writers) for i := 0; i < writers; i++ { rng := rand.New(rand.NewSource(uint64(now) + uint64(i))) go func() { const ops = 100 defer wg.Done() var k1, k2 [32]byte for j := 0; j < ops; j++ { switch rng.Intn(3) { case 0: randStr(k1[:], rng) randStr(k2[:], rng) require.NoError(t, d.DeleteRange(k1[:], k2[:], nil)) case 1: randStr(k1[:], rng) randStr(k2[:], rng) require.NoError(t, d.RangeKeySet(k1[:], k2[:], []byte("@2"), nil, nil)) case 2: randStr(k1[:], rng) randStr(k2[:], rng) require.NoError(t, d.Set(k1[:], k2[:], nil)) default: panic("unreachable") } } }() } wg.Wait() time.Sleep(time.Duration(rng.Intn(10)+1) * time.Millisecond) require.NoError(t, d.Close()) } } // Verify that range tombstones at higher levels do not unintentionally delete // newer keys at lower levels. This test sets up one such scenario. The base // problem is that range tombstones are not truncated to sstable boundaries on // disk, only in memory. func TestRangeDelCompactionTruncation(t *testing.T) { runTest := func(formatVersion FormatMajorVersion) { // Use a small target file size so that there is a single key per sstable. d, err := Open("", &Options{ FS: vfs.NewMem(), Levels: []LevelOptions{ {TargetFileSize: 100}, {TargetFileSize: 100}, {TargetFileSize: 1}, }, DebugCheck: DebugCheckLevels, FormatMajorVersion: formatVersion, }) require.NoError(t, err) defer d.Close() d.mu.Lock() d.mu.versions.dynamicBaseLevel = false d.mu.Unlock() lsm := func() string { d.mu.Lock() s := d.mu.versions.currentVersion().String() d.mu.Unlock() return s } expectLSM := func(expected string) { t.Helper() expected = strings.TrimSpace(expected) actual := strings.TrimSpace(lsm()) if expected != actual { t.Fatalf("expected\n%s\nbut found\n%s", expected, actual) } } require.NoError(t, d.Set([]byte("a"), bytes.Repeat([]byte("b"), 100), nil)) snap1 := d.NewSnapshot() defer snap1.Close() // Flush so that each version of "a" ends up in its own L0 table. If we // allowed both versions in the same L0 table, compaction could trivially // move the single L0 table to L1. require.NoError(t, d.Flush()) require.NoError(t, d.Set([]byte("b"), bytes.Repeat([]byte("c"), 100), nil)) snap2 := d.NewSnapshot() defer snap2.Close() require.NoError(t, d.DeleteRange([]byte("a"), []byte("d"), nil)) // Compact to produce the L1 tables. require.NoError(t, d.Compact([]byte("c"), []byte("c\x00"), false)) expectLSM(` 1: 000008:[a#12,RANGEDEL-b#inf,RANGEDEL] 000009:[b#12,RANGEDEL-d#inf,RANGEDEL] `) // Compact again to move one of the tables to L2. require.NoError(t, d.Compact([]byte("c"), []byte("c\x00"), false)) expectLSM(` 1: 000008:[a#12,RANGEDEL-b#inf,RANGEDEL] 2: 000009:[b#12,RANGEDEL-d#inf,RANGEDEL] `) // Write "b" and "c" to a new table. require.NoError(t, d.Set([]byte("b"), []byte("d"), nil)) require.NoError(t, d.Set([]byte("c"), []byte("e"), nil)) require.NoError(t, d.Flush()) expectLSM(` 0.0: 000011:[b#13,SET-c#14,SET] 1: 000008:[a#12,RANGEDEL-b#inf,RANGEDEL] 2: 000009:[b#12,RANGEDEL-d#inf,RANGEDEL] `) // "b" is still visible at this point as it should be. if _, closer, err := d.Get([]byte("b")); err != nil { t.Fatalf("expected success, but found %v", err) } else { closer.Close() } keys := func() string { iter, _ := d.NewIter(nil) defer iter.Close() var buf bytes.Buffer var sep string for iter.First(); iter.Valid(); iter.Next() { fmt.Fprintf(&buf, "%s%s", sep, iter.Key()) sep = " " } return buf.String() } if expected, actual := `b c`, keys(); expected != actual { t.Fatalf("expected %q, but found %q", expected, actual) } // Compact the L0 table. This will compact the L0 table into L1 and do to the // sstable target size settings will create 2 tables in L1. Then L1 table // containing "c" will be compacted again with the L2 table creating two // tables in L2. Lastly, the L2 table containing "c" will be compacted // creating the L3 table. require.NoError(t, d.Compact([]byte("c"), []byte("c\x00"), false)) if formatVersion < FormatSetWithDelete { expectLSM(` 1: 000008:[a#12,RANGEDEL-b#inf,RANGEDEL] 2: 000012:[b#13,SET-c#inf,RANGEDEL] 3: 000013:[c#14,SET-d#inf,RANGEDEL] `) } else { expectLSM(` 1: 000008:[a#12,RANGEDEL-b#inf,RANGEDEL] 2: 000012:[b#13,SETWITHDEL-c#inf,RANGEDEL] 3: 000013:[c#14,SET-d#inf,RANGEDEL] `) } // The L1 table still contains a tombstone from [a,d) which will improperly // delete the newer version of "b" in L2. if _, closer, err := d.Get([]byte("b")); err != nil { t.Errorf("expected success, but found %v", err) } else { closer.Close() } if expected, actual := `b c`, keys(); expected != actual { t.Errorf("expected %q, but found %q", expected, actual) } } versions := []FormatMajorVersion{ FormatMostCompatible, FormatSetWithDelete - 1, FormatSetWithDelete, FormatNewest, } for _, version := range versions { t.Run(fmt.Sprintf("version-%s", version), func(t *testing.T) { runTest(version) }) } } // This is an alternate scenario to the one created in // TestRangeDelCompactionTruncation that would result in the bounds for an // sstable expanding to overlap its left neighbor if we failed to truncate an // sstable's boundaries to the compaction input boundaries. func TestRangeDelCompactionTruncation2(t *testing.T) { // Use a small target file size so that there is a single key per sstable. d, err := Open("", &Options{ FS: vfs.NewMem(), Levels: []LevelOptions{ {TargetFileSize: 200}, {TargetFileSize: 200}, {TargetFileSize: 1}, }, DebugCheck: DebugCheckLevels, }) require.NoError(t, err) defer d.Close() lsm := func() string { d.mu.Lock() s := d.mu.versions.currentVersion().String() d.mu.Unlock() return s } expectLSM := func(expected string) { t.Helper() expected = strings.TrimSpace(expected) actual := strings.TrimSpace(lsm()) if expected != actual { t.Fatalf("expected\n%s\nbut found\n%s", expected, actual) } } require.NoError(t, d.Set([]byte("b"), bytes.Repeat([]byte("b"), 100), nil)) snap1 := d.NewSnapshot() defer snap1.Close() // Flush so that each version of "b" ends up in its own L0 table. If we // allowed both versions in the same L0 table, compaction could trivially // move the single L0 table to L1. require.NoError(t, d.Flush()) require.NoError(t, d.Set([]byte("b"), bytes.Repeat([]byte("c"), 100), nil)) snap2 := d.NewSnapshot() defer snap2.Close() require.NoError(t, d.DeleteRange([]byte("a"), []byte("d"), nil)) // Compact to produce the L1 tables. require.NoError(t, d.Compact([]byte("b"), []byte("b\x00"), false)) expectLSM(` 6: 000009:[a#12,RANGEDEL-d#inf,RANGEDEL] `) require.NoError(t, d.Set([]byte("c"), bytes.Repeat([]byte("d"), 100), nil)) require.NoError(t, d.Compact([]byte("c"), []byte("c\x00"), false)) expectLSM(` 6: 000012:[a#12,RANGEDEL-c#inf,RANGEDEL] 000013:[c#13,SET-d#inf,RANGEDEL] `) } // TODO(peter): rewrite this test, TestRangeDelCompactionTruncation, and // TestRangeDelCompactionTruncation2 as data-driven tests. func TestRangeDelCompactionTruncation3(t *testing.T) { // Use a small target file size so that there is a single key per sstable. d, err := Open("tmp", &Options{ Cleaner: ArchiveCleaner{}, FS: vfs.NewMem(), Levels: []LevelOptions{ {TargetFileSize: 200}, {TargetFileSize: 200}, {TargetFileSize: 1}, }, DebugCheck: DebugCheckLevels, }) require.NoError(t, err) defer d.Close() d.mu.Lock() d.mu.versions.dynamicBaseLevel = false d.mu.Unlock() lsm := func() string { d.mu.Lock() s := d.mu.versions.currentVersion().String() d.mu.Unlock() return s } expectLSM := func(expected string) { t.Helper() expected = strings.TrimSpace(expected) actual := strings.TrimSpace(lsm()) if expected != actual { t.Fatalf("expected\n%s\nbut found\n%s", expected, actual) } } require.NoError(t, d.Set([]byte("b"), bytes.Repeat([]byte("b"), 100), nil)) snap1 := d.NewSnapshot() defer snap1.Close() // Flush so that each version of "b" ends up in its own L0 table. If we // allowed both versions in the same L0 table, compaction could trivially // move the single L0 table to L1. require.NoError(t, d.Flush()) require.NoError(t, d.Set([]byte("b"), bytes.Repeat([]byte("c"), 100), nil)) snap2 := d.NewSnapshot() defer snap2.Close() require.NoError(t, d.DeleteRange([]byte("a"), []byte("d"), nil)) snap3 := d.NewSnapshot() defer snap3.Close() if _, _, err := d.Get([]byte("b")); err != ErrNotFound { t.Fatalf("expected not found, but found %v", err) } // Compact a few times to move the tables down to L3. for i := 0; i < 3; i++ { require.NoError(t, d.Compact([]byte("b"), []byte("b\x00"), false)) } expectLSM(` 3: 000009:[a#12,RANGEDEL-d#inf,RANGEDEL] `) require.NoError(t, d.Set([]byte("c"), bytes.Repeat([]byte("d"), 100), nil)) require.NoError(t, d.Compact([]byte("c"), []byte("c\x00"), false)) expectLSM(` 3: 000013:[a#12,RANGEDEL-c#inf,RANGEDEL] 4: 000014:[c#13,SET-d#inf,RANGEDEL] `) require.NoError(t, d.Compact([]byte("c"), []byte("c\x00"), false)) expectLSM(` 3: 000013:[a#12,RANGEDEL-c#inf,RANGEDEL] 5: 000014:[c#13,SET-d#inf,RANGEDEL] `) if _, _, err := d.Get([]byte("b")); err != ErrNotFound { t.Fatalf("expected not found, but found %v", err) } require.NoError(t, d.Compact([]byte("a"), []byte("a\x00"), false)) expectLSM(` 4: 000013:[a#12,RANGEDEL-c#inf,RANGEDEL] 5: 000014:[c#13,SET-d#inf,RANGEDEL] `) if v, _, err := d.Get([]byte("b")); err != ErrNotFound { t.Fatalf("expected not found, but found %v [%s]", err, v) } } func BenchmarkRangeDelIterate(b *testing.B) { for _, entries := range []int{10, 1000, 100000} { b.Run(fmt.Sprintf("entries=%d", entries), func(b *testing.B) { for _, deleted := range []int{entries, entries - 1} { b.Run(fmt.Sprintf("deleted=%d", deleted), func(b *testing.B) { for _, snapshotCompact := range []bool{false, true} { b.Run(fmt.Sprintf("snapshotAndCompact=%t", snapshotCompact), func(b *testing.B) { benchmarkRangeDelIterate(b, entries, deleted, snapshotCompact) }) } }) } }) } } func benchmarkRangeDelIterate(b *testing.B, entries, deleted int, snapshotCompact bool) { mem := vfs.NewMem() cache := NewCache(128 << 20) // 128 MB defer cache.Unref() d, err := Open("", &Options{ Cache: cache, FS: mem, DebugCheck: DebugCheckLevels, }) if err != nil { b.Fatal(err) } defer d.Close() makeKey := func(i int) []byte { return []byte(fmt.Sprintf("%09d", i)) } // Create an sstable with N entries and ingest it. This is a fast way // to get a lot of entries into pebble. f, err := mem.Create("ext") if err != nil { b.Fatal(err) } w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{ BlockSize: 32 << 10, // 32 KB }) for i := 0; i < entries; i++ { key := base.MakeInternalKey(makeKey(i), 0, InternalKeyKindSet) if err := w.Add(key, nil); err != nil { b.Fatal(err) } } if err := w.Close(); err != nil { b.Fatal(err) } if err := d.Ingest([]string{"ext"}); err != nil { b.Fatal(err) } // Some benchmarks test snapshots that force the range tombstone into the // same level as the covered data. // See https://github.com/cockroachdb/pebble/issues/1070. if snapshotCompact { s := d.NewSnapshot() defer func() { require.NoError(b, s.Close()) }() } // Create a range tombstone that deletes most (or all) of those entries. from := makeKey(0) to := makeKey(deleted) if err := d.DeleteRange(from, to, nil); err != nil { b.Fatal(err) } if snapshotCompact { require.NoError(b, d.Compact(makeKey(0), makeKey(entries), false)) } b.ResetTimer() for i := 0; i < b.N; i++ { iter, _ := d.NewIter(nil) iter.SeekGE(from) if deleted < entries { if !iter.Valid() { b.Fatal("key not found") } } else if iter.Valid() { b.Fatal("unexpected key found") } if err := iter.Close(); err != nil { b.Fatal(err) } } }