Skip to content

Commit

Permalink
meta: add SET -> DELETE -> SINGLEDEL sequence generator
Browse files Browse the repository at this point in the history
Add a `sequenceGenerator` instance with a transition map that generates
the following sequence of operations, similar to the problematic
sequence generated for cockroachdb/cockroach#69414:

```
((SET -> GET)+ -> DELETE -> GET)+ -> SINGLEDEL -> (GET)+
```

See also cockroachdb/cockroach#69891.
  • Loading branch information
nicktrav committed Sep 7, 2021
1 parent 565ec92 commit bc89d40
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 1 deletion.
4 changes: 3 additions & 1 deletion internal/metamorphic/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ func newGenerator(rng *rand.Rand) *generator {

// Specific scenarios for which to generate operations for.
// TODO(travers): Add additional sequences.
g.scenarios = []*sequenceGenerator{}
g.scenarios = []*sequenceGenerator{
makeDelToSingleDelSequence(g.randValue(4, 12), func() []byte { return g.randValue(4, 12) }),
}

return g
}
Expand Down
82 changes: 82 additions & 0 deletions internal/metamorphic/scenario.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package metamorphic

import (
"github.com/cockroachdb/pebble/internal/randvar"
"golang.org/x/exp/rand"
)

// makeDelToSingleDelSequence returns a sequenceGenerator that will generate
// sequences of the form:
//
// (SET -> GET)+ -> DELETE -> GET -> SET -> GET -> SINGLEDEL -> GET+
//
// i.e. one or more SETs, followed by a DELETE, followed by a single SET. This
// SET is then removed via a SINGLEDEL. GETs are interspersed between all
// operations. The sequence completes with a fixed number of GETs, to simulate
// fetching the key after it has been single-deleted.
//
// This sequenceGenerator is regression test for cockroachdb/cockroach#69414.
func makeDelToSingleDelSequence(key []byte, valueFn func() []byte) *sequenceGenerator {
reader, writer := makeObjID(dbTag, 0), makeObjID(dbTag, 0)

var statePrev opType
var doneDel, doneSingleDel bool
var setCount, trailingGetCount int
return newSequenceGenerator(
transitionMap{
writerSet: func(rng *rand.Rand) (current op, next opType) {
setCount++
current = &setOp{writerID: writer, key: key, value: valueFn()}
statePrev = writerSet
next = readerGet
return
},
writerDelete: func(rng *rand.Rand) (current op, next opType) {
doneDel = true
current = &deleteOp{writerID: writer, key: key}
statePrev = writerDelete
next = readerGet
return
},
writerSingleDelete: func(rng *rand.Rand) (current op, next opType) {
doneSingleDel = true
current = &singleDeleteOp{writerID: writer, key: key}
statePrev = writerSingleDelete
next = readerGet
return
},
readerGet: func(rng *rand.Rand) (current op, next opType) {
switch statePrev {
case writerSet:
if doneDel && setCount > 1 {
// Transition to SINGLEDEL only when we've performed a
// DEL followed by a single SET.
next = writerSingleDelete
} else {
// Else, bias towards performing more sets.
r := randvar.NewWeighted(rng, 0.8, 0.2)
next = []opType{writerSet, writerDelete}[r.Int()]
}
case writerDelete:
// Transition back to SET after performing a DELETE.
next = writerSet
case writerSingleDelete, readerGet:
// Perform a fixed number of trailing GETs to fetch the key
// after it has been single-deleted.
if doneSingleDel && trailingGetCount == 10 {
next = stateDone
return
}
trailingGetCount++
next = readerGet
}

current = &getOp{readerID: reader, key: key}
statePrev = readerGet
return
},
},
// Start with a SET.
writerSet,
)
}
86 changes: 86 additions & 0 deletions internal/metamorphic/scenario_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package metamorphic

import (
"fmt"
"testing"
"time"

"github.com/stretchr/testify/require"
"golang.org/x/exp/rand"
)

func TestDelToSingleDel(t *testing.T) {
rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano())))
g := newGenerator(rng)

key := g.randValue(4, 12)
valueFn := func() []byte { return g.randValue(4, 12) }
s := makeDelToSingleDelSequence(key, valueFn)

// Track the positions of each operation in the sequence.
var sets, gets, dels, singleDels []int
var i int
for {
op := s.next(rng)
if op == nil {
break
}
fmt.Println(op)
switch op.(type) {
case *setOp:
sets = append(sets, i)
case *getOp:
gets = append(gets, i)
case *deleteOp:
dels = append(dels, i)
case *singleDeleteOp:
singleDels = append(singleDels, i)
}
i++
}

// All ops occurred at least once.
if sets == nil || gets == nil || dels == nil || singleDels == nil {
t.Fatalf("expected each operation to occur at least once")
}

// Exactly one SINGLEDEL.
require.Len(t, singleDels, 1)

// A single SET in between the DELETE and SINGLEDEL.
idxSingleDel := singleDels[0]
idxLastDel := dels[len(dels)-1]
cnt := countOps(sets, func(i int) bool {
return i > idxLastDel && i < idxSingleDel
})
require.Equal(t, 1, cnt)

// No SETs or DELs after the SINGLEDEL.
idxLastSet := sets[len(sets)-1]
require.Less(t, idxLastSet, idxSingleDel)
require.Less(t, idxLastDel, idxSingleDel)

// A single GET follows every SET and DEL.
cnt = countOps(gets, func(i int) bool {
return i < idxSingleDel && i%2 == 1 /* odd numbered indices */
})
require.Equal(t, cnt, len(sets)+len(dels))

// A fixed number of GETS (10) following the SINGLEDEL.
cnt = countOps(gets, func(i int) bool {
return i > idxSingleDel
})
require.Equal(t, 10, cnt)
}

// countOps returns the number of operations with indices satisfying the given
// predicate.
func countOps(is []int, predicate func(i int) bool) int {
var count int
for _, idx := range is {
if predicate(idx) {
count++
}
}
return count
}

0 comments on commit bc89d40

Please sign in to comment.