diff --git a/internal/metamorphic/config.go b/internal/metamorphic/config.go index af42a1e035..ee9d40cd67 100644 --- a/internal/metamorphic/config.go +++ b/internal/metamorphic/config.go @@ -41,6 +41,7 @@ const ( writerMerge writerSet writerSingleDelete + pickScenario ) type config struct { @@ -93,5 +94,6 @@ var defaultConfig = config{ writerMerge: 100, writerSet: 100, writerSingleDelete: 25, + pickScenario: 100, }, } diff --git a/internal/metamorphic/generator.go b/internal/metamorphic/generator.go index 7d8cc5f863..93190c4c47 100644 --- a/internal/metamorphic/generator.go +++ b/internal/metamorphic/generator.go @@ -67,6 +67,8 @@ type generator struct { // snapshotID -> snapshot iters: used to keep track of the open iterators on // a snapshot. The iter set value will also be indexed by the readers map. snapshots map[objID]objIDSet + // Set of custom scenarios for which to generate ops for. + scenarios []*sequenceGenerator } func newGenerator(rng *rand.Rand) *generator { @@ -86,6 +88,11 @@ func newGenerator(rng *rand.Rand) *generator { g.writerToSingleSetKeys[makeObjID(dbTag, 0)] = g.singleSetKeysInDB // Note that the initOp fields are populated during generation. g.ops = append(g.ops, g.init) + + // Specific scenarios for which to generate operations for. + // TODO(travers): Add additional sequences. + g.scenarios = []*sequenceGenerator{} + return g } @@ -126,12 +133,13 @@ func generate(rng *rand.Rand, count uint64, cfg config) []op { writerMerge: g.writerMerge, writerSet: g.writerSet, writerSingleDelete: g.writerSingleDelete, + pickScenario: g.pickScenario, } // TPCC-style deck of cards randomization. Every time the end of the deck is // reached, we shuffle the deck. deck := randvar.NewDeck(g.rng, cfg.ops...) - for i := uint64(0); i < count; i++ { + for uint64(len(g.ops)) < count { generators[deck.Int()]() } @@ -862,6 +870,24 @@ func (g *generator) tryRepositionBatchIters(writerID objID) { } } +func (g *generator) pickScenario() { + // All scenarios have been exhausted. + if len(g.scenarios) == 0 { + return + } + // Pick a random scenario to generate the next operation. + idx := g.rng.Intn(len(g.scenarios)) + s := g.scenarios[idx] + op := s.next(g.rng) + if op == nil { + // The scenario ran to completion. Remove the scenario from the slice. + g.scenarios[idx] = g.scenarios[len(g.scenarios)-1] + g.scenarios = g.scenarios[:len(g.scenarios)-1] + return + } + g.add(op) +} + func (g *generator) String() string { var buf bytes.Buffer for _, op := range g.ops { diff --git a/internal/metamorphic/sequence.go b/internal/metamorphic/sequence.go new file mode 100644 index 0000000000..2f762faef2 --- /dev/null +++ b/internal/metamorphic/sequence.go @@ -0,0 +1,49 @@ +package metamorphic + +import "golang.org/x/exp/rand" + +// transitionFn is a node in the state machine graph. The node is a function +// that generates the op resulting from moving to this node and the next opType +// representing the next state in the state machine. +type transitionFn func(rng *rand.Rand) (current op, next opType) + +// transitionMap is a blueprint for generating sequences of ops. It functions as +// a graph representing a state machine. Each node in the graph is a function +// returning the current op and the next opType to transition to. +type transitionMap map[opType]transitionFn + +// stateDone is the terminal state for the state machine. +var stateDone opType = -1 + +// sequenceGenerator generates a sequence of ops from an transitionMap +// corresponding to a specific sequenceGenerator. +type sequenceGenerator struct { + tMap transitionMap + nextState opType + steps int +} + +// newSequenceGenerator constructs a new sequenceGenerator with the given +// transition map, post-ops, and the initial state of the sequence. +func newSequenceGenerator(m transitionMap, initial opType) *sequenceGenerator { + return &sequenceGenerator{ + tMap: m, + nextState: initial, + } +} + +// next generates the next ops from the transitionMap state machine, and +// transitions the transitionMap to the next state. +func (g *sequenceGenerator) next(rng *rand.Rand) op { + if g.nextState == stateDone { + return nil + } + + // Randomly pick the next state to transition into. Generate the op from + // this transition and move to the next state. + var o op + o, g.nextState = g.tMap[g.nextState](rng) + g.steps++ + + return o +} diff --git a/internal/metamorphic/sequence_test.go b/internal/metamorphic/sequence_test.go new file mode 100644 index 0000000000..7a6de4a257 --- /dev/null +++ b/internal/metamorphic/sequence_test.go @@ -0,0 +1,68 @@ +package metamorphic + +import ( + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" +) + +func TestSequenceGenerator(t *testing.T) { + n := 10 + key, value := []byte("foo"), []byte("bar") + + // A simple state machine that alternates states between GET and SET n times + // before transitioning into a terminal state. + tMap := transitionMap{ + readerGet: func(rng *rand.Rand) (current op, next opType) { + current = &getOp{key: key, readerID: makeObjID(dbTag, 0)} + n-- + if n > 0 { + next = writerSet + } else { + next = stateDone + } + return + }, + writerSet: func(rng *rand.Rand) (current op, next opType) { + current = &setOp{key: key, value: value, writerID: makeObjID(dbTag, 0)} + n-- + if n > 0 { + next = readerGet + } else { + next = stateDone + } + return + }, + } + + rng := rand.New(rand.NewSource(0)) + g := newSequenceGenerator(tMap, readerGet) + + // Generate the sequence from the state machine, counting GETs and SETs. + var nGet, nSet int + var last opType + for { + op := g.next(rng) + if op == nil { + break // Terminal state. + } + switch op.(type) { + case *getOp: + nGet++ + if last > 0 { + require.Equal(t, writerSet, last) + } + last = readerGet + case *setOp: + nSet++ + require.Equal(t, readerGet, last) + last = writerSet + default: + t.Fatalf("unknown op type: %s", op) + } + } + + require.Equal(t, nGet, 5) + require.Equal(t, nSet, 5) +}