Upgrade versions of vendored libs
This commit is contained in:
324
vendor/github.com/cockroachdb/pebble/internal/manifest/btree.go
generated
vendored
324
vendor/github.com/cockroachdb/pebble/internal/manifest/btree.go
generated
vendored
@@ -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]
|
||||
}
|
||||
|
||||
609
vendor/github.com/cockroachdb/pebble/internal/manifest/l0_sublevels.go
generated
vendored
609
vendor/github.com/cockroachdb/pebble/internal/manifest/l0_sublevels.go
generated
vendored
@@ -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.
|
||||
|
||||
273
vendor/github.com/cockroachdb/pebble/internal/manifest/level_metadata.go
generated
vendored
273
vendor/github.com/cockroachdb/pebble/internal/manifest/level_metadata.go
generated
vendored
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
542
vendor/github.com/cockroachdb/pebble/internal/manifest/version.go
generated
vendored
542
vendor/github.com/cockroachdb/pebble/internal/manifest/version.go
generated
vendored
@@ -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 {
|
||||
|
||||
324
vendor/github.com/cockroachdb/pebble/internal/manifest/version_edit.go
generated
vendored
324
vendor/github.com/cockroachdb/pebble/internal/manifest/version_edit.go
generated
vendored
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user