Skip to content

Commit

Permalink
metamorphic: add testing for prefix synthesis
Browse files Browse the repository at this point in the history
This commit adds prefix synthesis (i.e. `PrefixReplacement` with empty
`ContentPrefix`) to the metamorphic test.

We generate a random prefix and prepend it to the initially-generated
bounds. We take it into account when emulating the ingestion and when
determining key histories.
  • Loading branch information
RaduBerinde committed Mar 4, 2024
1 parent da5edaf commit 83d1741
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 36 deletions.
41 changes: 28 additions & 13 deletions metamorphic/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func writeSSTForIngestion(
rangeKeyIter keyspan.FragmentIterator,
uniquePrefixes bool,
syntheticSuffix sstable.SyntheticSuffix,
prefixChange *sstable.PrefixReplacement,
writable objstorage.Writable,
targetFMV pebble.FormatMajorVersion,
) (*sstable.WriterMetadata, error) {
Expand All @@ -46,14 +47,17 @@ func writeSSTForIngestion(
defer rangeKeyIterCloser.Close()

outputKey := func(key []byte) []byte {
return slices.Clone(key)
}

if len(syntheticSuffix) > 0 {
outputKey = func(key []byte) []byte {
if prefixChange == nil && !syntheticSuffix.IsSet() {
return slices.Clone(key)
}
if prefixChange != nil {
key = prefixChange.Apply(key)
}
if syntheticSuffix.IsSet() {
n := t.opts.Comparer.Split(key)
return append(key[:n:n], syntheticSuffix...)
key = append(key[:n:n], syntheticSuffix...)
}
return key
}

var lastUserKey []byte
Expand Down Expand Up @@ -175,6 +179,7 @@ func buildForIngest(
iter, rangeDelIter, rangeKeyIter,
false, /* uniquePrefixes */
nil, /* syntheticSuffix */
nil, /* prefixChange */
writable,
db.FormatMajorVersion(),
)
Expand All @@ -190,13 +195,14 @@ func buildForIngestExternalEmulation(
externalObjID objID,
bounds pebble.KeyRange,
syntheticSuffix sstable.SyntheticSuffix,
prefixChange *sstable.PrefixReplacement,
i int,
) (path string, _ *sstable.WriterMetadata) {
path = t.opts.FS.PathJoin(t.tmpDir, fmt.Sprintf("ext%d-%d", dbID.slot(), i))
f, err := t.opts.FS.Create(path)
panicIfErr(err)

reader, pointIter, rangeDelIter, rangeKeyIter := openExternalObj(t, externalObjID, bounds)
reader, pointIter, rangeDelIter, rangeKeyIter := openExternalObj(t, externalObjID, bounds, prefixChange)
defer reader.Close()

writable := objstorageprovider.NewFileWritable(f)
Expand All @@ -209,6 +215,7 @@ func buildForIngestExternalEmulation(
pointIter, rangeDelIter, rangeKeyIter,
uniquePrefixes,
syntheticSuffix,
prefixChange,
writable,
t.minFMV(),
)
Expand All @@ -219,7 +226,7 @@ func buildForIngestExternalEmulation(
}

func openExternalObj(
t *Test, externalObjID objID, bounds pebble.KeyRange,
t *Test, externalObjID objID, bounds pebble.KeyRange, prefixChange *sstable.PrefixReplacement,
) (
reader *sstable.Reader,
pointIter base.InternalIterator,
Expand All @@ -234,7 +241,13 @@ func openExternalObj(
reader, err = sstable.NewReader(objstorageprovider.NewRemoteReadable(objReader, objSize), opts)
panicIfErr(err)

pointIter, err = reader.NewIter(sstable.NoTransforms, bounds.Start, bounds.End)
start := bounds.Start
end := bounds.End
if prefixChange != nil {
start = prefixChange.Invert(start)
end = prefixChange.Invert(end)
}
pointIter, err = reader.NewIter(sstable.NoTransforms, start, end)
panicIfErr(err)

rangeDelIter, err = reader.NewRawRangeDelIter(sstable.NoTransforms)
Expand All @@ -243,7 +256,7 @@ func openExternalObj(
rangeDelIter = keyspan.Truncate(
t.opts.Comparer.Compare,
rangeDelIter,
bounds.Start, bounds.End,
start, end,
nil /* start */, nil /* end */, false, /* panicOnUpperTruncate */
)
}
Expand All @@ -254,7 +267,7 @@ func openExternalObj(
rangeKeyIter = keyspan.Truncate(
t.opts.Comparer.Compare,
rangeKeyIter,
bounds.Start, bounds.End,
start, end,
nil /* start */, nil /* end */, false, /* panicOnUpperTruncate */
)
}
Expand All @@ -263,8 +276,10 @@ func openExternalObj(

// externalObjIsEmpty returns true if the given external object has no point or
// range keys withing the given bounds.
func externalObjIsEmpty(t *Test, externalObjID objID, bounds pebble.KeyRange) bool {
reader, pointIter, rangeDelIter, rangeKeyIter := openExternalObj(t, externalObjID, bounds)
func externalObjIsEmpty(
t *Test, externalObjID objID, bounds pebble.KeyRange, prefixChange *sstable.PrefixReplacement,
) bool {
reader, pointIter, rangeDelIter, rangeKeyIter := openExternalObj(t, externalObjID, bounds, prefixChange)
defer reader.Close()
defer closeIters(pointIter, rangeDelIter, rangeKeyIter)

Expand Down
4 changes: 2 additions & 2 deletions metamorphic/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,8 @@ func DefaultOpConfig() OpConfig {
OpWriterRangeKeyDelete: 5,
OpWriterSet: 100,
OpWriterSingleDelete: 50,
OpNewExternalObj: 2,
OpWriterIngestExternalFiles: 20,
OpNewExternalObj: 5,
OpWriterIngestExternalFiles: 100,
},
// Use a new prefix 75% of the time (and 25% of the time use an existing
// prefix with an alternative suffix).
Expand Down
26 changes: 26 additions & 0 deletions metamorphic/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

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

Expand Down Expand Up @@ -1313,12 +1314,37 @@ func (g *generator) writerIngestExternalFiles() {
if g.cmp(start, end) == 0 {
end = objEnd
}
// Randomly set up prefix change.
var prefixChange *sstable.PrefixReplacement
// We can only use a synthetic prefix if we don't have range dels.
// TODO(radu): we will want to support this at some point.
if !g.keyManager.objKeyMeta(id).hasRangeDels && g.rng.Intn(2) == 0 {
prefixChange = &sstable.PrefixReplacement{
SyntheticPrefix: randBytes(g.rng, 1, 5),
}
// TODO(radu): fix prefix replacement implementation or remove
// ContentPrefix altogether.
if false {
prefixLen := 0
limit := min(len(start), len(end))
for ; prefixLen < limit && start[prefixLen] == end[prefixLen]; prefixLen++ {
}
prefixLen = g.rng.Intn(prefixLen + 1)
if prefixLen > 0 {
prefixChange.ContentPrefix = start[:prefixLen]
}
}
start = prefixChange.Apply(start)
end = prefixChange.Apply(end)
}

objs[i] = externalObjWithBounds{
externalObjID: id,
bounds: pebble.KeyRange{
Start: start,
End: end,
},
prefixChange: prefixChange,
}
}

Expand Down
5 changes: 1 addition & 4 deletions metamorphic/key_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,10 +305,7 @@ func (kg *keyGenerator) parseKey(k []byte) (prefix []byte, suffix int64) {
}

func randBytes(rng *rand.Rand, minLen, maxLen int) []byte {
n := minLen
if maxLen > minLen {
n += rng.Intn(maxLen - minLen)
}
n := minLen + rng.Intn(maxLen-minLen+1)
if n == 0 {
return nil
}
Expand Down
19 changes: 13 additions & 6 deletions metamorphic/key_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,14 +299,21 @@ func (k *keyManager) InRangeKeysForObj(o objID, lower, upper []byte) []keyMeta {
// KeysForExternalIngest returns the keys that will be ingested with an external
// object (taking into consideration the bounds, synthetic suffix, etc).
func (k *keyManager) KeysForExternalIngest(obj externalObjWithBounds) []keyMeta {
keys := k.InRangeKeysForObj(obj.externalObjID, obj.bounds.Start, obj.bounds.End)
if obj.syntheticSuffix.IsSet() {
for i := range keys {
n := k.comparer.Split(keys[i].key)
keys[i].key = append(keys[i].key[:n:n], obj.syntheticSuffix...)
var res []keyMeta
for _, km := range k.SortedKeysForObj(obj.externalObjID) {
// Apply prefix and suffix changes, then check the bounds.
if obj.prefixChange != nil {
km.key = obj.prefixChange.Apply(km.key)
}
if obj.syntheticSuffix.IsSet() {
n := k.comparer.Split(km.key)
km.key = append(km.key[:n:n], obj.syntheticSuffix...)
}
if k.comparer.Compare(km.key, obj.bounds.Start) >= 0 && k.comparer.Compare(km.key, obj.bounds.End) < 0 {
res = append(res, km)
}
}
return keys
return res
}

func (k *keyManager) nextMetaTimestamp() int {
Expand Down
44 changes: 35 additions & 9 deletions metamorphic/ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -925,10 +925,13 @@ type ingestExternalFilesOp struct {

type externalObjWithBounds struct {
externalObjID objID
bounds pebble.KeyRange
// We will only apply the synthetic suffix if it compares before all the
// suffixes inside the sst.

// bounds for the external object. These bounds apply after keys undergo
// any prefix or suffix transforms.
bounds pebble.KeyRange

syntheticSuffix sstable.SyntheticSuffix
prefixChange *sstable.PrefixReplacement
}

func (o *ingestExternalFilesOp) run(t *Test, h historyRecorder) {
Expand All @@ -942,7 +945,7 @@ func (o *ingestExternalFilesOp) run(t *Test, h historyRecorder) {
if m := objMeta.sstMeta; !m.HasPointKeys && !m.HasRangeKeys && !m.HasRangeDelKeys {
continue
}
if externalObjIsEmpty(t, obj.externalObjID, obj.bounds) {
if externalObjIsEmpty(t, obj.externalObjID, obj.bounds, obj.prefixChange) {
// Currently we don't support ingesting external objects that have no point
// or range keys in the given range. Filter out any such objects.
// TODO(radu): even though we don't expect this case in practice, eventually
Expand All @@ -962,7 +965,9 @@ func (o *ingestExternalFilesOp) run(t *Test, h historyRecorder) {
var paths []string
for i, obj := range objs {
// Make sure the object exists and is not empty.
path, sstMeta := buildForIngestExternalEmulation(t, o.dbID, obj.externalObjID, obj.bounds, obj.syntheticSuffix, i)
path, sstMeta := buildForIngestExternalEmulation(
t, o.dbID, obj.externalObjID, obj.bounds, obj.syntheticSuffix, obj.prefixChange, i,
)
if sstMeta.HasPointKeys || sstMeta.HasRangeKeys || sstMeta.HasRangeDelKeys {
paths = append(paths, path)
}
Expand All @@ -984,7 +989,10 @@ func (o *ingestExternalFilesOp) run(t *Test, h historyRecorder) {
HasPointKey: meta.sstMeta.HasPointKeys || meta.sstMeta.HasRangeDelKeys,
HasRangeKey: meta.sstMeta.HasRangeKeys,
SyntheticSuffix: obj.syntheticSuffix,
// TODO(radu): test prefix replacement.
}
if obj.prefixChange != nil {
external[i].ContentPrefix = obj.prefixChange.ContentPrefix
external[i].SyntheticPrefix = obj.prefixChange.SyntheticPrefix
}
}
_, err = db.IngestExternalFiles(external)
Expand All @@ -1007,12 +1015,28 @@ func (o *ingestExternalFilesOp) syncObjs() objIDSlice {
func (o *ingestExternalFilesOp) String() string {
strs := make([]string, len(o.objs))
for i, obj := range o.objs {
strs[i] = fmt.Sprintf("%s, %q, %q, %q /* syntheticSuffix */", obj.externalObjID, obj.bounds.Start, obj.bounds.End, obj.syntheticSuffix)
var contentPrefix, syntheticPrefix []byte
if obj.prefixChange != nil {
contentPrefix = obj.prefixChange.ContentPrefix
syntheticPrefix = obj.prefixChange.SyntheticPrefix
}
strs[i] = fmt.Sprintf("%s, %q /* start */, %q /* end */, %q /* syntheticSuffix */, %q /* contentPrefix */, %q /* syntheticPrefix */",
obj.externalObjID, obj.bounds.Start, obj.bounds.End, obj.syntheticSuffix,
contentPrefix, syntheticPrefix,
)
}
return fmt.Sprintf("%s.IngestExternalFiles(%s)", o.dbID, strings.Join(strs, ", "))
}

func (o *ingestExternalFilesOp) keys() []*[]byte {
// If any of the objects have synthetic prefixes, we can't allow modification
// of external object bounds.
for i := range o.objs {
if o.objs[i].prefixChange != nil {
return nil
}
}

var res []*[]byte
for i := range o.objs {
res = append(res, &o.objs[i].bounds.Start, &o.objs[i].bounds.End)
Expand Down Expand Up @@ -1275,7 +1299,7 @@ func (o *iterSetOptionsOp) String() string {
}

func iterOptions(o iterOpts) *pebble.IterOptions {
if o.IsZero() {
if o.IsZero() && !debugIterators {
return nil
}
var lower, upper []byte
Expand All @@ -1292,7 +1316,8 @@ func iterOptions(o iterOpts) *pebble.IterOptions {
RangeKeyMasking: pebble.RangeKeyMasking{
Suffix: o.maskSuffix,
},
UseL6Filters: o.useL6Filters,
UseL6Filters: o.useL6Filters,
DebugRangeKeyStack: debugIterators,
}
if opts.RangeKeyMasking.Suffix != nil {
opts.RangeKeyMasking.Filter = func() pebble.BlockPropertyFilterMask {
Expand Down Expand Up @@ -1763,6 +1788,7 @@ func (o *newExternalObjOp) run(t *Test, h historyRecorder) {
iter, rangeDelIter, rangeKeyIter,
true, /* uniquePrefixes */
nil, /* syntheticSuffix */
nil, /* prefixChange */
writable,
t.minFMV(),
)
Expand Down
25 changes: 23 additions & 2 deletions metamorphic/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package metamorphic

import (
"bytes"
"fmt"
"go/scanner"
"go/token"
Expand All @@ -14,6 +15,7 @@ import (

"github.com/cockroachdb/errors"
"github.com/cockroachdb/pebble"
"github.com/cockroachdb/pebble/sstable"
)

type methodInfo struct {
Expand Down Expand Up @@ -466,7 +468,7 @@ func (p *parser) parseCheckpointSpans(list []listElem) []pebble.CheckpointSpan {
}

func (p *parser) parseExternalObjsWithBounds(list []listElem) []externalObjWithBounds {
const numArgs = 4
const numArgs = 6
if len(list)%numArgs != 0 {
panic(p.errorf(list[0].pos, "expected number of arguments to be multiple of %d", numArgs))
}
Expand All @@ -476,13 +478,32 @@ func (p *parser) parseExternalObjsWithBounds(list []listElem) []externalObjWithB
list[1].expectToken(p, token.STRING)
list[2].expectToken(p, token.STRING)
list[3].expectToken(p, token.STRING)
list[4].expectToken(p, token.STRING)
list[5].expectToken(p, token.STRING)
objs[i] = externalObjWithBounds{
externalObjID: p.parseObjID(list[0].pos, list[0].lit),
bounds: pebble.KeyRange{
Start: unquoteBytes(list[1].lit),
End: unquoteBytes(list[2].lit),
},
syntheticSuffix: unquoteBytes(list[3].lit),
}
if syntheticSuffix := unquoteBytes(list[3].lit); len(syntheticSuffix) > 0 {
objs[i].syntheticSuffix = syntheticSuffix
}

contentPrefix := unquoteBytes(list[4].lit)
syntheticPrefix := unquoteBytes(list[5].lit)
if len(syntheticPrefix) > 0 || len(contentPrefix) > 0 {
if !bytes.HasPrefix(objs[i].bounds.Start, syntheticPrefix) {
panic(fmt.Sprintf("invalid synthetic prefix %q %q", objs[i].bounds.Start, syntheticPrefix))
}
if !bytes.HasPrefix(objs[i].bounds.End, syntheticPrefix) {
panic(fmt.Sprintf("invalid synthetic prefix %q %q", objs[i].bounds.End, syntheticPrefix))
}
objs[i].prefixChange = &sstable.PrefixReplacement{
ContentPrefix: contentPrefix,
SyntheticPrefix: syntheticPrefix,
}
}
list = list[numArgs:]
}
Expand Down

0 comments on commit 83d1741

Please sign in to comment.