github.com/cockroachdb/pebble@v0.0.0-20231214172447-ab4952c5f87b/checkpoint_test.go (about)

     1  // Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use
     2  // of this source code is governed by a BSD-style license that can be found in
     3  // the LICENSE file.
     4  
     5  package pebble
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"fmt"
    11  	"math/rand"
    12  	"slices"
    13  	"sort"
    14  	"strings"
    15  	"sync"
    16  	"testing"
    17  
    18  	"github.com/cockroachdb/datadriven"
    19  	"github.com/cockroachdb/pebble/internal/base"
    20  	"github.com/cockroachdb/pebble/vfs"
    21  	"github.com/stretchr/testify/require"
    22  )
    23  
    24  func TestCheckpoint(t *testing.T) {
    25  	dbs := make(map[string]*DB)
    26  	defer func() {
    27  		for _, db := range dbs {
    28  			if db.closed.Load() == nil {
    29  				require.NoError(t, db.Close())
    30  			}
    31  		}
    32  	}()
    33  
    34  	mem := vfs.NewMem()
    35  	var memLog base.InMemLogger
    36  	opts := &Options{
    37  		FS:                          vfs.WithLogging(mem, memLog.Infof),
    38  		FormatMajorVersion:          internalFormatNewest,
    39  		L0CompactionThreshold:       10,
    40  		DisableAutomaticCompactions: true,
    41  	}
    42  	opts.private.disableTableStats = true
    43  	opts.private.testingAlwaysWaitForCleanup = true
    44  
    45  	datadriven.RunTest(t, "testdata/checkpoint", func(t *testing.T, td *datadriven.TestData) string {
    46  		switch td.Cmd {
    47  		case "batch":
    48  			if len(td.CmdArgs) != 1 {
    49  				return "batch <db>"
    50  			}
    51  			memLog.Reset()
    52  			d := dbs[td.CmdArgs[0].String()]
    53  			b := d.NewBatch()
    54  			if err := runBatchDefineCmd(td, b); err != nil {
    55  				return err.Error()
    56  			}
    57  			if err := b.Commit(Sync); err != nil {
    58  				return err.Error()
    59  			}
    60  			return memLog.String()
    61  
    62  		case "checkpoint":
    63  			if !(len(td.CmdArgs) == 2 || (len(td.CmdArgs) == 3 && td.CmdArgs[2].Key == "restrict")) {
    64  				return "checkpoint <db> <dir> [restrict=(start-end, ...)]"
    65  			}
    66  			var opts []CheckpointOption
    67  			if len(td.CmdArgs) == 3 {
    68  				var spans []CheckpointSpan
    69  				for _, v := range td.CmdArgs[2].Vals {
    70  					splits := strings.SplitN(v, "-", 2)
    71  					if len(splits) != 2 {
    72  						return fmt.Sprintf("invalid restrict range %q", v)
    73  					}
    74  					spans = append(spans, CheckpointSpan{
    75  						Start: []byte(splits[0]),
    76  						End:   []byte(splits[1]),
    77  					})
    78  				}
    79  				opts = append(opts, WithRestrictToSpans(spans))
    80  			}
    81  			memLog.Reset()
    82  			d := dbs[td.CmdArgs[0].String()]
    83  			if err := d.Checkpoint(td.CmdArgs[1].String(), opts...); err != nil {
    84  				return err.Error()
    85  			}
    86  			return memLog.String()
    87  
    88  		case "ingest-and-excise":
    89  			d := dbs[td.CmdArgs[0].String()]
    90  
    91  			// Hacky but the command doesn't expect a db string. Get rid of it.
    92  			td.CmdArgs = td.CmdArgs[1:]
    93  			if err := runIngestAndExciseCmd(td, d, mem); err != nil {
    94  				return err.Error()
    95  			}
    96  			return ""
    97  
    98  		case "build":
    99  			d := dbs[td.CmdArgs[0].String()]
   100  
   101  			// Hacky but the command doesn't expect a db string. Get rid of it.
   102  			td.CmdArgs = td.CmdArgs[1:]
   103  			if err := runBuildCmd(td, d, mem); err != nil {
   104  				return err.Error()
   105  			}
   106  			return ""
   107  
   108  		case "lsm":
   109  			d := dbs[td.CmdArgs[0].String()]
   110  
   111  			// Hacky but the command doesn't expect a db string. Get rid of it.
   112  			td.CmdArgs = td.CmdArgs[1:]
   113  			return runLSMCmd(td, d)
   114  
   115  		case "compact":
   116  			if len(td.CmdArgs) != 1 {
   117  				return "compact <db>"
   118  			}
   119  			memLog.Reset()
   120  			d := dbs[td.CmdArgs[0].String()]
   121  			if err := d.Compact(nil, []byte("\xff"), false); err != nil {
   122  				return err.Error()
   123  			}
   124  			d.TestOnlyWaitForCleaning()
   125  			return memLog.String()
   126  
   127  		case "print-backing":
   128  			// prints contents of the file backing map in the version. Used to
   129  			// test whether the checkpoint removed the filebackings correctly.
   130  			if len(td.CmdArgs) != 1 {
   131  				return "print-backing <db>"
   132  			}
   133  			d := dbs[td.CmdArgs[0].String()]
   134  			d.mu.Lock()
   135  			d.mu.versions.logLock()
   136  			var fileNums []base.DiskFileNum
   137  			for _, b := range d.mu.versions.backingState.fileBackingMap {
   138  				fileNums = append(fileNums, b.DiskFileNum)
   139  			}
   140  			d.mu.versions.logUnlock()
   141  			d.mu.Unlock()
   142  
   143  			slices.Sort(fileNums)
   144  			var buf bytes.Buffer
   145  			for _, f := range fileNums {
   146  				buf.WriteString(fmt.Sprintf("%s\n", f.String()))
   147  			}
   148  			return buf.String()
   149  
   150  		case "close":
   151  			if len(td.CmdArgs) != 1 {
   152  				return "close <db>"
   153  			}
   154  			d := dbs[td.CmdArgs[0].String()]
   155  			require.NoError(t, d.Close())
   156  			return ""
   157  
   158  		case "flush":
   159  			if len(td.CmdArgs) != 1 {
   160  				return "flush <db>"
   161  			}
   162  			memLog.Reset()
   163  			d := dbs[td.CmdArgs[0].String()]
   164  			if err := d.Flush(); err != nil {
   165  				return err.Error()
   166  			}
   167  			return memLog.String()
   168  
   169  		case "list":
   170  			if len(td.CmdArgs) != 1 {
   171  				return "list <dir>"
   172  			}
   173  			paths, err := mem.List(td.CmdArgs[0].String())
   174  			if err != nil {
   175  				return err.Error()
   176  			}
   177  			sort.Strings(paths)
   178  			return fmt.Sprintf("%s\n", strings.Join(paths, "\n"))
   179  
   180  		case "open":
   181  			if len(td.CmdArgs) != 1 && len(td.CmdArgs) != 2 {
   182  				return "open <dir> [readonly]"
   183  			}
   184  			opts.ReadOnly = false
   185  			if len(td.CmdArgs) == 2 {
   186  				if td.CmdArgs[1].String() != "readonly" {
   187  					return "open <dir> [readonly]"
   188  				}
   189  				opts.ReadOnly = true
   190  			}
   191  
   192  			memLog.Reset()
   193  			dir := td.CmdArgs[0].String()
   194  			d, err := Open(dir, opts)
   195  			if err != nil {
   196  				return err.Error()
   197  			}
   198  			dbs[dir] = d
   199  			return memLog.String()
   200  
   201  		case "scan":
   202  			if len(td.CmdArgs) != 1 {
   203  				return "scan <db>"
   204  			}
   205  			memLog.Reset()
   206  			d := dbs[td.CmdArgs[0].String()]
   207  			iter, _ := d.NewIter(nil)
   208  			for valid := iter.First(); valid; valid = iter.Next() {
   209  				memLog.Infof("%s %s", iter.Key(), iter.Value())
   210  			}
   211  			memLog.Infof(".")
   212  			if err := iter.Close(); err != nil {
   213  				memLog.Infof("%v\n", err)
   214  			}
   215  			return memLog.String()
   216  
   217  		default:
   218  			return fmt.Sprintf("unknown command: %s", td.Cmd)
   219  		}
   220  	})
   221  }
   222  
   223  func TestCheckpointCompaction(t *testing.T) {
   224  	fs := vfs.NewMem()
   225  	d, err := Open("", &Options{FS: fs})
   226  	require.NoError(t, err)
   227  
   228  	ctx, cancel := context.WithCancel(context.Background())
   229  
   230  	var wg sync.WaitGroup
   231  	wg.Add(4)
   232  	go func() {
   233  		defer cancel()
   234  		defer wg.Done()
   235  		for i := 0; ctx.Err() == nil; i++ {
   236  			if err := d.Set([]byte(fmt.Sprintf("key%06d", i)), nil, nil); err != nil {
   237  				t.Error(err)
   238  				return
   239  			}
   240  		}
   241  	}()
   242  	go func() {
   243  		defer cancel()
   244  		defer wg.Done()
   245  		for ctx.Err() == nil {
   246  			if err := d.Compact([]byte("key"), []byte("key999999"), false); err != nil {
   247  				t.Error(err)
   248  				return
   249  			}
   250  		}
   251  	}()
   252  	check := make(chan string, 100)
   253  	go func() {
   254  		defer cancel()
   255  		defer close(check)
   256  		defer wg.Done()
   257  		for i := 0; ctx.Err() == nil && i < 200; i++ {
   258  			dir := fmt.Sprintf("checkpoint%06d", i)
   259  			if err := d.Checkpoint(dir); err != nil {
   260  				t.Error(err)
   261  				return
   262  			}
   263  			select {
   264  			case <-ctx.Done():
   265  				return
   266  			case check <- dir:
   267  			}
   268  		}
   269  	}()
   270  	go func() {
   271  		opts := &Options{FS: fs}
   272  		defer cancel()
   273  		defer wg.Done()
   274  		for dir := range check {
   275  			d2, err := Open(dir, opts)
   276  			if err != nil {
   277  				t.Error(err)
   278  				return
   279  			}
   280  			// Check the checkpoint has all the sstables that the manifest
   281  			// claims it has.
   282  			tableInfos, _ := d2.SSTables()
   283  			for _, tables := range tableInfos {
   284  				for _, tbl := range tables {
   285  					if tbl.Virtual {
   286  						continue
   287  					}
   288  					if _, err := fs.Stat(base.MakeFilepath(fs, dir, base.FileTypeTable, tbl.FileNum.DiskFileNum())); err != nil {
   289  						t.Error(err)
   290  						return
   291  					}
   292  				}
   293  			}
   294  			if err := d2.Close(); err != nil {
   295  				t.Error(err)
   296  				return
   297  			}
   298  		}
   299  	}()
   300  	<-ctx.Done()
   301  	wg.Wait()
   302  	require.NoError(t, d.Close())
   303  }
   304  
   305  func TestCheckpointFlushWAL(t *testing.T) {
   306  	const checkpointPath = "checkpoints/checkpoint"
   307  	fs := vfs.NewStrictMem()
   308  	opts := &Options{FS: fs}
   309  	key, value := []byte("key"), []byte("value")
   310  
   311  	// Create a checkpoint from an unsynced DB.
   312  	{
   313  		d, err := Open("", opts)
   314  		require.NoError(t, err)
   315  		{
   316  			wb := d.NewBatch()
   317  			err = wb.Set(key, value, nil)
   318  			require.NoError(t, err)
   319  			err = d.Apply(wb, NoSync)
   320  			require.NoError(t, err)
   321  		}
   322  		err = d.Checkpoint(checkpointPath, WithFlushedWAL())
   323  		require.NoError(t, err)
   324  		require.NoError(t, d.Close())
   325  		fs.ResetToSyncedState()
   326  	}
   327  
   328  	// Check that the WAL has been flushed in the checkpoint.
   329  	{
   330  		files, err := fs.List(checkpointPath)
   331  		require.NoError(t, err)
   332  		hasLogFile := false
   333  		for _, f := range files {
   334  			info, err := fs.Stat(fs.PathJoin(checkpointPath, f))
   335  			require.NoError(t, err)
   336  			if strings.HasSuffix(f, ".log") {
   337  				hasLogFile = true
   338  				require.NotZero(t, info.Size())
   339  			}
   340  		}
   341  		require.True(t, hasLogFile)
   342  	}
   343  
   344  	// Check that the checkpoint contains the expected data.
   345  	{
   346  		d, err := Open(checkpointPath, opts)
   347  		require.NoError(t, err)
   348  		iter, _ := d.NewIter(nil)
   349  		require.True(t, iter.First())
   350  		require.Equal(t, key, iter.Key())
   351  		require.Equal(t, value, iter.Value())
   352  		require.False(t, iter.Next())
   353  		require.NoError(t, iter.Close())
   354  		require.NoError(t, d.Close())
   355  	}
   356  }
   357  
   358  func TestCheckpointManyFiles(t *testing.T) {
   359  	if testing.Short() {
   360  		t.Skip("skipping because of short flag")
   361  	}
   362  	const checkpointPath = "checkpoint"
   363  	opts := &Options{
   364  		FS:                          vfs.NewMem(),
   365  		FormatMajorVersion:          internalFormatNewest,
   366  		DisableAutomaticCompactions: true,
   367  	}
   368  	// Disable compression to speed up the test.
   369  	opts.EnsureDefaults()
   370  	for i := range opts.Levels {
   371  		opts.Levels[i].Compression = NoCompression
   372  	}
   373  
   374  	d, err := Open("", opts)
   375  	require.NoError(t, err)
   376  	defer d.Close()
   377  
   378  	mkKey := func(x int) []byte {
   379  		return []byte(fmt.Sprintf("key%06d", x))
   380  	}
   381  	// We want to test the case where the appended record with the excluded files
   382  	// makes the manifest cross 32KB. This will happen for a range of values
   383  	// around 450.
   384  	n := 400 + rand.Intn(100)
   385  	for i := 0; i < n; i++ {
   386  		err := d.Set(mkKey(i), nil, nil)
   387  		require.NoError(t, err)
   388  		err = d.Flush()
   389  		require.NoError(t, err)
   390  	}
   391  	err = d.Checkpoint(checkpointPath, WithRestrictToSpans([]CheckpointSpan{
   392  		{
   393  			Start: mkKey(0),
   394  			End:   mkKey(10),
   395  		},
   396  	}))
   397  	require.NoError(t, err)
   398  
   399  	// Open the checkpoint and iterate through all the keys.
   400  	{
   401  		d, err := Open(checkpointPath, opts)
   402  		require.NoError(t, err)
   403  		iter, _ := d.NewIter(nil)
   404  		require.True(t, iter.First())
   405  		require.NoError(t, iter.Error())
   406  		n := 1
   407  		for iter.Next() {
   408  			n++
   409  		}
   410  		require.NoError(t, iter.Error())
   411  		require.NoError(t, iter.Close())
   412  		require.NoError(t, d.Close())
   413  		require.Equal(t, 10, n)
   414  	}
   415  }