ceremonyclient/pebble/version_set_test.go

510 lines
16 KiB
Go
Raw Permalink Normal View History

2024-01-03 07:31:42 +00:00
// Copyright 2020 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 (
"io"
"testing"
"time"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/internal/manifest"
"github.com/cockroachdb/pebble/objstorage/objstorageprovider"
"github.com/cockroachdb/pebble/record"
"github.com/cockroachdb/pebble/sstable"
"github.com/cockroachdb/pebble/vfs"
"github.com/stretchr/testify/require"
)
func writeAndIngest(t *testing.T, mem vfs.FS, d *DB, k InternalKey, v []byte, filename string) {
path := mem.PathJoin("ext", filename)
f, err := mem.Create(path)
require.NoError(t, err)
w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{})
require.NoError(t, w.Add(k, v))
require.NoError(t, w.Close())
require.NoError(t, d.Ingest([]string{path}))
}
// d.mu should be help. logLock should not be held.
func checkBackingSize(t *testing.T, d *DB) {
d.mu.versions.logLock()
var backingSizeSum uint64
for _, backing := range d.mu.versions.backingState.fileBackingMap {
backingSizeSum += backing.Size
}
require.Equal(t, backingSizeSum, d.mu.versions.backingState.fileBackingSize)
d.mu.versions.logUnlock()
}
// TestLatestRefCounting sanity checks the ref counting implementation for
// FileMetadata.latestRefs, and makes sure that the zombie table implementation
// works when the version edit contains virtual sstables. It also checks that
// we're adding the physical sstable to the obsolete tables list iff the file is
// truly obsolete.
func TestLatestRefCounting(t *testing.T) {
mem := vfs.NewMem()
require.NoError(t, mem.MkdirAll("ext", 0755))
opts := &Options{
FS: mem,
MaxManifestFileSize: 1,
DisableAutomaticCompactions: true,
FormatMajorVersion: FormatVirtualSSTables,
}
d, err := Open("", opts)
require.NoError(t, err)
err = d.Set([]byte{'a'}, []byte{'a'}, nil)
require.NoError(t, err)
err = d.Set([]byte{'b'}, []byte{'b'}, nil)
require.NoError(t, err)
err = d.Flush()
require.NoError(t, err)
iter := d.mu.versions.currentVersion().Levels[0].Iter()
var f *fileMetadata = iter.First()
require.NotNil(t, f)
require.Equal(t, 1, int(f.LatestRefs()))
require.Equal(t, 0, len(d.mu.versions.obsoleteTables))
// Grab some new file nums.
d.mu.Lock()
f1 := FileNum(d.mu.versions.nextFileNum)
f2 := f1 + 1
d.mu.versions.nextFileNum += 2
d.mu.Unlock()
m1 := &manifest.FileMetadata{
FileBacking: f.FileBacking,
FileNum: f1,
CreationTime: time.Now().Unix(),
Size: f.Size / 2,
SmallestSeqNum: f.SmallestSeqNum,
LargestSeqNum: f.LargestSeqNum,
Smallest: base.MakeInternalKey([]byte{'a'}, f.Smallest.SeqNum(), InternalKeyKindSet),
Largest: base.MakeInternalKey([]byte{'a'}, f.Smallest.SeqNum(), InternalKeyKindSet),
HasPointKeys: true,
Virtual: true,
}
m2 := &manifest.FileMetadata{
FileBacking: f.FileBacking,
FileNum: f2,
CreationTime: time.Now().Unix(),
Size: f.Size - m1.Size,
SmallestSeqNum: f.SmallestSeqNum,
LargestSeqNum: f.LargestSeqNum,
Smallest: base.MakeInternalKey([]byte{'b'}, f.Largest.SeqNum(), InternalKeyKindSet),
Largest: base.MakeInternalKey([]byte{'b'}, f.Largest.SeqNum(), InternalKeyKindSet),
HasPointKeys: true,
Virtual: true,
}
m1.LargestPointKey = m1.Largest
m1.SmallestPointKey = m1.Smallest
m2.LargestPointKey = m2.Largest
m2.SmallestPointKey = m2.Smallest
m1.ValidateVirtual(f)
d.checkVirtualBounds(m1)
m2.ValidateVirtual(f)
d.checkVirtualBounds(m2)
fileMetrics := func(ve *versionEdit) map[int]*LevelMetrics {
metrics := newFileMetrics(ve.NewFiles)
for de, f := range ve.DeletedFiles {
lm := metrics[de.Level]
if lm == nil {
lm = &LevelMetrics{}
metrics[de.Level] = lm
}
metrics[de.Level].NumFiles--
metrics[de.Level].Size -= int64(f.Size)
}
return metrics
}
d.mu.Lock()
defer d.mu.Unlock()
applyVE := func(ve *versionEdit) error {
d.mu.versions.logLock()
jobID := d.mu.nextJobID
d.mu.nextJobID++
err := d.mu.versions.logAndApply(jobID, ve, fileMetrics(ve), false, func() []compactionInfo {
return d.getInProgressCompactionInfoLocked(nil)
})
d.updateReadStateLocked(nil)
return err
}
// Virtualize f.
ve := manifest.VersionEdit{}
d1 := manifest.DeletedFileEntry{Level: 0, FileNum: f.FileNum}
n1 := manifest.NewFileEntry{Level: 0, Meta: m1}
n2 := manifest.NewFileEntry{Level: 0, Meta: m2}
ve.DeletedFiles = make(map[manifest.DeletedFileEntry]*manifest.FileMetadata)
ve.DeletedFiles[d1] = f
ve.NewFiles = append(ve.NewFiles, n1)
ve.NewFiles = append(ve.NewFiles, n2)
ve.CreatedBackingTables = append(ve.CreatedBackingTables, f.FileBacking)
require.NoError(t, applyVE(&ve))
// 2 latestRefs from 2 virtual sstables in the latest version which refer
// to the physical sstable.
require.Equal(t, 2, int(m1.LatestRefs()))
require.Equal(t, 0, len(d.mu.versions.obsoleteTables))
require.Equal(t, 1, len(d.mu.versions.backingState.fileBackingMap))
_, ok := d.mu.versions.backingState.fileBackingMap[f.FileBacking.DiskFileNum]
require.True(t, ok)
require.Equal(t, f.Size, m2.FileBacking.VirtualizedSize.Load())
checkBackingSize(t, d)
// Make sure that f is not present in zombie list, because it is not yet a
// zombie.
require.Equal(t, 0, len(d.mu.versions.zombieTables))
// Delete the virtual sstable m1.
ve = manifest.VersionEdit{}
d1 = manifest.DeletedFileEntry{Level: 0, FileNum: m1.FileNum}
ve.DeletedFiles = make(map[manifest.DeletedFileEntry]*manifest.FileMetadata)
ve.DeletedFiles[d1] = m1
require.NoError(t, applyVE(&ve))
// Only one virtual sstable in the latest version, confirm that the latest
// version ref counting is correct.
require.Equal(t, 1, int(m2.LatestRefs()))
require.Equal(t, 0, len(d.mu.versions.zombieTables))
require.Equal(t, 0, len(d.mu.versions.obsoleteTables))
require.Equal(t, 1, len(d.mu.versions.backingState.fileBackingMap))
_, ok = d.mu.versions.backingState.fileBackingMap[f.FileBacking.DiskFileNum]
require.True(t, ok)
require.Equal(t, m2.Size, m2.FileBacking.VirtualizedSize.Load())
checkBackingSize(t, d)
// Move m2 from L0 to L6 to test the move compaction case.
ve = manifest.VersionEdit{}
d1 = manifest.DeletedFileEntry{Level: 0, FileNum: m2.FileNum}
n1 = manifest.NewFileEntry{Level: 6, Meta: m2}
ve.DeletedFiles = make(map[manifest.DeletedFileEntry]*manifest.FileMetadata)
ve.DeletedFiles[d1] = m2
ve.NewFiles = append(ve.NewFiles, n1)
require.NoError(t, applyVE(&ve))
checkBackingSize(t, d)
require.Equal(t, 1, int(m2.LatestRefs()))
require.Equal(t, 0, len(d.mu.versions.zombieTables))
require.Equal(t, 0, len(d.mu.versions.obsoleteTables))
require.Equal(t, 1, len(d.mu.versions.backingState.fileBackingMap))
_, ok = d.mu.versions.backingState.fileBackingMap[f.FileBacking.DiskFileNum]
require.True(t, ok)
require.Equal(t, m2.Size, m2.FileBacking.VirtualizedSize.Load())
// Delete m2 from L6.
ve = manifest.VersionEdit{}
d1 = manifest.DeletedFileEntry{Level: 6, FileNum: m2.FileNum}
ve.DeletedFiles = make(map[manifest.DeletedFileEntry]*manifest.FileMetadata)
ve.DeletedFiles[d1] = m2
require.NoError(t, applyVE(&ve))
checkBackingSize(t, d)
// All virtual sstables are gone.
require.Equal(t, 0, int(m2.LatestRefs()))
require.Equal(t, 1, len(d.mu.versions.zombieTables))
require.Equal(t, f.Size, d.mu.versions.zombieTables[f.FileBacking.DiskFileNum])
require.Equal(t, 0, len(d.mu.versions.backingState.fileBackingMap))
_, ok = d.mu.versions.backingState.fileBackingMap[f.FileBacking.DiskFileNum]
require.False(t, ok)
require.Equal(t, 0, int(m2.FileBacking.VirtualizedSize.Load()))
checkBackingSize(t, d)
}
// TODO(bananabrick): Convert TestLatestRefCounting and this test into a single
// datadriven test.
func TestVirtualSSTableManifestReplay(t *testing.T) {
mem := vfs.NewMem()
require.NoError(t, mem.MkdirAll("ext", 0755))
opts := &Options{
FormatMajorVersion: FormatVirtualSSTables,
FS: mem,
MaxManifestFileSize: 1,
DisableAutomaticCompactions: true,
}
d, err := Open("", opts)
require.NoError(t, err)
err = d.Set([]byte{'a'}, []byte{'a'}, nil)
require.NoError(t, err)
err = d.Set([]byte{'b'}, []byte{'b'}, nil)
require.NoError(t, err)
err = d.Flush()
require.NoError(t, err)
iter := d.mu.versions.currentVersion().Levels[0].Iter()
var f *fileMetadata = iter.First()
require.NotNil(t, f)
require.Equal(t, 1, int(f.LatestRefs()))
require.Equal(t, 0, len(d.mu.versions.obsoleteTables))
// Grab some new file nums.
d.mu.Lock()
f1 := FileNum(d.mu.versions.nextFileNum)
f2 := f1 + 1
d.mu.versions.nextFileNum += 2
d.mu.Unlock()
m1 := &manifest.FileMetadata{
FileBacking: f.FileBacking,
FileNum: f1,
CreationTime: time.Now().Unix(),
Size: f.Size / 2,
SmallestSeqNum: f.SmallestSeqNum,
LargestSeqNum: f.LargestSeqNum,
Smallest: base.MakeInternalKey([]byte{'a'}, f.Smallest.SeqNum(), InternalKeyKindSet),
Largest: base.MakeInternalKey([]byte{'a'}, f.Smallest.SeqNum(), InternalKeyKindSet),
HasPointKeys: true,
Virtual: true,
}
m2 := &manifest.FileMetadata{
FileBacking: f.FileBacking,
FileNum: f2,
CreationTime: time.Now().Unix(),
Size: f.Size - m1.Size,
SmallestSeqNum: f.SmallestSeqNum,
LargestSeqNum: f.LargestSeqNum,
Smallest: base.MakeInternalKey([]byte{'b'}, f.Largest.SeqNum(), InternalKeyKindSet),
Largest: base.MakeInternalKey([]byte{'b'}, f.Largest.SeqNum(), InternalKeyKindSet),
HasPointKeys: true,
Virtual: true,
}
m1.LargestPointKey = m1.Largest
m1.SmallestPointKey = m1.Smallest
m1.Stats.NumEntries = 1
m2.LargestPointKey = m2.Largest
m2.SmallestPointKey = m2.Smallest
m2.Stats.NumEntries = 1
m1.ValidateVirtual(f)
d.checkVirtualBounds(m1)
m2.ValidateVirtual(f)
d.checkVirtualBounds(m2)
fileMetrics := func(ve *versionEdit) map[int]*LevelMetrics {
metrics := newFileMetrics(ve.NewFiles)
for de, f := range ve.DeletedFiles {
lm := metrics[de.Level]
if lm == nil {
lm = &LevelMetrics{}
metrics[de.Level] = lm
}
metrics[de.Level].NumFiles--
metrics[de.Level].Size -= int64(f.Size)
}
return metrics
}
d.mu.Lock()
applyVE := func(ve *versionEdit) error {
d.mu.versions.logLock()
jobID := d.mu.nextJobID
d.mu.nextJobID++
err := d.mu.versions.logAndApply(jobID, ve, fileMetrics(ve), false, func() []compactionInfo {
return d.getInProgressCompactionInfoLocked(nil)
})
d.updateReadStateLocked(nil)
return err
}
// Virtualize f.
ve := manifest.VersionEdit{}
d1 := manifest.DeletedFileEntry{Level: 0, FileNum: f.FileNum}
n1 := manifest.NewFileEntry{Level: 0, Meta: m1}
n2 := manifest.NewFileEntry{Level: 0, Meta: m2}
ve.DeletedFiles = make(map[manifest.DeletedFileEntry]*manifest.FileMetadata)
ve.DeletedFiles[d1] = f
ve.NewFiles = append(ve.NewFiles, n1)
ve.NewFiles = append(ve.NewFiles, n2)
ve.CreatedBackingTables = append(ve.CreatedBackingTables, f.FileBacking)
require.NoError(t, applyVE(&ve))
checkBackingSize(t, d)
d.mu.Unlock()
require.Equal(t, 2, int(m1.LatestRefs()))
require.Equal(t, 0, len(d.mu.versions.obsoleteTables))
require.Equal(t, 1, len(d.mu.versions.backingState.fileBackingMap))
_, ok := d.mu.versions.backingState.fileBackingMap[f.FileBacking.DiskFileNum]
require.True(t, ok)
require.Equal(t, f.Size, m2.FileBacking.VirtualizedSize.Load())
// Snapshot version edit will be written to a new manifest due to the flush.
d.Set([]byte{'c'}, []byte{'c'}, nil)
d.Flush()
require.NoError(t, d.Close())
d, err = Open("", opts)
require.NoError(t, err)
d.mu.Lock()
it := d.mu.versions.currentVersion().Levels[0].Iter()
var virtualFile *fileMetadata
for f := it.First(); f != nil; f = it.Next() {
if f.Virtual {
virtualFile = f
break
}
}
require.Equal(t, 2, int(virtualFile.LatestRefs()))
require.Equal(t, 0, len(d.mu.versions.obsoleteTables))
require.Equal(t, 1, len(d.mu.versions.backingState.fileBackingMap))
_, ok = d.mu.versions.backingState.fileBackingMap[f.FileBacking.DiskFileNum]
require.True(t, ok)
require.Equal(t, f.Size, virtualFile.FileBacking.VirtualizedSize.Load())
checkBackingSize(t, d)
d.mu.Unlock()
// Will cause the virtual sstables to be deleted, and the file backing should
// also be removed.
d.Compact([]byte{'a'}, []byte{'z'}, false)
d.mu.Lock()
virtualFile = nil
it = d.mu.versions.currentVersion().Levels[0].Iter()
for f := it.First(); f != nil; f = it.Next() {
if f.Virtual {
virtualFile = f
break
}
}
require.Nil(t, virtualFile)
require.Equal(t, 0, len(d.mu.versions.obsoleteTables))
require.Equal(t, 0, len(d.mu.versions.backingState.fileBackingMap))
checkBackingSize(t, d)
d.mu.Unlock()
// Close and restart to make sure that the new snapshot written during
// compaction doesn't have the file backing.
require.NoError(t, d.Close())
d, err = Open("", opts)
require.NoError(t, err)
d.mu.Lock()
virtualFile = nil
it = d.mu.versions.currentVersion().Levels[0].Iter()
for f := it.First(); f != nil; f = it.Next() {
if f.Virtual {
virtualFile = f
break
}
}
require.Nil(t, virtualFile)
require.Equal(t, 0, len(d.mu.versions.obsoleteTables))
require.Equal(t, 0, len(d.mu.versions.backingState.fileBackingMap))
checkBackingSize(t, d)
d.mu.Unlock()
require.NoError(t, d.Close())
}
func TestVersionSetCheckpoint(t *testing.T) {
mem := vfs.NewMem()
require.NoError(t, mem.MkdirAll("ext", 0755))
opts := &Options{
FS: mem,
MaxManifestFileSize: 1,
}
d, err := Open("", opts)
require.NoError(t, err)
// Multiple manifest files are created such that the latest one must have a correct snapshot
// of the preceding state for the DB to be opened correctly and see the written data.
// Snapshot has no files, so first edit will cause manifest rotation.
writeAndIngest(t, mem, d, base.MakeInternalKey([]byte("a"), 0, InternalKeyKindSet), []byte("b"), "a")
// Snapshot has no files, and manifest has an edit from the previous ingest,
// so this second ingest will cause manifest rotation.
writeAndIngest(t, mem, d, base.MakeInternalKey([]byte("c"), 0, InternalKeyKindSet), []byte("d"), "c")
require.NoError(t, d.Close())
d, err = Open("", opts)
require.NoError(t, err)
checkValue := func(k string, expected string) {
v, closer, err := d.Get([]byte(k))
require.NoError(t, err)
require.Equal(t, expected, string(v))
closer.Close()
}
checkValue("a", "b")
checkValue("c", "d")
require.NoError(t, d.Close())
}
func TestVersionSetSeqNums(t *testing.T) {
mem := vfs.NewMem()
require.NoError(t, mem.MkdirAll("ext", 0755))
opts := &Options{
FS: mem,
MaxManifestFileSize: 1,
}
d, err := Open("", opts)
require.NoError(t, err)
// Snapshot has no files, so first edit will cause manifest rotation.
writeAndIngest(t, mem, d, base.MakeInternalKey([]byte("a"), 0, InternalKeyKindSet), []byte("b"), "a")
// Snapshot has no files, and manifest has an edit from the previous ingest,
// so this second ingest will cause manifest rotation.
writeAndIngest(t, mem, d, base.MakeInternalKey([]byte("c"), 0, InternalKeyKindSet), []byte("d"), "c")
require.NoError(t, d.Close())
d, err = Open("", opts)
require.NoError(t, err)
defer d.Close()
d.TestOnlyWaitForCleaning()
// Check that the manifest has the correct LastSeqNum, equalling the highest
// observed SeqNum.
filenames, err := mem.List("")
require.NoError(t, err)
var manifest vfs.File
for _, filename := range filenames {
fileType, _, ok := base.ParseFilename(mem, filename)
if ok && fileType == fileTypeManifest {
manifest, err = mem.Open(filename)
require.NoError(t, err)
}
}
require.NotNil(t, manifest)
defer manifest.Close()
rr := record.NewReader(manifest, 0 /* logNum */)
lastSeqNum := uint64(0)
for {
r, err := rr.Next()
if err == io.EOF {
break
}
require.NoError(t, err)
var ve versionEdit
err = ve.Decode(r)
require.NoError(t, err)
if ve.LastSeqNum != 0 {
lastSeqNum = ve.LastSeqNum
}
}
// 2 ingestions happened, so LastSeqNum should equal base.SeqNumStart + 1.
require.Equal(t, uint64(11), lastSeqNum)
// logSeqNum is always one greater than the last assigned sequence number.
require.Equal(t, d.mu.versions.logSeqNum.Load(), lastSeqNum+1)
}