Repository: cockroachdb/pebble
Pull request #1253 (2021-09-07 20:10:46)

sstable: Free cache value when checksum type is corrupt
In all the instances in `Reader.readBlock` where we return
an error, we free the cache handle before returning the error.
One exception to this was the switch for the checksum type,
so this change adds that Free there as well.

cockroach-teamcity: 

<!-- Reviewable:start -->
This change is [<img src="https://reviewable.io/review_button.png" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/cockroachdb/pebble/1253)
<!-- Reviewable:end -->


Patch:
diff --git a/internal/metamorphic/generator.go b/internal/metamorphic/generator.go
index 7d8cc5f8..8e1e6eb0 100644
--- a/internal/metamorphic/generator.go
+++ b/internal/metamorphic/generator.go
@@ -843,6 +843,12 @@ func (g *generator) writerSingleDelete() {
 	g.add(&singleDeleteOp{
 		writerID: writerID,
 		key:      key,
+		// Keys eligible for single deletes can be removed with a regular
+		// delete. Mutate a percentage of SINGLEDEL ops into DELETEs. Note that
+		// here we are only determining whether the replacement *could* happen.
+		// At test runtime, the `replaceSingleDelete` test option must also be
+		// set to true for the single delete to be replaced.
+		maybeReplaceDelete: g.rng.Float64() < 0.25,
 	})
 	g.tryRepositionBatchIters(writerID)
 }
diff --git a/internal/metamorphic/ops.go b/internal/metamorphic/ops.go
index 41aec405..a413ee84 100644
--- a/internal/metamorphic/ops.go
+++ b/internal/metamorphic/ops.go
@@ -132,18 +132,29 @@ func (o *deleteOp) String() string {
 
 // singleDeleteOp models a Write.SingleDelete operation.
 type singleDeleteOp struct {
-	writerID objID
-	key      []byte
+	writerID           objID
+	key                []byte
+	maybeReplaceDelete bool
 }
 
 func (o *singleDeleteOp) run(t *test, h *history) {
 	w := t.getWriter(o.writerID)
-	err := w.SingleDelete(o.key, t.writeOpts)
+	var err error
+	if t.testOpts.replaceSingleDelete && o.maybeReplaceDelete {
+		err = w.Delete(o.key, t.writeOpts)
+	} else {
+		err = w.SingleDelete(o.key, t.writeOpts)
+	}
+	// NOTE: even if the SINGLEDEL was replaced with a DELETE, we must still
+	// write the former to the history log. The log line will indicate whether
+	// or not the delete *could* have been replaced. The OPTIONS file should
+	// also be consulted to determine what happened at runtime (i.e. by taking
+	// the logical AND).
 	h.Recordf("%s // %v", o, err)
 }
 
 func (o *singleDeleteOp) String() string {
-	return fmt.Sprintf("%s.SingleDelete(%q)", o.writerID, o.key)
+	return fmt.Sprintf("%s.SingleDelete(%q, %v /* maybeReplaceDelete */)", o.writerID, o.key, o.maybeReplaceDelete)
 }
 
 // deleteRangeOp models a Write.DeleteRange operation.
diff --git a/internal/metamorphic/options.go b/internal/metamorphic/options.go
index 3bec7bb5..4eca1994 100644
--- a/internal/metamorphic/options.go
+++ b/internal/metamorphic/options.go
@@ -8,7 +8,7 @@ import (
 	"github.com/cockroachdb/pebble"
 	"github.com/cockroachdb/pebble/bloom"
 	"github.com/cockroachdb/pebble/internal/cache"
-	"github.com/cockroachdb/pebble/vfs"
+	"github.com/cockroachdb/pebble/internal/randvar"
 	"golang.org/x/exp/rand"
 )
 
@@ -39,6 +39,12 @@ func parseOptions(opts *testOptions, data string) error {
 			case "TestOptions.ingest_using_apply":
 				opts.ingestUsingApply = true
 				return true
+			case "TestOptions.replace_single_delete":
+				opts.replaceSingleDelete = true
+				return true
+			case "TestOptions.use_disk":
+				opts.useDisk = true
+				return true
 			default:
 				return false
 			}
@@ -50,7 +56,7 @@ func parseOptions(opts *testOptions, data string) error {
 
 func optionsToString(opts *testOptions) string {
 	str := opts.opts.String()
-	if opts.strictFS || opts.ingestUsingApply {
+	if opts.strictFS || opts.ingestUsingApply || opts.replaceSingleDelete || opts.useDisk {
 		str += "\n[TestOptions]\n"
 	}
 	if opts.strictFS {
@@ -59,13 +65,18 @@ func optionsToString(opts *testOptions) string {
 	if opts.ingestUsingApply {
 		str += "  ingest_using_apply=true\n"
 	}
+	if opts.replaceSingleDelete {
+		str += "  replace_single_delete=true\n"
+	}
+	if opts.useDisk {
+		str += "  use_disk=true\n"
+	}
 	return str
 }
 
 func defaultOptions() *pebble.Options {
 	opts := &pebble.Options{
 		Comparer: &comparer,
-		FS:       vfs.NewMem(),
 		Levels: []pebble.LevelOptions{{
 			FilterPolicy: bloom.FilterPolicy(10),
 		}},
@@ -76,9 +87,12 @@ func defaultOptions() *pebble.Options {
 
 type testOptions struct {
 	opts     *pebble.Options
+	useDisk  bool
 	strictFS bool
 	// Use Batch.Apply rather than DB.Ingest.
 	ingestUsingApply bool
+	// Replace a SINGLEDEL with a DELETE.
+	replaceSingleDelete bool
 }
 
 func standardOptions() []*testOptions {
@@ -165,6 +179,14 @@ func standardOptions() []*testOptions {
 		19: `
 [TestOptions]
   ingest_using_apply=true
+`,
+		20: `
+[TestOptions]
+  replace_single_delete=true
+`,
+		21: `
+[TestOptions]
+  use_disk=true
 `,
 	}
 
@@ -209,10 +231,14 @@ func randomOptions(rng *rand.Rand) *testOptions {
 	opts.Levels = []pebble.LevelOptions{lopts}
 
 	testOpts.opts = opts
-	testOpts.strictFS = rng.Intn(2) != 0
+	testOpts.useDisk = randvar.NewWeighted(rng, 0.9, 0.1).Int() == 1 // Use disk 10% of the time.
+	if !testOpts.useDisk {
+		testOpts.strictFS = rng.Intn(2) != 0 // Only relevant for MemFS.
+	}
 	if testOpts.strictFS {
 		opts.DisableWAL = false
 	}
 	testOpts.ingestUsingApply = rng.Intn(2) != 0
+	testOpts.replaceSingleDelete = rng.Intn(2) != 0
 	return testOpts
 }
diff --git a/internal/metamorphic/parser.go b/internal/metamorphic/parser.go
index 6fd65347..b811810a 100644
--- a/internal/metamorphic/parser.go
+++ b/internal/metamorphic/parser.go
@@ -97,7 +97,7 @@ func opArgs(op op) (receiverID *objID, targetID *objID, args []interface{}) {
 	case *iterSetBoundsOp:
 		return &t.iterID, nil, []interface{}{&t.lower, &t.upper}
 	case *singleDeleteOp:
-		return &t.writerID, nil, []interface{}{&t.key}
+		return &t.writerID, nil, []interface{}{&t.key, &t.maybeReplaceDelete}
 	}
 	panic(fmt.Sprintf("unsupported op type: %T", op))
 }
@@ -262,6 +262,14 @@ func (p *parser) parseArgs(op op, methodName string, args []interface{}) {
 				*t = []byte(s)
 			}
 
+		case *bool:
+			_, lit := p.scanToken(token.IDENT)
+			b, err := strconv.ParseBool(lit)
+			if err != nil {
+				panic(err)
+			}
+			*t = b
+
 		case *objID:
 			pos, lit := p.scanToken(token.IDENT)
 			*t = p.parseObjID(pos, lit)
diff --git a/sstable/reader.go b/sstable/reader.go
index 5c1a56bc..fb260cc3 100644
--- a/sstable/reader.go
+++ b/sstable/reader.go
@@ -1922,6 +1922,7 @@ func (r *Reader) readBlock(
 	case ChecksumTypeXXHash64:
 		computedChecksum = uint32(xxhash.Sum64(b[:bh.Length+1]))
 	default:
+		r.opts.Cache.Free(v)
 		return cache.Handle{}, errors.Errorf("unsupported checksum type: %d", r.checksumType)
 	}
 
diff --git a/vfs/atomicfs/marker.go b/vfs/atomicfs/marker.go
index 99bf6024..22bbd554 100644
--- a/vfs/atomicfs/marker.go
+++ b/vfs/atomicfs/marker.go
@@ -2,7 +2,7 @@
 // of this source code is governed by a BSD-style license that can be found in
 // the LICENSE file.
 
-package vfs
+package atomicfs
 
 import (
 	"fmt"
@@ -195,6 +195,13 @@ func (a *Marker) Move(filename string) error {
 	return nil
 }
 
+// NextIter returns the next iteration number that the marker will use.
+// Clients may use this number for formulating filenames that are
+// unused.
+func (a *Marker) NextIter() uint64 {
+	return a.iter + 1
+}
+
 // RemoveObsolete removes any obsolete files discovered while locating
 // the marker or files unable to be removed during Move.
 func (a *Marker) RemoveObsolete() error {


Test patch:
diff --git a/compaction_test.go b/compaction_test.go
index 027d1fb3..697b92b2 100644
--- a/compaction_test.go
+++ b/compaction_test.go
@@ -1135,167 +1135,170 @@ func TestManualCompaction(t *testing.T) {
 
 	var ongoingCompaction *compaction
 
-	datadriven.RunTest(t, "testdata/manual_compaction", func(td *datadriven.TestData) string {
-		switch td.Cmd {
-		case "reset":
-			reset()
-			return ""
-
-		case "batch":
-			b := d.NewIndexedBatch()
-			if err := runBatchDefineCmd(td, b); err != nil {
-				return err.Error()
-			}
-			require.NoError(t, b.Commit(nil))
-			return ""
+	paths := []string{"testdata/manual_compaction", "testdata/singledel_manual_compaction"}
+	for _, path := range paths {
+		datadriven.RunTest(t, path, func(td *datadriven.TestData) string {
+			switch td.Cmd {
+			case "reset":
+				reset()
+				return ""
 
-		case "build":
-			if err := runBuildCmd(td, d, mem); err != nil {
-				return err.Error()
-			}
-			return ""
+			case "batch":
+				b := d.NewIndexedBatch()
+				if err := runBatchDefineCmd(td, b); err != nil {
+					return err.Error()
+				}
+				require.NoError(t, b.Commit(nil))
+				return ""
 
-		case "compact":
-			if err := runCompactCmd(td, d); err != nil {
-				return err.Error()
-			}
-			return runLSMCmd(td, d)
+			case "build":
+				if err := runBuildCmd(td, d, mem); err != nil {
+					return err.Error()
+				}
+				return ""
 
-		case "define":
-			if d != nil {
-				if err := d.Close(); err != nil {
+			case "compact":
+				if err := runCompactCmd(td, d); err != nil {
 					return err.Error()
 				}
-			}
+				return runLSMCmd(td, d)
 
-			mem = vfs.NewMem()
-			opts := &Options{
-				FS:         mem,
-				DebugCheck: DebugCheckLevels,
-			}
-			opts.private.disableAutomaticCompactions = true
+			case "define":
+				if d != nil {
+					if err := d.Close(); err != nil {
+						return err.Error()
+					}
+				}
 
-			var err error
-			if d, err = runDBDefineCmd(td, opts); err != nil {
-				return err.Error()
-			}
+				mem = vfs.NewMem()
+				opts := &Options{
+					FS:         mem,
+					DebugCheck: DebugCheckLevels,
+				}
+				opts.private.disableAutomaticCompactions = true
 
-			d.mu.Lock()
-			s := d.mu.versions.currentVersion().String()
-			d.mu.Unlock()
-			return s
+				var err error
+				if d, err = runDBDefineCmd(td, opts); err != nil {
+					return err.Error()
+				}
 
-		case "ingest":
-			if err := runIngestCmd(td, d, mem); err != nil {
-				return err.Error()
-			}
-			return runLSMCmd(td, d)
-
-		case "iter":
-			// TODO(peter): runDBDefineCmd doesn't properly update the visible
-			// sequence number. So we have to use a snapshot with a very large
-			// sequence number, otherwise the DB appears empty.
-			snap := Snapshot{
-				db:     d,
-				seqNum: InternalKeySeqNumMax,
-			}
-			iter := snap.NewIter(nil)
-			return runIterCmd(td, iter, true)
+				d.mu.Lock()
+				s := d.mu.versions.currentVersion().String()
+				d.mu.Unlock()
+				return s
 
-		case "async-compact":
-			var s string
-			ch := make(chan error, 1)
-			go func() {
-				if err := runCompactCmd(td, d); err != nil {
-					ch <- err
+			case "ingest":
+				if err := runIngestCmd(td, d, mem); err != nil {
+					return err.Error()
+				}
+				return runLSMCmd(td, d)
+
+			case "iter":
+				// TODO(peter): runDBDefineCmd doesn't properly update the visible
+				// sequence number. So we have to use a snapshot with a very large
+				// sequence number, otherwise the DB appears empty.
+				snap := Snapshot{
+					db:     d,
+					seqNum: InternalKeySeqNumMax,
+				}
+				iter := snap.NewIter(nil)
+				return runIterCmd(td, iter, true)
+
+			case "async-compact":
+				var s string
+				ch := make(chan error, 1)
+				go func() {
+					if err := runCompactCmd(td, d); err != nil {
+						ch <- err
+						close(ch)
+						return
+					}
+					d.mu.Lock()
+					s = d.mu.versions.currentVersion().DebugString(base.DefaultFormatter)
+					d.mu.Unlock()
 					close(ch)
-					return
+				}()
+
+				manualDone := func() bool {
+					select {
+					case <-ch:
+						return true
+					default:
+						return false
+					}
 				}
-				d.mu.Lock()
-				s = d.mu.versions.currentVersion().DebugString(base.DefaultFormatter)
-				d.mu.Unlock()
-				close(ch)
-			}()
 
-			manualDone := func() bool {
-				select {
-				case <-ch:
-					return true
-				default:
-					return false
+				err := try(100*time.Microsecond, 20*time.Second, func() error {
+					if manualDone() {
+						return nil
+					}
+
+					d.mu.Lock()
+					defer d.mu.Unlock()
+					if len(d.mu.compact.manual) == 0 {
+						return errors.New("no manual compaction queued")
+					}
+					manual := d.mu.compact.manual[0]
+					if manual.retries == 0 {
+						return errors.New("manual compaction has not been retried")
+					}
+					return nil
+				})
+				if err != nil {
+					return err.Error()
 				}
-			}
 
-			err := try(100*time.Microsecond, 20*time.Second, func() error {
 				if manualDone() {
-					return nil
+					return "manual compaction did not block for ongoing\n" + s
 				}
 
 				d.mu.Lock()
-				defer d.mu.Unlock()
-				if len(d.mu.compact.manual) == 0 {
-					return errors.New("no manual compaction queued")
-				}
-				manual := d.mu.compact.manual[0]
-				if manual.retries == 0 {
-					return errors.New("manual compaction has not been retried")
+				delete(d.mu.compact.inProgress, ongoingCompaction)
+				d.mu.compact.compactingCount--
+				ongoingCompaction = nil
+				d.maybeScheduleCompaction()
+				d.mu.Unlock()
+				if err := <-ch; err != nil {
+					return err.Error()
 				}
-				return nil
-			})
-			if err != nil {
-				return err.Error()
-			}
-
-			if manualDone() {
-				return "manual compaction did not block for ongoing\n" + s
-			}
+				return "manual compaction blocked until ongoing finished\n" + s
 
-			d.mu.Lock()
-			delete(d.mu.compact.inProgress, ongoingCompaction)
-			d.mu.compact.compactingCount--
-			ongoingCompaction = nil
-			d.maybeScheduleCompaction()
-			d.mu.Unlock()
-			if err := <-ch; err != nil {
-				return err.Error()
-			}
-			return "manual compaction blocked until ongoing finished\n" + s
-
-		case "add-ongoing-compaction":
-			var startLevel int
-			var outputLevel int
-			td.ScanArgs(t, "startLevel", &startLevel)
-			td.ScanArgs(t, "outputLevel", &outputLevel)
-			ongoingCompaction = &compaction{
-				inputs: []compactionLevel{{level: startLevel}, {level: outputLevel}},
-			}
-			ongoingCompaction.startLevel = &ongoingCompaction.inputs[0]
-			ongoingCompaction.outputLevel = &ongoingCompaction.inputs[1]
-			d.mu.Lock()
-			d.mu.compact.inProgress[ongoingCompaction] = struct{}{}
-			d.mu.compact.compactingCount++
-			d.mu.Unlock()
-			return ""
+			case "add-ongoing-compaction":
+				var startLevel int
+				var outputLevel int
+				td.ScanArgs(t, "startLevel", &startLevel)
+				td.ScanArgs(t, "outputLevel", &outputLevel)
+				ongoingCompaction = &compaction{
+					inputs: []compactionLevel{{level: startLevel}, {level: outputLevel}},
+				}
+				ongoingCompaction.startLevel = &ongoingCompaction.inputs[0]
+				ongoingCompaction.outputLevel = &ongoingCompaction.inputs[1]
+				d.mu.Lock()
+				d.mu.compact.inProgress[ongoingCompaction] = struct{}{}
+				d.mu.compact.compactingCount++
+				d.mu.Unlock()
+				return ""
 
-		case "remove-ongoing-compaction":
-			d.mu.Lock()
-			delete(d.mu.compact.inProgress, ongoingCompaction)
-			d.mu.compact.compactingCount--
-			ongoingCompaction = nil
-			d.mu.Unlock()
-			return ""
+			case "remove-ongoing-compaction":
+				d.mu.Lock()
+				delete(d.mu.compact.inProgress, ongoingCompaction)
+				d.mu.compact.compactingCount--
+				ongoingCompaction = nil
+				d.mu.Unlock()
+				return ""
 
-		case "set-concurrent-compactions":
-			td.ScanArgs(t, "num", &d.opts.MaxConcurrentCompactions)
-			return ""
+			case "set-concurrent-compactions":
+				td.ScanArgs(t, "num", &d.opts.MaxConcurrentCompactions)
+				return ""
 
-		case "wait-pending-table-stats":
-			return runTableStatsCmd(td, d)
+			case "wait-pending-table-stats":
+				return runTableStatsCmd(td, d)
 
-		default:
-			return fmt.Sprintf("unknown command: %s", td.Cmd)
-		}
-	})
+			default:
+				return fmt.Sprintf("unknown command: %s", td.Cmd)
+			}
+		})
+	}
 }
 
 func TestCompactionFindGrandparentLimit(t *testing.T) {
diff --git a/internal/metamorphic/meta_test.go b/internal/metamorphic/meta_test.go
index 45ba3812..e66984de 100644
--- a/internal/metamorphic/meta_test.go
+++ b/internal/metamorphic/meta_test.go
@@ -40,8 +40,8 @@ import (
 var (
 	dir = flag.String("dir", "_meta",
 		"the directory storing test state")
-	disk = flag.Bool("disk", false,
-		"whether to use an in-mem DB or on-disk (in-mem is significantly faster)")
+	fs = flag.String("fs", "rand",
+		`force the tests to use either memory or disk-backed filesystems (valid: "mem", "disk", "rand")`)
 	// TODO: default error rate to a non-zero value. Currently, retrying is
 	// non-deterministic because of the Ierator.*WithLimit() methods since
 	// they may say that the Iterator is not valid, but be positioned at a
@@ -93,7 +93,7 @@ func testMetaRun(t *testing.T, runDir string, seed uint64) {
 
 	// Set up the filesystem to use for the test. Note that by default we use an
 	// in-memory FS.
-	if *disk && !testOpts.strictFS {
+	if testOpts.useDisk {
 		opts.FS = vfs.Default
 		require.NoError(t, os.RemoveAll(opts.FS.PathJoin(runDir, "data")))
 	} else {
@@ -134,7 +134,7 @@ func testMetaRun(t *testing.T, runDir string, seed uint64) {
 		}
 	}
 
-	if *keep && !*disk {
+	if *keep && !testOpts.useDisk {
 		m.maybeSaveData()
 	}
 }
@@ -211,12 +211,23 @@ func TestMeta(t *testing.T) {
 		runDir := filepath.Join(metaDir, path.Base(t.Name()))
 		require.NoError(t, os.MkdirAll(runDir, 0755))
 
+		// If the filesystem type was forced, all tests will use that value.
+		switch *fs {
+		case "rand":
+			// No-op. Use the generated value for the filesystem.
+		case "disk":
+			opts.useDisk = true
+		case "mem":
+			opts.useDisk = false
+		default:
+			t.Fatalf("unknown forced filesystem type: %q", *fs)
+		}
+
 		optionsPath := filepath.Join(runDir, "OPTIONS")
 		optionsStr := optionsToString(opts)
 		require.NoError(t, ioutil.WriteFile(optionsPath, []byte(optionsStr), 0644))
 
 		args := []string{
-			"-disk=" + fmt.Sprint(*disk),
 			"-keep=" + fmt.Sprint(*keep),
 			"-run-dir=" + runDir,
 			"-test.run=" + rootName + "$",
@@ -256,8 +267,10 @@ func TestMeta(t *testing.T) {
 		})
 	}
 
-	// Perform runs with random options.
-	for i := 0; i < 20; i++ {
+	// Perform runs with random options. We make an arbitrary choice to run with
+	// as many random options as we have standard options.
+	nOpts := len(options)
+	for i := 0; i < nOpts; i++ {
 		name := fmt.Sprintf("random-%03d", i)
 		names = append(names, name)
 		opts := randomOptions(rng)
diff --git a/testdata/singledel_manual_compaction b/testdata/singledel_manual_compaction
new file mode 100644
index 00000000..ff83e376
--- /dev/null
+++ b/testdata/singledel_manual_compaction
@@ -0,0 +1,77 @@
+# This is not actually a manual compaction test, and simply uses manual
+# compaction to demonstrate single delete semantics. Specifically, it
+# demonstrates that the behavior can be non-deterministic if not used
+# correctly.
+
+# Define a sequence of SET=>SET=>DEL=>SET=>SINGLEDEL.
+define target-file-sizes=(1, 1, 1, 1, 1)
+L1
+  a.SINGLEDEL.10:
+L2
+  a.SET.9:v3
+L3
+  a.DEL.8:
+L4
+  a.SET.7:v2
+L5
+  a.SET.6:v1
+----
+1:
+  000004:[a-a]
+2:
+  000005:[a-a]
+3:
+  000006:[a-a]
+4:
+  000007:[a-a]
+5:
+  000008:[a-a]
+
+# No data.
+iter
+first
+----
+.
+
+# Compact away the DEL.
+compact a-b L2
+----
+1:
+  000004:[a#10,SINGLEDEL-a#10,SINGLEDEL]
+3:
+  000009:[a#9,SET-a#9,SET]
+4:
+  000007:[a#7,SET-a#7,SET]
+5:
+  000008:[a#6,SET-a#6,SET]
+
+# No data.
+iter
+first
+----
+.
+
+# Do two compactions to compact away the SINGLEDEL and 1 SET.
+compact a-b L1
+----
+2:
+  000010:[a#10,SINGLEDEL-a#10,SINGLEDEL]
+3:
+  000009:[a#9,SET-a#9,SET]
+4:
+  000007:[a#7,SET-a#7,SET]
+5:
+  000008:[a#6,SET-a#6,SET]
+
+compact a-b L2
+----
+4:
+  000007:[a#7,SET-a#7,SET]
+5:
+  000008:[a#6,SET-a#6,SET]
+
+# Deleted data reappears.
+iter
+first
+----
+a:v2
diff --git a/vfs/atomicfs/marker_test.go b/vfs/atomicfs/marker_test.go
index 37d03f02..5668d253 100644
--- a/vfs/atomicfs/marker_test.go
+++ b/vfs/atomicfs/marker_test.go
@@ -2,7 +2,7 @@
 // of this source code is governed by a BSD-style license that can be found in
 // the LICENSE file.
 
-package vfs
+package atomicfs
 
 import (
 	"bytes"
@@ -117,6 +117,14 @@ func TestMarker(t *testing.T) {
 			}
 			return ""
 
+		case "next-iter":
+			var dir, marker string
+			td.ScanArgs(t, "dir", &dir)
+			td.ScanArgs(t, "marker", &marker)
+			m := markers[memFS.PathJoin(dir, marker)]
+			require.NotNil(t, m)
+			return fmt.Sprintf("%d", m.NextIter())
+
 		case "remove-obsolete":
 			var dir, marker string
 			td.ScanArgs(t, "dir", &dir)
diff --git a/vfs/atomicfs/testdata/marker b/vfs/atomicfs/testdata/marker
index ffb7a7a7..2450bd24 100644
--- a/vfs/atomicfs/testdata/marker
+++ b/vfs/atomicfs/testdata/marker
@@ -10,6 +10,14 @@ mkdir-all data
 locate dir=data marker=foo
 ----
 
+next-iter  dir=data marker=foo
+----
+1
+
+next-iter  dir=data marker=foo
+----
+1
+
 # The directory should still be empty.
 list data
 ----
@@ -23,11 +31,19 @@ list data
 ----
 marker.foo.000001.MANIFEST-000010
 
+next-iter  dir=data marker=foo
+----
+2
+
 # Moving the marker should move the existing marker file.
 move dir=data marker=foo
 MANIFEST-000016
 ----
 
+next-iter  dir=data marker=foo
+----
+3
+
 list data
 ----
 marker.foo.000002.MANIFEST-000016