// 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 sstable import ( "bytes" "context" "encoding/binary" "fmt" "io" "math" "os" "path" "path/filepath" "strings" "testing" "time" "github.com/cockroachdb/datadriven" "github.com/cockroachdb/errors" "github.com/cockroachdb/pebble/bloom" "github.com/cockroachdb/pebble/internal/base" "github.com/cockroachdb/pebble/internal/cache" "github.com/cockroachdb/pebble/internal/humanize" "github.com/cockroachdb/pebble/internal/manifest" "github.com/cockroachdb/pebble/internal/testkeys" "github.com/cockroachdb/pebble/objstorage" "github.com/cockroachdb/pebble/objstorage/objstorageprovider" "github.com/cockroachdb/pebble/vfs" "github.com/cockroachdb/pebble/vfs/errorfs" "github.com/stretchr/testify/require" "golang.org/x/exp/rand" ) // get is a testing helper that simulates a read and helps verify bloom filters // until they are available through iterators. func (r *Reader) get(key []byte) (value []byte, err error) { if r.err != nil { return nil, r.err } if r.tableFilter != nil { dataH, err := r.readFilter(context.Background(), nil /* stats */, nil) if err != nil { return nil, err } var lookupKey []byte if r.Split != nil { lookupKey = key[:r.Split(key)] } else { lookupKey = key } mayContain := r.tableFilter.mayContain(dataH.Get(), lookupKey) dataH.Release() if !mayContain { return nil, base.ErrNotFound } } i, err := r.NewIter(nil /* lower */, nil /* upper */) if err != nil { return nil, err } var v base.LazyValue ikey, v := i.SeekGE(key, base.SeekGEFlagsNone) value, _, err = v.Value(nil) if err != nil { return nil, err } if ikey == nil || r.Compare(key, ikey.UserKey) != 0 { err := i.Close() if err == nil { err = base.ErrNotFound } return nil, err } // The value will be "freed" when the iterator is closed, so make a copy // which will outlast the lifetime of the iterator. newValue := make([]byte, len(value)) copy(newValue, value) if err := i.Close(); err != nil { return nil, err } return newValue, nil } // iterAdapter adapts the new Iterator API which returns the key and value from // positioning methods (Seek*, First, Last, Next, Prev) to the old API which // returned a boolean corresponding to Valid. Only used by test code. type iterAdapter struct { Iterator key *InternalKey val []byte } func newIterAdapter(iter Iterator) *iterAdapter { return &iterAdapter{ Iterator: iter, } } func (i *iterAdapter) update(key *InternalKey, val base.LazyValue) bool { i.key = key if v, _, err := val.Value(nil); err != nil { i.key = nil i.val = nil } else { i.val = v } return i.key != nil } func (i *iterAdapter) String() string { return "iter-adapter" } func (i *iterAdapter) SeekGE(key []byte, flags base.SeekGEFlags) bool { return i.update(i.Iterator.SeekGE(key, flags)) } func (i *iterAdapter) SeekPrefixGE(prefix, key []byte, flags base.SeekGEFlags) bool { return i.update(i.Iterator.SeekPrefixGE(prefix, key, flags)) } func (i *iterAdapter) SeekLT(key []byte, flags base.SeekLTFlags) bool { return i.update(i.Iterator.SeekLT(key, flags)) } func (i *iterAdapter) First() bool { return i.update(i.Iterator.First()) } func (i *iterAdapter) Last() bool { return i.update(i.Iterator.Last()) } func (i *iterAdapter) Next() bool { return i.update(i.Iterator.Next()) } func (i *iterAdapter) NextPrefix(succKey []byte) bool { return i.update(i.Iterator.NextPrefix(succKey)) } func (i *iterAdapter) NextIgnoreResult() { i.Iterator.Next() i.update(nil, base.LazyValue{}) } func (i *iterAdapter) Prev() bool { return i.update(i.Iterator.Prev()) } func (i *iterAdapter) Key() *InternalKey { return i.key } func (i *iterAdapter) Value() []byte { return i.val } func (i *iterAdapter) Valid() bool { return i.key != nil } func (i *iterAdapter) SetBounds(lower, upper []byte) { i.Iterator.SetBounds(lower, upper) i.key = nil } func (i *iterAdapter) SetContext(ctx context.Context) { i.Iterator.SetContext(ctx) } func TestVirtualReader(t *testing.T) { // A faux filenum used to create fake filemetadata for testing. var fileNum int = 1 nextFileNum := func() base.FileNum { fileNum++ return base.FileNum(fileNum - 1) } // Set during the latest build command. var r *Reader var meta manifest.PhysicalFileMeta var bp BufferPool // Set during the latest virtualize command. var vMeta1 manifest.VirtualFileMeta var v VirtualReader defer func() { if r != nil { require.NoError(t, r.Close()) bp.Release() } }() createPhysicalMeta := func(w *WriterMetadata, r *Reader) (manifest.PhysicalFileMeta, error) { meta := &manifest.FileMetadata{} meta.FileNum = nextFileNum() meta.CreationTime = time.Now().Unix() meta.Size = w.Size meta.SmallestSeqNum = w.SmallestSeqNum meta.LargestSeqNum = w.LargestSeqNum if w.HasPointKeys { meta.ExtendPointKeyBounds(r.Compare, w.SmallestPoint, w.LargestPoint) } if w.HasRangeDelKeys { meta.ExtendPointKeyBounds(r.Compare, w.SmallestRangeDel, w.LargestRangeDel) } if w.HasRangeKeys { meta.ExtendRangeKeyBounds(r.Compare, w.SmallestRangeKey, w.LargestRangeKey) } meta.InitPhysicalBacking() if err := meta.Validate(r.Compare, r.opts.Comparer.FormatKey); err != nil { return manifest.PhysicalFileMeta{}, err } return meta.PhysicalMeta(), nil } formatWMeta := func(m *WriterMetadata) string { var b bytes.Buffer if m.HasPointKeys { fmt.Fprintf(&b, "point: [%s-%s]\n", m.SmallestPoint, m.LargestPoint) } if m.HasRangeDelKeys { fmt.Fprintf(&b, "rangedel: [%s-%s]\n", m.SmallestRangeDel, m.LargestRangeDel) } if m.HasRangeKeys { fmt.Fprintf(&b, "rangekey: [%s-%s]\n", m.SmallestRangeKey, m.LargestRangeKey) } fmt.Fprintf(&b, "seqnums: [%d-%d]\n", m.SmallestSeqNum, m.LargestSeqNum) return b.String() } formatVirtualReader := func(v *VirtualReader) string { var b bytes.Buffer fmt.Fprintf(&b, "bounds: [%s-%s]\n", v.vState.lower, v.vState.upper) fmt.Fprintf(&b, "filenum: %s\n", v.vState.fileNum.String()) fmt.Fprintf( &b, "props: %s: %d, %s: %d, %s: %d, %s: %d, %s: %d, %s: %d, %s: %d, %s: %d, %s: %d, %s: %d, %s: %d\n", "NumEntries", v.Properties.NumEntries, "RawKeySize", v.Properties.RawKeySize, "RawValueSize", v.Properties.RawValueSize, "RawPointTombstoneKeySize", v.Properties.RawPointTombstoneKeySize, "RawPointTombstoneValueSize", v.Properties.RawPointTombstoneValueSize, "NumSizedDeletions", v.Properties.NumSizedDeletions, "NumDeletions", v.Properties.NumDeletions, "NumRangeDeletions", v.Properties.NumRangeDeletions, "NumRangeKeyDels", v.Properties.NumRangeKeyDels, "NumRangeKeySets", v.Properties.NumRangeKeySets, "ValueBlocksSize", v.Properties.ValueBlocksSize, ) return b.String() } datadriven.RunTest(t, "testdata/virtual_reader", func(t *testing.T, td *datadriven.TestData) string { switch td.Cmd { case "build": if r != nil { bp.Release() _ = r.Close() r = nil meta.FileMetadata = nil vMeta1.FileMetadata = nil v = VirtualReader{} } var wMeta *WriterMetadata var err error writerOpts := &WriterOptions{ TableFormat: TableFormatMax, } // Use a single level index by default. writerOpts.IndexBlockSize = 100000 if len(td.CmdArgs) == 1 { if td.CmdArgs[0].String() == "twoLevel" { // Force a two level index. writerOpts.IndexBlockSize = 1 writerOpts.BlockSize = 1 } } wMeta, r, err = runBuildCmd(td, writerOpts, 0) if err != nil { return err.Error() } bp.Init(5) // Create a fake filemetada using the writer meta. meta, err = createPhysicalMeta(wMeta, r) if err != nil { return err.Error() } r.fileNum = meta.FileBacking.DiskFileNum return formatWMeta(wMeta) case "virtualize": // virtualize will split the previously built physical sstable into // a single sstable with virtual bounds. The command assumes that // the bounds for the virtual sstable are valid. For the purposes of // this command the bounds must be valid keys. In general, and for // this command, range key/range del spans must also not span across // virtual sstable bounds. if meta.FileMetadata == nil { return "build must be called at least once before virtualize" } if vMeta1.FileMetadata != nil { vMeta1.FileMetadata = nil v = VirtualReader{} } vMeta := &manifest.FileMetadata{ FileBacking: meta.FileBacking, SmallestSeqNum: meta.SmallestSeqNum, LargestSeqNum: meta.LargestSeqNum, Virtual: true, } // Parse the virtualization bounds. bounds := strings.Split(td.CmdArgs[0].String(), "-") vMeta.Smallest = base.ParseInternalKey(bounds[0]) vMeta.Largest = base.ParseInternalKey(bounds[1]) vMeta.FileNum = nextFileNum() var err error vMeta.Size, err = r.EstimateDiskUsage(vMeta.Smallest.UserKey, vMeta.Largest.UserKey) if err != nil { return err.Error() } vMeta.ValidateVirtual(meta.FileMetadata) vMeta1 = vMeta.VirtualMeta() v = MakeVirtualReader(r, vMeta1, false /* isForeign */) return formatVirtualReader(&v) case "citer": // Creates a compaction iterator from the virtual reader, and then // just scans the keyspace. Which is all a compaction iterator is // used for. This tests the First and Next calls. if vMeta1.FileMetadata == nil { return "virtualize must be called before creating compaction iters" } var rp ReaderProvider var bytesIterated uint64 iter, err := v.NewCompactionIter(&bytesIterated, CategoryAndQoS{}, nil, rp, &bp) if err != nil { return err.Error() } var buf bytes.Buffer for key, val := iter.First(); key != nil; key, val = iter.Next() { fmt.Fprintf(&buf, "%s:%s\n", key.String(), val.InPlaceValue()) } err = iter.Close() if err != nil { return err.Error() } return buf.String() case "constrain": if vMeta1.FileMetadata == nil { return "virtualize must be called before constrain" } splits := strings.Split(td.CmdArgs[0].String(), ",") of, ol := []byte(splits[0]), []byte(splits[1]) inclusive, f, l := v.vState.constrainBounds(of, ol, splits[2] == "true") var buf bytes.Buffer buf.Write(f) buf.WriteByte(',') buf.Write(l) buf.WriteByte(',') if inclusive { buf.WriteString("true") } else { buf.WriteString("false") } buf.WriteByte('\n') return buf.String() case "scan-range-del": if vMeta1.FileMetadata == nil { return "virtualize must be called before scan-range-del" } iter, err := v.NewRawRangeDelIter() if err != nil { return err.Error() } if iter == nil { return "" } defer iter.Close() var buf bytes.Buffer for s := iter.First(); s != nil; s = iter.Next() { fmt.Fprintf(&buf, "%s\n", s) } return buf.String() case "scan-range-key": if vMeta1.FileMetadata == nil { return "virtualize must be called before scan-range-key" } iter, err := v.NewRawRangeKeyIter() if err != nil { return err.Error() } if iter == nil { return "" } defer iter.Close() var buf bytes.Buffer for s := iter.First(); s != nil; s = iter.Next() { fmt.Fprintf(&buf, "%s\n", s) } return buf.String() case "iter": if vMeta1.FileMetadata == nil { return "virtualize must be called before iter" } var lower, upper []byte if len(td.CmdArgs) > 0 { splits := strings.Split(td.CmdArgs[0].String(), "-") lower, upper = []byte(splits[0]), []byte(splits[1]) } var stats base.InternalIteratorStats iter, err := v.NewIterWithBlockPropertyFiltersAndContextEtc( context.Background(), lower, upper, nil, false, false, &stats, CategoryAndQoS{}, nil, TrivialReaderProvider{Reader: r}) if err != nil { return err.Error() } return runIterCmd(td, iter, true, runIterCmdStats(&stats)) default: return fmt.Sprintf("unknown command: %s", td.Cmd) } }) } func TestReader(t *testing.T) { writerOpts := map[string]WriterOptions{ // No bloom filters. "default": {}, "bloom10bit": { // The standard policy. FilterPolicy: bloom.FilterPolicy(10), FilterType: base.TableFilter, }, "bloom1bit": { // A policy with many false positives. FilterPolicy: bloom.FilterPolicy(1), FilterType: base.TableFilter, }, "bloom100bit": { // A policy unlikely to have false positives. FilterPolicy: bloom.FilterPolicy(100), FilterType: base.TableFilter, }, } blockSizes := map[string]int{ "1bytes": 1, "5bytes": 5, "10bytes": 10, "25bytes": 25, "Maxbytes": math.MaxInt32, } opts := map[string]*Comparer{ "default": testkeys.Comparer, "prefixFilter": fixtureComparer, } testDirs := map[string]string{ "default": "testdata/reader", "prefixFilter": "testdata/prefixreader", } for format := TableFormatPebblev2; format <= TableFormatMax; format++ { for dName, blockSize := range blockSizes { for iName, indexBlockSize := range blockSizes { for lName, tableOpt := range writerOpts { for oName, cmp := range opts { tableOpt.BlockSize = blockSize tableOpt.Comparer = cmp tableOpt.IndexBlockSize = indexBlockSize tableOpt.TableFormat = format t.Run( fmt.Sprintf("format=%d,opts=%s,writerOpts=%s,blockSize=%s,indexSize=%s", format, oName, lName, dName, iName), func(t *testing.T) { runTestReader( t, tableOpt, testDirs[oName], nil /* Reader */, true) }) } } } } } } func TestReaderHideObsolete(t *testing.T) { blockSizes := map[string]int{ "1bytes": 1, "5bytes": 5, "10bytes": 10, "25bytes": 25, "Maxbytes": math.MaxInt32, } for dName, blockSize := range blockSizes { opts := WriterOptions{ TableFormat: TableFormatPebblev4, BlockSize: blockSize, IndexBlockSize: blockSize, Comparer: testkeys.Comparer, } t.Run(fmt.Sprintf("blockSize=%s", dName), func(t *testing.T) { runTestReader( t, opts, "testdata/reader_hide_obsolete", nil /* Reader */, true) }) } } func TestHamletReader(t *testing.T) { prebuiltSSTs := []string{ "testdata/h.ldb", "testdata/h.sst", "testdata/h.no-compression.sst", "testdata/h.no-compression.two_level_index.sst", "testdata/h.block-bloom.no-compression.sst", "testdata/h.table-bloom.no-compression.prefix_extractor.no_whole_key_filter.sst", "testdata/h.table-bloom.no-compression.sst", } for _, prebuiltSST := range prebuiltSSTs { f, err := os.Open(filepath.FromSlash(prebuiltSST)) require.NoError(t, err) r, err := newReader(f, ReaderOptions{}) require.NoError(t, err) t.Run( fmt.Sprintf("sst=%s", prebuiltSST), func(t *testing.T) { runTestReader(t, WriterOptions{}, "testdata/hamletreader", r, false) }, ) } } func forEveryTableFormat[I any]( t *testing.T, formatTable [NumTableFormats]I, runTest func(*testing.T, TableFormat, I), ) { t.Helper() for tf := TableFormatUnspecified + 1; tf <= TableFormatMax; tf++ { t.Run(tf.String(), func(t *testing.T) { runTest(t, tf, formatTable[tf]) }) } } func TestReaderStats(t *testing.T) { forEveryTableFormat[string](t, [NumTableFormats]string{ TableFormatUnspecified: "", TableFormatLevelDB: "testdata/readerstats_LevelDB", TableFormatRocksDBv2: "testdata/readerstats_LevelDB", TableFormatPebblev1: "testdata/readerstats_LevelDB", TableFormatPebblev2: "testdata/readerstats_LevelDB", TableFormatPebblev3: "testdata/readerstats_Pebblev3", TableFormatPebblev4: "testdata/readerstats_Pebblev3", }, func(t *testing.T, format TableFormat, dir string) { if dir == "" { t.Skip() } writerOpt := WriterOptions{ BlockSize: 32 << 10, IndexBlockSize: 32 << 10, Comparer: testkeys.Comparer, TableFormat: format, } runTestReader(t, writerOpt, dir, nil /* Reader */, false /* printValue */) }) } func TestReaderWithBlockPropertyFilter(t *testing.T) { // Some of these tests examine internal iterator state, so they require // determinism. When the invariants tag is set, disableBoundsOpt may disable // the bounds optimization depending on the iterator pointer address. This // can add nondeterminism to the internal iterator statae. Disable this // nondeterminism for the duration of this test. ensureBoundsOptDeterminism = true defer func() { ensureBoundsOptDeterminism = false }() forEveryTableFormat[string](t, [NumTableFormats]string{ TableFormatUnspecified: "", // Block properties unsupported TableFormatLevelDB: "", // Block properties unsupported TableFormatRocksDBv2: "", // Block properties unsupported TableFormatPebblev1: "", // Block properties unsupported TableFormatPebblev2: "testdata/reader_bpf/Pebblev2", TableFormatPebblev3: "testdata/reader_bpf/Pebblev3", TableFormatPebblev4: "testdata/reader_bpf/Pebblev3", }, func(t *testing.T, format TableFormat, dir string) { if dir == "" { t.Skip("Block-properties unsupported") } writerOpt := WriterOptions{ Comparer: testkeys.Comparer, TableFormat: format, BlockPropertyCollectors: []func() BlockPropertyCollector{NewTestKeysBlockPropertyCollector}, } runTestReader(t, writerOpt, dir, nil /* Reader */, false) }) } func TestInjectedErrors(t *testing.T) { prebuiltSSTs := []string{ "testdata/h.ldb", "testdata/h.sst", "testdata/h.no-compression.sst", "testdata/h.no-compression.two_level_index.sst", "testdata/h.block-bloom.no-compression.sst", "testdata/h.table-bloom.no-compression.prefix_extractor.no_whole_key_filter.sst", "testdata/h.table-bloom.no-compression.sst", } for _, prebuiltSST := range prebuiltSSTs { run := func(i int) (reterr error) { f, err := vfs.Default.Open(filepath.FromSlash(prebuiltSST)) require.NoError(t, err) r, err := newReader(errorfs.WrapFile(f, errorfs.ErrInjected.If(errorfs.OnIndex(int32(i)))), ReaderOptions{}) if err != nil { return firstError(err, f.Close()) } defer func() { reterr = firstError(reterr, r.Close()) }() _, err = r.EstimateDiskUsage([]byte("borrower"), []byte("lender")) if err != nil { return err } iter, err := r.NewIter(nil, nil) if err != nil { return err } defer func() { reterr = firstError(reterr, iter.Close()) }() for k, v := iter.First(); k != nil; k, v = iter.Next() { val, _, err := v.Value(nil) if err != nil { return err } if val == nil { break } } if err = iter.Error(); err != nil { return err } return nil } for i := 0; ; i++ { err := run(i) if errors.Is(err, errorfs.ErrInjected) { t.Logf("%q, index %d: %s", prebuiltSST, i, err) continue } if err != nil { t.Errorf("%q, index %d: non-injected error: %+v", prebuiltSST, i, err) break } t.Logf("%q: no error at index %d", prebuiltSST, i) break } } } func TestInvalidReader(t *testing.T) { invalid, err := NewSimpleReadable(vfs.NewMemFile([]byte("invalid sst bytes"))) if err != nil { t.Fatal(err) } testCases := []struct { readable objstorage.Readable expected string }{ {nil, "nil file"}, {invalid, "invalid table"}, } for _, tc := range testCases { r, err := NewReader(tc.readable, ReaderOptions{}) if !strings.Contains(err.Error(), tc.expected) { t.Fatalf("expected %q, but found %q", tc.expected, err.Error()) } if r != nil { t.Fatalf("found non-nil reader returned with non-nil error %q", err.Error()) } } } func indexLayoutString(t *testing.T, r *Reader) string { indexH, err := r.readIndex(context.Background(), nil, nil) require.NoError(t, err) defer indexH.Release() var buf strings.Builder twoLevelIndex := r.Properties.IndexType == twoLevelIndex buf.WriteString("index entries:\n") iter, err := newBlockIter(r.Compare, indexH.Get()) defer func() { require.NoError(t, iter.Close()) }() require.NoError(t, err) for key, value := iter.First(); key != nil; key, value = iter.Next() { bh, err := decodeBlockHandleWithProperties(value.InPlaceValue()) require.NoError(t, err) fmt.Fprintf(&buf, " %s: size %d\n", string(key.UserKey), bh.Length) if twoLevelIndex { b, err := r.readBlock( context.Background(), bh.BlockHandle, nil, nil, nil, nil, nil) require.NoError(t, err) defer b.Release() iter2, err := newBlockIter(r.Compare, b.Get()) defer func() { require.NoError(t, iter2.Close()) }() require.NoError(t, err) for key, value := iter2.First(); key != nil; key, value = iter2.Next() { bh, err := decodeBlockHandleWithProperties(value.InPlaceValue()) require.NoError(t, err) fmt.Fprintf(&buf, " %s: size %d\n", string(key.UserKey), bh.Length) } } } return buf.String() } func runTestReader(t *testing.T, o WriterOptions, dir string, r *Reader, printValue bool) { datadriven.Walk(t, dir, func(t *testing.T, path string) { defer func() { if r != nil { r.Close() r = nil } }() datadriven.RunTest(t, path, func(t *testing.T, d *datadriven.TestData) string { switch d.Cmd { case "build": if r != nil { r.Close() r = nil } var cacheSize int var printLayout bool d.MaybeScanArgs(t, "cache-size", &cacheSize) d.MaybeScanArgs(t, "print-layout", &printLayout) d.MaybeScanArgs(t, "block-size", &o.BlockSize) d.MaybeScanArgs(t, "index-block-size", &o.IndexBlockSize) var err error _, r, err = runBuildCmd(d, &o, cacheSize) if err != nil { return err.Error() } if printLayout { return indexLayoutString(t, r) } return "" case "iter": seqNum, err := scanGlobalSeqNum(d) if err != nil { return err.Error() } var stats base.InternalIteratorStats r.Properties.GlobalSeqNum = seqNum var bpfs []BlockPropertyFilter if d.HasArg("block-property-filter") { var filterMin, filterMax uint64 d.ScanArgs(t, "block-property-filter", &filterMin, &filterMax) bpf := NewTestKeysBlockPropertyFilter(filterMin, filterMax) bpfs = append(bpfs, bpf) } hideObsoletePoints := false if d.HasArg("hide-obsolete-points") { d.ScanArgs(t, "hide-obsolete-points", &hideObsoletePoints) if hideObsoletePoints { hideObsoletePoints, bpfs = r.TryAddBlockPropertyFilterForHideObsoletePoints( InternalKeySeqNumMax, InternalKeySeqNumMax-1, bpfs) require.True(t, hideObsoletePoints) } } var filterer *BlockPropertiesFilterer if len(bpfs) > 0 { filterer = newBlockPropertiesFilterer(bpfs, nil) intersects, err := filterer.intersectsUserPropsAndFinishInit(r.Properties.UserProperties) if err != nil { return err.Error() } if !intersects { return "table does not intersect BlockPropertyFilter" } } iter, err := r.NewIterWithBlockPropertyFiltersAndContextEtc( context.Background(), nil, /* lower */ nil, /* upper */ filterer, hideObsoletePoints, true, /* use filter block */ &stats, CategoryAndQoS{}, nil, TrivialReaderProvider{Reader: r}, ) if err != nil { return err.Error() } return runIterCmd(d, iter, printValue, runIterCmdStats(&stats)) case "get": var b bytes.Buffer for _, k := range strings.Split(d.Input, "\n") { v, err := r.get([]byte(k)) if err != nil { fmt.Fprintf(&b, "\n", err) } else { fmt.Fprintln(&b, string(v)) } } return b.String() default: return fmt.Sprintf("unknown command: %s", d.Cmd) } }) }) } func TestReaderCheckComparerMerger(t *testing.T) { const testTable = "test" testComparer := &base.Comparer{ Name: "test.comparer", Compare: base.DefaultComparer.Compare, Equal: base.DefaultComparer.Equal, Separator: base.DefaultComparer.Separator, Successor: base.DefaultComparer.Successor, } testMerger := &base.Merger{ Name: "test.merger", Merge: base.DefaultMerger.Merge, } writerOpts := WriterOptions{ Comparer: testComparer, MergerName: "test.merger", } mem := vfs.NewMem() f0, err := mem.Create(testTable) require.NoError(t, err) w := NewWriter(objstorageprovider.NewFileWritable(f0), writerOpts) require.NoError(t, w.Set([]byte("test"), nil)) require.NoError(t, w.Close()) testCases := []struct { comparers []*base.Comparer mergers []*base.Merger expected string }{ { []*base.Comparer{testComparer}, []*base.Merger{testMerger}, "", }, { []*base.Comparer{testComparer, base.DefaultComparer}, []*base.Merger{testMerger, base.DefaultMerger}, "", }, { []*base.Comparer{}, []*base.Merger{testMerger}, "unknown comparer test.comparer", }, { []*base.Comparer{base.DefaultComparer}, []*base.Merger{testMerger}, "unknown comparer test.comparer", }, { []*base.Comparer{testComparer}, []*base.Merger{}, "unknown merger test.merger", }, { []*base.Comparer{testComparer}, []*base.Merger{base.DefaultMerger}, "unknown merger test.merger", }, } for _, c := range testCases { t.Run("", func(t *testing.T) { f1, err := mem.Open(testTable) require.NoError(t, err) comparers := make(Comparers) for _, comparer := range c.comparers { comparers[comparer.Name] = comparer } mergers := make(Mergers) for _, merger := range c.mergers { mergers[merger.Name] = merger } r, err := newReader(f1, ReaderOptions{}, comparers, mergers) if err != nil { if r != nil { t.Fatalf("found non-nil reader returned with non-nil error %q", err.Error()) } if !strings.HasSuffix(err.Error(), c.expected) { t.Fatalf("expected %q, but found %q", c.expected, err.Error()) } } else if c.expected != "" { t.Fatalf("expected %q, but found success", c.expected) } if r != nil { _ = r.Close() } }) } } func checkValidPrefix(prefix, key []byte) bool { return prefix == nil || bytes.HasPrefix(key, prefix) } func testBytesIteratedWithCompression( t *testing.T, compression Compression, allowedSizeDeviationPercent uint64, blockSizes []int, maxNumEntries []uint64, ) { for i, blockSize := range blockSizes { for _, indexBlockSize := range blockSizes { for _, numEntries := range []uint64{0, 1, maxNumEntries[i]} { r := buildTestTable(t, numEntries, blockSize, indexBlockSize, compression) var bytesIterated, prevIterated uint64 var pool BufferPool pool.Init(5) citer, err := r.NewCompactionIter( &bytesIterated, CategoryAndQoS{}, nil, TrivialReaderProvider{Reader: r}, &pool) require.NoError(t, err) for key, _ := citer.First(); key != nil; key, _ = citer.Next() { if bytesIterated < prevIterated { t.Fatalf("bytesIterated moved backward: %d < %d", bytesIterated, prevIterated) } prevIterated = bytesIterated } expected := r.Properties.DataSize allowedSizeDeviation := expected * allowedSizeDeviationPercent / 100 // There is some inaccuracy due to compression estimation. if bytesIterated < expected-allowedSizeDeviation || bytesIterated > expected+allowedSizeDeviation { t.Fatalf("bytesIterated: got %d, want %d", bytesIterated, expected) } require.NoError(t, citer.Close()) require.NoError(t, r.Close()) pool.Release() } } } } func TestBytesIterated(t *testing.T) { blockSizes := []int{10, 100, 1000, 4096, math.MaxInt32} t.Run("Compressed", func(t *testing.T) { testBytesIteratedWithCompression(t, SnappyCompression, 1, blockSizes, []uint64{1e5, 1e5, 1e5, 1e5, 1e5}) }) t.Run("Uncompressed", func(t *testing.T) { testBytesIteratedWithCompression(t, NoCompression, 0, blockSizes, []uint64{1e5, 1e5, 1e5, 1e5, 1e5}) }) t.Run("Zstd", func(t *testing.T) { // compression with zstd is extremely slow with small block size (esp the nocgo version). // use less numEntries to make the test run at reasonable speed (under 10 seconds). maxNumEntries := []uint64{1e2, 1e2, 1e3, 4e3, 1e5} if useStandardZstdLib { maxNumEntries = []uint64{1e3, 1e3, 1e4, 4e4, 1e5} } testBytesIteratedWithCompression(t, ZstdCompression, 1, blockSizes, maxNumEntries) }) } func TestCompactionIteratorSetupForCompaction(t *testing.T) { tmpDir := path.Join(t.TempDir()) provider, err := objstorageprovider.Open(objstorageprovider.DefaultSettings(vfs.Default, tmpDir)) require.NoError(t, err) defer provider.Close() blockSizes := []int{10, 100, 1000, 4096, math.MaxInt32} for _, blockSize := range blockSizes { for _, indexBlockSize := range blockSizes { for _, numEntries := range []uint64{0, 1, 1e5} { r := buildTestTableWithProvider(t, provider, numEntries, blockSize, indexBlockSize, DefaultCompression) var bytesIterated uint64 var pool BufferPool pool.Init(5) citer, err := r.NewCompactionIter( &bytesIterated, CategoryAndQoS{}, nil, TrivialReaderProvider{Reader: r}, &pool) require.NoError(t, err) switch i := citer.(type) { case *compactionIterator: require.True(t, objstorageprovider.TestingCheckMaxReadahead(i.dataRH)) // Each key has one version, so no value block, regardless of // sstable version. require.Nil(t, i.vbRH) case *twoLevelCompactionIterator: require.True(t, objstorageprovider.TestingCheckMaxReadahead(i.dataRH)) // Each key has one version, so no value block, regardless of // sstable version. require.Nil(t, i.vbRH) default: require.Failf(t, fmt.Sprintf("unknown compaction iterator type: %T", citer), "") } require.NoError(t, citer.Close()) require.NoError(t, r.Close()) pool.Release() } } } } func TestReadaheadSetupForV3TablesWithMultipleVersions(t *testing.T) { tmpDir := path.Join(t.TempDir()) provider, err := objstorageprovider.Open(objstorageprovider.DefaultSettings(vfs.Default, tmpDir)) require.NoError(t, err) defer provider.Close() f0, _, err := provider.Create(context.Background(), base.FileTypeTable, base.FileNum(0).DiskFileNum(), objstorage.CreateOptions{}) require.NoError(t, err) w := NewWriter(f0, WriterOptions{ TableFormat: TableFormatPebblev3, Comparer: testkeys.Comparer, }) keys := testkeys.Alpha(1) keyBuf := make([]byte, 1+testkeys.MaxSuffixLen) // Write a few keys with multiple timestamps (MVCC versions). for i := int64(0); i < 2; i++ { for j := int64(2); j >= 1; j-- { n := testkeys.WriteKeyAt(keyBuf[:], keys, i, j) key := keyBuf[:n] require.NoError(t, w.Set(key, key)) } } require.NoError(t, w.Close()) f1, err := provider.OpenForReading(context.Background(), base.FileTypeTable, base.FileNum(0).DiskFileNum(), objstorage.OpenOptions{}) require.NoError(t, err) r, err := NewReader(f1, ReaderOptions{Comparer: testkeys.Comparer}) require.NoError(t, err) defer r.Close() { var pool BufferPool pool.Init(5) citer, err := r.NewCompactionIter( nil, CategoryAndQoS{}, nil, TrivialReaderProvider{Reader: r}, &pool) require.NoError(t, err) defer citer.Close() i := citer.(*compactionIterator) require.True(t, objstorageprovider.TestingCheckMaxReadahead(i.dataRH)) require.True(t, objstorageprovider.TestingCheckMaxReadahead(i.vbRH)) } { iter, err := r.NewIter(nil, nil) require.NoError(t, err) defer iter.Close() i := iter.(*singleLevelIterator) require.False(t, objstorageprovider.TestingCheckMaxReadahead(i.dataRH)) require.False(t, objstorageprovider.TestingCheckMaxReadahead(i.vbRH)) } } func TestReaderChecksumErrors(t *testing.T) { for _, checksumType := range []ChecksumType{ChecksumTypeCRC32c, ChecksumTypeXXHash64} { t.Run(fmt.Sprintf("checksum-type=%d", checksumType), func(t *testing.T) { for _, twoLevelIndex := range []bool{false, true} { t.Run(fmt.Sprintf("two-level-index=%t", twoLevelIndex), func(t *testing.T) { mem := vfs.NewMem() { // Create an sstable with 3 data blocks. f, err := mem.Create("test") require.NoError(t, err) const blockSize = 32 indexBlockSize := 4096 if twoLevelIndex { indexBlockSize = 1 } w := NewWriter(objstorageprovider.NewFileWritable(f), WriterOptions{ BlockSize: blockSize, IndexBlockSize: indexBlockSize, Checksum: checksumType, }) require.NoError(t, w.Set(bytes.Repeat([]byte("a"), blockSize), nil)) require.NoError(t, w.Set(bytes.Repeat([]byte("b"), blockSize), nil)) require.NoError(t, w.Set(bytes.Repeat([]byte("c"), blockSize), nil)) require.NoError(t, w.Close()) } // Load the layout so that we no the location of the data blocks. var layout *Layout { f, err := mem.Open("test") require.NoError(t, err) r, err := newReader(f, ReaderOptions{}) require.NoError(t, err) layout, err = r.Layout() require.NoError(t, err) require.EqualValues(t, len(layout.Data), 3) require.NoError(t, r.Close()) } for _, bh := range layout.Data { // Read the sstable and corrupt the first byte in the target data // block. orig, err := mem.Open("test") require.NoError(t, err) data, err := io.ReadAll(orig) require.NoError(t, err) require.NoError(t, orig.Close()) // Corrupt the first byte in the block. data[bh.Offset] ^= 0xff corrupted, err := mem.Create("corrupted") require.NoError(t, err) _, err = corrupted.Write(data) require.NoError(t, err) require.NoError(t, corrupted.Close()) // Verify that we encounter a checksum mismatch error while iterating // over the sstable. corrupted, err = mem.Open("corrupted") require.NoError(t, err) r, err := newReader(corrupted, ReaderOptions{}) require.NoError(t, err) iter, err := r.NewIter(nil, nil) require.NoError(t, err) for k, _ := iter.First(); k != nil; k, _ = iter.Next() { } require.Regexp(t, `checksum mismatch`, iter.Error()) require.Regexp(t, `checksum mismatch`, iter.Close()) iter, err = r.NewIter(nil, nil) require.NoError(t, err) for k, _ := iter.Last(); k != nil; k, _ = iter.Prev() { } require.Regexp(t, `checksum mismatch`, iter.Error()) require.Regexp(t, `checksum mismatch`, iter.Close()) require.NoError(t, r.Close()) } }) } }) } } func TestValidateBlockChecksums(t *testing.T) { seed := uint64(time.Now().UnixNano()) rng := rand.New(rand.NewSource(seed)) t.Logf("using seed = %d", seed) allFiles := []string{ "testdata/h.no-compression.sst", "testdata/h.no-compression.two_level_index.sst", "testdata/h.sst", "testdata/h.table-bloom.no-compression.prefix_extractor.no_whole_key_filter.sst", "testdata/h.table-bloom.no-compression.sst", "testdata/h.table-bloom.sst", "testdata/h.zstd-compression.sst", } type corruptionLocation int const ( corruptionLocationData corruptionLocation = iota corruptionLocationIndex corruptionLocationTopIndex corruptionLocationFilter corruptionLocationRangeDel corruptionLocationProperties corruptionLocationMetaIndex ) testCases := []struct { name string files []string corruptionLocations []corruptionLocation }{ { name: "no corruption", corruptionLocations: []corruptionLocation{}, }, { name: "data block corruption", corruptionLocations: []corruptionLocation{ corruptionLocationData, }, }, { name: "index block corruption", corruptionLocations: []corruptionLocation{ corruptionLocationIndex, }, }, { name: "top index block corruption", files: []string{ "testdata/h.no-compression.two_level_index.sst", }, corruptionLocations: []corruptionLocation{ corruptionLocationTopIndex, }, }, { name: "filter block corruption", files: []string{ "testdata/h.table-bloom.no-compression.prefix_extractor.no_whole_key_filter.sst", "testdata/h.table-bloom.no-compression.sst", "testdata/h.table-bloom.sst", }, corruptionLocations: []corruptionLocation{ corruptionLocationFilter, }, }, { name: "range deletion block corruption", corruptionLocations: []corruptionLocation{ corruptionLocationRangeDel, }, }, { name: "properties block corruption", corruptionLocations: []corruptionLocation{ corruptionLocationProperties, }, }, { name: "metaindex block corruption", corruptionLocations: []corruptionLocation{ corruptionLocationMetaIndex, }, }, { name: "multiple blocks corrupted", corruptionLocations: []corruptionLocation{ corruptionLocationData, corruptionLocationIndex, corruptionLocationRangeDel, corruptionLocationProperties, corruptionLocationMetaIndex, }, }, } testFn := func(t *testing.T, file string, corruptionLocations []corruptionLocation) { // Create a copy of the SSTable that we can freely corrupt. f, err := os.Open(filepath.FromSlash(file)) require.NoError(t, err) pathCopy := path.Join(t.TempDir(), path.Base(file)) fCopy, err := os.OpenFile(pathCopy, os.O_CREATE|os.O_RDWR, 0600) require.NoError(t, err) defer fCopy.Close() _, err = io.Copy(fCopy, f) require.NoError(t, err) err = fCopy.Sync() require.NoError(t, err) require.NoError(t, f.Close()) filter := bloom.FilterPolicy(10) r, err := newReader(fCopy, ReaderOptions{ Filters: map[string]FilterPolicy{ filter.Name(): filter, }, }) require.NoError(t, err) defer func() { require.NoError(t, r.Close()) }() // Prior to corruption, validation is successful. require.NoError(t, r.ValidateBlockChecksums()) // If we are not testing for corruption, we can stop here. if len(corruptionLocations) == 0 { return } // Perform bit flips in various corruption locations. layout, err := r.Layout() require.NoError(t, err) for _, location := range corruptionLocations { var bh BlockHandle switch location { case corruptionLocationData: bh = layout.Data[rng.Intn(len(layout.Data))].BlockHandle case corruptionLocationIndex: bh = layout.Index[rng.Intn(len(layout.Index))] case corruptionLocationTopIndex: bh = layout.TopIndex case corruptionLocationFilter: bh = layout.Filter case corruptionLocationRangeDel: bh = layout.RangeDel case corruptionLocationProperties: bh = layout.Properties case corruptionLocationMetaIndex: bh = layout.MetaIndex default: t.Fatalf("unknown location") } // Corrupt a random byte within the selected block. pos := int64(bh.Offset) + rng.Int63n(int64(bh.Length)) t.Logf("altering file=%s @ offset = %d", file, pos) b := make([]byte, 1) n, err := fCopy.ReadAt(b, pos) require.NoError(t, err) require.Equal(t, 1, n) t.Logf("data (before) = %08b", b) b[0] ^= 0xff t.Logf("data (after) = %08b", b) _, err = fCopy.WriteAt(b, pos) require.NoError(t, err) } // Write back to the file. err = fCopy.Sync() require.NoError(t, err) // Confirm that checksum validation fails. err = r.ValidateBlockChecksums() require.Error(t, err) require.Regexp(t, `checksum mismatch`, err.Error()) } for _, tc := range testCases { // By default, test across all files, unless overridden. files := tc.files if files == nil { files = allFiles } for _, file := range files { t.Run(tc.name+" "+path.Base(file), func(t *testing.T) { testFn(t, file, tc.corruptionLocations) }) } } } func TestReader_TableFormat(t *testing.T) { test := func(t *testing.T, want TableFormat) { fs := vfs.NewMem() f, err := fs.Create("test") require.NoError(t, err) opts := WriterOptions{TableFormat: want} w := NewWriter(objstorageprovider.NewFileWritable(f), opts) err = w.Close() require.NoError(t, err) f, err = fs.Open("test") require.NoError(t, err) r, err := newReader(f, ReaderOptions{}) require.NoError(t, err) defer r.Close() got, err := r.TableFormat() require.NoError(t, err) require.Equal(t, want, got) } for tf := TableFormatLevelDB; tf <= TableFormatMax; tf++ { t.Run(tf.String(), func(t *testing.T) { test(t, tf) }) } } func buildTestTable( t *testing.T, numEntries uint64, blockSize, indexBlockSize int, compression Compression, ) *Reader { provider, err := objstorageprovider.Open(objstorageprovider.DefaultSettings(vfs.NewMem(), "" /* dirName */)) require.NoError(t, err) defer provider.Close() return buildTestTableWithProvider(t, provider, numEntries, blockSize, indexBlockSize, compression) } func buildTestTableWithProvider( t *testing.T, provider objstorage.Provider, numEntries uint64, blockSize, indexBlockSize int, compression Compression, ) *Reader { f0, _, err := provider.Create(context.Background(), base.FileTypeTable, base.FileNum(0).DiskFileNum(), objstorage.CreateOptions{}) require.NoError(t, err) w := NewWriter(f0, WriterOptions{ BlockSize: blockSize, IndexBlockSize: indexBlockSize, Compression: compression, FilterPolicy: nil, }) var ikey InternalKey for i := uint64(0); i < numEntries; i++ { key := make([]byte, 8+i%3) value := make([]byte, i%100) binary.BigEndian.PutUint64(key, i) ikey.UserKey = key w.Add(ikey, value) } require.NoError(t, w.Close()) // Re-open that filename for reading. f1, err := provider.OpenForReading(context.Background(), base.FileTypeTable, base.FileNum(0).DiskFileNum(), objstorage.OpenOptions{}) require.NoError(t, err) c := cache.New(128 << 20) defer c.Unref() r, err := NewReader(f1, ReaderOptions{ Cache: c, }) require.NoError(t, err) return r } func buildBenchmarkTable( b *testing.B, options WriterOptions, confirmTwoLevelIndex bool, offset int, ) (*Reader, [][]byte) { mem := vfs.NewMem() f0, err := mem.Create("bench") if err != nil { b.Fatal(err) } w := NewWriter(objstorageprovider.NewFileWritable(f0), options) var keys [][]byte var ikey InternalKey for i := uint64(0); i < 1e6; i++ { key := make([]byte, 8) binary.BigEndian.PutUint64(key, i+uint64(offset)) keys = append(keys, key) ikey.UserKey = key w.Add(ikey, nil) } if err := w.Close(); err != nil { b.Fatal(err) } // Re-open that filename for reading. f1, err := mem.Open("bench") if err != nil { b.Fatal(err) } c := cache.New(128 << 20) defer c.Unref() r, err := newReader(f1, ReaderOptions{ Cache: c, }) if err != nil { b.Fatal(err) } if confirmTwoLevelIndex && r.Properties.IndexPartitions == 0 { b.Fatalf("should have constructed two level index") } return r, keys } var basicBenchmarks = []struct { name string options WriterOptions }{ { name: "restart=16,compression=Snappy", options: WriterOptions{ BlockSize: 32 << 10, BlockRestartInterval: 16, FilterPolicy: nil, Compression: SnappyCompression, TableFormat: TableFormatPebblev2, }, }, { name: "restart=16,compression=ZSTD", options: WriterOptions{ BlockSize: 32 << 10, BlockRestartInterval: 16, FilterPolicy: nil, Compression: ZstdCompression, TableFormat: TableFormatPebblev2, }, }, } func BenchmarkTableIterSeekGE(b *testing.B) { for _, bm := range basicBenchmarks { b.Run(bm.name, func(b *testing.B) { r, keys := buildBenchmarkTable(b, bm.options, false, 0) it, err := r.NewIter(nil /* lower */, nil /* upper */) require.NoError(b, err) rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) b.ResetTimer() for i := 0; i < b.N; i++ { it.SeekGE(keys[rng.Intn(len(keys))], base.SeekGEFlagsNone) } b.StopTimer() it.Close() r.Close() }) } } func BenchmarkTableIterSeekLT(b *testing.B) { for _, bm := range basicBenchmarks { b.Run(bm.name, func(b *testing.B) { r, keys := buildBenchmarkTable(b, bm.options, false, 0) it, err := r.NewIter(nil /* lower */, nil /* upper */) require.NoError(b, err) rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) b.ResetTimer() for i := 0; i < b.N; i++ { it.SeekLT(keys[rng.Intn(len(keys))], base.SeekLTFlagsNone) } b.StopTimer() it.Close() r.Close() }) } } func BenchmarkTableIterNext(b *testing.B) { for _, bm := range basicBenchmarks { b.Run(bm.name, func(b *testing.B) { r, _ := buildBenchmarkTable(b, bm.options, false, 0) it, err := r.NewIter(nil /* lower */, nil /* upper */) require.NoError(b, err) b.ResetTimer() var sum int64 var key *InternalKey for i := 0; i < b.N; i++ { if key == nil { key, _ = it.First() } sum += int64(binary.BigEndian.Uint64(key.UserKey)) key, _ = it.Next() } if testing.Verbose() { fmt.Fprint(io.Discard, sum) } b.StopTimer() it.Close() r.Close() }) } } func BenchmarkTableIterPrev(b *testing.B) { for _, bm := range basicBenchmarks { b.Run(bm.name, func(b *testing.B) { r, _ := buildBenchmarkTable(b, bm.options, false, 0) it, err := r.NewIter(nil /* lower */, nil /* upper */) require.NoError(b, err) b.ResetTimer() var sum int64 var key *InternalKey for i := 0; i < b.N; i++ { if key == nil { key, _ = it.Last() } sum += int64(binary.BigEndian.Uint64(key.UserKey)) key, _ = it.Prev() } if testing.Verbose() { fmt.Fprint(io.Discard, sum) } b.StopTimer() it.Close() r.Close() }) } } func BenchmarkLayout(b *testing.B) { r, _ := buildBenchmarkTable(b, WriterOptions{}, false, 0) b.ResetTimer() for i := 0; i < b.N; i++ { r.Layout() } b.StopTimer() r.Close() } func BenchmarkSeqSeekGEExhausted(b *testing.B) { // Snappy with no bloom filter. options := basicBenchmarks[0].options for _, twoLevelIndex := range []bool{false, true} { switch twoLevelIndex { case false: options.IndexBlockSize = 0 case true: options.IndexBlockSize = 512 } const offsetCount = 5000 reader, keys := buildBenchmarkTable(b, options, twoLevelIndex, offsetCount) var preKeys [][]byte for i := 0; i < offsetCount; i++ { key := make([]byte, 8) binary.BigEndian.PutUint64(key, uint64(i)) preKeys = append(preKeys, key) } var postKeys [][]byte for i := 0; i < offsetCount; i++ { key := make([]byte, 8) binary.BigEndian.PutUint64(key, uint64(i+offsetCount+len(keys))) postKeys = append(postKeys, key) } for _, exhaustedBounds := range []bool{false, true} { for _, prefixSeek := range []bool{false, true} { exhausted := "file" if exhaustedBounds { exhausted = "bounds" } seekKind := "ge" if prefixSeek { seekKind = "prefix-ge" } b.Run(fmt.Sprintf( "two-level=%t/exhausted=%s/seek=%s", twoLevelIndex, exhausted, seekKind), func(b *testing.B) { var upper []byte var seekKeys [][]byte if exhaustedBounds { seekKeys = preKeys upper = keys[0] } else { seekKeys = postKeys } it, err := reader.NewIter(nil /* lower */, upper) require.NoError(b, err) b.ResetTimer() pos := 0 var seekGEFlags SeekGEFlags for i := 0; i < b.N; i++ { seekKey := seekKeys[0] var k *InternalKey if prefixSeek { k, _ = it.SeekPrefixGE(seekKey, seekKey, seekGEFlags) } else { k, _ = it.SeekGE(seekKey, seekGEFlags) } if k != nil { b.Fatal("found a key") } if it.Error() != nil { b.Fatalf("%s", it.Error().Error()) } pos++ if pos == len(seekKeys) { pos = 0 seekGEFlags = seekGEFlags.DisableTrySeekUsingNext() } else { seekGEFlags = seekGEFlags.EnableTrySeekUsingNext() } } b.StopTimer() it.Close() }) } } reader.Close() } } func BenchmarkIteratorScanManyVersions(b *testing.B) { options := WriterOptions{ BlockSize: 32 << 10, BlockRestartInterval: 16, FilterPolicy: nil, Compression: SnappyCompression, Comparer: testkeys.Comparer, } // 10,000 key prefixes, each with 100 versions. const keyCount = 10000 const sharedPrefixLen = 32 const unsharedPrefixLen = 8 const versionCount = 100 // Take the very large keyspace consisting of alphabetic characters of // lengths up to unsharedPrefixLen and reduce it down to keyCount keys by // picking every 1 key every keyCount keys. keys := testkeys.Alpha(unsharedPrefixLen) keys = keys.EveryN(keys.Count() / keyCount) if keys.Count() < keyCount { b.Fatalf("expected %d keys, found %d", keyCount, keys.Count()) } keyBuf := make([]byte, sharedPrefixLen+unsharedPrefixLen+testkeys.MaxSuffixLen) for i := 0; i < sharedPrefixLen; i++ { keyBuf[i] = 'A' + byte(i) } // v2 sstable is 115,178,070 bytes. v3 sstable is 107,181,105 bytes with // 99,049,269 bytes in value blocks. setupBench := func(b *testing.B, tableFormat TableFormat, cacheSize int64) *Reader { mem := vfs.NewMem() f0, err := mem.Create("bench") require.NoError(b, err) options.TableFormat = tableFormat w := NewWriter(objstorageprovider.NewFileWritable(f0), options) val := make([]byte, 100) rng := rand.New(rand.NewSource(100)) for i := int64(0); i < keys.Count(); i++ { for v := 0; v < versionCount; v++ { n := testkeys.WriteKeyAt(keyBuf[sharedPrefixLen:], keys, i, int64(versionCount-v+1)) key := keyBuf[:n+sharedPrefixLen] rng.Read(val) require.NoError(b, w.Set(key, val)) } } require.NoError(b, w.Close()) c := cache.New(cacheSize) defer c.Unref() // Re-open the filename for reading. f0, err = mem.Open("bench") require.NoError(b, err) r, err := newReader(f0, ReaderOptions{ Cache: c, Comparer: testkeys.Comparer, }) require.NoError(b, err) return r } for _, format := range []TableFormat{TableFormatPebblev2, TableFormatPebblev3} { b.Run(fmt.Sprintf("format=%s", format.String()), func(b *testing.B) { // 150MiB results in a high cache hit rate for both formats. 20MiB // results in a high cache hit rate for the data blocks in // TableFormatPebblev3. for _, cacheSize := range []int64{20 << 20, 150 << 20} { b.Run(fmt.Sprintf("cache-size=%s", humanize.Bytes.Int64(cacheSize)), func(b *testing.B) { r := setupBench(b, format, cacheSize) defer func() { require.NoError(b, r.Close()) }() for _, readValue := range []bool{false, true} { b.Run(fmt.Sprintf("read-value=%t", readValue), func(b *testing.B) { iter, err := r.NewIter(nil, nil) require.NoError(b, err) var k *InternalKey var v base.LazyValue var valBuf [100]byte b.ResetTimer() for i := 0; i < b.N; i++ { if k == nil { k, _ = iter.First() if k == nil { b.Fatalf("k is nil") } } k, v = iter.Next() if k != nil && readValue { _, callerOwned, err := v.Value(valBuf[:]) if err != nil { b.Fatal(err) } else if callerOwned { b.Fatalf("unexpected callerOwned: %t", callerOwned) } } } }) } }) } }) } } func BenchmarkIteratorScanNextPrefix(b *testing.B) { options := WriterOptions{ BlockSize: 32 << 10, BlockRestartInterval: 16, FilterPolicy: nil, Compression: SnappyCompression, TableFormat: TableFormatPebblev3, Comparer: testkeys.Comparer, } const keyCount = 10000 const sharedPrefixLen = 32 const unsharedPrefixLen = 8 val := make([]byte, 100) rand.New(rand.NewSource(100)).Read(val) // Take the very large keyspace consisting of alphabetic characters of // lengths up to unsharedPrefixLen and reduce it down to keyCount keys by // picking every 1 key every keyCount keys. keys := testkeys.Alpha(unsharedPrefixLen) keys = keys.EveryN(keys.Count() / keyCount) if keys.Count() < keyCount { b.Fatalf("expected %d keys, found %d", keyCount, keys.Count()) } keyBuf := make([]byte, sharedPrefixLen+unsharedPrefixLen+testkeys.MaxSuffixLen) for i := 0; i < sharedPrefixLen; i++ { keyBuf[i] = 'A' + byte(i) } setupBench := func(b *testing.B, versCount int) (r *Reader, succKeys [][]byte) { mem := vfs.NewMem() f0, err := mem.Create("bench") require.NoError(b, err) w := NewWriter(objstorageprovider.NewFileWritable(f0), options) for i := int64(0); i < keys.Count(); i++ { for v := 0; v < versCount; v++ { n := testkeys.WriteKeyAt(keyBuf[sharedPrefixLen:], keys, i, int64(versCount-v+1)) key := keyBuf[:n+sharedPrefixLen] require.NoError(b, w.Set(key, val)) if v == 0 { prefixLen := testkeys.Comparer.Split(key) prefixKey := key[:prefixLen] succKey := testkeys.Comparer.ImmediateSuccessor(nil, prefixKey) succKeys = append(succKeys, succKey) } } } require.NoError(b, w.Close()) // NB: This 200MiB cache is sufficient for even the largest file: 10,000 // keys * 100 versions = 1M keys, where each key-value pair is ~140 bytes // = 140MB. So we are not measuring the caching benefit of // TableFormatPebblev3 storing older values in value blocks. c := cache.New(200 << 20) defer c.Unref() // Re-open the filename for reading. f0, err = mem.Open("bench") require.NoError(b, err) r, err = newReader(f0, ReaderOptions{ Cache: c, Comparer: testkeys.Comparer, }) require.NoError(b, err) return r, succKeys } // Analysis of some sample results with TableFormatPebblev2: // versions=1/method=seek-ge-10 22107622 53.57 ns/op // versions=1/method=next-prefix-10 36292837 33.07 ns/op // versions=2/method=seek-ge-10 14429138 82.92 ns/op // versions=2/method=next-prefix-10 19676055 60.78 ns/op // versions=10/method=seek-ge-10 1453726 825.2 ns/op // versions=10/method=next-prefix-10 2450498 489.6 ns/op // versions=100/method=seek-ge-10 965143 1257 ns/op // versions=100/method=next-prefix-10 1000000 1054 ns/op // // With 1 version, both SeekGE and NextPrefix will be able to complete after // doing a single call to blockIter.Next. However, SeekGE has to do two key // comparisons unlike the one key comparison in NextPrefix. This is because // SeekGE also compares *before* calling Next since it is possible that the // preceding SeekGE is already at the right place. // // With 2 versions, both will do two calls to blockIter.Next. The difference // in the cost is the same as in the 1 version case. // // With 10 versions, it is still likely that the desired key is in the same // data block. NextPrefix will seek only the blockIter. And in the rare case // that the key is in the next data block, it will step the index block (not // seek). In comparison, SeekGE will seek the index block too. // // With 100 versions we more often cross from one data block to the next, so // the difference in cost declines. // // Some sample results with TableFormatPebblev3: // versions=1/method=seek-ge-10 18702609 53.90 ns/op // versions=1/method=next-prefix-10 77440167 15.41 ns/op // versions=2/method=seek-ge-10 13554286 87.91 ns/op // versions=2/method=next-prefix-10 62148526 19.25 ns/op // versions=10/method=seek-ge-10 1316676 910.5 ns/op // versions=10/method=next-prefix-10 18829448 62.61 ns/op // versions=100/method=seek-ge-10 1166139 1025 ns/op // versions=100/method=next-prefix-10 4443386 265.3 ns/op // // NextPrefix is much cheaper than in TableFormatPebblev2 with larger number // of versions. It is also cheaper with 1 and 2 versions since // setHasSamePrefix=false eliminates a key comparison. for _, versionCount := range []int{1, 2, 10, 100} { b.Run(fmt.Sprintf("versions=%d", versionCount), func(b *testing.B) { r, succKeys := setupBench(b, versionCount) defer func() { require.NoError(b, r.Close()) }() for _, method := range []string{"seek-ge", "next-prefix"} { b.Run(fmt.Sprintf("method=%s", method), func(b *testing.B) { for _, readValue := range []bool{false, true} { b.Run(fmt.Sprintf("read-value=%t", readValue), func(b *testing.B) { iter, err := r.NewIter(nil, nil) require.NoError(b, err) var nextFunc func(index int) (*InternalKey, base.LazyValue) switch method { case "seek-ge": nextFunc = func(index int) (*InternalKey, base.LazyValue) { var flags base.SeekGEFlags return iter.SeekGE(succKeys[index], flags.EnableTrySeekUsingNext()) } case "next-prefix": nextFunc = func(index int) (*InternalKey, base.LazyValue) { return iter.NextPrefix(succKeys[index]) } default: b.Fatalf("unknown method %s", method) } n := keys.Count() j := n var k *InternalKey var v base.LazyValue var valBuf [100]byte b.ResetTimer() for i := 0; i < b.N; i++ { if k == nil { if j != n { b.Fatalf("unexpected %d != %d", j, n) } k, _ = iter.First() j = 0 } else { k, v = nextFunc(int(j - 1)) if k != nil && readValue { _, callerOwned, err := v.Value(valBuf[:]) if err != nil { b.Fatal(err) } else if callerOwned { b.Fatalf("unexpected callerOwned: %t", callerOwned) } } } if k != nil { j++ } } }) } }) } }) } } func BenchmarkIteratorScanObsolete(b *testing.B) { options := WriterOptions{ BlockSize: 32 << 10, BlockRestartInterval: 16, FilterPolicy: nil, Compression: SnappyCompression, Comparer: testkeys.Comparer, } const keyCount = 1 << 20 const keyLen = 10 // Take the very large keyspace consisting of alphabetic characters of // lengths up to unsharedPrefixLen and reduce it down to keyCount keys by // picking every 1 key every keyCount keys. keys := testkeys.Alpha(keyLen) keys = keys.EveryN(keys.Count() / keyCount) if keys.Count() < keyCount { b.Fatalf("expected %d keys, found %d", keyCount, keys.Count()) } expectedKeyCount := keys.Count() keyBuf := make([]byte, keyLen) setupBench := func(b *testing.B, tableFormat TableFormat, cacheSize int64) *Reader { mem := vfs.NewMem() f0, err := mem.Create("bench") require.NoError(b, err) options.TableFormat = tableFormat w := NewWriter(objstorageprovider.NewFileWritable(f0), options) val := make([]byte, 100) rng := rand.New(rand.NewSource(100)) for i := int64(0); i < keys.Count(); i++ { n := testkeys.WriteKey(keyBuf, keys, i) key := keyBuf[:n] rng.Read(val) forceObsolete := true if i == 0 { forceObsolete = false } require.NoError(b, w.AddWithForceObsolete( base.MakeInternalKey(key, 0, InternalKeyKindSet), val, forceObsolete)) } require.NoError(b, w.Close()) c := cache.New(cacheSize) defer c.Unref() // Re-open the filename for reading. f0, err = mem.Open("bench") require.NoError(b, err) r, err := newReader(f0, ReaderOptions{ Cache: c, Comparer: testkeys.Comparer, }) require.NoError(b, err) return r } for _, format := range []TableFormat{TableFormatPebblev3, TableFormatPebblev4} { b.Run(fmt.Sprintf("format=%s", format.String()), func(b *testing.B) { // 150MiB results in a high cache hit rate for both formats. for _, cacheSize := range []int64{1, 150 << 20} { b.Run(fmt.Sprintf("cache-size=%s", humanize.Bytes.Int64(cacheSize)), func(b *testing.B) { r := setupBench(b, format, cacheSize) defer func() { require.NoError(b, r.Close()) }() for _, hideObsoletePoints := range []bool{false, true} { b.Run(fmt.Sprintf("hide-obsolete=%t", hideObsoletePoints), func(b *testing.B) { var filterer *BlockPropertiesFilterer if format == TableFormatPebblev4 && hideObsoletePoints { filterer = newBlockPropertiesFilterer( []BlockPropertyFilter{obsoleteKeyBlockPropertyFilter{}}, nil) intersects, err := filterer.intersectsUserPropsAndFinishInit(r.Properties.UserProperties) if err != nil { b.Fatalf("%s", err.Error()) } if !intersects { b.Fatalf("sstable does not intersect") } } iter, err := r.NewIterWithBlockPropertyFiltersAndContextEtc( context.Background(), nil, nil, filterer, hideObsoletePoints, true, nil, CategoryAndQoS{}, nil, TrivialReaderProvider{Reader: r}) require.NoError(b, err) b.ResetTimer() for i := 0; i < b.N; i++ { count := int64(0) k, _ := iter.First() for k != nil { count++ k, _ = iter.Next() } if format == TableFormatPebblev4 && hideObsoletePoints { if count != 1 { b.Fatalf("found %d points", count) } } else { if count != expectedKeyCount { b.Fatalf("found %d points", count) } } } }) } }) } }) } } func newReader(r ReadableFile, o ReaderOptions, extraOpts ...ReaderOption) (*Reader, error) { readable, err := NewSimpleReadable(r) if err != nil { return nil, err } return NewReader(readable, o, extraOpts...) }