Repository: cockroachdb/pebble (go)

Issue:
cmd/pebble parameter --rate is invalid
```
pebble # ./pebble.exe bench sync --rate 10000 data_sync
dir data_sync
concurrency 1
_elapsed____ops/sec___mb/sec__p50(ms)__p95(ms)__p99(ms)_pMax(ms)
      1s      696.2      0.2      1.0      2.0      4.1      6.0
      2s      749.6      0.2      1.0      2.0      4.1      5.2
      3s      757.1      0.3      1.0      2.0      3.0      5.2
      4s      742.3      0.2      1.0      2.0      4.1      5.2
      5s      773.4      0.3      1.0      2.0      2.1      4.1

pebble # ./pebble.exe bench sync --rate 10 data_sync
dir data_sync
concurrency 1
_elapsed____ops/sec___mb/sec__p50(ms)__p95(ms)__p99(ms)_pMax(ms)
      1s      722.0      0.2      1.0      2.0      4.1      5.2
      2s      752.8      0.3      1.0      2.0      2.2      8.1
      3s      746.1      0.2      1.0      2.0      4.1      5.2
      4s      731.9      0.2      1.0      2.0      4.1      5.2
      5s      749.3      0.2      1.0      2.0      2.2      5.2
```

Pull request:
cmd/pebble: fix --rate flag
The `maxOpsPerSec` was being initialized in a loop, making only the last
declaration (for the `ycsb` command) effective.

Fixes #509

Patch:
diff --git a/cmd/pebble/main.go b/cmd/pebble/main.go
index ecf1b1c2..819beec4 100644
--- a/cmd/pebble/main.go
+++ b/cmd/pebble/main.go
@@ -20,7 +20,7 @@ var (
 	disableWAL      bool
 	duration        time.Duration
 	engineType      string
-	maxOpsPerSec    *rateFlag
+	maxOpsPerSec    *rateFlag = newRateFlag("1000000")
 	verbose         bool
 	waitCompactions bool
 	wipe            bool
@@ -70,7 +70,6 @@ func main() {
 			&duration, "duration", "d", 10*time.Second, "the duration to run (0, run forever)")
 		cmd.Flags().StringVarP(
 			&engineType, "engine", "e", "pebble", "engine type (pebble, badger, boltdb, rocksdb)")
-		maxOpsPerSec = newRateFlag("1000000")
 		cmd.Flags().VarP(
 			maxOpsPerSec, "rate", "m", "max ops per second [{zipf,uniform}:]min[-max][/period (sec)]")
 		cmd.Flags().BoolVarP(
diff --git a/compaction.go b/compaction.go
index ce62f48c..8e905e48 100644
--- a/compaction.go
+++ b/compaction.go
@@ -1546,51 +1546,30 @@ func (d *DB) enableFileDeletions() {
 	d.deleteObsoleteFiles(jobID)
 }
 
-// d.mu must be held when calling this.
-func (d *DB) acquireCleaningTurn(waitForOngoing bool) bool {
+// deleteObsoleteFiles deletes those files that are no longer needed.
+//
+// d.mu must be held when calling this, but the mutex may be dropped and
+// re-acquired during the course of this method.
+func (d *DB) deleteObsoleteFiles(jobID int) {
 	// Only allow a single delete obsolete files job to run at a time.
-	for d.mu.cleaner.cleaning && d.mu.cleaner.disabled == 0 && waitForOngoing {
+	for d.mu.cleaner.cleaning && d.mu.cleaner.disabled == 0 {
 		d.mu.cleaner.cond.Wait()
 	}
-	if d.mu.cleaner.cleaning {
-		return false
-	}
 	if d.mu.cleaner.disabled > 0 {
 		// File deletions are currently disabled. When they are re-enabled a new
 		// job will be created to catch up on file deletions.
-		return false
-	}
-	d.mu.cleaner.cleaning = true
-	return true
-}
-
-// d.mu must be held when calling this.
-func (d *DB) releaseCleaningTurn() {
-	d.mu.cleaner.cleaning = false
-	d.mu.cleaner.cond.Signal()
-}
-
-// deleteObsoleteFiles deletes those files that are no longer needed.
-//
-// d.mu must be held when calling this, but the mutex may be dropped and
-// re-acquired during the course of this method.
-func (d *DB) deleteObsoleteFiles(jobID int) {
-	if !d.acquireCleaningTurn(true) {
 		return
 	}
-	d.doDeleteObsoleteFiles(jobID)
-	d.releaseCleaningTurn()
-}
 
-// d.mu must be held when calling this, but the mutex may be dropped and
-// re-acquired during the course of this method.
-func (d *DB) doDeleteObsoleteFiles(jobID int) {
 	var obsoleteTables []uint64
 
+	d.mu.cleaner.cleaning = true
 	defer func() {
 		for _, fileNum := range obsoleteTables {
 			delete(d.mu.versions.zombieTables, fileNum)
 		}
+		d.mu.cleaner.cleaning = false
+		d.mu.cleaner.cond.Signal()
 	}()
 
 	var obsoleteLogs []uint64
@@ -1659,10 +1638,10 @@ func (d *DB) maybeScheduleObsoleteTableDeletion() {
 	d.mu.Lock()
 	defer d.mu.Unlock()
 
-	if len(d.mu.versions.obsoleteTables) == 0 {
+	if d.mu.cleaner.disabled > 0 || d.mu.cleaner.cleaning {
 		return
 	}
-	if !d.acquireCleaningTurn(false) {
+	if len(d.mu.versions.obsoleteTables) == 0 {
 		return
 	}
 
@@ -1673,8 +1652,7 @@ func (d *DB) maybeScheduleObsoleteTableDeletion() {
 
 			jobID := d.mu.nextJobID
 			d.mu.nextJobID++
-			d.doDeleteObsoleteFiles(jobID)
-			d.releaseCleaningTurn()
+			d.deleteObsoleteFiles(jobID)
 		})
 	}()
 }
diff --git a/db.go b/db.go
index a47d7967..208e0ddb 100644
--- a/db.go
+++ b/db.go
@@ -307,9 +307,7 @@ type DB struct {
 
 		cleaner struct {
 			// Condition variable used to signal the completion of a file cleaning
-			// operation or an increment to the value of disabled. File cleaning operations are
-			// serialized, and a caller trying to do a file cleaning operation may wait
-			// until the ongoing one is complete.
+			// operation.
 			cond sync.Cond
 			// True when a file cleaning operation is in progress.
 			cleaning bool
@@ -858,12 +856,6 @@ func (d *DB) Close() error {
 			return fmt.Errorf("leaked memtable reservation: %d", reserved)
 		}
 	}
-
-	// No more cleaning can start. Wait for any async cleaning to complete.
-	for d.mu.cleaner.cleaning {
-		d.mu.cleaner.cond.Wait()
-	}
-
 	return err
 }
 
diff --git a/internal/metamorphic/config.go b/internal/metamorphic/config.go
index ddfd5f66..d3c6e8eb 100644
--- a/internal/metamorphic/config.go
+++ b/internal/metamorphic/config.go
@@ -9,8 +9,6 @@ type opType int
 const (
 	batchAbort opType = iota
 	batchCommit
-	dbClose
-	dbRestart
 	iterClose
 	iterFirst
 	iterLast
@@ -49,11 +47,9 @@ type config struct {
 }
 
 var defaultConfig = config{
-	// dbClose is not in this list since it is deterministically generated once, at the end of the test.
 	ops: []int{
 		batchAbort:        5,
 		batchCommit:       5,
-		dbRestart:         2,
 		iterClose:         10,
 		iterFirst:         100,
 		iterLast:          100,
diff --git a/internal/metamorphic/generator.go b/internal/metamorphic/generator.go
index 4525f300..5a7a0f80 100644
--- a/internal/metamorphic/generator.go
+++ b/internal/metamorphic/generator.go
@@ -31,12 +31,11 @@ type generator struct {
 	liveBatches objIDSlice
 	// liveIters contains the live iterators.
 	liveIters objIDSlice
-	// liveReaders contains the DB, and any live indexed batches and snapshots. The DB is always
-	// at index 0.
+	// liveReaders contains the DB, and any live indexed batches and snapshots.
 	liveReaders objIDSlice
 	// liveSnapshots contains the live snapshots.
 	liveSnapshots objIDSlice
-	// liveWriters contains the DB, and any live batches. The DB is always at index 0.
+	// liveWriters contains the DB, and any live batches.
 	liveWriters objIDSlice
 
 	// Maps used to find associated objects during generation. These maps are not
@@ -50,7 +49,7 @@ type generator struct {
 	// or snapshots maps.
 	iters map[objID]objIDSet
 	// readerID -> reader iters: used to keep track of the open iterators on a
-	// reader. The iter set value will also be indexed by either the batches or
+	// reader. The iter set value will also be inexed by either the batches or
 	// snapshots maps. This map is the union of batches and snapshots maps.
 	readers map[objID]objIDSet
 	// snapshotID -> snapshot iters: used to keep track of the open iterators on
@@ -80,7 +79,6 @@ func generate(count uint64, cfg config) []op {
 	generators := []func(){
 		batchAbort:        g.batchAbort,
 		batchCommit:       g.batchCommit,
-		dbRestart:         g.dbRestart,
 		iterClose:         g.iterClose,
 		iterFirst:         g.iterFirst,
 		iterLast:          g.iterLast,
@@ -111,7 +109,14 @@ func generate(count uint64, cfg config) []op {
 		generators[deck.Int()]()
 	}
 
-	g.dbClose()
+	// Close any live iterators and snapshots, so that we can close the DB
+	// cleanly.
+	for len(g.liveIters) > 0 {
+		g.iterClose()
+	}
+	for len(g.liveSnapshots) > 0 {
+		g.snapshotClose()
+	}
 	return g.ops
 }
 
@@ -231,38 +236,6 @@ func (g *generator) batchCommit() {
 	})
 }
 
-func (g *generator) dbClose() {
-	// Close any live iterators and snapshots, so that we can close the DB
-	// cleanly.
-	for len(g.liveIters) > 0 {
-		g.iterClose()
-	}
-	for len(g.liveSnapshots) > 0 {
-		g.snapshotClose()
-	}
-	g.add(&closeOp{objID: makeObjID(dbTag, 0)})
-}
-
-func (g *generator) dbRestart() {
-	// Close any live iterators and snapshots, so that we can close the DB
-	// cleanly.
-	for len(g.liveIters) > 0 {
-		g.iterClose()
-	}
-	for len(g.liveSnapshots) > 0 {
-		g.snapshotClose()
-	}
-	// Close the batches.
-	for len(g.liveBatches) > 0 {
-		g.batchClose(g.liveBatches[0])
-	}
-	if len(g.liveReaders) != 1 || len(g.liveWriters) != 1 {
-		panic(fmt.Sprintf("unexpected counts: liveReaders %d, liveWriters: %d",
-			len(g.liveReaders), len(g.liveWriters)))
-	}
-	g.add(&dbRestartOp{})
-}
-
 func (g *generator) newIter() {
 	iterID := makeObjID(iterTag, g.init.iterSlots)
 	g.init.iterSlots++
diff --git a/internal/metamorphic/ops.go b/internal/metamorphic/ops.go
index 46ed9410..9654154d 100644
--- a/internal/metamorphic/ops.go
+++ b/internal/metamorphic/ops.go
@@ -6,7 +6,6 @@ package metamorphic
 
 import (
 	"fmt"
-	"os"
 	"strings"
 
 	"github.com/cockroachdb/pebble"
@@ -49,7 +48,7 @@ type applyOp struct {
 func (o *applyOp) run(t *test, h *history) {
 	b := t.getBatch(o.batchID)
 	w := t.getWriter(o.writerID)
-	err := w.Apply(b, t.writeOpts)
+	err := w.Apply(b, pebble.NoSync)
 	h.Recordf("%s // %v", o, err)
 	_ = b.Close()
 	t.clearObj(o.batchID)
@@ -83,7 +82,7 @@ type deleteOp struct {
 
 func (o *deleteOp) run(t *test, h *history) {
 	w := t.getWriter(o.writerID)
-	err := w.Delete(o.key, t.writeOpts)
+	err := w.Delete(o.key, pebble.NoSync)
 	h.Recordf("%s // %v", o, err)
 }
 
@@ -100,7 +99,7 @@ type deleteRangeOp struct {
 
 func (o *deleteRangeOp) run(t *test, h *history) {
 	w := t.getWriter(o.writerID)
-	err := w.DeleteRange(o.start, o.end, t.writeOpts)
+	err := w.DeleteRange(o.start, o.end, pebble.NoSync)
 	h.Recordf("%s // %v", o, err)
 }
 
@@ -117,7 +116,7 @@ type mergeOp struct {
 
 func (o *mergeOp) run(t *test, h *history) {
 	w := t.getWriter(o.writerID)
-	err := w.Merge(o.key, o.value, t.writeOpts)
+	err := w.Merge(o.key, o.value, pebble.NoSync)
 	h.Recordf("%s // %v", o, err)
 }
 
@@ -134,7 +133,7 @@ type setOp struct {
 
 func (o *setOp) run(t *test, h *history) {
 	w := t.getWriter(o.writerID)
-	err := w.Set(o.key, o.value, t.writeOpts)
+	err := w.Set(o.key, o.value, pebble.NoSync)
 	h.Recordf("%s // %v", o, err)
 }
 
@@ -180,7 +179,7 @@ type batchCommitOp struct {
 func (o *batchCommitOp) run(t *test, h *history) {
 	b := t.getBatch(o.batchID)
 	t.clearObj(o.batchID)
-	err := b.Commit(t.writeOpts)
+	err := b.Commit(pebble.NoSync)
 	h.Recordf("%s // %v", o, err)
 }
 
@@ -505,23 +504,6 @@ func (o *newSnapshotOp) String() string {
 	return fmt.Sprintf("%s = db.NewSnapshot()", o.snapID)
 }
 
-type dbRestartOp struct {
-}
-
-func (o *dbRestartOp) run(t *test, h *history) {
-	if err := t.restartDB(); err != nil {
-		h.Recordf("%s // %v", o, err)
-		fmt.Fprintf(os.Stderr, "terminating since dbRestartOp returned err %s\n", err)
-		h.failed = true
-	} else {
-		h.Recordf("%s", o)
-	}
-}
-
-func (o *dbRestartOp) String() string {
-	return fmt.Sprintf("db.Restart()")
-}
-
 func formatOps(ops []op) string {
 	var buf strings.Builder
 	for _, op := range ops {
diff --git a/internal/metamorphic/options.go b/internal/metamorphic/options.go
index 06932838..f3ed3056 100644
--- a/internal/metamorphic/options.go
+++ b/internal/metamorphic/options.go
@@ -20,7 +20,7 @@ var comparer = func() pebble.Comparer {
 	return c
 }()
 
-func parseOptions(opts *testOptions, data string) error {
+func parseOptions(opts *pebble.Options, data string) error {
 	hooks := &pebble.ParseHooks{
 		NewFilterPolicy: func(name string) (pebble.FilterPolicy, error) {
 			if name == "none" {
@@ -28,31 +28,8 @@ func parseOptions(opts *testOptions, data string) error {
 			}
 			return bloom.FilterPolicy(10), nil
 		},
-		SkipUnknown: func(name string) bool {
-			switch name {
-			case "TestOptions":
-				return true
-			case "TestOptions.strictfs":
-				opts.strictFS = true
-				return true
-			default:
-				return false
-			}
-		},
-	}
-	err := opts.opts.Parse(data, hooks)
-	return err
-}
-
-func optionsToString(opts *testOptions) string {
-	str := opts.opts.String()
-	if opts.strictFS {
-		str += `
-[TestOptions]
-  strictfs=true
-`
 	}
-	return str
+	return opts.Parse(data, hooks)
 }
 
 func defaultOptions() *pebble.Options {
@@ -67,12 +44,7 @@ func defaultOptions() *pebble.Options {
 	return opts
 }
 
-type testOptions struct {
-	opts     *pebble.Options
-	strictFS bool
-}
-
-func standardOptions() []*testOptions {
+func standardOptions() []*pebble.Options {
 	// The index labels are not strictly necessary, but they make it easier to
 	// find which options correspond to a failure.
 	stdOpts := []string{
@@ -141,19 +113,12 @@ func standardOptions() []*testOptions {
 		16: `
 [Level "0"]
   filter_policy=none
-`,
-		// 1GB
-		17: `
-[Options]
-  bytes_per_sync=1073741824
-[TestOptions]
-  strictfs=true
 `,
 	}
 
-	opts := make([]*testOptions, len(stdOpts))
+	opts := make([]*pebble.Options, len(stdOpts))
 	for i := range opts {
-		opts[i] = &testOptions{opts: defaultOptions()}
+		opts[i] = defaultOptions()
 		if err := parseOptions(opts[i], stdOpts[i]); err != nil {
 			panic(err)
 		}
@@ -161,10 +126,8 @@ func standardOptions() []*testOptions {
 	return opts
 }
 
-func randomOptions(rng *rand.Rand) *testOptions {
-	var testOpts = &testOptions{}
+func randomOptions(rng *rand.Rand) *pebble.Options {
 	opts := defaultOptions()
-	opts.BytesPerSync = 1 << uint(rng.Intn(28))     // 1B - 256MB
 	opts.Cache = cache.New(1 << uint(rng.Intn(30))) // 1B - 1GB
 	opts.DisableWAL = rng.Intn(2) == 0
 	opts.L0CompactionThreshold = 1 + rng.Intn(100) // 1 - 100
@@ -186,11 +149,5 @@ func randomOptions(rng *rand.Rand) *testOptions {
 	lopts.IndexBlockSize = 1 << uint(rng.Intn(24)) // 1 - 16MB
 	lopts.TargetFileSize = 1 << uint(rng.Intn(28)) // 1 - 256MB
 	opts.Levels = []pebble.LevelOptions{lopts}
-
-	testOpts.opts = opts
-	testOpts.strictFS = rng.Intn(2) != 0
-	if testOpts.strictFS {
-		opts.DisableWAL = false
-	}
-	return testOpts
+	return opts
 }
diff --git a/internal/metamorphic/parser.go b/internal/metamorphic/parser.go
index 7214ec49..81d3d368 100644
--- a/internal/metamorphic/parser.go
+++ b/internal/metamorphic/parser.go
@@ -47,8 +47,6 @@ func opArgs(op op) (receiverID *objID, targetID *objID, args []interface{}) {
 		return &t.objID, nil, nil
 	case *batchCommitOp:
 		return &t.batchID, nil, nil
-	case *dbRestartOp:
-		return nil, nil, nil
 	case *deleteOp:
 		return &t.writerID, nil, []interface{}{&t.key}
 	case *deleteRangeOp:
@@ -93,7 +91,7 @@ func opArgs(op op) (receiverID *objID, targetID *objID, args []interface{}) {
 
 var methods = map[string]*methodInfo{
 	"Apply":           makeMethod(applyOp{}, dbTag, batchTag),
-	"Close":           makeMethod(closeOp{}, dbTag, batchTag, iterTag, snapTag),
+	"Close":           makeMethod(closeOp{}, batchTag, iterTag, snapTag),
 	"Commit":          makeMethod(batchCommitOp{}, batchTag),
 	"Delete":          makeMethod(deleteOp{}, dbTag, batchTag),
 	"DeleteRange":     makeMethod(deleteRangeOp{}, dbTag, batchTag),
@@ -109,7 +107,6 @@ var methods = map[string]*methodInfo{
 	"NewSnapshot":     makeMethod(newSnapshotOp{}, dbTag),
 	"Next":            makeMethod(iterNextOp{}, iterTag),
 	"Prev":            makeMethod(iterPrevOp{}, iterTag),
-	"Restart":         makeMethod(dbRestartOp{}, dbTag),
 	"SeekGE":          makeMethod(iterSeekGEOp{}, iterTag),
 	"SeekLT":          makeMethod(iterSeekLTOp{}, iterTag),
 	"SeekPrefixGE":    makeMethod(iterSeekPrefixGEOp{}, iterTag),
diff --git a/options.go b/options.go
index 6519ef80..9b4a72c7 100644
--- a/options.go
+++ b/options.go
@@ -787,7 +787,7 @@ func (o *Options) Parse(s string, hooks *ParseHooks) error {
 			}
 			return err
 		}
-		if hooks != nil && hooks.SkipUnknown != nil && hooks.SkipUnknown(section+"."+key) {
+		if hooks != nil && hooks.SkipUnknown != nil && hooks.SkipUnknown(section) {
 			return nil
 		}
 		return fmt.Errorf("pebble: unknown section: %q", section)


Test patch:
diff --git a/db_test.go b/db_test.go
index f1f124cf..2aa4c60d 100644
--- a/db_test.go
+++ b/db_test.go
@@ -1041,41 +1041,6 @@ func TestDBApplyBatchMismatch(t *testing.T) {
 	}
 }
 
-func TestCloseCleanerRace(t *testing.T) {
-	mem := vfs.NewMem()
-	for i := 0; i < 20; i++ {
-		db, err := Open("", &Options{FS: mem})
-		require.NoError(t, err)
-		require.NoError(t, db.Set([]byte("a"), []byte("something"), Sync))
-		require.NoError(t, db.Flush())
-		// Ref the sstables so cannot be deleted.
-		it := db.NewIter(nil)
-		require.NotNil(t, it)
-		require.NoError(t, db.DeleteRange([]byte("a"), []byte("b"), Sync))
-		require.NoError(t, db.Compact([]byte("a"), []byte("b")))
-		// Only the iterator is keeping the sstables alive.
-		files, err := mem.List("/")
-		var found bool
-		for _, f := range files {
-			if strings.HasSuffix(f, ".sst") {
-				found = true
-				break
-			}
-		}
-		require.True(t, found)
-		// Close the iterator and the db in succession so file cleaning races with DB.Close() --
-		// latter should wait for file cleaning to finish.
-		require.NoError(t, it.Close())
-		require.NoError(t, db.Close())
-		files, err = mem.List("/")
-		for _, f := range files {
-			if strings.HasSuffix(f, ".sst") {
-				t.Fatalf("found sst: %s", f)
-			}
-		}
-	}
-}
-
 func BenchmarkDelete(b *testing.B) {
 	rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano())))
 	const keyCount = 10000
diff --git a/internal/metamorphic/meta_test.go b/internal/metamorphic/meta_test.go
index 86185179..07ec9284 100644
--- a/internal/metamorphic/meta_test.go
+++ b/internal/metamorphic/meta_test.go
@@ -77,8 +77,7 @@ func testMetaRun(t *testing.T, runDir string) {
 		t.Fatal(err)
 	}
 	opts := &pebble.Options{}
-	testOpts := &testOptions{opts: opts}
-	if err := parseOptions(testOpts, string(optionsData)); err != nil {
+	if err := parseOptions(opts, string(optionsData)); err != nil {
 		t.Fatal(err)
 	}
 
@@ -89,18 +88,14 @@ func testMetaRun(t *testing.T, runDir string) {
 
 	// Set up the filesystem to use for the test. Note that by default we use an
 	// in-memory FS.
-	if *disk && !testOpts.strictFS {
+	if *disk {
 		opts.FS = vfs.Default
 		if err := os.RemoveAll(opts.FS.PathJoin(runDir, "data")); err != nil {
 			t.Fatal(err)
 		}
 	} else {
 		opts.Cleaner = base.ArchiveCleaner{}
-		if testOpts.strictFS {
-			opts.FS = vfs.NewStrictMem()
-		} else {
-			opts.FS = vfs.NewMem()
-		}
+		opts.FS = vfs.NewMem()
 	}
 	if opts.WALDir != "" {
 		opts.WALDir = opts.FS.PathJoin(runDir, opts.WALDir)
@@ -120,18 +115,17 @@ func testMetaRun(t *testing.T, runDir string) {
 	h := newHistory(*failRE, writers...)
 
 	m := newTest(ops)
-	if err := m.init(h, opts.FS.PathJoin(runDir, "data"), testOpts); err != nil {
+	if err := m.init(h, opts.FS.PathJoin(runDir, "data"), opts); err != nil {
 		t.Fatal(err)
 	}
 	for m.step(h) {
 		if h.Failed() {
-			if len(*failRE) > 0 {
-				fmt.Fprintf(os.Stderr, "failure regex %q matched\n", *failRE)
-			}
+			fmt.Fprintf(os.Stderr, "failure regex %q matched\n", *failRE)
 			m.maybeSaveData()
 			os.Exit(1)
 		}
 	}
+	m.finish(h)
 
 	if *keep && !*disk {
 		m.maybeSaveData()
@@ -186,15 +180,14 @@ func TestMeta(t *testing.T) {
 	// Perform a particular test run with the specified options. The options are
 	// written to <run-dir>/OPTIONS and a child process is created to actually
 	// execute the test.
-	runOptions := func(t *testing.T, opts *testOptions) {
+	runOptions := func(t *testing.T, opts *pebble.Options) {
 		runDir := filepath.Join(metaDir, path.Base(t.Name()))
 		if err := os.MkdirAll(runDir, 0755); err != nil {
 			t.Fatal(err)
 		}
 
 		optionsPath := filepath.Join(runDir, "OPTIONS")
-		str := optionsToString(opts)
-		if err := ioutil.WriteFile(optionsPath, []byte(str), 0644); err != nil {
+		if err := ioutil.WriteFile(optionsPath, []byte(opts.String()), 0644); err != nil {
 			t.Fatal(err)
 		}
 
diff --git a/internal/metamorphic/test.go b/internal/metamorphic/test.go
index d813cf79..a8991d06 100644
--- a/internal/metamorphic/test.go
+++ b/internal/metamorphic/test.go
@@ -20,12 +20,10 @@ type test struct {
 	ops []op
 	idx int
 	// The DB the test is run on.
-	dir       string
-	db        *pebble.DB
-	opts      *pebble.Options
-	testOpts  *testOptions
-	writeOpts *pebble.WriteOptions
-	tmpDir    string
+	dir    string
+	db     *pebble.DB
+	opts   *pebble.Options
+	tmpDir string
 	// The slots for the batches, iterators, and snapshots. These are read and
 	// written by the ops to pass state from one op to another.
 	batches   []*pebble.Batch
@@ -39,14 +37,9 @@ func newTest(ops []op) *test {
 	}
 }
 
-func (t *test) init(h *history, dir string, testOpts *testOptions) error {
+func (t *test) init(h *history, dir string, opts *pebble.Options) error {
 	t.dir = dir
-	t.testOpts = testOpts
-	t.writeOpts = pebble.NoSync
-	if testOpts.strictFS {
-		t.writeOpts = pebble.Sync
-	}
-	t.opts = testOpts.opts.EnsureDefaults()
+	t.opts = opts.EnsureDefaults()
 	t.opts.Logger = h.Logger()
 	t.opts.EventListener = pebble.MakeLoggingEventListener(t.opts.Logger)
 	t.opts.DebugCheck = true
@@ -114,54 +107,11 @@ func (t *test) init(h *history, dir string, testOpts *testOptions) error {
 	if err = t.opts.FS.MkdirAll(t.tmpDir, 0755); err != nil {
 		return err
 	}
-	if t.testOpts.strictFS {
-		// Sync the whole directory path for the tmpDir, since restartDB() is executed during
-		// the test. That would reset MemFS to the synced state, which would make an unsynced
-		// directory disappear in the middle of the test. It is the responsibility of the test
-		// (not Pebble) to ensure that it can write the ssts that it will subsequently ingest
-		// into Pebble.
-		for {
-			f, err := t.opts.FS.OpenDir(dir)
-			if err != nil {
-				return err
-			}
-			if err = f.Sync(); err != nil {
-				return err
-			}
-			if err = f.Close(); err != nil {
-				return err
-			}
-			if len(dir) == 1 {
-				break
-			}
-			dir = t.opts.FS.PathDir(dir)
-			// TODO(sbhola): PathDir returns ".", which OpenDir() complains about. Fix.
-			if len(dir) == 1 {
-				dir = "/"
-			}
-		}
-	}
 
 	t.db = db
 	return nil
 }
 
-func (t *test) restartDB() error {
-	if !t.testOpts.strictFS {
-		return nil
-	}
-	fs := t.opts.FS.(*vfs.MemFS)
-	fs.SetIgnoreSyncs(true)
-	if err := t.db.Close(); err != nil {
-		return err
-	}
-	fs.ResetToSyncedState()
-	fs.SetIgnoreSyncs(false)
-	var err error
-	t.db, err = pebble.Open(t.dir, t.opts)
-	return err
-}
-
 // If an in-memory FS is being used, save the contents to disk.
 func (t *test) maybeSaveData() {
 	if t.opts.FS == vfs.Default {
@@ -182,6 +132,12 @@ func (t *test) step(h *history) bool {
 	return true
 }
 
+func (t *test) finish(h *history) {
+	db := t.db
+	t.db = nil
+	h.Recordf("db.Close() // %v", db.Close())
+}
+
 func (t *test) setBatch(id objID, b *pebble.Batch) {
 	if id.tag() != batchTag {
 		panic(fmt.Sprintf("invalid batch ID: %s", id))
@@ -206,7 +162,7 @@ func (t *test) setSnapshot(id objID, s *pebble.Snapshot) {
 func (t *test) clearObj(id objID) {
 	switch id.tag() {
 	case dbTag:
-		t.db = nil
+		panic("cannot clear DB ID")
 	case batchTag:
 		t.batches[id.slot()] = nil
 	case iterTag:
@@ -225,8 +181,6 @@ func (t *test) getBatch(id objID) *pebble.Batch {
 
 func (t *test) getCloser(id objID) io.Closer {
 	switch id.tag() {
-	case dbTag:
-		return t.db
 	case batchTag:
 		return t.batches[id.slot()]
 	case iterTag: