0

Upgrade versions of vendored libs

This commit is contained in:
eleksir
2023-05-18 23:55:10 +03:00
parent 4869c146fe
commit 7958db9c0d
555 changed files with 36170 additions and 15565 deletions

View File

@@ -12,6 +12,7 @@ import (
"unsafe"
"github.com/cockroachdb/errors"
"github.com/cockroachdb/pebble/internal/invariants"
)
// The Annotator type defined below is used by other packages to lazily
@@ -110,7 +111,16 @@ type leafNode struct {
ref int32
count int16
leaf bool
items [maxItems]*FileMetadata
// subtreeCount holds the count of files in the entire subtree formed by
// this node. For leaf nodes, subtreeCount is always equal to count. For
// non-leaf nodes, it's the sum of count plus all the children's
// subtreeCounts.
//
// NB: We could move this field to the end of the node struct, since leaf =>
// count=subtreeCount, however the unsafe casting [leafToNode] performs make
// it risky and cumbersome.
subtreeCount int
items [maxItems]*FileMetadata
// annot contains one annotation per annotator, merged over the entire
// node's files (and all descendants for non-leaf nodes).
annot []annotation
@@ -167,7 +177,7 @@ func mut(n **node) *node {
// reference count to be greater than 1, we might be racing
// with another call to decRef on this node.
c := (*n).clone()
(*n).decRef(true /* recursive */, nil)
(*n).decRef(true /* contentsToo */, nil)
*n = c
// NB: We don't need to clear annotations, because (*node).clone does not
// copy them.
@@ -179,19 +189,25 @@ func (n *node) incRef() {
atomic.AddInt32(&n.ref, 1)
}
// decRef releases a reference to the node. If requested, the method
// will recurse into child nodes and decrease their refcounts as well.
// decRef releases a reference to the node. If requested, the method will unref
// its items and recurse into child nodes and decrease their refcounts as well.
// Some internal codepaths that manually copy the node's items or children to
// new nodes pass contentsToo=false to preserve existing reference counts during
// operations that should yield a net-zero change to descendant refcounts.
// When a node is released, its contained files are dereferenced.
func (n *node) decRef(recursive bool, obsolete *[]*FileMetadata) {
func (n *node) decRef(contentsToo bool, obsolete *[]*FileMetadata) {
if atomic.AddInt32(&n.ref, -1) > 0 {
// Other references remain. Can't free.
return
}
// Dereference the node's metadata and release child references.
if recursive {
// Dereference the node's metadata and release child references if
// requested. Some internal callers may not want to propagate the deref
// because they're manually copying the filemetadata and children to other
// nodes, and they want to preserve the existing reference count.
if contentsToo {
for _, f := range n.items[:n.count] {
if atomic.AddInt32(&f.refs, -1) == 0 {
if f.Unref() == 0 {
// There are two sources of node dereferences: tree mutations
// and Version dereferences. Files should only be made obsolete
// during Version dereferences, during which `obsolete` will be
@@ -204,7 +220,7 @@ func (n *node) decRef(recursive bool, obsolete *[]*FileMetadata) {
}
if !n.leaf {
for i := int16(0); i <= n.count; i++ {
n.children[i].decRef(true /* recursive */, obsolete)
n.children[i].decRef(true /* contentsToo */, obsolete)
}
}
}
@@ -222,9 +238,10 @@ func (n *node) clone() *node {
// triggering the race detector and looking like a data race.
c.count = n.count
c.items = n.items
c.subtreeCount = n.subtreeCount
// Increase the refcount of each contained item.
for _, f := range n.items[:n.count] {
atomic.AddInt32(&f.refs, 1)
f.Ref()
}
if !c.leaf {
// Copy children and increase each refcount.
@@ -236,6 +253,9 @@ func (n *node) clone() *node {
return c
}
// insertAt inserts the provided file and node at the provided index. This
// function is for use only as a helper function for internal B-Tree code.
// Clients should not invoke it directly.
func (n *node) insertAt(index int, item *FileMetadata, nd *node) {
if index < int(n.count) {
copy(n.items[index+1:n.count+1], n.items[index:n.count])
@@ -250,6 +270,9 @@ func (n *node) insertAt(index int, item *FileMetadata, nd *node) {
n.count++
}
// pushBack inserts the provided file and node at the tail of the node's items.
// This function is for use only as a helper function for internal B-Tree code.
// Clients should not invoke it directly.
func (n *node) pushBack(item *FileMetadata, nd *node) {
n.items[n.count] = item
if !n.leaf {
@@ -258,6 +281,9 @@ func (n *node) pushBack(item *FileMetadata, nd *node) {
n.count++
}
// pushFront inserts the provided file and node at the head of the
// node's items. This function is for use only as a helper function for internal B-Tree
// code. Clients should not invoke it directly.
func (n *node) pushFront(item *FileMetadata, nd *node) {
if !n.leaf {
copy(n.children[1:n.count+2], n.children[:n.count+1])
@@ -269,7 +295,8 @@ func (n *node) pushFront(item *FileMetadata, nd *node) {
}
// removeAt removes a value at a given index, pulling all subsequent values
// back.
// back. This function is for use only as a helper function for internal B-Tree
// code. Clients should not invoke it directly.
func (n *node) removeAt(index int) (*FileMetadata, *node) {
var child *node
if !n.leaf {
@@ -284,7 +311,9 @@ func (n *node) removeAt(index int) (*FileMetadata, *node) {
return out, child
}
// popBack removes and returns the last element in the list.
// popBack removes and returns the last element in the list. This function is
// for use only as a helper function for internal B-Tree code. Clients should
// not invoke it directly.
func (n *node) popBack() (*FileMetadata, *node) {
n.count--
out := n.items[n.count]
@@ -297,7 +326,9 @@ func (n *node) popBack() (*FileMetadata, *node) {
return out, child
}
// popFront removes and returns the first element in the list.
// popFront removes and returns the first element in the list. This function is
// for use only as a helper function for internal B-Tree code. Clients should
// not invoke it directly.
func (n *node) popFront() (*FileMetadata, *node) {
n.count--
var child *node
@@ -315,6 +346,9 @@ func (n *node) popFront() (*FileMetadata, *node) {
// find returns the index where the given item should be inserted into this
// list. 'found' is true if the item already exists in the list at the given
// index.
//
// This function is for use only as a helper function for internal B-Tree code.
// Clients should not invoke it directly.
func (n *node) find(cmp btreeCmp, item *FileMetadata) (index int, found bool) {
// Logic copied from sort.Search. Inlining this gave
// an 11% speedup on BenchmarkBTreeDeleteInsert.
@@ -338,23 +372,34 @@ func (n *node) find(cmp btreeCmp, item *FileMetadata) (index int, found bool) {
// and this function returns the item that existed at that index and a new
// node containing all items/children after it.
//
// Before:
// split is called when we want to perform a transformation like the one
// depicted in the following diagram.
//
// +-----------+
// | x y z |
// +--/-/-\-\--+
// Before:
// +-----------+
// n *node | x y z |
// +--/-/-\-\--+
//
// After:
// After:
// +-----------+
// | y | n's parent
// +----/-\----+
// / \
// v v
// +-----------+ +-----------+
// n *node | x | | z | next *node
// +-----------+ +-----------+
//
// +-----------+
// | y |
// +----/-\----+
// / \
// v v
// +-----------+ +-----------+
// | x | | z |
// +-----------+ +-----------+
// split does not perform the complete transformation; the caller is responsible
// for updating the parent appropriately. split splits `n` into two nodes, `n`
// and `next`, returning `next` and the file that separates them. In the diagram
// above, `n.split` removes y and z from `n`, returning y in the first return
// value and `next` in the second return value. The caller is responsible for
// updating n's parent to now contain `y` as the separator between nodes `n` and
// `next`.
//
// This function is for use only as a helper function for internal B-Tree code.
// Clients should not invoke it directly.
func (n *node) split(i int) (*FileMetadata, *node) {
out := n.items[i]
var next *node
@@ -370,17 +415,27 @@ func (n *node) split(i int) (*FileMetadata, *node) {
}
if !n.leaf {
copy(next.children[:], n.children[i+1:n.count+1])
descendantsMoved := 0
for j := int16(i + 1); j <= n.count; j++ {
descendantsMoved += n.children[j].subtreeCount
n.children[j] = nil
}
n.subtreeCount -= descendantsMoved
next.subtreeCount += descendantsMoved
}
n.count = int16(i)
// NB: We subtract one more than `next.count` from n's subtreeCount because
// the item at index `i` was removed from `n.items`. We'll return the item
// at index `i`, and the caller is responsible for updating the subtree
// count of whichever node adopts it.
n.subtreeCount -= int(next.count) + 1
next.subtreeCount += int(next.count)
return out, next
}
// insert inserts a item into the subtree rooted at this node, making sure no
// Insert inserts a item into the subtree rooted at this node, making sure no
// nodes in the subtree exceed maxItems items.
func (n *node) insert(cmp btreeCmp, item *FileMetadata) error {
func (n *node) Insert(cmp btreeCmp, item *FileMetadata) error {
i, found := n.find(cmp, item)
if found {
// cmp provides a total ordering of the files within a level.
@@ -391,6 +446,7 @@ func (n *node) insert(cmp btreeCmp, item *FileMetadata) error {
}
if n.leaf {
n.insertAt(i, item, nil)
n.subtreeCount++
return nil
}
if n.children[i].count >= maxItems {
@@ -410,14 +466,21 @@ func (n *node) insert(cmp btreeCmp, item *FileMetadata) error {
errors.Safe(item.FileNum), errors.Safe(n.items[i].FileNum))
}
}
return mut(&n.children[i]).insert(cmp, item)
err := mut(&n.children[i]).Insert(cmp, item)
if err == nil {
n.subtreeCount++
}
return err
}
// removeMax removes and returns the maximum item from the subtree rooted
// at this node.
// removeMax removes and returns the maximum item from the subtree rooted at
// this node. This function is for use only as a helper function for internal
// B-Tree code. Clients should not invoke it directly.
func (n *node) removeMax() *FileMetadata {
if n.leaf {
n.count--
n.subtreeCount--
out := n.items[n.count]
n.items[n.count] = nil
return out
@@ -427,16 +490,18 @@ func (n *node) removeMax() *FileMetadata {
n.rebalanceOrMerge(int(n.count))
return n.removeMax()
}
n.subtreeCount--
return child.removeMax()
}
// remove removes a item from the subtree rooted at this node. Returns
// Remove removes a item from the subtree rooted at this node. Returns
// the item that was removed or nil if no matching item was found.
func (n *node) remove(cmp btreeCmp, item *FileMetadata) (out *FileMetadata) {
func (n *node) Remove(cmp btreeCmp, item *FileMetadata) (out *FileMetadata) {
i, found := n.find(cmp, item)
if n.leaf {
if found {
out, _ = n.removeAt(i)
n.subtreeCount--
return out
}
return nil
@@ -444,22 +509,28 @@ func (n *node) remove(cmp btreeCmp, item *FileMetadata) (out *FileMetadata) {
if n.children[i].count <= minItems {
// Child not large enough to remove from.
n.rebalanceOrMerge(i)
return n.remove(cmp, item)
return n.Remove(cmp, item)
}
child := mut(&n.children[i])
if found {
// Replace the item being removed with the max item in our left child.
out = n.items[i]
n.items[i] = child.removeMax()
n.subtreeCount--
return out
}
// Latch is not in this node and child is large enough to remove from.
out = child.remove(cmp, item)
// File is not in this node and child is large enough to remove from.
out = child.Remove(cmp, item)
if out != nil {
n.subtreeCount--
}
return out
}
// rebalanceOrMerge grows child 'i' to ensure it has sufficient room to remove
// a item from it while keeping it at or above minItems.
// rebalanceOrMerge grows child 'i' to ensure it has sufficient room to remove a
// item from it while keeping it at or above minItems. This function is for use
// only as a helper function for internal B-Tree code. Clients should not invoke
// it directly.
func (n *node) rebalanceOrMerge(i int) {
switch {
case i > 0 && n.children[i-1].count > minItems:
@@ -497,6 +568,12 @@ func (n *node) rebalanceOrMerge(i int) {
yLa := n.items[i-1]
child.pushFront(yLa, grandChild)
n.items[i-1] = xLa
child.subtreeCount++
left.subtreeCount--
if grandChild != nil {
child.subtreeCount += grandChild.subtreeCount
left.subtreeCount -= grandChild.subtreeCount
}
case i < int(n.count) && n.children[i+1].count > minItems:
// Rebalance from right sibling.
@@ -532,6 +609,12 @@ func (n *node) rebalanceOrMerge(i int) {
xLa, grandChild := right.popFront()
yLa := n.items[i]
child.pushBack(yLa, grandChild)
child.subtreeCount++
right.subtreeCount--
if grandChild != nil {
child.subtreeCount += grandChild.subtreeCount
right.subtreeCount -= grandChild.subtreeCount
}
n.items[i] = xLa
default:
@@ -570,12 +653,15 @@ func (n *node) rebalanceOrMerge(i int) {
copy(child.children[child.count+1:], mergeChild.children[:mergeChild.count+1])
}
child.count += mergeChild.count + 1
child.subtreeCount += mergeChild.subtreeCount + 1
mergeChild.decRef(false /* recursive */, nil)
mergeChild.decRef(false /* contentsToo */, nil)
}
}
func (n *node) invalidateAnnotation(a Annotator) {
// InvalidateAnnotation removes any existing cached annotations for the provided
// annotator from this node's subtree.
func (n *node) InvalidateAnnotation(a Annotator) {
// Find this annotator's annotation on this node.
var annot *annotation
for i := range n.annot {
@@ -590,12 +676,17 @@ func (n *node) invalidateAnnotation(a Annotator) {
}
if !n.leaf {
for i := int16(0); i <= n.count; i++ {
n.children[i].invalidateAnnotation(a)
n.children[i].InvalidateAnnotation(a)
}
}
}
func (n *node) annotation(a Annotator) (interface{}, bool) {
// Annotation retrieves, computing if not already computed, the provided
// annotator's annotation of this node. The second return value indicates
// whether the future reads of this annotation may use the first return value
// as-is. If false, the annotation is not stable and may change on a subsequent
// computation.
func (n *node) Annotation(a Annotator) (interface{}, bool) {
// Find this annotator's annotation on this node.
var annot *annotation
for i := range n.annot {
@@ -627,7 +718,7 @@ func (n *node) annotation(a Annotator) (interface{}, bool) {
annot.valid = true
for i := int16(0); i <= n.count; i++ {
if !n.leaf {
v, ok := n.children[i].annotation(a)
v, ok := n.children[i].Annotation(a)
annot.v = a.Merge(v, annot.v)
annot.valid = annot.valid && ok
}
@@ -640,6 +731,20 @@ func (n *node) annotation(a Annotator) (interface{}, bool) {
return annot.v, annot.valid
}
func (n *node) verifyInvariants() {
recomputedSubtreeCount := int(n.count)
if !n.leaf {
for i := int16(0); i <= n.count; i++ {
n.children[i].verifyInvariants()
recomputedSubtreeCount += n.children[i].subtreeCount
}
}
if recomputedSubtreeCount != n.subtreeCount {
panic(fmt.Sprintf("recomputed subtree count (%d) ≠ n.subtreeCount (%d)",
recomputedSubtreeCount, n.subtreeCount))
}
}
// btree is an implementation of a B-Tree.
//
// btree stores FileMetadata in an ordered structure, allowing easy insertion,
@@ -650,25 +755,23 @@ func (n *node) annotation(a Annotator) (interface{}, bool) {
// Write operations are not safe for concurrent mutation by multiple
// goroutines, but Read operations are.
type btree struct {
root *node
length int
cmp btreeCmp
root *node
cmp btreeCmp
}
// release dereferences and clears the root node of the btree, removing all
// Release dereferences and clears the root node of the btree, removing all
// items from the btree. In doing so, it decrements contained file counts.
// It returns a slice of newly obsolete files, if any.
func (t *btree) release() (obsolete []*FileMetadata) {
func (t *btree) Release() (obsolete []*FileMetadata) {
if t.root != nil {
t.root.decRef(true /* recursive */, &obsolete)
t.root.decRef(true /* contentsToo */, &obsolete)
t.root = nil
}
t.length = 0
return obsolete
}
// clone clones the btree, lazily. It does so in constant time.
func (t *btree) clone() btree {
// Clone clones the btree, lazily. It does so in constant time.
func (t *btree) Clone() btree {
c := *t
if c.root != nil {
// Incrementing the reference count on the root node is sufficient to
@@ -690,15 +793,17 @@ func (t *btree) clone() btree {
return c
}
// delete removes the provided file from the tree.
// Delete removes the provided file from the tree.
// It returns true if the file now has a zero reference count.
func (t *btree) delete(item *FileMetadata) (obsolete bool) {
func (t *btree) Delete(item *FileMetadata) (obsolete bool) {
if t.root == nil || t.root.count == 0 {
return false
}
if out := mut(&t.root).remove(t.cmp, item); out != nil {
t.length--
obsolete = atomic.AddInt32(&out.refs, -1) == 0
if out := mut(&t.root).Remove(t.cmp, item); out != nil {
obsolete = out.Unref() == 0
}
if invariants.Enabled {
t.root.verifyInvariants()
}
if t.root.count == 0 {
old := t.root
@@ -707,14 +812,14 @@ func (t *btree) delete(item *FileMetadata) (obsolete bool) {
} else {
t.root = t.root.children[0]
}
old.decRef(false /* recursive */, nil)
old.decRef(false /* contentsToo */, nil)
}
return obsolete
}
// insert adds the given item to the tree. If a item in the tree already
// equals the given one, insert panics.
func (t *btree) insert(item *FileMetadata) error {
// Insert adds the given item to the tree. If a item in the tree already
// equals the given one, Insert panics.
func (t *btree) Insert(item *FileMetadata) error {
if t.root == nil {
t.root = newLeafNode()
} else if t.root.count >= maxItems {
@@ -724,39 +829,36 @@ func (t *btree) insert(item *FileMetadata) error {
newRoot.items[0] = splitLa
newRoot.children[0] = t.root
newRoot.children[1] = splitNode
newRoot.subtreeCount = t.root.subtreeCount + splitNode.subtreeCount + 1
t.root = newRoot
}
atomic.AddInt32(&item.refs, 1)
err := mut(&t.root).insert(t.cmp, item)
t.length++
item.Ref()
err := mut(&t.root).Insert(t.cmp, item)
if invariants.Enabled {
t.root.verifyInvariants()
}
return err
}
// iter returns a new iterator object. It is not safe to continue using an
// Iter returns a new iterator object. It is not safe to continue using an
// iterator after modifications are made to the tree. If modifications are made,
// create a new iterator.
func (t *btree) iter() iterator {
func (t *btree) Iter() iterator {
return iterator{r: t.root, pos: -1, cmp: t.cmp}
}
// height returns the height of the tree.
func (t *btree) height() int {
// Count returns the number of files contained within the B-Tree.
func (t *btree) Count() int {
if t.root == nil {
return 0
}
h := 1
n := t.root
for !n.leaf {
n = n.children[0]
h++
}
return h
return t.root.subtreeCount
}
// String returns a string description of the tree. The format is
// similar to the https://en.wikipedia.org/wiki/Newick_format.
func (t *btree) String() string {
if t.length == 0 {
if t.Count() == 0 {
return ";"
}
var b strings.Builder
@@ -873,6 +975,8 @@ type iterator struct {
r *node
// n and pos make up the current position of the iterator.
// If valid, n.items[pos] is the current value of the iterator.
//
// n may be nil iff i.r is nil.
n *node
pos int16
// cmp dictates the ordering of the FileMetadata.
@@ -883,6 +987,59 @@ type iterator struct {
s iterStack
}
// countLeft returns the count of files that are to the left of the current
// iterator position.
func (i *iterator) countLeft() int {
if i.r == nil {
return 0
}
// Each iterator has a stack of frames marking the path from the root node
// to the current iterator position. All files (n.items) and all subtrees
// (n.children) with indexes less than [pos] are to the left of the current
// iterator position.
//
// +------------------------+ -
// | Root pos:5 | |
// +------------------------+ | stack
// | Root/5 pos:3 | | frames
// +------------------------+ | [i.s]
// | Root/5/3 pos:9 | |
// +========================+ -
// | |
// | i.n: Root/5/3/9 i.pos:2|
// +------------------------+
//
var count int
// Walk all the ancestors in the iterator stack [i.s], tallying up all the
// files and subtrees to the left of the stack frame's position.
f, ok := i.s.nth(0)
for fi := 0; ok; fi++ {
// There are [f.pos] files contained within [f.n.items] that sort to the
// left of the subtree the iterator has descended.
count += int(f.pos)
// Any subtrees that fall before the stack frame's position are entirely
// to the left of the iterator's current position.
for j := int16(0); j < f.pos; j++ {
count += f.n.children[j].subtreeCount
}
f, ok = i.s.nth(fi + 1)
}
// The bottommost stack frame is inlined within the iterator struct. Again,
// [i.pos] files fall to the left of the current iterator position.
count += int(i.pos)
if !i.n.leaf {
// NB: Unlike above, we use a `<= i.pos` comparison. The iterator is
// positioned at item `i.n.items[i.pos]`, which sorts after everything
// in the subtree at `i.n.children[i.pos]`.
for j := int16(0); j <= i.pos; j++ {
count += i.n.children[j].subtreeCount
}
}
return count
}
func (i *iterator) clone() iterator {
c := *i
c.s = i.s.clone()
@@ -904,7 +1061,7 @@ func (i iterator) String() string {
}
fmt.Fprintf(&buf, "%p: %02d/%02d\n", f.n, f.pos, f.n.count)
}
if i.n == nil {
if i.r == nil {
fmt.Fprintf(&buf, "<nil>: %02d", i.pos)
} else {
fmt.Fprintf(&buf, "%p: %02d/%02d", i.n, i.pos, i.n.count)
@@ -1034,7 +1191,7 @@ func (i *iterator) ascend() {
// tree's files, and then true for the (possibly empty) remainder.
func (i *iterator) seek(fn func(*FileMetadata) bool) {
i.reset()
if i.n == nil {
if i.r == nil {
return
}
@@ -1066,7 +1223,7 @@ func (i *iterator) seek(fn func(*FileMetadata) bool) {
// first seeks to the first item in the btree.
func (i *iterator) first() {
i.reset()
if i.n == nil {
if i.r == nil {
return
}
for !i.n.leaf {
@@ -1078,7 +1235,7 @@ func (i *iterator) first() {
// last seeks to the last item in the btree.
func (i *iterator) last() {
i.reset()
if i.n == nil {
if i.r == nil {
return
}
for !i.n.leaf {
@@ -1090,7 +1247,7 @@ func (i *iterator) last() {
// next positions the iterator to the item immediately following
// its current position.
func (i *iterator) next() {
if i.n == nil {
if i.r == nil {
return
}
@@ -1117,7 +1274,7 @@ func (i *iterator) next() {
// prev positions the iterator to the item immediately preceding
// its current position.
func (i *iterator) prev() {
if i.n == nil {
if i.r == nil {
return
}
@@ -1148,5 +1305,8 @@ func (i *iterator) valid() bool {
// cur returns the item at the iterator's current position. It is illegal
// to call cur if the iterator is not valid.
func (i *iterator) cur() *FileMetadata {
if invariants.Enabled && !i.valid() {
panic("btree iterator.cur invoked on invalid iterator")
}
return i.n.items[i.pos]
}

View File

@@ -112,13 +112,15 @@ func sortAndSweep(keys []intervalKeyTemp, cmp Compare) []intervalKeyTemp {
sorter := intervalKeySorter{keys: keys, cmp: cmp}
sort.Sort(sorter)
// intervalKeys are generated using the file bounds. Specifically, there are 2 intervalKeys
// for each file, and len(keys) = 2 * number of files. Each intervalKeyTemp stores information
// about which file it was generated from, and whether the key represents the end key of the file.
// So, as we're deduplicating the `keys` slice, we're guaranteed to iterate over the interval
// keys belonging to each of the files. Since the file.{min,max}IntervalIndex points to the position
// of the files bounds in the deduplicated `keys` slice, we can determine file.{min,max}IntervalIndex
// during the iteration.
// intervalKeys are generated using the file bounds. Specifically, there are
// 2 intervalKeys for each file, and len(keys) = 2 * number of files. Each
// `intervalKeyTemp` stores information about which file it was generated
// from, and whether the key represents the end key of the file. So, as
// we're deduplicating the `keys` slice, we're guaranteed to iterate over
// the interval keys belonging to each of the files. Since the
// file.{min,max}IntervalIndex points to the position of the files bounds in
// the deduplicated `keys` slice, we can determine
// file.{min,max}IntervalIndex during the iteration.
i := 0
j := 0
for i < len(keys) {
@@ -151,24 +153,24 @@ type fileInterval struct {
// cannot have any files participate in L0 -> Lbase compactions.
isBaseCompacting bool
// The min and max intervals index across all the files that overlap with this
// interval. Inclusive on both sides.
// The min and max intervals index across all the files that overlap with
// this interval. Inclusive on both sides.
filesMinIntervalIndex int
filesMaxIntervalIndex int
// True if another interval that has a file extending into this interval is
// undergoing a compaction into Lbase. In other words, this bool is true
// if any interval in [filesMinIntervalIndex,
// filesMaxIntervalIndex] has isBaseCompacting set to true. This
// lets the compaction picker de-prioritize this interval for picking
// compactions, since there's a high chance that a base compaction with a
// sufficient height of sublevels rooted at this interval could not be
// chosen due to the ongoing base compaction in the
// other interval. If the file straddling the two intervals is at a
// sufficiently high sublevel (with enough compactible files below it to
// satisfy minCompactionDepth), this is not an issue, but to optimize for
// quickly picking base compactions far away from other base compactions,
// this bool is used as a heuristic (but not as a complete disqualifier).
// undergoing a compaction into Lbase. In other words, this bool is true if
// any interval in [filesMinIntervalIndex, filesMaxIntervalIndex] has
// isBaseCompacting set to true. This lets the compaction picker
// de-prioritize this interval for picking compactions, since there's a high
// chance that a base compaction with a sufficient height of sublevels
// rooted at this interval could not be chosen due to the ongoing base
// compaction in the other interval. If the file straddling the two
// intervals is at a sufficiently high sublevel (with enough compactible
// files below it to satisfy minCompactionDepth), this is not an issue, but
// to optimize for quickly picking base compactions far away from other base
// compactions, this bool is used as a heuristic (but not as a complete
// disqualifier).
intervalRangeIsBaseCompacting bool
// All files in this interval, in increasing sublevel order.
@@ -178,9 +180,8 @@ type fileInterval struct {
// starting new compactions. This metric is not precise since the
// compactingFileCount can include files that are part of N (where N > 1)
// intra-L0 compactions, so the stack depth after those complete will be
// len(files) - compactingFileCount + N. We ignore this imprecision since
// we don't want to track which files are part of which intra-L0
// compaction.
// len(files) - compactingFileCount + N. We ignore this imprecision since we
// don't want to track which files are part of which intra-L0 compaction.
compactingFileCount int
// Interpolated from files in this interval. For files spanning multiple
@@ -332,17 +333,17 @@ func NewL0Sublevels(
for _, sublevelFiles := range s.levelFiles {
tr, ls := makeBTree(btreeCmpSmallestKey(cmp), sublevelFiles)
s.Levels = append(s.Levels, ls)
tr.release()
tr.Release()
}
s.calculateFlushSplitKeys(flushSplitMaxBytes)
return s, nil
}
// Helper function to merge new intervalKeys into an existing slice
// of old fileIntervals, into result. Returns the new result and a slice of ints
// mapping old interval indices to new ones. The added intervalKeys do not
// need to be sorted; they get sorted and deduped in this function.
// Helper function to merge new intervalKeys into an existing slice of old
// fileIntervals, into result. Returns the new result and a slice of ints
// mapping old interval indices to new ones. The added intervalKeys do not need
// to be sorted; they get sorted and deduped in this function.
func mergeIntervals(
old, result []fileInterval, added []intervalKeyTemp, compare Compare,
) ([]fileInterval, []int) {
@@ -413,35 +414,36 @@ func mergeIntervals(
return result, oldToNewMap
}
// AddL0Files incrementally builds a new L0Sublevels for when the only
// change since the receiver L0Sublevels was an addition of the specified files,
// with no L0 deletions. The common case of this is an ingestion or a flush.
// These files can "sit on top" of existing sublevels, creating at most one
// new sublevel for a flush (and possibly multiple for an ingestion), and at
// most 2*len(files) additions to s.orderedIntervals. No files must have been
// deleted from L0, and the added files must all be newer in sequence numbers
// than existing files in L0Sublevels. The files parameter must be sorted in
// seqnum order. The levelMetadata parameter corresponds to the new L0 post
// addition of files. This method is meant to be significantly more performant
// than NewL0Sublevels.
// AddL0Files incrementally builds a new L0Sublevels for when the only change
// since the receiver L0Sublevels was an addition of the specified files, with
// no L0 deletions. The common case of this is an ingestion or a flush. These
// files can "sit on top" of existing sublevels, creating at most one new
// sublevel for a flush (and possibly multiple for an ingestion), and at most
// 2*len(files) additions to s.orderedIntervals. No files must have been deleted
// from L0, and the added files must all be newer in sequence numbers than
// existing files in L0Sublevels. The files parameter must be sorted in seqnum
// order. The levelMetadata parameter corresponds to the new L0 post addition of
// files. This method is meant to be significantly more performant than
// NewL0Sublevels.
//
// Note that this function can only be called once on a given receiver; it
// appends to some slices in s which is only safe when done once. This is okay,
// as the common case (generating a new L0Sublevels after a flush/ingestion) is
// only going to necessitate one call of this method on a given receiver. The
// returned value, if non-nil, can then have AddL0Files called on it again, and
// so on. If errInvalidL0SublevelsOpt is returned as an error, it likely means
// the optimization could not be applied (i.e. files added were older than files
// already in the sublevels, which is possible around ingestions and in tests).
// Eg. it can happen when an ingested file was ingested without queueing a flush
// since it did not actually overlap with any keys in the memtable. Later on the
// memtable was flushed, and the memtable had keys spanning around the ingested
// file, producing a flushed file that overlapped with the ingested file in file
// bounds but not in keys. It's possible for that flushed file to have a lower
// LargestSeqNum than the ingested file if all the additions after the ingestion
// were to another flushed file that was split into a separate sstable during
// flush. Any other non-nil error means L0Sublevels generation failed in the same
// way as NewL0Sublevels would likely fail.
// returned value, if non-nil, can then have [*L0Sublevels.AddL0Files] called on
// it again, and so on. If [errInvalidL0SublevelsOpt] is returned as an error,
// it likely means the optimization could not be applied (i.e. files added were
// older than files already in the sublevels, which is possible around
// ingestions and in tests). Eg. it can happen when an ingested file was
// ingested without queueing a flush since it did not actually overlap with any
// keys in the memtable. Later on the memtable was flushed, and the memtable had
// keys spanning around the ingested file, producing a flushed file that
// overlapped with the ingested file in file bounds but not in keys. It's
// possible for that flushed file to have a lower LargestSeqNum than the
// ingested file if all the additions after the ingestion were to another
// flushed file that was split into a separate sstable during flush. Any other
// non-nil error means [L0Sublevels] generation failed in the same way as
// [NewL0Sublevels] would likely fail.
func (s *L0Sublevels) AddL0Files(
files []*FileMetadata, flushSplitMaxBytes int64, levelMetadata *LevelMetadata,
) (*L0Sublevels, error) {
@@ -485,8 +487,8 @@ func (s *L0Sublevels) AddL0Files(
keys := make([]fileInterval, 0, 2*levelMetadata.Len())
var oldToNewMap []int
// We can avoid the sortAndSweep step on the combined length of
// s.orderedIntervals and fileKeys by treating this as a merge of two
// sorted runs, fileKeys and s.orderedIntervals, into `keys` which will form
// s.orderedIntervals and fileKeys by treating this as a merge of two sorted
// runs, fileKeys and s.orderedIntervals, into `keys` which will form
// newVal.orderedIntervals.
keys, oldToNewMap = mergeIntervals(s.orderedIntervals, keys, fileKeys, s.cmp)
if invariants.Enabled {
@@ -502,20 +504,21 @@ func (s *L0Sublevels) AddL0Files(
newInterval := &keys[newIdx]
newInterval.index = newIdx
// This code, and related code in the for loop below, adjusts
// files{Min,Max}IntervalIndex just for interval indices shifting due to new
// intervals, and not for any of the new files being added to the same
// intervals. The goal is to produce a state of the system that's accurate
// for all existing files, and has all the new intervals to support new
// files. Once that's done, we can just call addFileToSublevel to adjust
// all relevant intervals for new files.
// files{Min,Max}IntervalIndex just for interval indices shifting due to
// new intervals, and not for any of the new files being added to the
// same intervals. The goal is to produce a state of the system that's
// accurate for all existing files, and has all the new intervals to
// support new files. Once that's done, we can just call
// addFileToSublevel to adjust all relevant intervals for new files.
newInterval.filesMinIntervalIndex = oldToNewMap[newInterval.filesMinIntervalIndex]
// maxIntervalIndexes are special. Since it's an inclusive end bound, we
// actually have to map it to the _next_ old interval's new previous
// interval. This logic is easier to understand if you see
// [f.minIntervalIndex, f.maxIntervalIndex] as [f.minIntervalIndex,
// f.maxIntervalIndex+1). The other case to remember is when the interval is
// completely empty (i.e. len(newInterval.files) == 0); in that case we want
// to refer back to ourselves regardless of additions to the right of us.
// f.maxIntervalIndex+1). The other case to remember is when the
// interval is completely empty (i.e. len(newInterval.files) == 0); in
// that case we want to refer back to ourselves regardless of additions
// to the right of us.
if newInterval.filesMaxIntervalIndex < len(oldToNewMap)-1 && len(newInterval.files) > 0 {
newInterval.filesMaxIntervalIndex = oldToNewMap[newInterval.filesMaxIntervalIndex+1] - 1
} else {
@@ -523,16 +526,16 @@ func (s *L0Sublevels) AddL0Files(
newInterval.filesMaxIntervalIndex = oldToNewMap[newInterval.filesMaxIntervalIndex]
}
}
// Loop through all instances of new intervals added between two old intervals
// and expand [filesMinIntervalIndex, filesMaxIntervalIndex] of new intervals
// to reflect that of adjacent old intervals.
// Loop through all instances of new intervals added between two old
// intervals and expand [filesMinIntervalIndex, filesMaxIntervalIndex] of
// new intervals to reflect that of adjacent old intervals.
{
// We can skip cases where new intervals were added to the left of all
// existing intervals (eg. if the first entry in oldToNewMap is
// oldToNewMap[0] >= 1). Those intervals will only contain newly added files
// and will have their parameters adjusted down in addFileToSublevels. The
// same can also be said about new intervals that are to the right of all
// existing intervals.
// oldToNewMap[0] >= 1). Those intervals will only contain newly added
// files and will have their parameters adjusted down in
// addFileToSublevels. The same can also be said about new intervals
// that are to the right of all existing intervals.
lastIdx := 0
for _, newIdx := range oldToNewMap {
for i := lastIdx + 1; i < newIdx; i++ {
@@ -555,28 +558,29 @@ func (s *L0Sublevels) AddL0Files(
// TODO(bilal): This is the only place in this method where we loop through
// all existing files, which could be much more in number than newly added
// files. See if we can avoid the need for this, either by getting rid of
// f.minIntervalIndex and f.maxIntervalIndex and calculating them on the
// fly with a binary search, or by only looping through files to the right
// of the first interval touched by this method.
// f.minIntervalIndex and f.maxIntervalIndex and calculating them on the fly
// with a binary search, or by only looping through files to the right of
// the first interval touched by this method.
for sublevel := range s.Levels {
s.Levels[sublevel].Each(func(f *FileMetadata) {
oldIntervalDelta := f.maxIntervalIndex - f.minIntervalIndex + 1
oldMinIntervalIndex := f.minIntervalIndex
f.minIntervalIndex = oldToNewMap[f.minIntervalIndex]
// maxIntervalIndex is special. Since it's an inclusive end bound, we
// actually have to map it to the _next_ old interval's new previous
// interval. This logic is easier to understand if you see
// [f.minIntervalIndex, f.maxIntervalIndex] as
// [f.minIntervalIndex, f.maxIntervalIndex+1).
// maxIntervalIndex is special. Since it's an inclusive end bound,
// we actually have to map it to the _next_ old interval's new
// previous interval. This logic is easier to understand if you see
// [f.minIntervalIndex, f.maxIntervalIndex] as [f.minIntervalIndex,
// f.maxIntervalIndex+1).
f.maxIntervalIndex = oldToNewMap[f.maxIntervalIndex+1] - 1
newIntervalDelta := f.maxIntervalIndex - f.minIntervalIndex + 1
// Recalculate estimatedBytes for all old files across new intervals, but
// only if new intervals were added in between.
// Recalculate estimatedBytes for all old files across new
// intervals, but only if new intervals were added in between.
if oldIntervalDelta != newIntervalDelta {
// j is incremented so that oldToNewMap[j] points to the next old
// interval. This is used to distinguish between old intervals (i.e.
// ones where we need to subtract f.Size/oldIntervalDelta) from new
// ones (where we don't need to subtract). In both cases we need to add
// j is incremented so that oldToNewMap[j] points to the next
// old interval. This is used to distinguish between old
// intervals (i.e. ones where we need to subtract
// f.Size/oldIntervalDelta) from new ones (where we don't need
// to subtract). In both cases we need to add
// f.Size/newIntervalDelta.
j := oldMinIntervalIndex
for i := f.minIntervalIndex; i <= f.maxIntervalIndex; i++ {
@@ -628,7 +632,7 @@ func (s *L0Sublevels) AddL0Files(
// populated correctly.
newVal.Levels[sublevel] = ls
}
tr.release()
tr.Release()
}
newVal.flushSplitUserKeys = nil
@@ -636,12 +640,12 @@ func (s *L0Sublevels) AddL0Files(
return newVal, nil
}
// addFileToSublevels is called during L0Sublevels generation, and adds f to
// the correct sublevel's levelFiles, the relevant intervals' files slices, and
// sets interval indices on f. This method, if called successively on multiple
// files, _must_ be called on successively newer files (by seqnum). If
// checkInvariant is true, it could check for this in some cases and return
// errInvalidL0SublevelsOpt if that invariant isn't held.
// addFileToSublevels is called during L0Sublevels generation, and adds f to the
// correct sublevel's levelFiles, the relevant intervals' files slices, and sets
// interval indices on f. This method, if called successively on multiple files,
// _must_ be called on successively newer files (by seqnum). If checkInvariant
// is true, it could check for this in some cases and return
// [errInvalidL0SublevelsOpt] if that invariant isn't held.
func (s *L0Sublevels) addFileToSublevels(f *FileMetadata, checkInvariant bool) error {
// This is a simple and not very accurate estimate of the number of
// bytes this SSTable contributes to the intervals it is a part of.
@@ -654,14 +658,18 @@ func (s *L0Sublevels) addFileToSublevels(f *FileMetadata, checkInvariant bool) e
// Update state in every fileInterval for this file.
for i := f.minIntervalIndex; i <= f.maxIntervalIndex; i++ {
interval := &s.orderedIntervals[i]
if len(interval.files) > 0 &&
subLevel <= interval.files[len(interval.files)-1].SubLevel {
if len(interval.files) > 0 {
if checkInvariant && interval.files[len(interval.files)-1].LargestSeqNum > f.LargestSeqNum {
// We are sliding this file "underneath" an existing file. Throw away
// and start over in NewL0Sublevels.
return errInvalidL0SublevelsOpt
}
subLevel = interval.files[len(interval.files)-1].SubLevel + 1
// interval.files is sorted by sublevels, from lowest to highest.
// AddL0Files can only add files at sublevels higher than existing files
// in the same key intervals.
if maxSublevel := interval.files[len(interval.files)-1].SubLevel; subLevel <= maxSublevel {
subLevel = maxSublevel + 1
}
}
interval.estimatedBytes += interpolatedBytes
if f.minIntervalIndex < interval.filesMinIntervalIndex {
@@ -738,11 +746,10 @@ func (s *L0Sublevels) InitCompactingFileInfo(inProgress []L0Compaction) {
}
}
// Some intervals may be base compacting without the files contained
// within those intervals being marked as compacting. This is possible if
// the files were added after the compaction initiated, and the active
// compaction files straddle the input file. Mark these intervals as base
// compacting.
// Some intervals may be base compacting without the files contained within
// those intervals being marked as compacting. This is possible if the files
// were added after the compaction initiated, and the active compaction
// files straddle the input file. Mark these intervals as base compacting.
for _, c := range inProgress {
startIK := intervalKey{key: c.Smallest.UserKey, isLargest: false}
endIK := intervalKey{key: c.Largest.UserKey, isLargest: !c.Largest.IsExclusiveSentinel()}
@@ -924,35 +931,35 @@ func (s *L0Sublevels) InUseKeyRanges(smallest, largest []byte) []UserKeyRange {
curr = &keyRanges[len(keyRanges)-1]
}
// If the filesMaxIntervalIndex is not the current index, we can jump
// to the max index, knowing that all intermediary intervals are
// overlapped by some file.
// If the filesMaxIntervalIndex is not the current index, we can jump to
// the max index, knowing that all intermediary intervals are overlapped
// by some file.
if maxIdx := s.orderedIntervals[i].filesMaxIntervalIndex; maxIdx != i {
// Note that end may be less than or equal to maxIdx if we're
// concerned with a key range that ends before the interval at
// maxIdx starts. We must set curr.End now, before making that
// leap, because this iteration may be the last.
// maxIdx starts. We must set curr.End now, before making that leap,
// because this iteration may be the last.
i = maxIdx
curr.End = s.orderedIntervals[i+1].startKey.key
continue
}
// No files overlapping with this interval overlap with the next
// interval. Update the current end to be the next interval's start
// key. Note that curr is not necessarily finished, because there may
// be an abutting non-empty interval.
// interval. Update the current end to be the next interval's start key.
// Note that curr is not necessarily finished, because there may be an
// abutting non-empty interval.
curr.End = s.orderedIntervals[i+1].startKey.key
i++
}
return keyRanges
}
// FlushSplitKeys returns a slice of user keys to split flushes at.
// Used by flushes to avoid writing sstables that straddle these split keys.
// These should be interpreted as the keys to start the next sstable (not the
// last key to include in the prev sstable). These are user keys so that
// range tombstones can be properly truncated (untruncated range tombstones
// are not permitted for L0 files).
// FlushSplitKeys returns a slice of user keys to split flushes at. Used by
// flushes to avoid writing sstables that straddle these split keys. These
// should be interpreted as the keys to start the next sstable (not the last key
// to include in the prev sstable). These are user keys so that range tombstones
// can be properly truncated (untruncated range tombstones are not permitted for
// L0 files).
func (s *L0Sublevels) FlushSplitKeys() [][]byte {
return s.flushSplitUserKeys
}
@@ -978,6 +985,7 @@ func (s *L0Sublevels) MaxDepthAfterOngoingCompactions() int {
//
// TODO(bilal): Simplify away the debugging statements in this method, and make
// this a pure sanity checker.
//
//lint:ignore U1000 - useful for debugging
func (s *L0Sublevels) checkCompaction(c *L0CompactionFiles) error {
includedFiles := newBitSet(s.levelMetadata.Len())
@@ -1074,7 +1082,7 @@ func (s *L0Sublevels) checkCompaction(c *L0CompactionFiles) error {
// recently started compaction. isBase specifies if this is a base compaction;
// if false, this is assumed to be an intra-L0 compaction. The specified
// compaction must be involving L0 SSTables. It's assumed that the Compacting
// and IsIntraL0Compacting fields are already set on all FileMetadatas passed
// and IsIntraL0Compacting fields are already set on all [FileMetadata]s passed
// in.
func (s *L0Sublevels) UpdateStateForStartedCompaction(inputs []LevelSlice, isBase bool) error {
minIntervalIndex := -1
@@ -1106,11 +1114,10 @@ func (s *L0Sublevels) UpdateStateForStartedCompaction(inputs []LevelSlice, isBas
return nil
}
// L0CompactionFiles represents a candidate set of L0 files for compaction.
// Also referred to as "lcf". Contains state information useful
// for generating the compaction (such as Files), as well as for picking
// between candidate compactions (eg. fileBytes and
// seedIntervalStackDepthReduction).
// L0CompactionFiles represents a candidate set of L0 files for compaction. Also
// referred to as "lcf". Contains state information useful for generating the
// compaction (such as Files), as well as for picking between candidate
// compactions (eg. fileBytes and seedIntervalStackDepthReduction).
type L0CompactionFiles struct {
Files []*FileMetadata
@@ -1146,6 +1153,23 @@ type L0CompactionFiles struct {
filesAdded []*FileMetadata
}
// Clone allocates a new L0CompactionFiles, with the same underlying data. Note
// that the two fileMetadata slices contain values that point to the same
// underlying fileMetadata object. This is safe because these objects are read
// only.
func (l *L0CompactionFiles) Clone() *L0CompactionFiles {
oldLcf := *l
return &oldLcf
}
// String merely prints the starting address of the first file, if it exists.
func (l *L0CompactionFiles) String() string {
if len(l.Files) > 0 {
return fmt.Sprintf("First File Address: %p", &l.Files[0])
}
return ""
}
// addFile adds the specified file to the LCF.
func (l *L0CompactionFiles) addFile(f *FileMetadata) {
if l.FilesIncluded[f.L0Index] {
@@ -1180,12 +1204,11 @@ func (is intervalSorterByDecreasingScore) Swap(i, j int) {
// Compactions:
//
// The sub-levels and intervals can be visualized in 2 dimensions as the X
// axis containing intervals in increasing order and the Y axis containing
// sub-levels (older to younger). The intervals can be sparse wrt sub-levels.
// We observe that the system is typically under severe pressure in L0 during
// large numbers of ingestions where most files added to L0 are narrow and
// non-overlapping.
// The sub-levels and intervals can be visualized in 2 dimensions as the X axis
// containing intervals in increasing order and the Y axis containing sub-levels
// (older to younger). The intervals can be sparse wrt sub-levels. We observe
// that the system is typically under severe pressure in L0 during large numbers
// of ingestions where most files added to L0 are narrow and non-overlapping.
//
// L0.1 d---g
// L0.0 c--e g--j o--s u--x
@@ -1203,23 +1226,23 @@ func (is intervalSorterByDecreasingScore) Swap(i, j int) {
// it amenable to concurrent L0 -> Lbase compactions.
//
// L0 -> Lbase: The high-level goal of a L0 -> Lbase compaction is to reduce
// stack depth, by compacting files in the intervals with the highest
// (fileCount - compactingCount). Additionally, we would like compactions to
// not involve a huge number of files, so that they finish quickly, and to
// allow for concurrent L0 -> Lbase compactions when needed. In order to
// achieve these goals we would like compactions to visualize as capturing
// thin and tall rectangles. The approach below is to consider intervals in
// some order and then try to construct a compaction using the interval. The
// first interval we can construct a compaction for is the compaction that is
// started. There can be multiple heuristics in choosing the ordering of the
// intervals -- the code uses one heuristic that worked well for a large
// ingestion stemming from a cockroachdb import, but additional experimentation
// is necessary to pick a general heuristic. Additionally, the compaction that
// gets picked may be not as desirable as one that could be constructed later
// in terms of reducing stack depth (since adding more files to the compaction
// can get blocked by needing to encompass files that are already being
// compacted). So an alternative would be to try to construct more than one
// compaction and pick the best one.
// stack depth, by compacting files in the intervals with the highest (fileCount
// - compactingCount). Additionally, we would like compactions to not involve a
// huge number of files, so that they finish quickly, and to allow for
// concurrent L0 -> Lbase compactions when needed. In order to achieve these
// goals we would like compactions to visualize as capturing thin and tall
// rectangles. The approach below is to consider intervals in some order and
// then try to construct a compaction using the interval. The first interval we
// can construct a compaction for is the compaction that is started. There can
// be multiple heuristics in choosing the ordering of the intervals -- the code
// uses one heuristic that worked well for a large ingestion stemming from a
// cockroachdb import, but additional experimentation is necessary to pick a
// general heuristic. Additionally, the compaction that gets picked may be not
// as desirable as one that could be constructed later in terms of reducing
// stack depth (since adding more files to the compaction can get blocked by
// needing to encompass files that are already being compacted). So an
// alternative would be to try to construct more than one compaction and pick
// the best one.
//
// Here's a visualization of an ideal L0->LBase compaction selection:
//
@@ -1243,9 +1266,9 @@ func (is intervalSorterByDecreasingScore) Swap(i, j int) {
// Lbase a--------i m---------w
//
// Note that running this compaction will mark the a--i file in Lbase as
// compacting, and when ExtendL0ForBaseCompactionTo is called with the bounds
// of that base file, it'll expand the compaction to also include all L0 files
// in the a-d interval. The resultant compaction would then be:
// compacting, and when ExtendL0ForBaseCompactionTo is called with the bounds of
// that base file, it'll expand the compaction to also include all L0 files in
// the a-d interval. The resultant compaction would then be:
//
// _____________
// L0.3 |a--d g-j|
@@ -1255,11 +1278,10 @@ func (is intervalSorterByDecreasingScore) Swap(i, j int) {
//
// Lbase a--------i m---------w
//
// The next best interval for base compaction would therefore
// be the one including r--t in L0.2 and p--x in L0.0, and both this compaction
// and the one picked earlier can run in parallel. This is assuming
// minCompactionDepth >= 2, otherwise the second compaction has too little
// depth to pick.
// The next best interval for base compaction would therefore be the one
// including r--t in L0.2 and p--x in L0.0, and both this compaction and the one
// picked earlier can run in parallel. This is assuming minCompactionDepth >= 2,
// otherwise the second compaction has too little depth to pick.
//
// _____________
// L0.3 |a--d g-j| _________
@@ -1292,24 +1314,23 @@ func (is intervalSorterByDecreasingScore) Swap(i, j int) {
//
// Lbase a--------ij--lm---------w
//
// Intra-L0: If the L0 score is high, but PickBaseCompaction() is unable to
// pick a compaction, PickIntraL0Compaction will be used to pick an intra-L0
// compaction. Similar to L0 -> Lbase compactions, we want to allow for
// multiple intra-L0 compactions and not generate wide output files that
// hinder later concurrency of L0 -> Lbase compactions. Also compactions
// that produce wide files don't reduce stack depth -- they represent wide
// rectangles in our visualization, which means many intervals have their
// depth reduced by a small amount. Typically, L0 files have non-overlapping
// sequence numbers, and sticking to that invariant would require us to
// consider intra-L0 compactions that proceed from youngest to oldest files,
// which could result in the aforementioned undesirable wide rectangle
// shape. But this non-overlapping sequence number is already relaxed in
// RocksDB -- sstables are primarily ordered by their largest sequence
// number. So we can arrange for intra-L0 compactions to capture thin and
// tall rectangles starting with the top of the stack (youngest files).
// Like the L0 -> Lbase case we order the intervals using a heuristic and
// consider each in turn. The same comment about better L0 -> Lbase heuristics
// and not being greedy applies here.
// Intra-L0: If the L0 score is high, but PickBaseCompaction() is unable to pick
// a compaction, PickIntraL0Compaction will be used to pick an intra-L0
// compaction. Similar to L0 -> Lbase compactions, we want to allow for multiple
// intra-L0 compactions and not generate wide output files that hinder later
// concurrency of L0 -> Lbase compactions. Also compactions that produce wide
// files don't reduce stack depth -- they represent wide rectangles in our
// visualization, which means many intervals have their depth reduced by a small
// amount. Typically, L0 files have non-overlapping sequence numbers, and
// sticking to that invariant would require us to consider intra-L0 compactions
// that proceed from youngest to oldest files, which could result in the
// aforementioned undesirable wide rectangle shape. But this non-overlapping
// sequence number is already relaxed in RocksDB -- sstables are primarily
// ordered by their largest sequence number. So we can arrange for intra-L0
// compactions to capture thin and tall rectangles starting with the top of the
// stack (youngest files). Like the L0 -> Lbase case we order the intervals
// using a heuristic and consider each in turn. The same comment about better L0
// -> Lbase heuristics and not being greedy applies here.
//
// Going back to a modified version of our example from earlier, let's say these
// are the base compactions in progress:
@@ -1387,10 +1408,9 @@ func (s *L0Sublevels) PickBaseCompaction(
// Pick the seed file for the interval as the file
// in the lowest sub-level.
f := interval.files[0]
// Don't bother considering the intervals that are
// covered by the seed file since they are likely
// nearby. Note that it is possible that those intervals
// have seed files at lower sub-levels so could be
// Don't bother considering the intervals that are covered by the seed
// file since they are likely nearby. Note that it is possible that
// those intervals have seed files at lower sub-levels so could be
// viable for compaction.
if f == nil {
return nil, errors.New("no seed file found in sublevel intervals")
@@ -1398,23 +1418,22 @@ func (s *L0Sublevels) PickBaseCompaction(
consideredIntervals.markBits(f.minIntervalIndex, f.maxIntervalIndex+1)
if f.IsCompacting() {
if f.IsIntraL0Compacting {
// If we're picking a base compaction and we came across a
// seed file candidate that's being intra-L0 compacted, skip
// the interval instead of erroring out.
// If we're picking a base compaction and we came across a seed
// file candidate that's being intra-L0 compacted, skip the
// interval instead of erroring out.
continue
}
// We chose a compaction seed file that should not be
// compacting. Usually means the score is not accurately
// accounting for files already compacting, or internal state is
// inconsistent.
// We chose a compaction seed file that should not be compacting.
// Usually means the score is not accurately accounting for files
// already compacting, or internal state is inconsistent.
return nil, errors.Errorf("file %s chosen as seed file for compaction should not be compacting", f.FileNum)
}
c := s.baseCompactionUsingSeed(f, interval.index, minCompactionDepth)
if c != nil {
// Check if the chosen compaction overlaps with any files
// in Lbase that have Compacting = true. If that's the case,
// this compaction cannot be chosen.
// Check if the chosen compaction overlaps with any files in Lbase
// that have Compacting = true. If that's the case, this compaction
// cannot be chosen.
baseIter := baseFiles.Iter()
// An interval starting at ImmediateSuccessor(key) can never be the
// first interval of a compaction since no file can start at that
@@ -1454,10 +1473,9 @@ func (s *L0Sublevels) baseCompactionUsingSeed(
c.addFile(f)
// The first iteration of this loop builds the compaction at the seed file's
// sublevel. Future iterations expand on this compaction by stacking
// more files from intervalIndex and repeating. This is an
// optional activity so when it fails we can fallback to the last
// successful candidate.
// sublevel. Future iterations expand on this compaction by stacking more
// files from intervalIndex and repeating. This is an optional activity so
// when it fails we can fallback to the last successful candidate.
var lastCandidate *L0CompactionFiles
interval := &s.orderedIntervals[intervalIndex]
@@ -1467,29 +1485,28 @@ func (s *L0Sublevels) baseCompactionUsingSeed(
c.seedIntervalStackDepthReduction++
c.seedIntervalMaxLevel = sl
c.addFile(f2)
// The seed file is in the lowest sublevel in the seed interval, but it may
// overlap with other files in even lower sublevels. For
// correctness we need to grow our interval to include those files, and
// capture all files in the next level that fall in this extended interval
// and so on. This can result in a triangular shape like the following
// where again the X axis is the key intervals and the Y axis
// is oldest to youngest. Note that it is not necessary for
// correctness to fill out the shape at the higher sub-levels
// to make it more rectangular since the invariant only requires
// that younger versions of a key not be moved to Lbase while
// leaving behind older versions.
// The seed file is in the lowest sublevel in the seed interval, but it
// may overlap with other files in even lower sublevels. For correctness
// we need to grow our interval to include those files, and capture all
// files in the next level that fall in this extended interval and so
// on. This can result in a triangular shape like the following where
// again the X axis is the key intervals and the Y axis is oldest to
// youngest. Note that it is not necessary for correctness to fill out
// the shape at the higher sub-levels to make it more rectangular since
// the invariant only requires that younger versions of a key not be
// moved to Lbase while leaving behind older versions.
// -
// ---
// -----
// It may be better for performance to have a more rectangular
// shape since the files being left behind will overlap with the
// same Lbase key range as that of this compaction. But there is
// also the danger that in trying to construct a more rectangular
// shape we will be forced to pull in a file that is already
// compacting. We expect extendCandidateToRectangle to eventually be called
// on this compaction if it's chosen, at which point we would iterate
// backward and choose those files. This logic is similar to compaction.grow
// for non-L0 compactions.
// It may be better for performance to have a more rectangular shape
// since the files being left behind will overlap with the same Lbase
// key range as that of this compaction. But there is also the danger
// that in trying to construct a more rectangular shape we will be
// forced to pull in a file that is already compacting. We expect
// extendCandidateToRectangle to eventually be called on this compaction
// if it's chosen, at which point we would iterate backward and choose
// those files. This logic is similar to compaction.grow for non-L0
// compactions.
done := false
for currLevel := sl - 1; currLevel >= 0; currLevel-- {
if !s.extendFiles(currLevel, math.MaxUint64, c) {
@@ -1509,8 +1526,8 @@ func (s *L0Sublevels) baseCompactionUsingSeed(
// depth, but long running compactions reduce flexibility in what can
// run concurrently in L0 and even Lbase -> Lbase+1. An increase more
// than 150% in bytes since the last candidate compaction (along with a
// total compaction size in excess of 100mb), or a total compaction
// size beyond a hard limit of 500mb, is criteria for rejecting this
// total compaction size in excess of 100mb), or a total compaction size
// beyond a hard limit of 500mb, is criteria for rejecting this
// candidate. This lets us prefer slow growths as we add files, while
// still having a hard limit. Note that if this is the first compaction
// candidate to reach a stack depth reduction of minCompactionDepth or
@@ -1568,7 +1585,7 @@ func (s *L0Sublevels) extendFiles(
// PickIntraL0Compaction picks an intra-L0 compaction for files in this
// sublevel. This method is only called when a base compaction cannot be chosen.
// See comment above PickBaseCompaction for heuristics involved in this
// See comment above [PickBaseCompaction] for heuristics involved in this
// selection.
func (s *L0Sublevels) PickIntraL0Compaction(
earliestUnflushedSeqNum uint64, minCompactionDepth int,
@@ -1584,9 +1601,8 @@ func (s *L0Sublevels) PickIntraL0Compaction(
}
sort.Sort(intervalSorterByDecreasingScore(scoredIntervals))
// Optimization to avoid considering different intervals that
// are likely to choose the same seed file. Again this is just
// to reduce wasted work.
// Optimization to avoid considering different intervals that are likely to
// choose the same seed file. Again this is just to reduce wasted work.
consideredIntervals := newBitSet(len(s.orderedIntervals))
for _, scoredInterval := range scoredIntervals {
interval := &s.orderedIntervals[scoredInterval.interval]
@@ -1595,8 +1611,8 @@ func (s *L0Sublevels) PickIntraL0Compaction(
}
var f *FileMetadata
// Pick the seed file for the interval as the file
// in the highest sub-level.
// Pick the seed file for the interval as the file in the highest
// sub-level.
stackDepthReduction := scoredInterval.score
for i := len(interval.files) - 1; i >= 0; i-- {
f = interval.files[i]
@@ -1604,9 +1620,8 @@ func (s *L0Sublevels) PickIntraL0Compaction(
break
}
consideredIntervals.markBits(f.minIntervalIndex, f.maxIntervalIndex+1)
// Can this be the seed file? Files with newer sequence
// numbers than earliestUnflushedSeqNum cannot be in
// the compaction.
// Can this be the seed file? Files with newer sequence numbers than
// earliestUnflushedSeqNum cannot be in the compaction.
if f.LargestSeqNum >= earliestUnflushedSeqNum {
stackDepthReduction--
if stackDepthReduction == 0 {
@@ -1669,10 +1684,10 @@ func (s *L0Sublevels) intraL0CompactionUsingSeed(
}
// The first iteration of this loop produces an intra-L0 compaction at the
// seed level. Iterations after that optionally add to the compaction by
// stacking more files from intervalIndex and repeating. This is an
// optional activity so when it fails we can fallback to the last
// successful candidate. The code stops adding when it can't add more, or
// when fileBytes grows too large.
// stacking more files from intervalIndex and repeating. This is an optional
// activity so when it fails we can fallback to the last successful
// candidate. The code stops adding when it can't add more, or when
// fileBytes grows too large.
for ; slIndex >= 0; slIndex-- {
f2 := interval.files[slIndex]
sl := f2.SubLevel
@@ -1684,23 +1699,24 @@ func (s *L0Sublevels) intraL0CompactionUsingSeed(
c.addFile(f2)
// The seed file captures all files in the higher level that fall in the
// range of intervals. That may extend the range of intervals so for
// correctness we need to capture all files in the next higher level that
// fall in this extended interval and so on. This can result in an
// inverted triangular shape like the following where again the X axis is the
// key intervals and the Y axis is oldest to youngest. Note that it is not
// necessary for correctness to fill out the shape at lower sub-levels to
// make it more rectangular since the invariant only requires that if we
// move an older seqnum for key k into a file that has a higher seqnum, we
// also move all younger seqnums for that key k into that file.
// correctness we need to capture all files in the next higher level
// that fall in this extended interval and so on. This can result in an
// inverted triangular shape like the following where again the X axis
// is the key intervals and the Y axis is oldest to youngest. Note that
// it is not necessary for correctness to fill out the shape at lower
// sub-levels to make it more rectangular since the invariant only
// requires that if we move an older seqnum for key k into a file that
// has a higher seqnum, we also move all younger seqnums for that key k
// into that file.
// -----
// ---
// -
//
// It may be better for performance to have a more rectangular shape since
// it will reduce the stack depth for more intervals. But there is also
// the danger that in explicitly trying to construct a more rectangular
// shape we will be forced to pull in a file that is already compacting.
// We assume that the performance concern is not a practical issue.
// It may be better for performance to have a more rectangular shape
// since it will reduce the stack depth for more intervals. But there is
// also the danger that in explicitly trying to construct a more
// rectangular shape we will be forced to pull in a file that is already
// compacting. We assume that the performance concern is not a practical
// issue.
done := false
for currLevel := sl + 1; currLevel < len(s.levelFiles); currLevel++ {
if !s.extendFiles(currLevel, earliestUnflushedSeqNum, c) {
@@ -1734,11 +1750,11 @@ func (s *L0Sublevels) intraL0CompactionUsingSeed(
}
// ExtendL0ForBaseCompactionTo extends the specified base compaction candidate
// L0CompactionFiles to optionally cover more files in L0 without "touching"
// any of the passed-in keys (i.e. the smallest/largest bounds are exclusive),
// as including any user keys for those internal keys
// could require choosing more files in LBase which is undesirable. Unbounded
// start/end keys are indicated by passing in the InvalidInternalKey.
// L0CompactionFiles to optionally cover more files in L0 without "touching" any
// of the passed-in keys (i.e. the smallest/largest bounds are exclusive), as
// including any user keys for those internal keys could require choosing more
// files in LBase which is undesirable. Unbounded start/end keys are indicated
// by passing in the InvalidInternalKey.
func (s *L0Sublevels) ExtendL0ForBaseCompactionTo(
smallest, largest InternalKey, candidate *L0CompactionFiles,
) bool {
@@ -1794,65 +1810,66 @@ func (s *L0Sublevels) ExtendL0ForBaseCompactionTo(
// Consider this scenario (original candidate is inside the rectangle), with
// isBase = true and interval bounds a-j (from the union of base file bounds and
// that of compaction candidate):
// _______
// L0.3 a--d | g-j|
// L0.2 | f--j| r-t
// L0.1 b-d |e---j|
// L0.0 a--d | f--j| l--o p-----x
//
// Lbase a--------i m---------w
// _______
// L0.3 a--d | g-j|
// L0.2 | f--j| r-t
// L0.1 b-d |e---j|
// L0.0 a--d | f--j| l--o p-----x
//
// Lbase a--------i m---------w
//
// This method will iterate from the bottom up. At L0.0, it will add a--d since
// it's in the bounds, then add b-d, then a--d, and so on, to produce this:
//
// _____________
// L0.3 |a--d g-j|
// L0.2 | f--j| r-t
// L0.1 | b-d e---j|
// L0.0 |a--d f--j| l--o p-----x
// _____________
// L0.3 |a--d g-j|
// L0.2 | f--j| r-t
// L0.1 | b-d e---j|
// L0.0 |a--d f--j| l--o p-----x
//
// Lbase a-------i m---------w
// Lbase a-------i m---------w
//
// Let's assume that, instead of a--d in the top sublevel, we had 3 files, a-b,
// bb-c, and cc-d, of which bb-c is compacting. Let's also add another sublevel
// L0.4 with some files, all of which aren't compacting:
//
// L0.4 a------c ca--d _______
// L0.3 a-b bb-c cc-d | g-j|
// L0.2 | f--j| r-t
// L0.1 b----------d |e---j|
// L0.0 a------------d | f--j| l--o p-----x
// L0.4 a------c ca--d _______
// L0.3 a-b bb-c cc-d | g-j|
// L0.2 | f--j| r-t
// L0.1 b----------d |e---j|
// L0.0 a------------d | f--j| l--o p-----x
//
// Lbase a------------------i m---------w
// Lbase a------------------i m---------w
//
// This method then needs to choose between the left side of L0.3 bb-c
// (i.e. a-b), or the right side (i.e. cc-d and g-j) for inclusion in this
// compaction. Since the right side has more files as well as one file that has
// already been picked, it gets chosen at that sublevel, resulting in this
// intermediate compaction:
// This method then needs to choose between the left side of L0.3 bb-c (i.e.
// a-b), or the right side (i.e. cc-d and g-j) for inclusion in this compaction.
// Since the right side has more files as well as one file that has already been
// picked, it gets chosen at that sublevel, resulting in this intermediate
// compaction:
//
// L0.4 a------c ca--d
// ______________
// L0.3 a-b bb-c| cc-d g-j|
// L0.2 _________| f--j| r-t
// L0.1 | b----------d e---j|
// L0.0 |a------------d f--j| l--o p-----x
// L0.4 a------c ca--d
// ______________
// L0.3 a-b bb-c| cc-d g-j|
// L0.2 _________| f--j| r-t
// L0.1 | b----------d e---j|
// L0.0 |a------------d f--j| l--o p-----x
//
// Lbase a------------------i m---------w
// Lbase a------------------i m---------w
//
// Since bb-c had to be excluded at L0.3, the interval bounds for L0.4 are
// actually ca-j, since ca is the next interval start key after the end interval
// of bb-c. This would result in only ca-d being chosen at that sublevel, even
// though a--c is also not compacting. This is the final result:
//
// ______________
// L0.4 a------c|ca--d |
// L0.3 a-b bb-c| cc-d g-j|
// L0.2 _________| f--j| r-t
// L0.1 | b----------d e---j|
// L0.0 |a------------d f--j| l--o p-----x
// ______________
// L0.4 a------c|ca--d |
// L0.3 a-b bb-c| cc-d g-j|
// L0.2 _________| f--j| r-t
// L0.1 | b----------d e---j|
// L0.0 |a------------d f--j| l--o p-----x
//
// Lbase a------------------i m---------w
// Lbase a------------------i m---------w
//
// TODO(bilal): Add more targeted tests for this method, through
// ExtendL0ForBaseCompactionTo and intraL0CompactionUsingSeed.
@@ -1883,19 +1900,19 @@ func (s *L0Sublevels) extendCandidateToRectangle(
}
// Stats for files.
addedCount := 0
// Iterate from the oldest sub-level for L0 -> Lbase and youngest
// sub-level for intra-L0. The idea here is that anything that can't
// be included from that level constrains what can be included from
// the next level. This change in constraint is directly incorporated
// into minIntervalIndex, maxIntervalIndex.
// Iterate from the oldest sub-level for L0 -> Lbase and youngest sub-level
// for intra-L0. The idea here is that anything that can't be included from
// that level constrains what can be included from the next level. This
// change in constraint is directly incorporated into minIntervalIndex,
// maxIntervalIndex.
for sl := startLevel; sl != endLevel; sl += increment {
files := s.levelFiles[sl]
// Find the first file that overlaps with minIntervalIndex.
index := sort.Search(len(files), func(i int) bool {
return minIntervalIndex <= files[i].maxIntervalIndex
})
// Track the files that are fully within the current constraint
// of [minIntervalIndex, maxIntervalIndex].
// Track the files that are fully within the current constraint of
// [minIntervalIndex, maxIntervalIndex].
firstIndex := -1
lastIndex := -1
for ; index < len(files); index++ {
@@ -1904,8 +1921,8 @@ func (s *L0Sublevels) extendCandidateToRectangle(
break
}
include := true
// Extends out on the left so can't be included. This narrows
// what we can included in the next level.
// Extends out on the left so can't be included. This narrows what
// we can included in the next level.
if f.minIntervalIndex < minIntervalIndex {
include = false
minIntervalIndex = f.maxIntervalIndex + 1
@@ -1932,13 +1949,13 @@ func (s *L0Sublevels) extendCandidateToRectangle(
continue
}
// We have the files in [firstIndex, lastIndex] as potential for
// inclusion. Some of these may already have been picked. Some
// of them may be already compacting. The latter is tricky since
// we have to decide whether to contract minIntervalIndex or
// maxIntervalIndex when we encounter an already compacting file.
// We pick the longest sequence between firstIndex
// and lastIndex of non-compacting files -- this is represented by
// [candidateNonCompactingFirst, candidateNonCompactingLast].
// inclusion. Some of these may already have been picked. Some of them
// may be already compacting. The latter is tricky since we have to
// decide whether to contract minIntervalIndex or maxIntervalIndex when
// we encounter an already compacting file. We pick the longest sequence
// between firstIndex and lastIndex of non-compacting files -- this is
// represented by [candidateNonCompactingFirst,
// candidateNonCompactingLast].
nonCompactingFirst := -1
currentRunHasAlreadyPickedFiles := false
candidateNonCompactingFirst := -1
@@ -1985,8 +2002,8 @@ func (s *L0Sublevels) extendCandidateToRectangle(
}
}
if candidateNonCompactingFirst == -1 {
// All files are compacting. There will be gaps that we could exploit
// to continue, but don't bother.
// All files are compacting. There will be gaps that we could
// exploit to continue, but don't bother.
break
}
// May need to shrink [minIntervalIndex, maxIntervalIndex] for the next level.

View File

@@ -9,6 +9,7 @@ import (
"fmt"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/internal/invariants"
)
// LevelMetadata contains metadata for all of the files within
@@ -23,12 +24,12 @@ type LevelMetadata struct {
func (lm *LevelMetadata) clone() LevelMetadata {
return LevelMetadata{
level: lm.level,
tree: lm.tree.clone(),
tree: lm.tree.Clone(),
}
}
func (lm *LevelMetadata) release() (obsolete []*FileMetadata) {
return lm.tree.release()
return lm.tree.Release()
}
func makeLevelMetadata(cmp Compare, level int, files []*FileMetadata) LevelMetadata {
@@ -46,29 +47,29 @@ func makeBTree(cmp btreeCmp, files []*FileMetadata) (btree, LevelSlice) {
var t btree
t.cmp = cmp
for _, f := range files {
t.insert(f)
t.Insert(f)
}
return t, LevelSlice{iter: t.iter(), length: t.length}
return t, newLevelSlice(t.Iter())
}
// Empty indicates whether there are any files in the level.
func (lm *LevelMetadata) Empty() bool {
return lm.tree.length == 0
return lm.tree.Count() == 0
}
// Len returns the number of files within the level.
func (lm *LevelMetadata) Len() int {
return lm.tree.length
return lm.tree.Count()
}
// Iter constructs a LevelIterator over the entire level.
func (lm *LevelMetadata) Iter() LevelIterator {
return LevelIterator{iter: lm.tree.iter()}
return LevelIterator{iter: lm.tree.Iter()}
}
// Slice constructs a slice containing the entire level.
func (lm *LevelMetadata) Slice() LevelSlice {
return LevelSlice{iter: lm.tree.iter(), length: lm.tree.length}
return newLevelSlice(lm.tree.Iter())
}
// Find finds the provided file in the level if it exists.
@@ -99,7 +100,7 @@ func (lm *LevelMetadata) Annotation(annotator Annotator) interface{} {
if lm.Empty() {
return annotator.Zero(nil)
}
v, _ := lm.tree.root.annotation(annotator)
v, _ := lm.tree.root.Annotation(annotator)
return v
}
@@ -112,7 +113,7 @@ func (lm *LevelMetadata) InvalidateAnnotation(annotator Annotator) {
if lm.Empty() {
return
}
lm.tree.root.invalidateAnnotation(annotator)
lm.tree.root.InvalidateAnnotation(annotator)
}
// LevelFile holds a file's metadata along with its position
@@ -133,7 +134,8 @@ func (lf LevelFile) Slice() LevelSlice {
// a slice constructor like this?
func NewLevelSliceSeqSorted(files []*FileMetadata) LevelSlice {
tr, slice := makeBTree(btreeCmpSeqNum, files)
tr.release()
tr.Release()
slice.verifyInvariants()
return slice
}
@@ -143,7 +145,8 @@ func NewLevelSliceSeqSorted(files []*FileMetadata) LevelSlice {
// a slice constructor like this?
func NewLevelSliceKeySorted(cmp base.Compare, files []*FileMetadata) LevelSlice {
tr, slice := makeBTree(btreeCmpSmallestKey(cmp), files)
tr.release()
tr.Release()
slice.verifyInvariants()
return slice
}
@@ -153,13 +156,56 @@ func NewLevelSliceKeySorted(cmp base.Compare, files []*FileMetadata) LevelSlice
// TODO(jackson): Update tests to avoid requiring this and remove it.
func NewLevelSliceSpecificOrder(files []*FileMetadata) LevelSlice {
tr, slice := makeBTree(btreeCmpSpecificOrder(files), files)
tr.release()
tr.Release()
slice.verifyInvariants()
return slice
}
// newLevelSlice constructs a new LevelSlice backed by iter.
func newLevelSlice(iter iterator) LevelSlice {
s := LevelSlice{iter: iter}
if iter.r != nil {
s.length = iter.r.subtreeCount
}
s.verifyInvariants()
return s
}
// newBoundedLevelSlice constructs a new LevelSlice backed by iter and bounded
// by the provided start and end bounds. The provided startBound and endBound
// iterators must be iterators over the same B-Tree. Both start and end bounds
// are inclusive.
func newBoundedLevelSlice(iter iterator, startBound, endBound *iterator) LevelSlice {
s := LevelSlice{
iter: iter,
start: startBound,
end: endBound,
}
if iter.valid() {
s.length = endBound.countLeft() - startBound.countLeft()
// NB: The +1 is a consequence of the end bound being inclusive.
if endBound.valid() {
s.length++
}
// NB: A slice that's empty due to its bounds may have an endBound
// positioned before the startBound due to the inclusive bounds.
// TODO(jackson): Consider refactoring the end boundary to be exclusive;
// it would simplify some areas (eg, here) and complicate others (eg,
// Reslice-ing to grow compactions).
if s.length < 0 {
s.length = 0
}
}
s.verifyInvariants()
return s
}
// LevelSlice contains a slice of the files within a level of the LSM.
// A LevelSlice is immutable once created, but may be used to construct a
// mutable LevelIterator over the slice's files.
//
// LevelSlices should be constructed through one of the existing constructors,
// not manually initialized.
type LevelSlice struct {
iter iterator
length int
@@ -170,6 +216,19 @@ type LevelSlice struct {
end *iterator
}
func (ls LevelSlice) verifyInvariants() {
if invariants.Enabled {
i := ls.Iter()
var length int
for f := i.First(); f != nil; f = i.Next() {
length++
}
if ls.length != length {
panic(fmt.Sprintf("LevelSlice %s has length %d value; actual length is %d", ls, ls.length, length))
}
}
}
// Each invokes fn for each element in the slice.
func (ls LevelSlice) Each(fn func(*FileMetadata)) {
iter := ls.Iter()
@@ -181,6 +240,7 @@ func (ls LevelSlice) Each(fn func(*FileMetadata)) {
// String implements fmt.Stringer.
func (ls LevelSlice) String() string {
var buf bytes.Buffer
fmt.Fprintf(&buf, "%d files: ", ls.length)
ls.Each(func(f *FileMetadata) {
if buf.Len() > 0 {
fmt.Fprintf(&buf, " ")
@@ -246,18 +306,7 @@ func (ls LevelSlice) Reslice(resliceFunc func(start, end *LevelIterator)) LevelS
end.iter = ls.end.clone()
}
resliceFunc(&start, &end)
s := LevelSlice{
iter: start.iter.clone(),
start: &start.iter,
end: &end.iter,
}
// Calculate the new slice's length.
iter := s.Iter()
for f := iter.First(); f != nil; f = iter.Next() {
s.length++
}
return s
return newBoundedLevelSlice(start.iter.clone(), &start.iter, &end.iter)
}
// KeyType is used to specify the type of keys we're looking for in
@@ -387,8 +436,13 @@ func (i *LevelIterator) Clone() LevelIterator {
}
// Current returns the item at the current iterator position.
//
// Current is deprecated. Callers should instead use the return value of a
// positioning operation.
func (i *LevelIterator) Current() *FileMetadata {
if !i.iter.valid() {
if !i.iter.valid() ||
(i.end != nil && cmpIter(i.iter, *i.end) > 0) ||
(i.start != nil && cmpIter(i.iter, *i.start) < 0) {
return nil
}
return i.iter.cur()
@@ -426,7 +480,7 @@ func (i *LevelIterator) First() *FileMetadata {
if !i.iter.valid() {
return nil
}
return i.filteredNextFile(i.iter.cur())
return i.skipFilteredForward(i.iter.cur())
}
// Last seeks to the last file in the iterator and returns it.
@@ -442,31 +496,37 @@ func (i *LevelIterator) Last() *FileMetadata {
if !i.iter.valid() {
return nil
}
return i.filteredPrevFile(i.iter.cur())
return i.skipFilteredBackward(i.iter.cur())
}
// Next advances the iterator to the next file and returns it.
func (i *LevelIterator) Next() *FileMetadata {
if i.iter.r == nil {
return nil
}
if invariants.Enabled && (i.iter.pos >= i.iter.n.count || (i.end != nil && cmpIter(i.iter, *i.end) > 0)) {
panic("pebble: cannot next forward-exhausted iterator")
}
i.iter.next()
if !i.iter.valid() {
return nil
}
if i.end != nil && cmpIter(i.iter, *i.end) > 0 {
return nil
}
return i.filteredNextFile(i.iter.cur())
return i.skipFilteredForward(i.iter.cur())
}
// Prev moves the iterator the previous file and returns it.
func (i *LevelIterator) Prev() *FileMetadata {
if i.iter.r == nil {
return nil
}
if invariants.Enabled && (i.iter.pos < 0 || (i.start != nil && cmpIter(i.iter, *i.start) < 0)) {
panic("pebble: cannot prev backward-exhausted iterator")
}
i.iter.prev()
if !i.iter.valid() {
return nil
}
if i.start != nil && cmpIter(i.iter, *i.start) < 0 {
return nil
}
return i.filteredPrevFile(i.iter.cur())
return i.skipFilteredBackward(i.iter.cur())
}
// SeekGE seeks to the first file in the iterator's file set with a largest
@@ -475,28 +535,23 @@ func (i *LevelIterator) Prev() *FileMetadata {
// be sorted by user keys and non-overlapping.
func (i *LevelIterator) SeekGE(cmp Compare, userKey []byte) *FileMetadata {
// TODO(jackson): Assert that i.iter.cmp == btreeCmpSmallestKey.
if i.empty() {
if i.iter.r == nil {
return nil
}
meta := i.seek(func(m *FileMetadata) bool {
m := i.seek(func(m *FileMetadata) bool {
return cmp(m.Largest.UserKey, userKey) >= 0
})
for meta != nil {
switch i.filter {
case KeyTypePointAndRange:
return meta
case KeyTypePoint:
if meta.HasPointKeys && cmp(meta.LargestPointKey.UserKey, userKey) >= 0 {
return meta
}
case KeyTypeRange:
if meta.HasRangeKeys && cmp(meta.LargestRangeKey.UserKey, userKey) >= 0 {
return meta
}
if i.filter != KeyTypePointAndRange && m != nil {
b, ok := m.LargestBound(i.filter)
if !ok {
m = i.Next()
} else if c := cmp(b.UserKey, userKey); c < 0 || c == 0 && b.IsExclusiveSentinel() {
// This file does not contain any keys of the type ≥ lower. It
// should be filtered, even though it does contain point keys.
m = i.Next()
}
meta = i.Next()
}
return i.filteredNextFile(meta)
return i.skipFilteredForward(m)
}
// SeekLT seeks to the last file in the iterator's file set with a smallest
@@ -505,71 +560,76 @@ func (i *LevelIterator) SeekGE(cmp Compare, userKey []byte) *FileMetadata {
// by user keys and non-overlapping.
func (i *LevelIterator) SeekLT(cmp Compare, userKey []byte) *FileMetadata {
// TODO(jackson): Assert that i.iter.cmp == btreeCmpSmallestKey.
if i.empty() {
if i.iter.r == nil {
return nil
}
i.seek(func(m *FileMetadata) bool {
return cmp(m.Smallest.UserKey, userKey) >= 0
})
meta := i.Prev()
for meta != nil {
switch i.filter {
case KeyTypePointAndRange:
return meta
case KeyTypePoint:
if meta.HasPointKeys && cmp(meta.SmallestPointKey.UserKey, userKey) < 0 {
return meta
}
case KeyTypeRange:
if meta.HasRangeKeys && cmp(meta.SmallestRangeKey.UserKey, userKey) < 0 {
return meta
}
m := i.Prev()
// Although i.Prev() guarantees that the current file contains keys of the
// relevant type, it doesn't guarantee that the keys of the relevant type
// are < userKey.
if i.filter != KeyTypePointAndRange && m != nil {
b, ok := m.SmallestBound(i.filter)
if !ok {
panic("unreachable")
}
if c := cmp(b.UserKey, userKey); c >= 0 {
// This file does not contain any keys of the type ≥ lower. It
// should be filtered, even though it does contain point keys.
m = i.Prev()
}
meta = i.Prev()
}
return i.filteredPrevFile(meta)
return i.skipFilteredBackward(m)
}
func (i *LevelIterator) filteredNextFile(meta *FileMetadata) *FileMetadata {
switch i.filter {
case KeyTypePoint:
for meta != nil && !meta.HasPointKeys {
meta = i.Next()
// skipFilteredForward takes the file metadata at the iterator's current
// position, and skips forward if the current key-type filter (i.filter)
// excludes the file. It skips until it finds an unfiltered file or exhausts the
// level. If lower is != nil, skipFilteredForward skips any files that do not
// contain keys with the provided key-type ≥ lower.
//
// skipFilteredForward also enforces the upper bound, returning nil if at any
// point the upper bound is exceeded.
func (i *LevelIterator) skipFilteredForward(meta *FileMetadata) *FileMetadata {
for meta != nil && !meta.ContainsKeyType(i.filter) {
i.iter.next()
if !i.iter.valid() {
meta = nil
} else {
meta = i.iter.cur()
}
return meta
case KeyTypeRange:
// TODO(bilal): Range keys are expected to be rare and sparse. Add an
// optimization to annotate the tree and efficiently skip over files that
// do not contain range keys right at the seek step, to reduce iterations
// here.
for meta != nil && !meta.HasRangeKeys {
meta = i.Next()
}
return meta
default:
return meta
}
if meta != nil && i.end != nil && cmpIter(i.iter, *i.end) > 0 {
// Exceeded upper bound.
meta = nil
}
return meta
}
func (i *LevelIterator) filteredPrevFile(meta *FileMetadata) *FileMetadata {
switch i.filter {
case KeyTypePoint:
for meta != nil && !meta.HasPointKeys {
meta = i.Prev()
// skipFilteredBackward takes the file metadata at the iterator's current
// position, and skips backward if the current key-type filter (i.filter)
// excludes the file. It skips until it finds an unfiltered file or exhausts the
// level. If upper is != nil, skipFilteredBackward skips any files that do not
// contain keys with the provided key-type < upper.
//
// skipFilteredBackward also enforces the lower bound, returning nil if at any
// point the lower bound is exceeded.
func (i *LevelIterator) skipFilteredBackward(meta *FileMetadata) *FileMetadata {
for meta != nil && !meta.ContainsKeyType(i.filter) {
i.iter.prev()
if !i.iter.valid() {
meta = nil
} else {
meta = i.iter.cur()
}
return meta
case KeyTypeRange:
// TODO(bilal): Range keys are expected to be rare and sparse. Add an
// optimization to annotate the tree and efficiently skip over files that
// do not contain range keys right at the seek step, to reduce iterations
// here.
for meta != nil && !meta.HasRangeKeys {
meta = i.Prev()
}
return meta
default:
return meta
}
if meta != nil && i.start != nil && cmpIter(i.iter, *i.start) < 0 {
// Exceeded lower bound.
meta = nil
}
return meta
}
func (i *LevelIterator) seek(fn func(*FileMetadata) bool) *FileMetadata {
@@ -594,7 +654,6 @@ func (i *LevelIterator) seek(fn func(*FileMetadata) bool) *FileMetadata {
return nil
} else if i.start != nil && cmpIter(i.iter, *i.start) < 0 {
i.iter = i.start.clone()
return i.iter.cur()
}
if !i.iter.valid() {
return nil
@@ -614,13 +673,9 @@ func (i *LevelIterator) Take() LevelFile {
// the same position for a LevelFile because they're inclusive, so we can
// share one iterator stack between the two bounds.
boundsIter := i.iter.clone()
s := newBoundedLevelSlice(i.iter.clone(), &boundsIter, &boundsIter)
return LevelFile{
FileMetadata: m,
slice: LevelSlice{
iter: i.iter.clone(),
start: &boundsIter,
end: &boundsIter,
length: 1,
},
slice: s,
}
}

View File

@@ -17,7 +17,6 @@ import (
"github.com/cockroachdb/errors"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/internal/invariants"
"github.com/cockroachdb/pebble/vfs"
)
// Compare exports the base.Compare type.
@@ -42,7 +41,8 @@ type TableInfo struct {
LargestSeqNum uint64
}
// TableStats contains statistics on a table used for compaction heuristics.
// TableStats contains statistics on a table used for compaction heuristics,
// and export via Metrics.
type TableStats struct {
// The total number of entries in the table.
NumEntries uint64
@@ -60,10 +60,14 @@ type TableStats struct {
// overlapping data in L0 and ignores L0 sublevels, but the error that
// introduces is expected to be small.
//
// Tables in the bottommost level of the LSM may have a nonzero estimate
// if snapshots or move compactions prevented the elision of their range
// tombstones.
// Tables in the bottommost level of the LSM may have a nonzero estimate if
// snapshots or move compactions prevented the elision of their range
// tombstones. A table in the bottommost level that was ingested into L6
// will have a zero estimate, because the file's sequence numbers indicate
// that the tombstone cannot drop any data contained within the file itself.
RangeDeletionsBytesEstimate uint64
// Total size of value blocks and value index block.
ValueBlocksSize uint64
}
// boundType represents the type of key (point or range) present as the smallest
@@ -79,10 +83,10 @@ const (
//
// The following shows the valid state transitions:
//
// NotCompacting --> Compacting --> Compacted
// ^ |
// | |
// +-------<-------+
// NotCompacting --> Compacting --> Compacted
// ^ |
// | |
// +-------<-------+
//
// Input files to a compaction transition to Compacting when a compaction is
// picked. A file that has finished compacting typically transitions into the
@@ -117,43 +121,81 @@ func (s CompactionState) String() string {
}
}
// FileMetadata holds the metadata for an on-disk table.
// FileMetadata is maintained for leveled-ssts, i.e., they belong to a level of
// some version. FileMetadata does not contain the actual level of the sst,
// since such leveled-ssts can move across levels in different versions, while
// sharing the same FileMetadata. There are two kinds of leveled-ssts, physical
// and virtual. Underlying both leveled-ssts is a backing-sst, for which the
// only state is FileBacking. A backing-sst is level-less. It is possible for a
// backing-sst to be referred to by a physical sst in one version and by one or
// more virtual ssts in one or more versions. A backing-sst becomes obsolete
// and can be deleted once it is no longer required by any physical or virtual
// sst in any version.
//
// We maintain some invariants:
//
// 1. Each physical and virtual sst will have a unique FileMetadata.FileNum,
// and there will be exactly one FileMetadata associated with the FileNum.
//
// 2. Within a version, a backing-sst is either only referred to by one
// physical sst or one or more virtual ssts.
//
// 3. Once a backing-sst is referred to by a virtual sst in the latest version,
// it cannot go back to being referred to by a physical sst in any future
// version.
//
// Once a physical sst is no longer needed by any version, we will no longer
// maintain the file metadata associated with it. We will still maintain the
// FileBacking associated with the physical sst if the backing sst is required
// by any virtual ssts in any version.
type FileMetadata struct {
// Atomic contains fields which are accessed atomically. Go allocations
// are guaranteed to be 64-bit aligned which we take advantage of by
// placing the 64-bit fields which we access atomically at the beginning
// of the FileMetadata struct. For more information, see
// https://golang.org/pkg/sync/atomic/#pkg-note-BUG.
Atomic struct {
// AllowedSeeks is used to determine if a file should be picked for
// a read triggered compaction. It is decremented when read sampling
// in pebble.Iterator after every after every positioning operation
// that returns a user key (eg. Next, Prev, SeekGE, SeekLT, etc).
AllowedSeeks int64
// AllowedSeeks is used to determine if a file should be picked for
// a read triggered compaction. It is decremented when read sampling
// in pebble.Iterator after every after every positioning operation
// that returns a user key (eg. Next, Prev, SeekGE, SeekLT, etc).
AllowedSeeks atomic.Int64
// statsValid is 1 if stats have been loaded for the table. The
// TableStats structure is populated only if valid is 1.
statsValid uint32
}
// statsValid indicates if stats have been loaded for the table. The
// TableStats structure is populated only if valid is true.
statsValid atomic.Bool
// FileBacking is the state which backs either a physical or virtual
// sstables.
FileBacking *FileBacking
// InitAllowedSeeks is the inital value of allowed seeks. This is used
// to re-set allowed seeks on a file once it hits 0.
InitAllowedSeeks int64
// Reference count for the file: incremented when a file is added to a
// version and decremented when the version is unreferenced. The file is
// obsolete when the reference count falls to zero.
refs int32
// FileNum is the file number.
//
// INVARIANT: when !FileMetadata.Virtual, FileNum == FileBacking.DiskFileNum.
//
// TODO(bananabrick): Consider creating separate types for
// FileMetadata.FileNum and FileBacking.FileNum. FileNum is used both as
// an indentifier for the FileMetadata in Pebble, and also as a handle to
// perform reads and writes. We should ensure through types that
// FileMetadata.FileNum isn't used to perform reads, and that
// FileBacking.FileNum isn't used as an identifier for the FileMetadata.
FileNum base.FileNum
// Size is the size of the file, in bytes.
// Size is the size of the file, in bytes. Size is an approximate value for
// virtual sstables.
//
// INVARIANT: when !FileMetadata.Virtual, Size == FileBacking.Size.
//
// TODO(bananabrick): Size is currently used in metrics, and for many key
// Pebble level heuristics. Make sure that the heuristics will still work
// appropriately with an approximate value of size.
Size uint64
// File creation time in seconds since the epoch (1970-01-01 00:00:00
// UTC). For ingested sstables, this corresponds to the time the file was
// ingested.
// ingested. For virtual sstables, this corresponds to the wall clock time
// when the FileMetadata for the virtual sstable was first created.
CreationTime int64
// Smallest and largest sequence numbers in the table, across both point and
// range keys.
// Lower and upper bounds for the smallest and largest sequence numbers in
// the table, across both point and range keys. For physical sstables, these
// values are tight bounds. For virtual sstables, there is no guarantee that
// there will be keys with SmallestSeqNum or LargestSeqNum within virtual
// sstable bounds.
SmallestSeqNum uint64
LargestSeqNum uint64
// SmallestPointKey and LargestPointKey are the inclusive bounds for the
@@ -176,6 +218,13 @@ type FileMetadata struct {
Smallest InternalKey
Largest InternalKey
// Stats describe table statistics. Protected by DB.mu.
//
// For virtual sstables, set stats upon virtual sstable creation as
// asynchronous computation of stats is not currently supported.
//
// TODO(bananabrick): To support manifest replay for virtual sstables, we
// probably need to compute virtual sstable stats asynchronously. Otherwise,
// we'd have to write virtual sstable stats to the version edit.
Stats TableStats
SubLevel int
@@ -224,6 +273,173 @@ type FileMetadata struct {
// key type (point or range) corresponds to the smallest and largest overall
// table bounds.
boundTypeSmallest, boundTypeLargest boundType
// Virtual is true if the FileMetadata belongs to a virtual sstable.
Virtual bool
}
// PhysicalFileMeta is used by functions which want a guarantee that their input
// belongs to a physical sst and not a virtual sst.
//
// NB: This type should only be constructed by calling
// FileMetadata.PhysicalMeta.
type PhysicalFileMeta struct {
*FileMetadata
}
// VirtualFileMeta is used by functions which want a guarantee that their input
// belongs to a virtual sst and not a physical sst.
//
// NB: This type should only be constructed by calling FileMetadata.VirtualMeta.
type VirtualFileMeta struct {
*FileMetadata
}
// PhysicalMeta should be the only source of creating the PhysicalFileMeta
// wrapper type.
func (m *FileMetadata) PhysicalMeta() PhysicalFileMeta {
if m.Virtual {
panic("pebble: file metadata does not belong to a physical sstable")
}
return PhysicalFileMeta{
m,
}
}
// VirtualMeta should be the only source of creating the VirtualFileMeta wrapper
// type.
func (m *FileMetadata) VirtualMeta() VirtualFileMeta {
if !m.Virtual {
panic("pebble: file metadata does not belong to a virtual sstable")
}
return VirtualFileMeta{
m,
}
}
// FileBacking either backs a single physical sstable, or one or more virtual
// sstables.
//
// See the comment above the FileMetadata type for sstable terminology.
type FileBacking struct {
// Reference count for the backing file on disk: incremented when a
// physical or virtual sstable which is backed by the FileBacking is
// added to a version and decremented when the version is unreferenced.
// We ref count in order to determine when it is safe to delete a
// backing sst file from disk. The backing file is obsolete when the
// reference count falls to zero.
refs atomic.Int32
// latestVersionRefs are the references to the FileBacking in the
// latest version. This reference can be through a single physical
// sstable in the latest version, or one or more virtual sstables in the
// latest version.
//
// INVARIANT: latestVersionRefs <= refs.
latestVersionRefs atomic.Int32
// VirtualizedSize is set iff the backing sst is only referred to by
// virtual ssts in the latest version. VirtualizedSize is the sum of the
// virtual sstable sizes of all of the virtual sstables in the latest
// version which are backed by the physical sstable. When a virtual
// sstable is removed from the latest version, we will decrement the
// VirtualizedSize. During compaction picking, we'll compensate a
// virtual sstable file size by
// (FileBacking.Size - FileBacking.VirtualizedSize) / latestVersionRefs.
// The intuition is that if FileBacking.Size - FileBacking.VirtualizedSize
// is high, then the space amplification due to virtual sstables is
// high, and we should pick the virtual sstable with a higher priority.
//
// TODO(bananabrick): Compensate the virtual sstable file size using
// the VirtualizedSize during compaction picking and test.
VirtualizedSize atomic.Uint64
DiskFileNum base.DiskFileNum
Size uint64
}
// InitPhysicalBacking allocates and sets the FileBacking which is required by a
// physical sstable FileMetadata.
//
// Ensure that the state required by FileBacking, such as the FileNum, is
// already set on the FileMetadata before InitPhysicalBacking is called.
// Calling InitPhysicalBacking only after the relevant state has been set in the
// FileMetadata is not necessary in tests which don't rely on FileBacking.
func (m *FileMetadata) InitPhysicalBacking() {
if m.Virtual {
panic("pebble: virtual sstables should use a pre-existing FileBacking")
}
if m.FileBacking == nil {
m.FileBacking = &FileBacking{Size: m.Size, DiskFileNum: m.FileNum.DiskFileNum()}
}
}
// ValidateVirtual should be called once the FileMetadata for a virtual sstable
// is created to verify that the fields of the virtual sstable are sound.
func (m *FileMetadata) ValidateVirtual(createdFrom *FileMetadata) {
if !m.Virtual {
panic("pebble: invalid virtual sstable")
}
if createdFrom.SmallestSeqNum != m.SmallestSeqNum {
panic("pebble: invalid smallest sequence number for virtual sstable")
}
if createdFrom.LargestSeqNum != m.LargestSeqNum {
panic("pebble: invalid largest sequence number for virtual sstable")
}
if createdFrom.FileBacking != nil && createdFrom.FileBacking != m.FileBacking {
panic("pebble: invalid physical sstable state for virtual sstable")
}
if m.Size == 0 {
panic("pebble: virtual sstable size must be set upon creation")
}
}
// Refs returns the refcount of backing sstable.
func (m *FileMetadata) Refs() int32 {
return m.FileBacking.refs.Load()
}
// Ref increments the ref count associated with the backing sstable.
func (m *FileMetadata) Ref() {
m.FileBacking.refs.Add(1)
}
// Unref decrements the ref count associated with the backing sstable.
func (m *FileMetadata) Unref() int32 {
v := m.FileBacking.refs.Add(-1)
if invariants.Enabled && v < 0 {
panic("pebble: invalid FileMetadata refcounting")
}
return v
}
// LatestRef increments the latest ref count associated with the backing
// sstable.
func (m *FileMetadata) LatestRef() {
m.FileBacking.latestVersionRefs.Add(1)
if m.Virtual {
m.FileBacking.VirtualizedSize.Add(m.Size)
}
}
// LatestUnref decrements the latest ref count associated with the backing
// sstable.
func (m *FileMetadata) LatestUnref() int32 {
if m.Virtual {
m.FileBacking.VirtualizedSize.Add(-m.Size)
}
v := m.FileBacking.latestVersionRefs.Add(-1)
if invariants.Enabled && v < 0 {
panic("pebble: invalid FileMetadata latest refcounting")
}
return v
}
// LatestRefs returns the latest ref count associated with the backing sstable.
func (m *FileMetadata) LatestRefs() int32 {
return m.FileBacking.latestVersionRefs.Load()
}
// SetCompactionState transitions this file's compaction state to the given
@@ -261,22 +477,14 @@ func (m *FileMetadata) IsCompacting() bool {
// returns true, the Stats field may be read (with or without holding the
// database mutex).
func (m *FileMetadata) StatsValid() bool {
return atomic.LoadUint32(&m.Atomic.statsValid) == 1
}
// StatsValidLocked returns true if the table stats have been populated.
// StatsValidLocked requires DB.mu is held when it's invoked, and it avoids the
// overhead of an atomic load. This is possible because table stats validity is
// only set while DB.mu is held.
func (m *FileMetadata) StatsValidLocked() bool {
return m.Atomic.statsValid == 1
return m.statsValid.Load()
}
// StatsMarkValid marks the TableStats as valid. The caller must hold DB.mu
// while populating TableStats and calling StatsMarkValud. Once stats are
// populated, they must not be mutated.
func (m *FileMetadata) StatsMarkValid() {
atomic.StoreUint32(&m.Atomic.statsValid, 1)
m.statsValid.Store(true)
}
// ExtendPointKeyBounds attempts to extend the lower and upper point key bounds
@@ -354,6 +562,66 @@ func (m *FileMetadata) extendOverallBounds(
}
}
// Overlaps returns true if the file key range overlaps with the given range.
func (m *FileMetadata) Overlaps(cmp Compare, start []byte, end []byte, exclusiveEnd bool) bool {
if c := cmp(m.Largest.UserKey, start); c < 0 || (c == 0 && m.Largest.IsExclusiveSentinel()) {
// f is completely before the specified range; no overlap.
return false
}
if c := cmp(m.Smallest.UserKey, end); c > 0 || (c == 0 && exclusiveEnd) {
// f is completely after the specified range; no overlap.
return false
}
return true
}
// ContainsKeyType returns whether or not the file contains keys of the provided
// type.
func (m *FileMetadata) ContainsKeyType(kt KeyType) bool {
switch kt {
case KeyTypePointAndRange:
return true
case KeyTypePoint:
return m.HasPointKeys
case KeyTypeRange:
return m.HasRangeKeys
default:
panic("unrecognized key type")
}
}
// SmallestBound returns the file's smallest bound of the key type. It returns a
// false second return value if the file does not contain any keys of the key
// type.
func (m *FileMetadata) SmallestBound(kt KeyType) (*InternalKey, bool) {
switch kt {
case KeyTypePointAndRange:
return &m.Smallest, true
case KeyTypePoint:
return &m.SmallestPointKey, m.HasPointKeys
case KeyTypeRange:
return &m.SmallestRangeKey, m.HasRangeKeys
default:
panic("unrecognized key type")
}
}
// LargestBound returns the file's largest bound of the key type. It returns a
// false second return value if the file does not contain any keys of the key
// type.
func (m *FileMetadata) LargestBound(kt KeyType) (*InternalKey, bool) {
switch kt {
case KeyTypePointAndRange:
return &m.Largest, true
case KeyTypePoint:
return &m.LargestPointKey, m.HasPointKeys
case KeyTypeRange:
return &m.LargestRangeKey, m.HasRangeKeys
default:
panic("unrecognized key type")
}
}
const (
maskContainsPointKeys = 1 << 0
maskSmallest = 1 << 1
@@ -417,7 +685,7 @@ func (m *FileMetadata) DebugString(format base.FormatKey, verbose bool) string {
// ParseFileMetadataDebug parses a FileMetadata from its DebugString
// representation.
func ParseFileMetadataDebug(s string) (m FileMetadata, err error) {
func ParseFileMetadataDebug(s string) (*FileMetadata, error) {
// Split lines of the form:
// 000000:[a#0,SET-z#0,SET] points:[...] ranges:[...]
fields := strings.FieldsFunc(s, func(c rune) bool {
@@ -429,8 +697,9 @@ func ParseFileMetadataDebug(s string) (m FileMetadata, err error) {
}
})
if len(fields)%3 != 0 {
return m, errors.Newf("malformed input: %s", s)
return nil, errors.Newf("malformed input: %s", s)
}
m := &FileMetadata{}
for len(fields) > 0 {
prefix := fields[0]
smallest := base.ParsePrettyInternalKey(fields[1])
@@ -460,7 +729,8 @@ func ParseFileMetadataDebug(s string) (m FileMetadata, err error) {
m.SmallestPointKey, m.LargestPointKey = m.Smallest, m.Largest
m.HasPointKeys = true
}
return
m.InitPhysicalBacking()
return m, nil
}
// Validate validates the metadata for consistency with itself, returning an
@@ -522,6 +792,11 @@ func (m *FileMetadata) Validate(cmp Compare, formatKey base.FormatKey) error {
}
}
// Ensure that FileMetadata.Init was called.
if m.FileBacking == nil {
return base.CorruptionErrorf("file metadata FileBacking not set")
}
return nil
}
@@ -626,51 +901,50 @@ func SortBySmallest(files []*FileMetadata, cmp Compare) {
func overlaps(iter LevelIterator, cmp Compare, start, end []byte, exclusiveEnd bool) LevelSlice {
startIter := iter.Clone()
startIter.SeekGE(cmp, start)
// SeekGE compares user keys. The user key `start` may be equal to the
// f.Largest because f.Largest is a range deletion sentinel, indicating that
// the user key `start` is NOT contained within the file f. If that's the
// case, we can narrow the overlapping bounds to exclude the file with the
// sentinel.
if f := startIter.Current(); f != nil && f.Largest.IsExclusiveSentinel() &&
cmp(f.Largest.UserKey, start) == 0 {
startIter.Next()
{
startIterFile := startIter.SeekGE(cmp, start)
// SeekGE compares user keys. The user key `start` may be equal to the
// f.Largest because f.Largest is a range deletion sentinel, indicating
// that the user key `start` is NOT contained within the file f. If
// that's the case, we can narrow the overlapping bounds to exclude the
// file with the sentinel.
if startIterFile != nil && startIterFile.Largest.IsExclusiveSentinel() &&
cmp(startIterFile.Largest.UserKey, start) == 0 {
startIterFile = startIter.Next()
}
_ = startIterFile // Ignore unused assignment.
}
endIter := iter.Clone()
endIter.SeekGE(cmp, end)
{
endIterFile := endIter.SeekGE(cmp, end)
if !exclusiveEnd {
// endIter is now pointing at the *first* file with a largest key >= end.
// If there are multiple files including the user key `end`, we want all
// of them, so move forward.
for f := endIter.Current(); f != nil && cmp(f.Largest.UserKey, end) == 0; {
f = endIter.Next()
if !exclusiveEnd {
// endIter is now pointing at the *first* file with a largest key >= end.
// If there are multiple files including the user key `end`, we want all
// of them, so move forward.
for endIterFile != nil && cmp(endIterFile.Largest.UserKey, end) == 0 {
endIterFile = endIter.Next()
}
}
}
// LevelSlice uses inclusive bounds, so if we seeked to the end sentinel
// or nexted too far because Largest.UserKey equaled `end`, go back.
//
// Consider !exclusiveEnd and end = 'f', with the following file bounds:
//
// [b,d] [e, f] [f, f] [g, h]
//
// the above for loop will Next until it arrives at [g, h]. We need to
// observe that g > f, and Prev to the file with bounds [f, f].
if !endIter.iter.valid() {
endIter.Prev()
} else if c := cmp(endIter.Current().Smallest.UserKey, end); c > 0 || c == 0 && exclusiveEnd {
endIter.Prev()
}
iter = startIter.Clone()
return LevelSlice{
iter: iter.iter,
start: &startIter.iter,
end: &endIter.iter,
// LevelSlice uses inclusive bounds, so if we seeked to the end sentinel
// or nexted too far because Largest.UserKey equaled `end`, go back.
//
// Consider !exclusiveEnd and end = 'f', with the following file bounds:
//
// [b,d] [e, f] [f, f] [g, h]
//
// the above for loop will Next until it arrives at [g, h]. We need to
// observe that g > f, and Prev to the file with bounds [f, f].
if endIterFile == nil {
endIterFile = endIter.Prev()
} else if c := cmp(endIterFile.Smallest.UserKey, end); c > 0 || c == 0 && exclusiveEnd {
endIterFile = endIter.Prev()
}
_ = endIterFile // Ignore unused assignment.
}
return newBoundedLevelSlice(startIter.Clone().iter, &startIter.iter, &endIter.iter)
}
// NumLevels is the number of levels a Version contains.
@@ -744,7 +1018,7 @@ type Version struct {
// usage.
//
// L0Sublevels.Levels contains L0 files ordered by sublevels. All the files
// in Files[0] are in L0Sublevels.Levels. L0SublevelFiles is also set to
// in Levels[0] are in L0Sublevels.Levels. L0SublevelFiles is also set to
// a reference to that slice, as that slice is necessary for iterator
// creation and needs to outlast L0Sublevels.
L0Sublevels *L0Sublevels
@@ -759,7 +1033,7 @@ type Version struct {
// The callback to invoke when the last reference to a version is
// removed. Will be called with list.mu held.
Deleted func(obsolete []*FileMetadata)
Deleted func(obsolete []*FileBacking)
// Stats holds aggregated stats about the version maintained from
// version to version.
@@ -788,15 +1062,21 @@ func (v *Version) DebugString(format base.FormatKey) string {
return v.string(format, true)
}
func describeSublevels(format base.FormatKey, verbose bool, sublevels []LevelSlice) string {
var buf bytes.Buffer
for sublevel := len(sublevels) - 1; sublevel >= 0; sublevel-- {
fmt.Fprintf(&buf, "0.%d:\n", sublevel)
sublevels[sublevel].Each(func(f *FileMetadata) {
fmt.Fprintf(&buf, " %s\n", f.DebugString(format, verbose))
})
}
return buf.String()
}
func (v *Version) string(format base.FormatKey, verbose bool) string {
var buf bytes.Buffer
if len(v.L0SublevelFiles) > 0 {
for sublevel := len(v.L0SublevelFiles) - 1; sublevel >= 0; sublevel-- {
fmt.Fprintf(&buf, "0.%d:\n", sublevel)
v.L0SublevelFiles[sublevel].Each(func(f *FileMetadata) {
fmt.Fprintf(&buf, " %s\n", f.DebugString(format, verbose))
})
}
fmt.Fprintf(&buf, "%s", describeSublevels(format, verbose, v.L0SublevelFiles))
}
for level := 1; level < NumLevels; level++ {
if v.Levels[level].Empty() {
@@ -837,7 +1117,7 @@ func ParseVersionDebug(
m.SmallestPointKey, m.LargestPointKey = m.Smallest, m.Largest
m.HasPointKeys = true
}
files[level] = append(files[level], &m)
files[level] = append(files[level], m)
}
}
// Reverse the order of L0 files. This ensures we construct the same
@@ -865,11 +1145,15 @@ func (v *Version) Ref() {
// locked.
func (v *Version) Unref() {
if atomic.AddInt32(&v.refs, -1) == 0 {
obsolete := v.unrefFiles()
l := v.list
l.mu.Lock()
l.Remove(v)
v.Deleted(obsolete)
obsolete := v.unrefFiles()
fileBacking := make([]*FileBacking, len(obsolete))
for i, f := range obsolete {
fileBacking[i] = f.FileBacking
}
v.Deleted(fileBacking)
l.mu.Unlock()
}
}
@@ -881,7 +1165,12 @@ func (v *Version) Unref() {
func (v *Version) UnrefLocked() {
if atomic.AddInt32(&v.refs, -1) == 0 {
v.list.Remove(v)
v.Deleted(v.unrefFiles())
obsolete := v.unrefFiles()
fileBacking := make([]*FileBacking, len(obsolete))
for i, f := range obsolete {
fileBacking[i] = f.FileBacking
}
v.Deleted(fileBacking)
}
}
@@ -933,11 +1222,11 @@ func (v *Version) Contains(level int, cmp Compare, m *FileMetadata) bool {
}
// Overlaps returns all elements of v.files[level] whose user key range
// intersects the inclusive range [start, end]. If level is non-zero then the
// user key ranges of v.files[level] are assumed to not overlap (although they
// may touch). If level is zero then that assumption cannot be made, and the
// [start, end] range is expanded to the union of those matching ranges so far
// and the computation is repeated until [start, end] stabilizes.
// intersects the given range. If level is non-zero then the user key ranges of
// v.files[level] are assumed to not overlap (although they may touch). If level
// is zero then that assumption cannot be made, and the [start, end] range is
// expanded to the union of those matching ranges so far and the computation is
// repeated until [start, end] stabilizes.
// The returned files are a subsequence of the input files, i.e., the ordering
// is not changed.
func (v *Version) Overlaps(
@@ -957,20 +1246,16 @@ func (v *Version) Overlaps(
if selected {
continue
}
smallest := meta.Smallest.UserKey
largest := meta.Largest.UserKey
if c := cmp(largest, start); c < 0 || c == 0 && meta.Largest.IsExclusiveSentinel() {
// meta is completely before the specified range; skip it.
continue
}
if c := cmp(smallest, end); c > 0 || c == 0 && exclusiveEnd {
// meta is completely after the specified range; skip it.
if !meta.Overlaps(cmp, start, end, exclusiveEnd) {
// meta is completely outside the specified range; skip it.
continue
}
// Overlaps.
selectedIndices[i] = true
numSelected++
smallest := meta.Smallest.UserKey
largest := meta.Largest.UserKey
// Since level == 0, check if the newly added fileMetadata has
// expanded the range. We expand the range immediately for files
// we have remaining to check in this loop. All already checked
@@ -998,17 +1283,17 @@ func (v *Version) Overlaps(
tr.cmp = v.Levels[level].tree.cmp
for i, meta := 0, l0Iter.First(); meta != nil; i, meta = i+1, l0Iter.Next() {
if selectedIndices[i] {
err := tr.insert(meta)
err := tr.Insert(meta)
if err != nil {
panic(err)
}
}
}
slice = LevelSlice{iter: tr.iter(), length: tr.length}
slice = newLevelSlice(tr.Iter())
// TODO(jackson): Avoid the oddity of constructing and
// immediately releasing a B-Tree. Make LevelSlice an
// interface?
tr.release()
tr.Release()
break
}
// Continue looping to retry the files that were not selected.
@@ -1038,37 +1323,6 @@ func (v *Version) CheckOrdering(cmp Compare, format base.FormatKey) error {
return nil
}
// CheckConsistency checks that all of the files listed in the version exist
// and their on-disk sizes match the sizes listed in the version.
func (v *Version) CheckConsistency(dirname string, fs vfs.FS) error {
var buf bytes.Buffer
var args []interface{}
for level, files := range v.Levels {
iter := files.Iter()
for f := iter.First(); f != nil; f = iter.Next() {
path := base.MakeFilepath(fs, dirname, base.FileTypeTable, f.FileNum)
info, err := fs.Stat(path)
if err != nil {
buf.WriteString("L%d: %s: %v\n")
args = append(args, errors.Safe(level), errors.Safe(f.FileNum), err)
continue
}
if info.Size() != int64(f.Size) {
buf.WriteString("L%d: %s: file size mismatch (%s): %d (disk) != %d (MANIFEST)\n")
args = append(args, errors.Safe(level), errors.Safe(f.FileNum), path,
errors.Safe(info.Size()), errors.Safe(f.Size))
continue
}
}
}
if buf.Len() == 0 {
return nil
}
return errors.Errorf(buf.String(), args...)
}
// VersionList holds a list of versions. The versions are ordered from oldest
// to newest.
type VersionList struct {

View File

@@ -8,11 +8,14 @@ import (
"bufio"
"bytes"
"encoding/binary"
"fmt"
"io"
"sync/atomic"
"sort"
"time"
"github.com/cockroachdb/errors"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/internal/invariants"
)
// TODO(peter): describe the MANIFEST file format, independently of the C++
@@ -107,9 +110,40 @@ type VersionEdit struct {
// found that there was no overlapping file at the higher level).
DeletedFiles map[DeletedFileEntry]*FileMetadata
NewFiles []NewFileEntry
// CreatedBackingTables can be used to preserve the FileBacking associated
// with a physical sstable. This is useful when virtual sstables in the
// latest version are reconstructed during manifest replay, and we also need
// to reconstruct the FileBacking which is required by these virtual
// sstables.
//
// INVARIANT: The FileBacking associated with a physical sstable must only
// be added as a backing file in the same version edit where the physical
// sstable is first virtualized. This means that the physical sstable must
// be present in DeletedFiles and that there must be at least one virtual
// sstable with the same FileBacking as the physical sstable in NewFiles. A
// file must be present in CreatedBackingTables in exactly one version edit.
// The physical sstable associated with the FileBacking must also not be
// present in NewFiles.
CreatedBackingTables []*FileBacking
// RemovedBackingTables is used to remove the FileBacking associated with a
// virtual sstable. Note that a backing sstable can be removed as soon as
// there are no virtual sstables in the latest version which are using the
// backing sstable, but the backing sstable doesn't necessarily have to be
// removed atomically with the version edit which removes the last virtual
// sstable associated with the backing sstable. The removal can happen in a
// future version edit.
//
// INVARIANT: A file must only be added to RemovedBackingTables if it was
// added to CreateBackingTables in a prior version edit. The same version
// edit also cannot have the same file present in both CreateBackingTables
// and RemovedBackingTables. A file must be present in RemovedBackingTables
// in exactly one version edit.
RemovedBackingTables []base.DiskFileNum
}
// Decode decodes an edit from the specified reader.
//
// TODO(bananabrick): Support decoding of virtual sstable state.
func (v *VersionEdit) Decode(r io.Reader) error {
br, ok := r.(byteReader)
if !ok {
@@ -342,6 +376,7 @@ func (v *VersionEdit) Decode(r io.Reader) error {
}
}
m.boundsSet = true
m.InitPhysicalBacking()
v.NewFiles = append(v.NewFiles, NewFileEntry{
Level: level,
Meta: m,
@@ -364,7 +399,51 @@ func (v *VersionEdit) Decode(r io.Reader) error {
return nil
}
// String implements fmt.Stringer for a VersionEdit.
func (v *VersionEdit) String() string {
var buf bytes.Buffer
if v.ComparerName != "" {
fmt.Fprintf(&buf, " comparer: %s", v.ComparerName)
}
if v.MinUnflushedLogNum != 0 {
fmt.Fprintf(&buf, " log-num: %d\n", v.MinUnflushedLogNum)
}
if v.ObsoletePrevLogNum != 0 {
fmt.Fprintf(&buf, " prev-log-num: %d\n", v.ObsoletePrevLogNum)
}
if v.NextFileNum != 0 {
fmt.Fprintf(&buf, " next-file-num: %d\n", v.NextFileNum)
}
if v.LastSeqNum != 0 {
fmt.Fprintf(&buf, " last-seq-num: %d\n", v.LastSeqNum)
}
entries := make([]DeletedFileEntry, 0, len(v.DeletedFiles))
for df := range v.DeletedFiles {
entries = append(entries, df)
}
sort.Slice(entries, func(i, j int) bool {
if entries[i].Level != entries[j].Level {
return entries[i].Level < entries[j].Level
}
return entries[i].FileNum < entries[j].FileNum
})
for _, df := range entries {
fmt.Fprintf(&buf, " deleted: L%d %s\n", df.Level, df.FileNum)
}
for _, nf := range v.NewFiles {
fmt.Fprintf(&buf, " added: L%d %s", nf.Level, nf.Meta.String())
if nf.Meta.CreationTime != 0 {
fmt.Fprintf(&buf, " (%s)",
time.Unix(nf.Meta.CreationTime, 0).UTC().Format(time.RFC3339))
}
fmt.Fprintln(&buf)
}
return buf.String()
}
// Encode encodes an edit to the specified writer.
//
// TODO(bananabrick): Support encoding of virtual sstable state.
func (v *VersionEdit) Encode(w io.Writer) error {
e := versionEditEncoder{new(bytes.Buffer)}
@@ -536,10 +615,28 @@ func (e versionEditEncoder) writeUvarint(u uint64) {
// BulkVersionEdit summarizes the files added and deleted from a set of version
// edits.
//
// INVARIANTS:
// No file can be added to a level more than once. This is true globally, and
// also true for all of the calls to Accumulate for a single bulk version edit.
//
// No file can be removed from a level more than once. This is true globally,
// and also true for all of the calls to Accumulate for a single bulk version
// edit.
//
// A file must not be added and removed from a given level in the same version
// edit.
//
// A file that is being removed from a level must have been added to that level
// before (in a prior version edit). Note that a given file can be deleted from
// a level and added to another level in a single version edit
type BulkVersionEdit struct {
Added [NumLevels][]*FileMetadata
Added [NumLevels]map[base.FileNum]*FileMetadata
Deleted [NumLevels]map[base.FileNum]*FileMetadata
AddedFileBacking []*FileBacking
RemovedFileBacking []base.DiskFileNum
// AddedByFileNum maps file number to file metadata for all added files
// from accumulated version edits. AddedByFileNum is only populated if set
// to non-nil by a caller. It must be set to non-nil when replaying
@@ -558,6 +655,19 @@ type BulkVersionEdit struct {
// Accumulate adds the file addition and deletions in the specified version
// edit to the bulk edit's internal state.
//
// INVARIANTS:
// If a file is added to a given level in a call to Accumulate and then removed
// from that level in a subsequent call, the file will not be present in the
// resulting BulkVersionEdit.Deleted for that level.
//
// After accumulation of version edits, the bulk version edit may have
// information about a file which has been deleted from a level, but it may
// not have information about the same file added to the same level. The add
// could've occurred as part of a previous bulk version edit. In this case,
// the deleted file must be present in BulkVersionEdit.Deleted, at the end
// of the accumulation, because we need to decrease the refcount of the
// deleted file in Apply.
func (b *BulkVersionEdit) Accumulate(ve *VersionEdit) error {
for df, m := range ve.DeletedFiles {
dmap := b.Deleted[df.Level]
@@ -579,7 +689,12 @@ func (b *BulkVersionEdit) Accumulate(ve *VersionEdit) error {
if m.MarkedForCompaction {
b.MarkedForCompactionCountDiff--
}
dmap[df.FileNum] = m
if _, ok := b.Added[df.Level][df.FileNum]; !ok {
dmap[df.FileNum] = m
} else {
// Present in b.Added for the same level.
delete(b.Added[df.Level], df.FileNum)
}
}
for _, nf := range ve.NewFiles {
@@ -590,7 +705,10 @@ func (b *BulkVersionEdit) Accumulate(ve *VersionEdit) error {
return base.CorruptionErrorf("pebble: file deleted L%d.%s before it was inserted", nf.Level, nf.Meta.FileNum)
}
}
b.Added[nf.Level] = append(b.Added[nf.Level], nf.Meta)
if b.Added[nf.Level] == nil {
b.Added[nf.Level] = make(map[base.FileNum]*FileMetadata)
}
b.Added[nf.Level][nf.Meta.FileNum] = nf.Meta
if b.AddedByFileNum != nil {
b.AddedByFileNum[nf.Meta.FileNum] = nf.Meta
}
@@ -598,36 +716,101 @@ func (b *BulkVersionEdit) Accumulate(ve *VersionEdit) error {
b.MarkedForCompactionCountDiff++
}
}
// Generate state for the backing files.
b.AddedFileBacking = append(b.AddedFileBacking, ve.CreatedBackingTables...)
// Since a file can be removed from backing files in exactly one version
// edit it is safe to just append without any de-duplication.
b.RemovedFileBacking = append(b.RemovedFileBacking, ve.RemovedBackingTables...)
return nil
}
// AccumulateIncompleteAndApplySingleVE should be called if a single version edit
// is to be applied to the provided curr Version and if the caller needs to
// update the versionSet.zombieTables map. This function exists separately from
// BulkVersionEdit.Apply because it is easier to reason about properties
// regarding BulkVersionedit.Accumulate/Apply and zombie table generation, if we
// know that exactly one version edit is being accumulated.
//
// Note that the version edit passed into this function may be incomplete
// because compactions don't have the ref counting information necessary to
// populate VersionEdit.RemovedBackingTables. This function will complete such a
// version edit by populating RemovedBackingTables.
//
// Invariant: Any file being deleted through ve must belong to the curr Version.
// We can't have a delete for some arbitrary file which does not exist in curr.
func AccumulateIncompleteAndApplySingleVE(
ve *VersionEdit,
curr *Version,
cmp Compare,
formatKey base.FormatKey,
flushSplitBytes int64,
readCompactionRate int64,
backingStateMap map[base.DiskFileNum]*FileBacking,
) (_ *Version, zombies map[base.DiskFileNum]uint64, _ error) {
if len(ve.RemovedBackingTables) != 0 {
panic("pebble: invalid incomplete version edit")
}
var b BulkVersionEdit
err := b.Accumulate(ve)
if err != nil {
return nil, nil, err
}
zombies = make(map[base.DiskFileNum]uint64)
v, err := b.Apply(
curr, cmp, formatKey, flushSplitBytes, readCompactionRate, zombies,
)
if err != nil {
return nil, nil, err
}
for _, s := range b.AddedFileBacking {
backingStateMap[s.DiskFileNum] = s
}
for fileNum := range zombies {
if _, ok := backingStateMap[fileNum]; ok {
// This table was backing some virtual sstable in the latest version,
// but is now a zombie. We add RemovedBackingTables entries for
// these, before the version edit is written to disk.
ve.RemovedBackingTables = append(
ve.RemovedBackingTables, fileNum,
)
delete(backingStateMap, fileNum)
}
}
return v, zombies, nil
}
// Apply applies the delta b to the current version to produce a new
// version. The new version is consistent with respect to the comparer cmp.
//
// curr may be nil, which is equivalent to a pointer to a zero version.
//
// On success, a map of zombie files containing the file numbers and sizes of
// deleted files is returned. These files are considered zombies because they
// are no longer referenced by the returned Version, but cannot be deleted from
// disk as they are still in use by the incoming Version.
// On success, if a non-nil zombies map is provided to Apply, the map is updated
// with file numbers and files sizes of deleted files. These files are
// considered zombies because they are no longer referenced by the returned
// Version, but cannot be deleted from disk as they are still in use by the
// incoming Version.
func (b *BulkVersionEdit) Apply(
curr *Version,
cmp Compare,
formatKey base.FormatKey,
flushSplitBytes int64,
readCompactionRate int64,
) (_ *Version, zombies map[base.FileNum]uint64, _ error) {
addZombie := func(fileNum base.FileNum, size uint64) {
if zombies == nil {
zombies = make(map[base.FileNum]uint64)
}
zombies[fileNum] = size
}
// The remove zombie function is used to handle tables that are moved from
// one level to another during a version edit (i.e. a "move" compaction).
removeZombie := func(fileNum base.FileNum) {
zombies map[base.DiskFileNum]uint64,
) (*Version, error) {
addZombie := func(state *FileBacking) {
if zombies != nil {
delete(zombies, fileNum)
zombies[state.DiskFileNum] = state.Size
}
}
removeZombie := func(state *FileBacking) {
if zombies != nil {
delete(zombies, state.DiskFileNum)
}
}
@@ -639,7 +822,7 @@ func (b *BulkVersionEdit) Apply(
}
v.Stats.MarkedForCompaction += b.MarkedForCompactionCountDiff
if v.Stats.MarkedForCompaction < 0 {
return nil, nil, base.CorruptionErrorf("pebble: version marked for compaction count negative")
return nil, base.CorruptionErrorf("pebble: version marked for compaction count negative")
}
for level := range v.Levels {
@@ -660,7 +843,7 @@ func (b *BulkVersionEdit) Apply(
// Initialize L0Sublevels.
if curr == nil || curr.L0Sublevels == nil {
if err := v.InitL0Sublevels(cmp, formatKey, flushSplitBytes); err != nil {
return nil, nil, errors.Wrap(err, "pebble: internal error")
return nil, errors.Wrap(err, "pebble: internal error")
}
} else {
v.L0Sublevels = curr.L0Sublevels
@@ -673,47 +856,70 @@ func (b *BulkVersionEdit) Apply(
// Some edits on this level.
lm := &v.Levels[level]
lmRange := &v.RangeKeyLevels[level]
addedFiles := b.Added[level]
deletedMap := b.Deleted[level]
if n := v.Levels[level].Len() + len(addedFiles); n == 0 {
return nil, nil, base.CorruptionErrorf(
addedFilesMap := b.Added[level]
deletedFilesMap := b.Deleted[level]
if n := v.Levels[level].Len() + len(addedFilesMap); n == 0 {
return nil, base.CorruptionErrorf(
"pebble: internal error: No current or added files but have deleted files: %d",
errors.Safe(len(deletedMap)))
errors.Safe(len(deletedFilesMap)))
}
// NB: addedFiles may be empty and it also is not necessarily
// internally consistent: it does not reflect deletions in deletedMap.
// NB: addedFilesMap may be empty. If a file is present in addedFilesMap
// for a level, it won't be present in deletedFilesMap for the same
// level.
for _, f := range deletedMap {
addZombie(f.FileNum, f.Size)
if obsolete := v.Levels[level].tree.delete(f); obsolete {
for _, f := range deletedFilesMap {
if obsolete := v.Levels[level].tree.Delete(f); obsolete {
// Deleting a file from the B-Tree may decrement its
// reference count. However, because we cloned the
// previous level's B-Tree, this should never result in a
// file's reference count dropping to zero.
err := errors.Errorf("pebble: internal error: file L%d.%s obsolete during B-Tree removal", level, f.FileNum)
return nil, nil, err
return nil, err
}
if f.HasRangeKeys {
if obsolete := v.RangeKeyLevels[level].tree.delete(f); obsolete {
if obsolete := v.RangeKeyLevels[level].tree.Delete(f); obsolete {
// Deleting a file from the B-Tree may decrement its
// reference count. However, because we cloned the
// previous level's B-Tree, this should never result in a
// file's reference count dropping to zero.
err := errors.Errorf("pebble: internal error: file L%d.%s obsolete during range-key B-Tree removal", level, f.FileNum)
return nil, nil, err
return nil, err
}
}
// Note that a backing sst will only become a zombie if the
// references to it in the latest version is 0. We will remove the
// backing sst from the zombie list in the next loop if one of the
// addedFiles in any of the levels is referencing the backing sst.
// This is possible if a physical sstable is virtualized, or if it
// is moved.
latestRefCount := f.LatestRefs()
if latestRefCount <= 0 {
// If a file is present in deletedFilesMap for a level, then it
// must have already been added to the level previously, which
// means that its latest ref count cannot be 0.
err := errors.Errorf("pebble: internal error: incorrect latestRefs reference counting for file", f.FileNum)
return nil, err
} else if f.LatestUnref() == 0 {
addZombie(f.FileBacking)
}
}
addedFiles := make([]*FileMetadata, 0, len(addedFilesMap))
for _, f := range addedFilesMap {
addedFiles = append(addedFiles, f)
}
// Sort addedFiles by file number. This isn't necessary, but tests which
// replay invalid manifests check the error output, and the error output
// depends on the order in which files are added to the btree.
sort.Slice(addedFiles, func(i, j int) bool {
return addedFiles[i].FileNum < addedFiles[j].FileNum
})
var sm, la *FileMetadata
for _, f := range addedFiles {
if _, ok := deletedMap[f.FileNum]; ok {
// Already called addZombie on this file in the preceding
// loop, so we don't need to do it here.
continue
}
// NB: allowedSeeks is used for read triggered compactions. It is set using
// Options.Experimental.ReadCompactionRate which defaults to 32KB.
var allowedSeeks int64
@@ -723,20 +929,23 @@ func (b *BulkVersionEdit) Apply(
if allowedSeeks < 100 {
allowedSeeks = 100
}
atomic.StoreInt64(&f.Atomic.AllowedSeeks, allowedSeeks)
f.AllowedSeeks.Store(allowedSeeks)
f.InitAllowedSeeks = allowedSeeks
err := lm.tree.insert(f)
err := lm.tree.Insert(f)
// We're adding this file to the new version, so increment the
// latest refs count.
f.LatestRef()
if err != nil {
return nil, nil, errors.Wrap(err, "pebble")
return nil, errors.Wrap(err, "pebble")
}
if f.HasRangeKeys {
err = lmRange.tree.insert(f)
err = lmRange.tree.Insert(f)
if err != nil {
return nil, nil, errors.Wrap(err, "pebble")
return nil, errors.Wrap(err, "pebble")
}
}
removeZombie(f.FileNum)
removeZombie(f.FileBacking)
// Track the keys with the smallest and largest keys, so that we can
// check consistency of the modified span.
if sm == nil || base.InternalCompare(cmp, sm.Smallest, f.Smallest) > 0 {
@@ -748,27 +957,36 @@ func (b *BulkVersionEdit) Apply(
}
if level == 0 {
if curr != nil && curr.L0Sublevels != nil && len(deletedMap) == 0 {
if curr != nil && curr.L0Sublevels != nil && len(deletedFilesMap) == 0 {
// Flushes and ingestions that do not delete any L0 files do not require
// a regeneration of L0Sublevels from scratch. We can instead generate
// it incrementally.
var err error
// AddL0Files requires addedFiles to be sorted in seqnum order.
addedFiles = append([]*FileMetadata(nil), addedFiles...)
SortBySeqNum(addedFiles)
v.L0Sublevels, err = curr.L0Sublevels.AddL0Files(addedFiles, flushSplitBytes, &v.Levels[0])
if errors.Is(err, errInvalidL0SublevelsOpt) {
err = v.InitL0Sublevels(cmp, formatKey, flushSplitBytes)
} else if invariants.Enabled && err == nil {
copyOfSublevels, err := NewL0Sublevels(&v.Levels[0], cmp, formatKey, flushSplitBytes)
if err != nil {
panic(fmt.Sprintf("error when regenerating sublevels: %s", err))
}
s1 := describeSublevels(base.DefaultFormatter, false /* verbose */, copyOfSublevels.Levels)
s2 := describeSublevels(base.DefaultFormatter, false /* verbose */, v.L0Sublevels.Levels)
if s1 != s2 {
panic(fmt.Sprintf("incremental L0 sublevel generation produced different output than regeneration: %s != %s", s1, s2))
}
}
if err != nil {
return nil, nil, errors.Wrap(err, "pebble: internal error")
return nil, errors.Wrap(err, "pebble: internal error")
}
v.L0SublevelFiles = v.L0Sublevels.Levels
} else if err := v.InitL0Sublevels(cmp, formatKey, flushSplitBytes); err != nil {
return nil, nil, errors.Wrap(err, "pebble: internal error")
return nil, errors.Wrap(err, "pebble: internal error")
}
if err := CheckOrdering(cmp, formatKey, Level(0), v.Levels[level].Iter()); err != nil {
return nil, nil, errors.Wrap(err, "pebble: internal error")
return nil, errors.Wrap(err, "pebble: internal error")
}
continue
}
@@ -789,9 +1007,9 @@ func (b *BulkVersionEdit) Apply(
}
})
if err := CheckOrdering(cmp, formatKey, Level(level), check.Iter()); err != nil {
return nil, nil, errors.Wrap(err, "pebble: internal error")
return nil, errors.Wrap(err, "pebble: internal error")
}
}
}
return v, zombies, nil
return v, nil
}