Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

db: support range keys in delete-only compactions #1750

Merged
merged 2 commits into from
Jun 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 76 additions & 8 deletions compaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -1816,13 +1816,63 @@ func (d *DB) maybeScheduleCompactionPicker(
}
}

// deleteCompactionHintType indicates whether the deleteCompactionHint was
// generated from a span containing a range del (point key only), a range key
// delete (range key only), or both a point and range key.
type deleteCompactionHintType uint8

const (
// NOTE: While these are primarily used as enumeration types, they are also
// used for some bitwise operations. Care should be taken when updating.
deleteCompactionHintTypeUnknown deleteCompactionHintType = iota
deleteCompactionHintTypePointKeyOnly
deleteCompactionHintTypeRangeKeyOnly
deleteCompactionHintTypePointAndRangeKey
)

// String implements fmt.Stringer.
func (h deleteCompactionHintType) String() string {
switch h {
case deleteCompactionHintTypeUnknown:
return "unknown"
case deleteCompactionHintTypePointKeyOnly:
return "point-key-only"
case deleteCompactionHintTypeRangeKeyOnly:
return "range-key-only"
case deleteCompactionHintTypePointAndRangeKey:
return "point-and-range-key"
default:
panic(fmt.Sprintf("unknown hint type: %d", h))
}
}

// compactionHintFromKeys returns a deleteCompactionHintType given a slice of
// keyspan.Keys.
func compactionHintFromKeys(keys []keyspan.Key) deleteCompactionHintType {
var hintType deleteCompactionHintType
for _, k := range keys {
switch k.Kind() {
case base.InternalKeyKindRangeDelete:
hintType |= deleteCompactionHintTypePointKeyOnly
case base.InternalKeyKindRangeKeyDelete:
hintType |= deleteCompactionHintTypeRangeKeyOnly
default:
panic(fmt.Sprintf("unsupported key kind: %s", k.Kind()))
}
}
return hintType
}

// A deleteCompactionHint records a user key and sequence number span that has been
// deleted by a range tombstone. A hint is recorded if at least one sstable
// falls completely within both the user key and sequence number spans.
// Once the tombstones and the observed completely-contained sstables fall
// into the same snapshot stripe, a delete-only compaction may delete any
// sstables within the range.
type deleteCompactionHint struct {
// The type of key span that generated this hint (point key, range key, or
// both).
hintType deleteCompactionHintType
// start and end are user keys specifying a key range [start, end) of
// deleted keys.
start []byte
Expand Down Expand Up @@ -1850,9 +1900,12 @@ type deleteCompactionHint struct {
}

func (h deleteCompactionHint) String() string {
return fmt.Sprintf("L%d.%s %s-%s seqnums(tombstone=%d-%d, file-smallest=%d)",
return fmt.Sprintf(
"L%d.%s %s-%s seqnums(tombstone=%d-%d, file-smallest=%d, type=%s)",
h.tombstoneLevel, h.tombstoneFile.FileNum, h.start, h.end,
h.tombstoneSmallestSeqNum, h.tombstoneLargestSeqNum, h.fileSmallestSeqNum)
h.tombstoneSmallestSeqNum, h.tombstoneLargestSeqNum, h.fileSmallestSeqNum,
h.hintType,
)
}

func (h *deleteCompactionHint) canDelete(cmp Compare, m *fileMetadata, snapshots []uint64) bool {
Expand All @@ -1873,6 +1926,27 @@ func (h *deleteCompactionHint) canDelete(cmp Compare, m *fileMetadata, snapshots
return false
}

switch h.hintType {
case deleteCompactionHintTypePointKeyOnly:
// A hint generated by a range del span cannot delete tables that contain
// range keys.
if m.HasRangeKeys {
return false
}
case deleteCompactionHintTypeRangeKeyOnly:
// A hint generated by a range key del span cannot delete tables that
// contain point keys.
if m.HasPointKeys {
return false
}
case deleteCompactionHintTypePointAndRangeKey:
// A hint from a span that contains both range dels *and* range keys can
// only be deleted if both bounds fall within the hint. The next check takes
// care of this.
default:
panic(fmt.Sprintf("pebble: unknown delete compaction hint type: %d", h.hintType))
}

// The file's keys must be completely contained within the hint range.
return cmp(h.start, m.Smallest.UserKey) <= 0 && cmp(m.Largest.UserKey, h.end) < 0
}
Expand Down Expand Up @@ -1991,12 +2065,6 @@ func checkDeleteCompactionHints(
if m.Compacting || !h.canDelete(cmp, m, snapshots) || files[m] {
continue
}
if m.HasRangeKeys {
// TODO(bilal): Remove this conditional when deletion hints work well
// with sstables containing range keys.
continue
}

if files == nil {
// Construct files lazily, assuming most calls will not
// produce delete-only compactions.
Expand Down
151 changes: 99 additions & 52 deletions compaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1848,32 +1848,67 @@ func TestCompactionDeleteOnlyHints(t *testing.T) {
}()

var compactInfo *CompactionInfo // protected by d.mu
reset := func() (*Options, error) {
if d != nil {
compactInfo = nil
if err := d.Close(); err != nil {
return nil, err
}
}
opts := &Options{
FS: vfs.NewMem(),
DebugCheck: DebugCheckLevels,
EventListener: EventListener{
CompactionEnd: func(info CompactionInfo) {
if compactInfo != nil {
return
}
compactInfo = &info
},
},
FormatMajorVersion: FormatNewest,
}

// Collection of table stats can trigger compactions. As we want full
// control over when compactions are run, disable stats by default.
opts.private.disableTableStats = true

// Disable automatic compactions to prevent the range key-only tables from
// being compacted away after they are created. Compactions do not yet
// understand that these tables need to remain in the LSM.
// TODO(travers): Revisit this once compactions support range keys.
opts.DisableAutomaticCompactions = true

return opts, nil
}

compactionString := func() string {
for d.mu.compact.compactingCount > 0 {
d.mu.compact.cond.Wait()
}

s := "(none)"
if compactInfo != nil {
// Fix the job ID and durations for determinism.
compactInfo.JobID = 100
compactInfo.Duration = time.Second
compactInfo.TotalDuration = 2 * time.Second
s = compactInfo.String()
compactInfo = nil
}
return s
}

var err error
var opts *Options
datadriven.RunTest(t, "testdata/compaction_delete_only_hints",
func(td *datadriven.TestData) string {
switch td.Cmd {
case "define":
if d != nil {
compactInfo = nil
if err := d.Close(); err != nil {
return err.Error()
}
}
opts := &Options{
FS: vfs.NewMem(),
DebugCheck: DebugCheckLevels,
EventListener: EventListener{
CompactionEnd: func(info CompactionInfo) {
compactInfo = &info
},
},
opts, err = reset()
if err != nil {
return err.Error()
}

// Collection of table stats can trigger compactions. As we want full
// control over when compactions are run, disable stats by default.
opts.private.disableTableStats = true

var err error
d, err = runDBDefineCmd(td, opts)
if err != nil {
return err.Error()
Expand Down Expand Up @@ -1902,7 +1937,20 @@ func TestCompactionDeleteOnlyHints(t *testing.T) {
FileNum: base.FileNum(parseUint64(parts[1])),
}

var hintType deleteCompactionHintType
switch typ := parts[7]; typ {
case "point_key_only":
hintType = deleteCompactionHintTypePointKeyOnly
case "range_key_only":
hintType = deleteCompactionHintTypeRangeKeyOnly
case "point_and_range_key":
hintType = deleteCompactionHintTypePointAndRangeKey
default:
return fmt.Sprintf("unknown hint type: %s", typ)
}

h := deleteCompactionHint{
hintType: hintType,
start: start,
end: end,
fileSmallestSeqNum: parseUint64(parts[4]),
Expand Down Expand Up @@ -1954,9 +2002,6 @@ func TestCompactionDeleteOnlyHints(t *testing.T) {
case "maybe-compact":
d.mu.Lock()
d.maybeScheduleCompaction()
for d.mu.compact.compactingCount > 0 {
d.mu.compact.cond.Wait()
}

var buf bytes.Buffer
fmt.Fprintf(&buf, "Deletion hints:\n")
Expand All @@ -1967,15 +2012,7 @@ func TestCompactionDeleteOnlyHints(t *testing.T) {
fmt.Fprintf(&buf, " (none)\n")
}
fmt.Fprintf(&buf, "Compactions:\n")
s := "(none)"
if compactInfo != nil {
// Fix the job ID and durations for determinism.
compactInfo.JobID = 100
compactInfo.Duration = time.Second
compactInfo.TotalDuration = 2 * time.Second
s = compactInfo.String()
}
fmt.Fprintf(&buf, " %s", s)
fmt.Fprintf(&buf, " %s", compactionString())
d.mu.Unlock()
return buf.String()

Expand Down Expand Up @@ -2009,32 +2046,12 @@ func TestCompactionDeleteOnlyHints(t *testing.T) {
return err.Error()
}

compactionString := func() string {
for d.mu.compact.compactingCount > 0 {
d.mu.compact.cond.Wait()
}

s := "(none)"
if compactInfo != nil {
// Fix the job ID and durations for determinism.
compactInfo.JobID = 100
compactInfo.Duration = time.Second
compactInfo.TotalDuration = 2 * time.Second
s = compactInfo.String()
compactInfo = nil
}
return s
}

d.mu.Lock()
// Closing the snapshot may have triggered a compaction.
str := compactionString()
d.mu.Unlock()
return str

case "wait-pending-table-stats":
return runTableStatsCmd(td, d)

case "iter":
snap := Snapshot{
db: d,
Expand All @@ -2043,6 +2060,36 @@ func TestCompactionDeleteOnlyHints(t *testing.T) {
iter := snap.NewIter(nil)
return runIterCmd(td, iter, true)

case "reset":
opts, err = reset()
if err != nil {
return err.Error()
}
d, err = Open("", opts)
if err != nil {
return err.Error()
}
return ""

case "ingest":
// Compactions / flushes do not yet fully support range keys. The
// "ingest" operation exists to allow tables containing range keys to be
// added to the LSM via ingest, rather than a flush.
// TODO(travers): Revisit this once compactions support range keys.
if err = runBuildCmd(td, d, d.opts.FS); err != nil {
return err.Error()
}
if err = runIngestCmd(td, d, d.opts.FS); err != nil {
return err.Error()
}
return "OK"

case "describe-lsm":
d.mu.Lock()
s := d.mu.versions.currentVersion().String()
d.mu.Unlock()
return s

default:
return fmt.Sprintf("unknown command: %s", td.Cmd)
}
Expand Down
8 changes: 8 additions & 0 deletions data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,14 @@ func runDBDefineCmd(td *datadriven.TestData, opts *Options) (*DB, error) {
return nil, errors.Errorf("%s: could not parse %q as bool: %s", td.Cmd, arg.Vals[0], err)
}
opts.private.disableTableStats = !enable
case "block-size":
size, err := strconv.Atoi(arg.Vals[0])
if err != nil {
return nil, err
}
for _, levelOpts := range opts.Levels {
levelOpts.BlockSize = size
}
default:
return nil, errors.Errorf("%s: unknown arg: %s", td.Cmd, arg.Key)
}
Expand Down
10 changes: 10 additions & 0 deletions internal/keyspan/span.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package keyspan // import "github.com/cockroachdb/pebble/internal/keyspan"

import (
"bytes"
"fmt"
"sort"
"strconv"
Expand Down Expand Up @@ -65,6 +66,15 @@ func (k Key) Kind() base.InternalKeyKind {
return base.InternalKeyKind(k.Trailer & 0xff)
}

// Equal returns true if this Key is equal to the given key. Two keys are said
// to be equal if the two Keys have equal trailers, suffix and value. Suffix
// comparison uses the provided base.Compare func. Value comparison is bytewise.
func (k Key) Equal(cmp base.Compare, b Key) bool {
return k.Trailer == b.Trailer &&
cmp(k.Suffix, b.Suffix) == 0 &&
bytes.Equal(k.Value, b.Value)
}

// Valid returns true if the span is defined.
func (s *Span) Valid() bool {
return s.Start != nil && s.End != nil
Expand Down
Loading