ceremonyclient/pebble/metamorphic/generator.go

1589 lines
45 KiB
Go
Raw Normal View History

2024-01-03 07:31:42 +00:00
// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use
// of this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
package metamorphic
import (
"bytes"
"fmt"
"slices"
"github.com/cockroachdb/pebble"
"github.com/cockroachdb/pebble/internal/randvar"
"github.com/cockroachdb/pebble/internal/testkeys"
"golang.org/x/exp/rand"
)
const maxValueSize = 20
type iterOpts struct {
lower []byte
upper []byte
keyTypes uint32 // pebble.IterKeyType
// maskSuffix may be set if keyTypes is IterKeyTypePointsAndRanges to
// configure IterOptions.RangeKeyMasking.Suffix.
maskSuffix []byte
// If filterMax is >0, this iterator will filter out any keys that have
// suffixes that don't fall within the range [filterMin,filterMax).
// Additionally, the iterator will be constructed with a block-property
// filter that filters out blocks accordingly. Not all OPTIONS hook up the
// corresponding block property collector, so block-filtering may still be
// effectively disabled in some runs. The iterator operations themselves
// however will always skip past any points that should be filtered to
// ensure determinism.
filterMin uint64
filterMax uint64
// see IterOptions.UseL6Filters.
useL6Filters bool
// NB: If adding or removing fields, ensure IsZero is in sync.
}
func (o iterOpts) IsZero() bool {
return o.lower == nil && o.upper == nil && o.keyTypes == 0 &&
o.maskSuffix == nil && o.filterMin == 0 && o.filterMax == 0 && !o.useL6Filters
}
type generator struct {
cfg config
rng *rand.Rand
init *initOp
ops []op
// keyManager tracks the state of keys a operation generation time.
keyManager *keyManager
dbs objIDSlice
// Unordered sets of object IDs for live objects. Used to randomly select on
// object when generating an operation. There are 4 concrete objects: the DB
// (of which there is exactly 1), batches, iterators, and snapshots.
//
// liveBatches contains the live indexed and write-only batches.
liveBatches objIDSlice
// liveIters contains the live iterators.
liveIters objIDSlice
itersLastOpts map[objID]iterOpts
// liveReaders contains the DB, and any live indexed batches and snapshots. The DB is always
// at index 0.
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 objIDSlice
// Maps used to find associated objects during generation. These maps are not
// needed during test execution.
//
// batchID -> batch iters: used to keep track of the open iterators on an
// indexed batch. The iter set value will also be indexed by the readers map.
batches map[objID]objIDSet
// iterID -> reader iters: used to keep track of all of the open
// iterators. The iter set value will also be indexed by either the batches
// or snapshots maps.
iters map[objID]objIDSet
// objectID -> db: used to keep track of the DB a batch, iter, or snapshot
// was created on.
objDB map[objID]objID
// 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
// 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
// a snapshot. The iter set value will also be indexed by the readers map.
snapshots map[objID]objIDSet
// snapshotID -> bounds of the snapshot: only populated for snapshots that
// are constrained by bounds.
snapshotBounds map[objID][]pebble.KeyRange
// iterSequenceNumber is the metaTimestamp at which the iter was created.
iterCreationTimestamp map[objID]int
// iterReaderID is a map from an iterID to a readerID.
iterReaderID map[objID]objID
}
func newGenerator(rng *rand.Rand, cfg config, km *keyManager) *generator {
g := &generator{
cfg: cfg,
rng: rng,
init: &initOp{dbSlots: uint32(cfg.numInstances)},
keyManager: km,
liveReaders: objIDSlice{makeObjID(dbTag, 1)},
liveWriters: objIDSlice{makeObjID(dbTag, 1)},
dbs: objIDSlice{makeObjID(dbTag, 1)},
objDB: make(map[objID]objID),
batches: make(map[objID]objIDSet),
iters: make(map[objID]objIDSet),
readers: make(map[objID]objIDSet),
snapshots: make(map[objID]objIDSet),
snapshotBounds: make(map[objID][]pebble.KeyRange),
itersLastOpts: make(map[objID]iterOpts),
iterCreationTimestamp: make(map[objID]int),
iterReaderID: make(map[objID]objID),
}
for i := 1; i < cfg.numInstances; i++ {
g.liveReaders = append(g.liveReaders, makeObjID(dbTag, uint32(i+1)))
g.liveWriters = append(g.liveWriters, makeObjID(dbTag, uint32(i+1)))
g.dbs = append(g.dbs, makeObjID(dbTag, uint32(i+1)))
}
// Note that the initOp fields are populated during generation.
g.ops = append(g.ops, g.init)
return g
}
func generate(rng *rand.Rand, count uint64, cfg config, km *keyManager) []op {
g := newGenerator(rng, cfg, km)
generators := []func(){
batchAbort: g.batchAbort,
batchCommit: g.batchCommit,
dbCheckpoint: g.dbCheckpoint,
dbCompact: g.dbCompact,
dbFlush: g.dbFlush,
dbRatchetFormatMajorVersion: g.dbRatchetFormatMajorVersion,
dbRestart: g.dbRestart,
iterClose: g.randIter(g.iterClose),
iterFirst: g.randIter(g.iterFirst),
iterLast: g.randIter(g.iterLast),
iterNext: g.randIter(g.iterNext),
iterNextWithLimit: g.randIter(g.iterNextWithLimit),
iterNextPrefix: g.randIter(g.iterNextPrefix),
iterCanSingleDelete: g.randIter(g.iterCanSingleDelete),
iterPrev: g.randIter(g.iterPrev),
iterPrevWithLimit: g.randIter(g.iterPrevWithLimit),
iterSeekGE: g.randIter(g.iterSeekGE),
iterSeekGEWithLimit: g.randIter(g.iterSeekGEWithLimit),
iterSeekLT: g.randIter(g.iterSeekLT),
iterSeekLTWithLimit: g.randIter(g.iterSeekLTWithLimit),
iterSeekPrefixGE: g.randIter(g.iterSeekPrefixGE),
iterSetBounds: g.randIter(g.iterSetBounds),
iterSetOptions: g.randIter(g.iterSetOptions),
newBatch: g.newBatch,
newIndexedBatch: g.newIndexedBatch,
newIter: g.newIter,
newIterUsingClone: g.newIterUsingClone,
newSnapshot: g.newSnapshot,
readerGet: g.readerGet,
replicate: g.replicate,
snapshotClose: g.snapshotClose,
writerApply: g.writerApply,
writerDelete: g.writerDelete,
writerDeleteRange: g.writerDeleteRange,
writerIngest: g.writerIngest,
writerMerge: g.writerMerge,
writerRangeKeyDelete: g.writerRangeKeyDelete,
writerRangeKeySet: g.writerRangeKeySet,
writerRangeKeyUnset: g.writerRangeKeyUnset,
writerSet: g.writerSet,
writerSingleDelete: g.writerSingleDelete,
}
// 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++ {
generators[deck.Int()]()
}
g.dbClose()
return g.ops
}
func (g *generator) add(op op) {
g.ops = append(g.ops, op)
g.keyManager.update(op)
}
// randKeyToWrite returns a key for any write other than SingleDelete.
//
// TODO(peter): make the size and distribution of keys configurable. See
// keyDist and keySizeDist in config.go.
func (g *generator) randKeyToWrite(newKey float64) []byte {
return g.randKeyHelper(g.keyManager.eligibleWriteKeys(), newKey, nil)
}
// prefixKeyRange generates a [start, end) pair consisting of two prefix keys.
func (g *generator) prefixKeyRange() ([]byte, []byte) {
start := g.randPrefixToWrite(0.001)
end := g.randPrefixToWrite(0.001)
for g.cmp(start, end) == 0 {
end = g.randPrefixToWrite(0.05)
}
if g.cmp(start, end) > 0 {
start, end = end, start
}
return start, end
}
// randPrefixToWrite returns a prefix key (a key with no suffix) for a range key
// write operation.
func (g *generator) randPrefixToWrite(newPrefix float64) []byte {
prefixes := g.keyManager.prefixes()
if len(prefixes) > 0 && g.rng.Float64() > newPrefix {
// Use an existing prefix.
p := g.rng.Intn(len(prefixes))
return prefixes[p]
}
// Use a new prefix.
var prefix []byte
for {
prefix = g.randKeyHelperSuffix(nil, 4, 12, 0)
if !g.keyManager.prefixExists(prefix) {
if !g.keyManager.addNewKey(prefix) {
panic("key must not exist if prefix doesn't exist")
}
return prefix
}
}
}
// randSuffixToWrite generates a random suffix according to the configuration's suffix
// distribution. It takes a probability 0 ≤ p ≤ 1.0 indicating the probability
// with which the generator should increase the max suffix generated by the
// generator.
//
// randSuffixToWrite may return a nil suffix, with the probability the
// configuration's suffix distribution assigns to the zero suffix.
func (g *generator) randSuffixToWrite(incMaxProb float64) []byte {
if g.rng.Float64() < incMaxProb {
g.cfg.writeSuffixDist.IncMax(1)
}
return suffixFromInt(int64(g.cfg.writeSuffixDist.Uint64(g.rng)))
}
// randSuffixToRead generates a random suffix used during reads. The suffixes
// generated by this function are within the same range as suffixes generated by
// randSuffixToWrite, however randSuffixToRead pulls from a uniform
// distribution.
func (g *generator) randSuffixToRead() []byte {
// When reading, don't apply the recency skewing in order to better exercise
// a reading a mix of older and newer keys.
max := g.cfg.writeSuffixDist.Max()
return suffixFromInt(g.rng.Int63n(int64(max)))
}
func suffixFromInt(suffix int64) []byte {
// Treat the zero as no suffix to match the behavior during point key
// generation in randKeyHelper.
if suffix == 0 {
return nil
}
return testkeys.Suffix(suffix)
}
func (g *generator) randKeyToSingleDelete(id, dbID objID) []byte {
keys := g.keyManager.eligibleSingleDeleteKeys(id, dbID)
length := len(keys)
if length == 0 {
return nil
}
return keys[g.rng.Intn(length)]
}
// randKeyToRead returns a key for read operations.
func (g *generator) randKeyToRead(newKey float64) []byte {
return g.randKeyHelper(g.keyManager.eligibleReadKeys(), newKey, nil)
}
// randKeyToReadInRange returns a key for read operations within the provided
// key range. The bounds of the provided key range must span a prefix boundary.
func (g *generator) randKeyToReadInRange(newKey float64, kr pebble.KeyRange) []byte {
return g.randKeyHelper(g.keyManager.eligibleReadKeysInRange(kr), newKey, &kr)
}
func (g *generator) randKeyHelper(
keys [][]byte, newKey float64, newKeyBounds *pebble.KeyRange,
) []byte {
switch {
case len(keys) > 0 && g.rng.Float64() > newKey:
// Use an existing user key.
return keys[g.rng.Intn(len(keys))]
case len(keys) > 0 && g.rng.Float64() > g.cfg.newPrefix:
// Use an existing prefix but a new suffix, producing a new user key.
prefixes := g.keyManager.prefixes()
// If we're constrained to a key range, find which existing prefixes
// fall within that key range.
if newKeyBounds != nil {
s, _ := slices.BinarySearchFunc(prefixes, newKeyBounds.Start, g.cmp)
e, _ := slices.BinarySearchFunc(prefixes, newKeyBounds.End, g.cmp)
prefixes = prefixes[s:e]
}
if len(prefixes) > 0 {
for {
// Pick a prefix on each iteration in case most or all suffixes are
// already in use for any individual prefix.
p := g.rng.Intn(len(prefixes))
suffix := int64(g.cfg.writeSuffixDist.Uint64(g.rng))
var key []byte
if suffix > 0 {
key = resizeBuffer(key, len(prefixes[p]), testkeys.SuffixLen(suffix))
n := copy(key, prefixes[p])
testkeys.WriteSuffix(key[n:], suffix)
} else {
key = resizeBuffer(key, len(prefixes[p]), 0)
copy(key, prefixes[p])
}
if (newKeyBounds == nil || (g.cmp(key, newKeyBounds.Start) >= 0 && g.cmp(key, newKeyBounds.End) < 0)) &&
g.keyManager.addNewKey(key) {
return key
}
// If the generated key already existed, or the generated key
// fell outside the provided bounds, increase the suffix
// distribution and loop.
g.cfg.writeSuffixDist.IncMax(1)
}
}
// Otherwise fall through to generating a new prefix.
fallthrough
default:
// Use a new prefix, producing a new user key.
var key []byte
suffix := int64(g.cfg.writeSuffixDist.Uint64(g.rng))
// If we have bounds in which we need to generate the key, use
// testkeys.RandomSeparator to generate a key between the bounds.
if newKeyBounds != nil {
targetLength := 4 + g.rng.Intn(8)
key = testkeys.RandomSeparator(nil, g.prefix(newKeyBounds.Start), g.prefix(newKeyBounds.End),
suffix, targetLength, g.rng)
} else {
for {
key = g.randKeyHelperSuffix(nil, 4, 12, suffix)
if !g.keyManager.prefixExists(key[:testkeys.Comparer.Split(key)]) {
if !g.keyManager.addNewKey(key) {
panic("key must not exist if prefix doesn't exist")
}
break
}
}
}
return key
}
}
// randKeyHelperSuffix is a helper function for randKeyHelper, and should not be
// invoked directly.
func (g *generator) randKeyHelperSuffix(
dst []byte, minPrefixLen, maxPrefixLen int, suffix int64,
) []byte {
n := minPrefixLen
if maxPrefixLen > minPrefixLen {
n += g.rng.Intn(maxPrefixLen - minPrefixLen)
}
// In order to test a mix of suffixed and unsuffixed keys, omit the zero
// suffix.
if suffix == 0 {
dst = resizeBuffer(dst, n, 0)
g.fillRand(dst)
return dst
}
suffixLen := testkeys.SuffixLen(suffix)
dst = resizeBuffer(dst, n, suffixLen)
g.fillRand(dst[:n])
testkeys.WriteSuffix(dst[n:], suffix)
return dst
}
func resizeBuffer(buf []byte, prefixLen, suffixLen int) []byte {
if cap(buf) >= prefixLen+suffixLen {
return buf[:prefixLen+suffixLen]
}
return make([]byte, prefixLen+suffixLen)
}
// TODO(peter): make the value size configurable. See valueSizeDist in
// config.go.
func (g *generator) randValue(min, max int) []byte {
n := min
if max > min {
n += g.rng.Intn(max - min)
}
if n == 0 {
return nil
}
buf := make([]byte, n)
g.fillRand(buf)
return buf
}
func (g *generator) fillRand(buf []byte) {
// NB: The actual random values are not particularly important. We only use
// lowercase letters because that makes visual determination of ordering
// easier, rather than having to remember the lexicographic ordering of
// uppercase vs lowercase, or letters vs numbers vs punctuation.
const letters = "abcdefghijklmnopqrstuvwxyz"
const lettersLen = uint64(len(letters))
const lettersCharsPerRand = 12 // floor(log(math.MaxUint64)/log(lettersLen))
var r uint64
var q int
for i := 0; i < len(buf); i++ {
if q == 0 {
r = g.rng.Uint64()
q = lettersCharsPerRand
}
buf[i] = letters[r%lettersLen]
r = r / lettersLen
q--
}
}
func (g *generator) newBatch() {
batchID := makeObjID(batchTag, g.init.batchSlots)
g.init.batchSlots++
g.liveBatches = append(g.liveBatches, batchID)
g.liveWriters = append(g.liveWriters, batchID)
dbID := g.dbs.rand(g.rng)
g.objDB[batchID] = dbID
g.add(&newBatchOp{
dbID: dbID,
batchID: batchID,
})
}
func (g *generator) newIndexedBatch() {
batchID := makeObjID(batchTag, g.init.batchSlots)
g.init.batchSlots++
g.liveBatches = append(g.liveBatches, batchID)
g.liveReaders = append(g.liveReaders, batchID)
g.liveWriters = append(g.liveWriters, batchID)
iters := make(objIDSet)
g.batches[batchID] = iters
g.readers[batchID] = iters
dbID := g.dbs.rand(g.rng)
g.objDB[batchID] = dbID
g.add(&newIndexedBatchOp{
dbID: dbID,
batchID: batchID,
})
}
// removeFromBatchGenerator will not generate a closeOp for the target batch as
// not every batch that is removed from the generator should be closed. For
// example, running a closeOp before an ingestOp that contains the closed batch
// will cause an error.
func (g *generator) removeBatchFromGenerator(batchID objID) {
g.liveBatches.remove(batchID)
iters := g.batches[batchID]
delete(g.batches, batchID)
if iters != nil {
g.liveReaders.remove(batchID)
delete(g.readers, batchID)
}
g.liveWriters.remove(batchID)
for _, id := range iters.sorted() {
g.liveIters.remove(id)
delete(g.iters, id)
g.add(&closeOp{objID: id, derivedDBID: g.objDB[batchID]})
}
}
func (g *generator) batchAbort() {
if len(g.liveBatches) == 0 {
return
}
batchID := g.liveBatches.rand(g.rng)
g.removeBatchFromGenerator(batchID)
g.add(&closeOp{objID: batchID, derivedDBID: g.objDB[batchID]})
}
func (g *generator) batchCommit() {
if len(g.liveBatches) == 0 {
return
}
batchID := g.liveBatches.rand(g.rng)
dbID := g.objDB[batchID]
g.removeBatchFromGenerator(batchID)
g.add(&batchCommitOp{
dbID: dbID,
batchID: batchID,
})
g.add(&closeOp{objID: batchID, derivedDBID: dbID})
}
func (g *generator) dbClose() {
// Close any live iterators and snapshots, so that we can close the DB
// cleanly.
for len(g.liveIters) > 0 {
g.randIter(g.iterClose)()
}
for len(g.liveSnapshots) > 0 {
g.snapshotClose()
}
for len(g.liveBatches) > 0 {
batchID := g.liveBatches[0]
dbID := g.objDB[batchID]
g.removeBatchFromGenerator(batchID)
g.add(&closeOp{objID: batchID, derivedDBID: dbID})
}
for len(g.dbs) > 0 {
db := g.dbs[0]
g.dbs = g.dbs[1:]
g.add(&closeOp{objID: db})
}
}
func (g *generator) dbCheckpoint() {
// 1/2 of the time we don't restrict the checkpoint;
// 1/4 of the time we restrict to 1 span;
// 1/8 of the time we restrict to 2 spans; etc.
numSpans := 0
var spans []pebble.CheckpointSpan
for g.rng.Intn(2) == 0 {
numSpans++
}
if numSpans > 0 {
spans = make([]pebble.CheckpointSpan, numSpans)
}
for i := range spans {
start := g.randKeyToRead(0.01)
end := g.randKeyToRead(0.01)
if g.cmp(start, end) > 0 {
start, end = end, start
}
spans[i].Start = start
spans[i].End = end
}
dbID := g.dbs.rand(g.rng)
g.add(&checkpointOp{
dbID: dbID,
spans: spans,
})
}
func (g *generator) dbCompact() {
// Generate new key(s) with a 1% probability.
start := g.randKeyToRead(0.01)
end := g.randKeyToRead(0.01)
if g.cmp(start, end) > 0 {
start, end = end, start
}
dbID := g.dbs.rand(g.rng)
g.add(&compactOp{
dbID: dbID,
start: start,
end: end,
parallelize: g.rng.Float64() < 0.5,
})
}
func (g *generator) dbFlush() {
g.add(&flushOp{g.dbs.rand(g.rng)})
}
func (g *generator) dbRatchetFormatMajorVersion() {
// Ratchet to a random format major version between the minimum the
// metamorphic tests support and the newest. At runtime, the generated
// version may be behind the database's format major version, in which case
// RatchetFormatMajorVersion should deterministically error.
dbID := g.dbs.rand(g.rng)
n := int(newestFormatMajorVersionToTest - minimumFormatMajorVersion)
vers := pebble.FormatMajorVersion(g.rng.Intn(n+1)) + minimumFormatMajorVersion
g.add(&dbRatchetFormatMajorVersionOp{dbID: dbID, vers: vers})
}
func (g *generator) dbRestart() {
// Close any live iterators and snapshots, so that we can close the DB
// cleanly.
dbID := g.dbs.rand(g.rng)
for len(g.liveIters) > 0 {
g.randIter(g.iterClose)()
}
for len(g.liveSnapshots) > 0 {
g.snapshotClose()
}
// Close the batches.
for len(g.liveBatches) > 0 {
batchID := g.liveBatches[0]
dbID := g.objDB[batchID]
g.removeBatchFromGenerator(batchID)
g.add(&closeOp{objID: batchID, derivedDBID: dbID})
}
if len(g.liveReaders) != len(g.dbs) || len(g.liveWriters) != len(g.dbs) {
panic(fmt.Sprintf("unexpected counts: liveReaders %d, liveWriters: %d",
len(g.liveReaders), len(g.liveWriters)))
}
g.add(&dbRestartOp{dbID: dbID})
}
// maybeSetSnapshotIterBounds must be called whenever creating a new iterator or
// modifying the bounds of an iterator. If the iterator is backed by a snapshot
// that only guarantees consistency within a limited set of key spans, then the
// iterator must set bounds within one of the snapshot's consistent keyspans. It
// returns true if the provided readerID is a bounded snapshot and bounds were
// set.
func (g *generator) maybeSetSnapshotIterBounds(readerID objID, opts *iterOpts) bool {
snapBounds, isBoundedSnapshot := g.snapshotBounds[readerID]
if !isBoundedSnapshot {
return false
}
// Pick a random keyrange within one of the snapshot's key ranges.
parentBounds := snapBounds[g.rng.Intn(len(snapBounds))]
// With 10% probability, use the parent start bound as-is.
if g.rng.Float64() <= 0.1 {
opts.lower = parentBounds.Start
} else {
opts.lower = testkeys.RandomSeparator(
nil, /* dst */
parentBounds.Start,
parentBounds.End,
0, /* suffix */
4+g.rng.Intn(8),
g.rng,
)
}
// With 10% probability, use the parent end bound as-is.
if g.rng.Float64() <= 0.1 {
opts.upper = parentBounds.End
} else {
opts.upper = testkeys.RandomSeparator(
nil, /* dst */
opts.lower,
parentBounds.End,
0, /* suffix */
4+g.rng.Intn(8),
g.rng,
)
}
return true
}
func (g *generator) newIter() {
iterID := makeObjID(iterTag, g.init.iterSlots)
g.init.iterSlots++
g.liveIters = append(g.liveIters, iterID)
readerID := g.liveReaders.rand(g.rng)
if iters := g.readers[readerID]; iters != nil {
iters[iterID] = struct{}{}
g.iters[iterID] = iters
//lint:ignore SA9003 - readability
} else {
// NB: the DB object does not track its open iterators because it never
// closes.
}
g.iterReaderID[iterID] = readerID
dbID := g.deriveDB(iterID)
var opts iterOpts
if !g.maybeSetSnapshotIterBounds(readerID, &opts) {
// Generate lower/upper bounds with a 10% probability.
if g.rng.Float64() <= 0.1 {
// Generate a new key with a .1% probability.
opts.lower = g.randKeyToRead(0.001)
}
if g.rng.Float64() <= 0.1 {
// Generate a new key with a .1% probability.
opts.upper = g.randKeyToRead(0.001)
}
if g.cmp(opts.lower, opts.upper) > 0 {
opts.lower, opts.upper = opts.upper, opts.lower
}
}
opts.keyTypes, opts.maskSuffix = g.randKeyTypesAndMask()
// With 10% probability, enable automatic filtering of keys with suffixes
// not in the provided range. This filtering occurs both through
// block-property filtering and explicitly within the iterator operations to
// ensure determinism.
if g.rng.Float64() <= 0.1 {
max := g.cfg.writeSuffixDist.Max()
opts.filterMin, opts.filterMax = g.rng.Uint64n(max)+1, g.rng.Uint64n(max)+1
if opts.filterMin > opts.filterMax {
opts.filterMin, opts.filterMax = opts.filterMax, opts.filterMin
} else if opts.filterMin == opts.filterMax {
opts.filterMax = opts.filterMin + 1
}
}
// Enable L6 filters with a 10% probability.
if g.rng.Float64() <= 0.1 {
opts.useL6Filters = true
}
g.itersLastOpts[iterID] = opts
g.iterCreationTimestamp[iterID] = g.keyManager.nextMetaTimestamp()
g.iterReaderID[iterID] = readerID
g.add(&newIterOp{
readerID: readerID,
iterID: iterID,
iterOpts: opts,
derivedDBID: dbID,
})
}
func (g *generator) randKeyTypesAndMask() (keyTypes uint32, maskSuffix []byte) {
// Iterate over different key types.
p := g.rng.Float64()
switch {
case p < 0.2: // 20% probability
keyTypes = uint32(pebble.IterKeyTypePointsOnly)
case p < 0.8: // 60% probability
keyTypes = uint32(pebble.IterKeyTypePointsAndRanges)
// With 50% probability, enable masking.
if g.rng.Intn(2) == 1 {
maskSuffix = g.randSuffixToRead()
}
default: // 20% probability
keyTypes = uint32(pebble.IterKeyTypeRangesOnly)
}
return keyTypes, maskSuffix
}
func (g *generator) deriveDB(readerID objID) objID {
if readerID.tag() == iterTag {
readerID = g.iterReaderID[readerID]
}
dbParentID := readerID
if dbParentID.tag() != dbTag {
dbParentID = g.objDB[dbParentID]
}
g.objDB[readerID] = dbParentID
return dbParentID
}
func (g *generator) newIterUsingClone() {
if len(g.liveIters) == 0 {
return
}
existingIterID := g.liveIters.rand(g.rng)
iterID := makeObjID(iterTag, g.init.iterSlots)
g.init.iterSlots++
g.liveIters = append(g.liveIters, iterID)
if iters := g.iters[existingIterID]; iters != nil {
iters[iterID] = struct{}{}
g.iters[iterID] = iters
//lint:ignore SA9003 - readability
} else {
// NB: the DB object does not track its open iterators because it never
// closes.
}
readerID := g.iterReaderID[existingIterID]
g.iterReaderID[iterID] = readerID
g.deriveDB(iterID)
var refreshBatch bool
if readerID.tag() == batchTag {
refreshBatch = g.rng.Intn(2) == 1
}
opts := g.itersLastOpts[existingIterID]
// With 50% probability, consider modifying the iterator options used by the
// clone.
if g.rng.Intn(2) == 1 {
g.maybeMutateOptions(readerID, &opts)
}
g.itersLastOpts[iterID] = opts
g.iterCreationTimestamp[iterID] = g.keyManager.nextMetaTimestamp()
g.iterReaderID[iterID] = g.iterReaderID[existingIterID]
g.add(&newIterUsingCloneOp{
existingIterID: existingIterID,
iterID: iterID,
refreshBatch: refreshBatch,
iterOpts: opts,
derivedReaderID: readerID,
})
}
func (g *generator) iterClose(iterID objID) {
g.liveIters.remove(iterID)
if readerIters, ok := g.iters[iterID]; ok {
delete(g.iters, iterID)
delete(readerIters, iterID)
//lint:ignore SA9003 - readability
} else {
// NB: the DB object does not track its open iterators because it never
// closes.
}
readerID := g.iterReaderID[iterID]
g.add(&closeOp{objID: iterID, derivedDBID: g.objDB[readerID]})
}
func (g *generator) iterSetBounds(iterID objID) {
iterLastOpts := g.itersLastOpts[iterID]
newOpts := iterLastOpts
// TODO(jackson): The logic to increase the probability of advancing bounds
// monotonically only applies if the snapshot is not bounded. Refactor to
// allow bounded snapshots to benefit too, when possible.
if !g.maybeSetSnapshotIterBounds(g.iterReaderID[iterID], &newOpts) {
var lower, upper []byte
genLower := g.rng.Float64() <= 0.9
genUpper := g.rng.Float64() <= 0.9
// When one of ensureLowerGE, ensureUpperLE is true, the new bounds
// don't overlap with the previous bounds.
var ensureLowerGE, ensureUpperLE bool
if genLower && iterLastOpts.upper != nil && g.rng.Float64() <= 0.9 {
ensureLowerGE = true
}
if (!ensureLowerGE || g.rng.Float64() < 0.5) && genUpper && iterLastOpts.lower != nil {
ensureUpperLE = true
ensureLowerGE = false
}
attempts := 0
for {
attempts++
if genLower {
// Generate a new key with a .1% probability.
lower = g.randKeyToRead(0.001)
}
if genUpper {
// Generate a new key with a .1% probability.
upper = g.randKeyToRead(0.001)
}
if g.cmp(lower, upper) > 0 {
lower, upper = upper, lower
}
if ensureLowerGE && g.cmp(iterLastOpts.upper, lower) > 0 {
if attempts < 25 {
continue
}
lower = iterLastOpts.upper
upper = lower
break
}
if ensureUpperLE && g.cmp(upper, iterLastOpts.lower) > 0 {
if attempts < 25 {
continue
}
upper = iterLastOpts.lower
lower = upper
break
}
break
}
newOpts.lower = lower
newOpts.upper = upper
}
g.itersLastOpts[iterID] = newOpts
g.add(&iterSetBoundsOp{
iterID: iterID,
lower: newOpts.lower,
upper: newOpts.upper,
})
// Additionally seek the iterator in a manner consistent with the bounds,
// and do some steps (Next/Prev). The seeking exercises typical
// CockroachDB behavior when using iterators and the steps are trying to
// stress the region near the bounds. Ideally, we should not do this as
// part of generating a single op, but this is easier than trying to
// control future op generation via generator state.
doSeekLT := newOpts.upper != nil && g.rng.Float64() < 0.5
doSeekGE := newOpts.lower != nil && g.rng.Float64() < 0.5
if doSeekLT && doSeekGE {
// Pick the seek.
if g.rng.Float64() < 0.5 {
doSeekGE = false
} else {
doSeekLT = false
}
}
if doSeekLT {
g.add(&iterSeekLTOp{
iterID: iterID,
key: newOpts.upper,
derivedReaderID: g.iterReaderID[iterID],
})
if g.rng.Float64() < 0.5 {
g.iterNext(iterID)
}
if g.rng.Float64() < 0.5 {
g.iterNext(iterID)
}
if g.rng.Float64() < 0.5 {
g.iterPrev(iterID)
}
} else if doSeekGE {
g.add(&iterSeekGEOp{
iterID: iterID,
key: newOpts.lower,
derivedReaderID: g.iterReaderID[iterID],
})
if g.rng.Float64() < 0.5 {
g.iterPrev(iterID)
}
if g.rng.Float64() < 0.5 {
g.iterPrev(iterID)
}
if g.rng.Float64() < 0.5 {
g.iterNext(iterID)
}
}
}
func (g *generator) iterSetOptions(iterID objID) {
opts := g.itersLastOpts[iterID]
g.maybeMutateOptions(g.iterReaderID[iterID], &opts)
g.itersLastOpts[iterID] = opts
g.add(&iterSetOptionsOp{
iterID: iterID,
iterOpts: opts,
derivedReaderID: g.iterReaderID[iterID],
})
// Additionally, perform a random absolute positioning operation. The
// SetOptions contract requires one before the next relative positioning
// operation. Ideally, we should not do this as part of generating a single
// op, but this is easier than trying to control future op generation via
// generator state.
g.pickOneUniform(
g.iterFirst,
g.iterLast,
g.iterSeekGE,
g.iterSeekGEWithLimit,
g.iterSeekPrefixGE,
g.iterSeekLT,
g.iterSeekLTWithLimit,
)(iterID)
}
func (g *generator) iterSeekGE(iterID objID) {
g.add(&iterSeekGEOp{
iterID: iterID,
key: g.randKeyToRead(0.001), // 0.1% new keys
derivedReaderID: g.iterReaderID[iterID],
})
}
func (g *generator) iterSeekGEWithLimit(iterID objID) {
// 0.1% new keys
key, limit := g.randKeyToRead(0.001), g.randKeyToRead(0.001)
if g.cmp(key, limit) > 0 {
key, limit = limit, key
}
g.add(&iterSeekGEOp{
iterID: iterID,
key: key,
limit: limit,
derivedReaderID: g.iterReaderID[iterID],
})
}
func (g *generator) randKeyToReadWithinBounds(lower, upper []byte, readerID objID) []*keyMeta {
var inRangeKeys []*keyMeta
for _, keyMeta := range g.keyManager.byObj[readerID] {
posKey := keyMeta.key
if g.cmp(posKey, lower) < 0 || g.cmp(posKey, upper) >= 0 {
continue
}
inRangeKeys = append(inRangeKeys, keyMeta)
}
return inRangeKeys
}
func (g *generator) iterSeekPrefixGE(iterID objID) {
lower := g.itersLastOpts[iterID].lower
upper := g.itersLastOpts[iterID].upper
iterCreationTimestamp := g.iterCreationTimestamp[iterID]
var key []byte
// We try to make sure that the SeekPrefixGE key is within the iter bounds,
// and that the iter can read the key. If the key was created on a batch
// which deleted the key, then the key will still be considered visible
// by the current logic. We're also not accounting for keys written to
// batches which haven't been presisted to the DB. But we're only picking
// keys in a best effort manner, and the logic is better than picking a
// random key.
if g.rng.Intn(10) >= 1 {
possibleKeys := make([][]byte, 0, 100)
inRangeKeys := g.randKeyToReadWithinBounds(lower, upper, g.objDB[iterID])
for _, keyMeta := range inRangeKeys {
posKey := keyMeta.key
var foundWriteWithoutDelete bool
for _, update := range keyMeta.updateOps {
if update.metaTimestamp > iterCreationTimestamp {
break
}
if update.deleted {
foundWriteWithoutDelete = false
} else {
foundWriteWithoutDelete = true
}
}
if foundWriteWithoutDelete {
possibleKeys = append(possibleKeys, posKey)
}
}
if len(possibleKeys) > 0 {
key = []byte(possibleKeys[g.rng.Int31n(int32(len(possibleKeys)))])
}
}
if key == nil {
// TODO(bananabrick): We should try and use keys within the bounds,
// even if we couldn't find any keys visible to the iterator. However,
// doing this in experiments didn't really increase the valid
// SeekPrefixGE calls by much.
key = g.randKeyToRead(0) // 0% new keys
}
g.add(&iterSeekPrefixGEOp{
iterID: iterID,
key: key,
derivedReaderID: g.iterReaderID[iterID],
})
}
func (g *generator) iterSeekLT(iterID objID) {
g.add(&iterSeekLTOp{
iterID: iterID,
key: g.randKeyToRead(0.001), // 0.1% new keys
derivedReaderID: g.iterReaderID[iterID],
})
}
func (g *generator) iterSeekLTWithLimit(iterID objID) {
// 0.1% new keys
key, limit := g.randKeyToRead(0.001), g.randKeyToRead(0.001)
if g.cmp(limit, key) > 0 {
key, limit = limit, key
}
g.add(&iterSeekLTOp{
iterID: iterID,
key: key,
limit: limit,
derivedReaderID: g.iterReaderID[iterID],
})
}
// randIter performs partial func application ("currying"), returning a new
// function that supplies the given func with a random iterator.
func (g *generator) randIter(gen func(objID)) func() {
return func() {
if len(g.liveIters) == 0 {
return
}
gen(g.liveIters.rand(g.rng))
}
}
func (g *generator) iterFirst(iterID objID) {
g.add(&iterFirstOp{
iterID: iterID,
derivedReaderID: g.iterReaderID[iterID],
})
}
func (g *generator) iterLast(iterID objID) {
g.add(&iterLastOp{
iterID: iterID,
derivedReaderID: g.iterReaderID[iterID],
})
}
func (g *generator) iterNext(iterID objID) {
g.add(&iterNextOp{
iterID: iterID,
derivedReaderID: g.iterReaderID[iterID],
})
}
func (g *generator) iterPrev(iterID objID) {
g.add(&iterPrevOp{
iterID: iterID,
derivedReaderID: g.iterReaderID[iterID],
})
}
func (g *generator) iterNextWithLimit(iterID objID) {
g.add(&iterNextOp{
iterID: iterID,
limit: g.randKeyToRead(0.001), // 0.1% new keys
derivedReaderID: g.iterReaderID[iterID],
})
}
func (g *generator) iterNextPrefix(iterID objID) {
g.add(&iterNextPrefixOp{
iterID: iterID,
derivedReaderID: g.iterReaderID[iterID],
})
}
func (g *generator) iterCanSingleDelete(iterID objID) {
g.add(&iterCanSingleDelOp{
iterID: iterID,
derivedReaderID: g.iterReaderID[iterID],
})
}
func (g *generator) iterPrevWithLimit(iterID objID) {
g.add(&iterPrevOp{
iterID: iterID,
limit: g.randKeyToRead(0.001), // 0.1% new keys
derivedReaderID: g.iterReaderID[iterID],
})
}
func (g *generator) readerGet() {
if len(g.liveReaders) == 0 {
return
}
readerID := g.liveReaders.rand(g.rng)
// If the chosen reader is a snapshot created with user-specified key
// ranges, restrict the read to fall within one of the provided key ranges.
var key []byte
if bounds := g.snapshotBounds[readerID]; len(bounds) > 0 {
kr := bounds[g.rng.Intn(len(bounds))]
key = g.randKeyToReadInRange(0.001, kr) // 0.1% new keys
} else {
key = g.randKeyToRead(0.001) // 0.1% new keys
}
derivedDBID := objID(0)
if readerID.tag() == batchTag || readerID.tag() == snapTag {
derivedDBID = g.deriveDB(readerID)
}
g.add(&getOp{readerID: readerID, key: key, derivedDBID: derivedDBID})
}
func (g *generator) replicate() {
if len(g.dbs) < 2 {
return
}
source := g.dbs.rand(g.rng)
dest := source
for dest == source {
dest = g.dbs.rand(g.rng)
}
var startKey, endKey []byte
startKey = g.randKeyToRead(0.001) // 0.1% new keys
endKey = g.randKeyToRead(0.001) // 0.1% new keys
for g.cmp(startKey, endKey) == 0 {
endKey = g.randKeyToRead(0.01) // 1% new keys
}
if g.cmp(startKey, endKey) > 0 {
startKey, endKey = endKey, startKey
}
g.add(&replicateOp{
source: source,
dest: dest,
start: startKey,
end: endKey,
})
}
// generateDisjointKeyRanges generates n disjoint key ranges.
func (g *generator) generateDisjointKeyRanges(n int) []pebble.KeyRange {
bounds := make([][]byte, 2*n)
used := map[string]bool{}
for i := 0; i < len(bounds); i++ {
k := g.prefix(g.randKeyToRead(0.1))
for used[string(k)] {
k = g.prefix(g.randKeyToRead(0.1))
}
bounds[i] = k
used[string(k)] = true
}
slices.SortFunc(bounds, g.cmp)
keyRanges := make([]pebble.KeyRange, n)
for i := range keyRanges {
keyRanges[i] = pebble.KeyRange{
Start: bounds[i*2],
End: bounds[i*2+1],
}
}
return keyRanges
}
func (g *generator) newSnapshot() {
snapID := makeObjID(snapTag, g.init.snapshotSlots)
g.init.snapshotSlots++
g.liveSnapshots = append(g.liveSnapshots, snapID)
g.liveReaders = append(g.liveReaders, snapID)
dbID := g.dbs.rand(g.rng)
g.objDB[snapID] = dbID
iters := make(objIDSet)
g.snapshots[snapID] = iters
g.readers[snapID] = iters
s := &newSnapshotOp{
dbID: dbID,
snapID: snapID,
}
// With 75% probability, impose bounds on the keys that may be read with the
// snapshot. Setting bounds allows some runs of the metamorphic test to use
// a EventuallyFileOnlySnapshot instead of a Snapshot, testing equivalence
// between the two for reads within those bounds.
//
// If we're in multi-instance mode, we must always create bounds, as we will
// always create EventuallyFileOnlySnapshots to allow commands that use excises
// (eg. replicateOp) to work.
if g.rng.Float64() < 0.75 || g.dbs.Len() > 1 {
s.bounds = g.generateDisjointKeyRanges(
g.rng.Intn(5) + 1, /* between 1-5 */
)
g.snapshotBounds[snapID] = s.bounds
}
g.add(s)
if g.dbs.Len() > 1 {
// Do a flush after each EFOS, if we're in multi-instance mode. This limits
// the testing area of EFOS, but allows them to be used alongside operations
// that do an excise (eg. replicateOp). This will be revisited when
// https://github.com/cockroachdb/pebble/issues/2885 is implemented.
g.add(&flushOp{dbID})
}
}
func (g *generator) snapshotClose() {
if len(g.liveSnapshots) == 0 {
return
}
snapID := g.liveSnapshots.rand(g.rng)
g.liveSnapshots.remove(snapID)
iters := g.snapshots[snapID]
delete(g.snapshots, snapID)
g.liveReaders.remove(snapID)
delete(g.readers, snapID)
for _, id := range iters.sorted() {
g.liveIters.remove(id)
delete(g.iters, id)
g.add(&closeOp{objID: id, derivedDBID: g.objDB[snapID]})
}
g.add(&closeOp{objID: snapID, derivedDBID: g.objDB[snapID]})
}
func (g *generator) writerApply() {
if len(g.liveBatches) == 0 {
return
}
if len(g.liveWriters) < 2 {
panic(fmt.Sprintf("insufficient liveWriters (%d) to apply batch", len(g.liveWriters)))
}
batchID := g.liveBatches.rand(g.rng)
dbID := g.objDB[batchID]
var writerID objID
for {
// NB: The writer we're applying to, as well as the batch we're applying,
// must be from the same DB. The writer could be the db itself. Applying
// a batch from one DB on another DB results in a panic, so avoid that.
writerID = g.liveWriters.rand(g.rng)
writerDBID := writerID
if writerID.tag() != dbTag {
writerDBID = g.objDB[writerID]
}
if writerID != batchID && writerDBID == dbID {
break
}
}
g.removeBatchFromGenerator(batchID)
g.add(&applyOp{
writerID: writerID,
batchID: batchID,
})
g.add(&closeOp{
objID: batchID,
derivedDBID: dbID,
})
}
func (g *generator) writerDelete() {
if len(g.liveWriters) == 0 {
return
}
writerID := g.liveWriters.rand(g.rng)
derivedDBID := writerID
if derivedDBID.tag() != dbTag {
derivedDBID = g.objDB[writerID]
}
g.add(&deleteOp{
writerID: writerID,
key: g.randKeyToWrite(0.001), // 0.1% new keys
derivedDBID: derivedDBID,
})
}
func (g *generator) writerDeleteRange() {
if len(g.liveWriters) == 0 {
return
}
start := g.randKeyToWrite(0.001)
end := g.randKeyToWrite(0.001)
if g.cmp(start, end) > 0 {
start, end = end, start
}
writerID := g.liveWriters.rand(g.rng)
g.add(&deleteRangeOp{
writerID: writerID,
start: start,
end: end,
})
}
func (g *generator) writerRangeKeyDelete() {
if len(g.liveWriters) == 0 {
return
}
start, end := g.prefixKeyRange()
writerID := g.liveWriters.rand(g.rng)
g.add(&rangeKeyDeleteOp{
writerID: writerID,
start: start,
end: end,
})
}
func (g *generator) writerRangeKeySet() {
if len(g.liveWriters) == 0 {
return
}
start, end := g.prefixKeyRange()
// 90% of the time, set a suffix.
var suffix []byte
if g.rng.Float64() < 0.90 {
// Increase the max suffix 5% of the time.
suffix = g.randSuffixToWrite(0.05)
}
writerID := g.liveWriters.rand(g.rng)
g.add(&rangeKeySetOp{
writerID: writerID,
start: start,
end: end,
suffix: suffix,
value: g.randValue(0, maxValueSize),
})
}
func (g *generator) writerRangeKeyUnset() {
if len(g.liveWriters) == 0 {
return
}
start, end := g.prefixKeyRange()
// 90% of the time, set a suffix.
var suffix []byte
if g.rng.Float64() < 0.90 {
// Increase the max suffix 5% of the time.
suffix = g.randSuffixToWrite(0.05)
}
// TODO(jackson): Increase probability of effective unsets? Purely random
// unsets are unlikely to remove an active range key.
writerID := g.liveWriters.rand(g.rng)
g.add(&rangeKeyUnsetOp{
writerID: writerID,
start: start,
end: end,
suffix: suffix,
})
}
func (g *generator) writerIngest() {
if len(g.liveBatches) == 0 {
return
}
// TODO(nicktrav): this is resulting in too many single batch ingests.
// Consider alternatives. One possibility would be to pass through whether
// we can tolerate failure or not, and if the ingestOp encounters a
// failure, it would retry after splitting into single batch ingests.
dbID := g.dbs.rand(g.rng)
// Ingest between 1 and 3 batches.
batchIDs := make([]objID, 0, 1+g.rng.Intn(3))
canFail := cap(batchIDs) > 1
for i := 0; i < cap(batchIDs); i++ {
batchID := g.liveBatches.rand(g.rng)
if canFail && !g.keyManager.canTolerateApplyFailure(batchID) {
continue
}
// After the ingest runs, it either succeeds and the keys are in the
// DB, or it fails and these keys never make it to the DB.
g.removeBatchFromGenerator(batchID)
batchIDs = append(batchIDs, batchID)
if len(g.liveBatches) == 0 {
break
}
}
if len(batchIDs) == 0 && len(g.liveBatches) > 0 {
// Unable to find multiple batches because of the
// canTolerateApplyFailure call above, so just pick one batch.
batchID := g.liveBatches.rand(g.rng)
g.removeBatchFromGenerator(batchID)
batchIDs = append(batchIDs, batchID)
}
derivedDBIDs := make([]objID, len(batchIDs))
for i := range batchIDs {
derivedDBIDs[i] = g.objDB[batchIDs[i]]
}
g.add(&ingestOp{
dbID: dbID,
batchIDs: batchIDs,
derivedDBIDs: derivedDBIDs,
})
}
func (g *generator) writerMerge() {
if len(g.liveWriters) == 0 {
return
}
writerID := g.liveWriters.rand(g.rng)
g.add(&mergeOp{
writerID: writerID,
// 20% new keys.
key: g.randKeyToWrite(0.2),
value: g.randValue(0, maxValueSize),
})
}
func (g *generator) writerSet() {
if len(g.liveWriters) == 0 {
return
}
writerID := g.liveWriters.rand(g.rng)
g.add(&setOp{
writerID: writerID,
// 50% new keys.
key: g.randKeyToWrite(0.5),
value: g.randValue(0, maxValueSize),
})
}
func (g *generator) writerSingleDelete() {
if len(g.liveWriters) == 0 {
return
}
writerID := g.liveWriters.rand(g.rng)
dbID := g.objDB[writerID]
key := g.randKeyToSingleDelete(writerID, dbID)
if key == nil {
return
}
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,
})
}
func (g *generator) maybeMutateOptions(readerID objID, opts *iterOpts) {
// With 95% probability, allow changes to any options at all. This ensures
// that in 5% of cases there are no changes, and SetOptions hits its fast
// path.
if g.rng.Intn(100) >= 5 {
if !g.maybeSetSnapshotIterBounds(readerID, opts) {
// With 1/3 probability, clear existing bounds.
if opts.lower != nil && g.rng.Intn(3) == 0 {
opts.lower = nil
}
if opts.upper != nil && g.rng.Intn(3) == 0 {
opts.upper = nil
}
// With 1/3 probability, update the bounds.
if g.rng.Intn(3) == 0 {
// Generate a new key with a .1% probability.
opts.lower = g.randKeyToRead(0.001)
}
if g.rng.Intn(3) == 0 {
// Generate a new key with a .1% probability.
opts.upper = g.randKeyToRead(0.001)
}
if g.cmp(opts.lower, opts.upper) > 0 {
opts.lower, opts.upper = opts.upper, opts.lower
}
}
// With 1/3 probability, update the key-types/mask.
if g.rng.Intn(3) == 0 {
opts.keyTypes, opts.maskSuffix = g.randKeyTypesAndMask()
}
// With 1/3 probability, clear existing filter.
if opts.filterMax > 0 && g.rng.Intn(3) == 0 {
opts.filterMax, opts.filterMin = 0, 0
}
// With 10% probability, set a filter range.
if g.rng.Intn(10) == 1 {
max := g.cfg.writeSuffixDist.Max()
opts.filterMin, opts.filterMax = g.rng.Uint64n(max)+1, g.rng.Uint64n(max)+1
if opts.filterMin > opts.filterMax {
opts.filterMin, opts.filterMax = opts.filterMax, opts.filterMin
} else if opts.filterMin == opts.filterMax {
opts.filterMax = opts.filterMin + 1
}
}
// With 10% probability, flip enablement of L6 filters.
if g.rng.Float64() <= 0.1 {
opts.useL6Filters = !opts.useL6Filters
}
}
}
func (g *generator) pickOneUniform(options ...func(objID)) func(objID) {
i := g.rng.Intn(len(options))
return options[i]
}
func (g *generator) cmp(a, b []byte) int {
return g.keyManager.comparer.Compare(a, b)
}
func (g *generator) equal(a, b []byte) bool {
return g.keyManager.comparer.Equal(a, b)
}
func (g *generator) split(a []byte) int {
return g.keyManager.comparer.Split(a)
}
func (g *generator) prefix(a []byte) []byte {
return a[:g.split(a)]
}
func (g *generator) String() string {
var buf bytes.Buffer
for _, op := range g.ops {
fmt.Fprintf(&buf, "%s\n", op)
}
return buf.String()
}