Skip to content

Commit 900855b

Browse files
committed
db: add TestDoubleRestart
Add a test that performs two restarts in quick sequence and checks the KVs are correct. Reproduces the bug in cockroachdb/cockroach#148419 We improve `RandomOptions` to allow a bit more control on what features we enable.
1 parent 7db16cb commit 900855b

File tree

7 files changed

+192
-14
lines changed

7 files changed

+192
-14
lines changed

external_test.go

Lines changed: 166 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
randv1 "math/rand"
1111
"math/rand/v2"
1212
"reflect"
13+
"slices"
1314
"strings"
1415
"testing"
1516
"testing/quick"
@@ -18,6 +19,7 @@ import (
1819
"github.com/cockroachdb/pebble"
1920
"github.com/cockroachdb/pebble/bloom"
2021
"github.com/cockroachdb/pebble/cockroachkvs"
22+
"github.com/cockroachdb/pebble/internal/base"
2123
"github.com/cockroachdb/pebble/metamorphic"
2224
"github.com/cockroachdb/pebble/sstable"
2325
"github.com/cockroachdb/pebble/vfs"
@@ -42,7 +44,7 @@ func TestIteratorErrors(t *testing.T) {
4244
// WriteOpConfig. We'll perform ~10,000 random operations that mutate the
4345
// state of the database.
4446
kf := metamorphic.TestkeysKeyFormat
45-
testOpts := metamorphic.RandomOptions(rng, kf, nil /* custom opt parsers */)
47+
testOpts := metamorphic.RandomOptions(rng, kf, metamorphic.RandomOptionsCfg{})
4648
// With even a very small injection probability, it's relatively
4749
// unlikely that pebble.DebugCheckLevels will successfully complete
4850
// without being interrupted by an ErrInjected. Omit these checks.
@@ -251,12 +253,174 @@ func buildSeparatedValuesDB(
251253
return db, keys
252254
}
253255

256+
// TestDoubleRestart checks that we are not in a precarious state immediately
257+
// after restart. This could happen if we remove WALs before all necessary
258+
// flushes are complete. Steps:
259+
//
260+
// 1. Use metamorphic to run operations on a store; the resulting filesystem is
261+
// cloned multiple times in subsequent steps.
262+
// 2. Clone the FS and start a "golden" store; read all KVs that will be used
263+
// as the source of truth.
264+
// 3. Independently clone the FS again and open a store; sleep for a small
265+
// random amount then close and reopen the store. Then read KVs and
266+
// cross-check against the "golden" KVs in step 2.
267+
// 4. Repeat step 3 multiple times, each time with a new independent clone.
268+
//
269+
// This test was used to reproduce the failure in
270+
// https://github.com/cockroachdb/cockroach/issues/148419.
271+
func TestDoubleRestart(t *testing.T) {
272+
seed := time.Now().UnixNano()
273+
t.Logf("Using seed %d", seed)
274+
rng := rand.New(rand.NewPCG(0, uint64(seed)))
275+
276+
kf := metamorphic.TestkeysKeyFormat
277+
testOpts := metamorphic.RandomOptions(rng, kf, metamorphic.RandomOptionsCfg{
278+
AlwaysStrictFS: true,
279+
NoRemoteStorage: true,
280+
})
281+
metaFS := testOpts.Opts.FS.(*vfs.MemFS)
282+
283+
var metaTestOutput bytes.Buffer
284+
{
285+
// Disable restart (it changes the FS internally and we don't have access to
286+
// the new one).
287+
opCfg := metamorphic.WriteOpConfig().WithOpWeight(metamorphic.OpDBRestart, 0)
288+
test, err := metamorphic.New(
289+
metamorphic.GenerateOps(rng, 2000, kf, opCfg),
290+
testOpts, "" /* dir */, &metaTestOutput,
291+
)
292+
require.NoError(t, err)
293+
require.NoError(t, metamorphic.Execute(test))
294+
}
295+
defer func() {
296+
if t.Failed() {
297+
t.Logf("Meta test output:\n%s", metaTestOutput.String())
298+
}
299+
}()
300+
301+
// makeOpts creates the options for a db that starts from a clone of metaFS.
302+
makeOpts := func(logger base.Logger) *pebble.Options {
303+
opts := testOpts.Opts.Clone()
304+
305+
// Make a copy of the metaFS.
306+
opts.FS = vfs.NewMem()
307+
ok, err := vfs.Clone(metaFS, opts.FS, "", "")
308+
require.NoError(t, err)
309+
require.True(t, ok)
310+
311+
opts.FS = errorfs.Wrap(opts.FS, errorfs.RandomLatency(
312+
errorfs.Randomly(0.8, rng.Int64()),
313+
10*time.Microsecond,
314+
rng.Int64(),
315+
time.Millisecond,
316+
))
317+
318+
if opts.WALFailover != nil {
319+
wf := *opts.WALFailover
320+
wf.Secondary.FS = opts.FS
321+
opts.WALFailover = &wf
322+
}
323+
opts.Logger = logger
324+
opts.LoggerAndTracer = nil
325+
lel := pebble.MakeLoggingEventListener(logger)
326+
opts.EventListener = &lel
327+
return opts
328+
}
329+
330+
// Open the "golden" filesystem and scan it to get the expected KVs.
331+
goldenLog := &base.InMemLogger{}
332+
defer func() {
333+
if t.Failed() {
334+
t.Logf("Golden db logs:\n%s\n", goldenLog.String())
335+
}
336+
}()
337+
db, err := pebble.Open("", makeOpts(goldenLog))
338+
require.NoError(t, err)
339+
goldenKeys, goldenVals := getKVs(t, db)
340+
require.NoError(t, db.Close())
341+
342+
// Repeatedly open and quickly close the database, verifying that we did not
343+
// lose any data during this restart (e.g. because we prematurely deleted
344+
// WALs).
345+
for iter := 0; iter < 10; iter++ {
346+
func() {
347+
dbLog := &base.InMemLogger{}
348+
defer func() {
349+
if t.Failed() {
350+
t.Logf("Db logs:\n%s\n", dbLog.String())
351+
}
352+
}()
353+
opts := makeOpts(dbLog)
354+
// Sometimes reduce the memtable size to trigger the large batch recovery
355+
// code path.
356+
if rng.IntN(2) == 0 {
357+
opts.MemTableSize = 1200
358+
}
359+
dbLog.Infof("Opening db\n")
360+
db, err := pebble.Open("", opts)
361+
require.NoError(t, err)
362+
if rng.IntN(2) == 0 {
363+
d := time.Duration(rng.IntN(1000)) * time.Microsecond
364+
dbLog.Infof("Sleeping %s", d)
365+
time.Sleep(d)
366+
}
367+
dbLog.Infof("Closing db")
368+
require.NoError(t, db.Close())
369+
370+
dbLog.Infof("Reopening db")
371+
db, err = pebble.Open("", opts)
372+
require.NoError(t, err)
373+
dbLog.Infof("Checking KVs")
374+
checkKVs(t, db, opts.Comparer, goldenKeys, goldenVals)
375+
dbLog.Infof("Closing db")
376+
require.NoError(t, db.Close())
377+
}()
378+
}
379+
}
380+
381+
// getKVs retrieves and returns all keys and values from the database in order.
382+
func getKVs(t *testing.T, db *pebble.DB) (keys [][]byte, vals [][]byte) {
383+
t.Helper()
384+
it, err := db.NewIter(&pebble.IterOptions{})
385+
require.NoError(t, err)
386+
for valid := it.First(); valid; valid = it.Next() {
387+
keys = append(keys, slices.Clone(it.Key()))
388+
val, err := it.ValueAndErr()
389+
require.NoError(t, err)
390+
vals = append(vals, slices.Clone(val))
391+
}
392+
require.NoError(t, it.Close())
393+
return keys, vals
394+
}
395+
396+
// checkKVs checks that the keys and values in the database match the expected ones.
397+
func checkKVs(
398+
t *testing.T, db *pebble.DB, cmp *base.Comparer, expectedKeys [][]byte, expectedVals [][]byte,
399+
) {
400+
keys, vals := getKVs(t, db)
401+
for i := 0; i < len(keys) || i < len(expectedKeys); i++ {
402+
if i < len(keys) && i < len(expectedKeys) && cmp.Equal(keys[i], expectedKeys[i]) {
403+
continue
404+
}
405+
if i < len(keys) && (i == len(expectedKeys) || cmp.Compare(keys[i], expectedKeys[i]) < 0) {
406+
t.Fatalf("extra key: %q\n", cmp.FormatKey(keys[i]))
407+
}
408+
t.Fatalf("missing key: %q\n", cmp.FormatKey(expectedKeys[i]))
409+
}
410+
for i := range vals {
411+
// require.Equalf by itself fails if one is nil and the other is a non-nil empty slice.
412+
if !bytes.Equal(vals[i], expectedVals[i]) {
413+
require.Equalf(t, expectedVals[i], vals[i], "key %q value msimatch", cmp.FormatKey(keys[i]))
414+
}
415+
}
416+
}
417+
254418
func TestOptionsClone(t *testing.T) {
255419
seed := time.Now().UnixNano()
256420
t.Logf("Using seed %d", seed)
257421
rng := rand.New(rand.NewPCG(0, uint64(seed)))
258422

259-
a := metamorphic.RandomOptions(rng, metamorphic.TestkeysKeyFormat, nil /* custom opt parsers */).Opts
423+
a := metamorphic.RandomOptions(rng, metamorphic.TestkeysKeyFormat, metamorphic.RandomOptionsCfg{}).Opts
260424
b := a.Clone()
261425
if rng.IntN(2) == 0 {
262426
a, b = b, a

internal/manifest/manifest_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func TestInuseKeyRangesRandomized(t *testing.T) {
3131
rng := rand.New(rand.NewPCG(0, seed))
3232

3333
// Generate a random database by running the metamorphic test.
34-
testOpts := metamorphic.RandomOptions(rng, metamorphic.TestkeysKeyFormat, nil /* custom opt parsers */)
34+
testOpts := metamorphic.RandomOptions(rng, metamorphic.TestkeysKeyFormat, metamorphic.RandomOptionsCfg{})
3535
{
3636
nOps := 10000
3737
if buildtags.SlowBuild {

metamorphic/example_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func ExampleExecute() {
1818

1919
kf := metamorphic.TestkeysKeyFormat
2020
// Generate a random database by running the metamorphic test.
21-
testOpts := metamorphic.RandomOptions(rng, kf, nil /* custom opt parsers */)
21+
testOpts := metamorphic.RandomOptions(rng, kf, metamorphic.RandomOptionsCfg{})
2222
ops := metamorphic.GenerateOps(rng, 10000, kf, metamorphic.DefaultOpConfig())
2323
test, err := metamorphic.New(ops, testOpts, "" /* dir */, io.Discard)
2424
if err != nil {

metamorphic/meta.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,9 @@ func buildOptions(
381381
for i := 0; i < nOpts; i++ {
382382
name := fmt.Sprintf("random-%03d", i)
383383
names = append(names, name)
384-
opts := RandomOptions(rng, runOpts.keyFormat, runOpts.customOptionParsers)
384+
opts := RandomOptions(rng, runOpts.keyFormat, RandomOptionsCfg{
385+
CustomOptionParsers: runOpts.customOptionParsers,
386+
})
385387
options[name] = opts
386388
}
387389

metamorphic/options.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -640,11 +640,18 @@ enabled = false
640640
return opts
641641
}
642642

643+
// RandomOptionsCfg contains knobs that can tweak RandomOptions.
644+
// The zero value is the default.
645+
type RandomOptionsCfg struct {
646+
CustomOptionParsers map[string]func(string) (CustomOption, bool)
647+
AlwaysStrictFS bool
648+
NoRemoteStorage bool
649+
NoWALFailover bool
650+
}
651+
643652
// RandomOptions generates a random set of operations, drawing randomness from
644653
// rng.
645-
func RandomOptions(
646-
rng *rand.Rand, kf KeyFormat, customOptionParsers map[string]func(string) (CustomOption, bool),
647-
) *TestOptions {
654+
func RandomOptions(rng *rand.Rand, kf KeyFormat, cfg RandomOptionsCfg) *TestOptions {
648655
testOpts := defaultTestOptions(kf)
649656
opts := testOpts.Opts
650657

@@ -664,7 +671,7 @@ func RandomOptions(
664671
fmt.Fprintln(&privateOpts, ` disable_lazy_combined_iteration=true`)
665672
}
666673
if privateOptsStr := privateOpts.String(); privateOptsStr != `[Options]\n` {
667-
if err := parseOptions(testOpts, privateOptsStr, customOptionParsers); err != nil {
674+
if err := parseOptions(testOpts, privateOptsStr, cfg.CustomOptionParsers); err != nil {
668675
panic(err)
669676
}
670677
}
@@ -715,7 +722,7 @@ func RandomOptions(
715722
}
716723

717724
// Half the time enable WAL failover.
718-
if rng.IntN(2) == 0 {
725+
if !cfg.NoWALFailover && rng.IntN(2) == 0 {
719726
// Use 10x longer durations when writing directly to FS; we don't want
720727
// WAL failover to trigger excessively frequently.
721728
referenceDur := time.Millisecond
@@ -830,7 +837,7 @@ func RandomOptions(
830837
// single standard test configuration that uses a disk-backed FS is
831838
// sufficient.
832839
testOpts.useDisk = false
833-
testOpts.strictFS = rng.IntN(2) != 0 // Only relevant for MemFS.
840+
testOpts.strictFS = cfg.AlwaysStrictFS || rng.IntN(2) != 0 // Only relevant for MemFS.
834841
// 50% of the time, enable IO latency injection.
835842
if rng.IntN(2) == 0 {
836843
// Note: we want ioLatencyProbability to be at least 1e-10, otherwise it
@@ -867,7 +874,7 @@ func RandomOptions(
867874
testOpts.disableValueBlocksForIngestSSTables = rng.IntN(2) == 0
868875
testOpts.asyncApplyToDB = rng.IntN(2) != 0
869876
// 20% of the time, enable shared storage.
870-
if rng.IntN(5) == 0 {
877+
if !cfg.NoRemoteStorage && rng.IntN(5) == 0 {
871878
testOpts.sharedStorageEnabled = true
872879
if testOpts.Opts.FormatMajorVersion < pebble.FormatMinForSharedObjects {
873880
testOpts.Opts.FormatMajorVersion = pebble.FormatMinForSharedObjects
@@ -889,7 +896,7 @@ func RandomOptions(
889896
}
890897

891898
// 50% of the time, enable external storage.
892-
if rng.IntN(2) == 0 {
899+
if !cfg.NoRemoteStorage && rng.IntN(2) == 0 {
893900
testOpts.externalStorageEnabled = true
894901
if testOpts.Opts.FormatMajorVersion < pebble.FormatSyntheticPrefixSuffix {
895902
testOpts.Opts.FormatMajorVersion = pebble.FormatSyntheticPrefixSuffix

metamorphic/options_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ func TestOptionsRoundtrip(t *testing.T) {
150150
rng := rand.New(rand.NewPCG(0, uint64(time.Now().UnixNano())))
151151
for i := 0; i < 100; i++ {
152152
t.Run(fmt.Sprintf("random-%03d", i), func(t *testing.T) {
153-
o := RandomOptions(rng, TestkeysKeyFormat, nil)
153+
o := RandomOptions(rng, TestkeysKeyFormat, RandomOptionsCfg{})
154154
checkOptions(t, o)
155155
})
156156
}

open.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"github.com/cockroachdb/pebble/record"
3535
"github.com/cockroachdb/pebble/vfs"
3636
"github.com/cockroachdb/pebble/wal"
37+
"github.com/cockroachdb/redact"
3738
"github.com/prometheus/client_golang/prometheus"
3839
)
3940

@@ -378,6 +379,10 @@ func Open(dirname string, opts *Options) (db *DB, err error) {
378379
if err != nil {
379380
return nil, err
380381
}
382+
d.opts.Logger.Infof("Found %d WALs", redact.Safe(len(wals)))
383+
for i := range wals {
384+
d.opts.Logger.Infof(" - %s", wals[i])
385+
}
381386
walManager, err := wal.Init(walOpts, wals)
382387
if err != nil {
383388
return nil, err

0 commit comments

Comments
 (0)