0

[WIP] lint a bit, update deps

This commit is contained in:
eleksir
2024-02-27 00:34:44 +03:00
parent 1892960896
commit 1f97424f31
201 changed files with 21474 additions and 9282 deletions

View File

@@ -10,7 +10,6 @@ import (
"github.com/cockroachdb/errors"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/internal/cache"
"github.com/cockroachdb/pebble/internal/invariants"
"github.com/cockroachdb/pebble/internal/keyspan"
"github.com/cockroachdb/pebble/internal/manual"
@@ -47,7 +46,10 @@ type blockWriter struct {
// ensuring that any of the keys with the same prefix can be used to
// assemble the full key when the prefix does change.
restarts []uint32
curKey []byte
// Do not read curKey directly from outside blockWriter since it can have
// the InternalKeyKindSSTableInternalObsoleteBit set. Use getCurKey() or
// getCurUserKey() instead.
curKey []byte
// curValue excludes the optional prefix provided to
// storeWithOptionalValuePrefix.
curValue []byte
@@ -79,6 +81,20 @@ const setHasSameKeyPrefixRestartMask uint32 = 1 << 31
const restartMaskLittleEndianHighByteWithoutSetHasSamePrefix byte = 0b0111_1111
const restartMaskLittleEndianHighByteOnlySetHasSamePrefix byte = 0b1000_0000
func (w *blockWriter) getCurKey() InternalKey {
k := base.DecodeInternalKey(w.curKey)
k.Trailer = k.Trailer & trailerObsoleteMask
return k
}
func (w *blockWriter) getCurUserKey() []byte {
n := len(w.curKey) - base.InternalTrailerLen
if n < 0 {
panic(errors.AssertionFailedf("corrupt key in blockWriter buffer"))
}
return w.curKey[:n:n]
}
// If !addValuePrefix, the valuePrefix is ignored.
func (w *blockWriter) storeWithOptionalValuePrefix(
keySize int,
@@ -191,12 +207,21 @@ func (w *blockWriter) storeWithOptionalValuePrefix(
func (w *blockWriter) add(key InternalKey, value []byte) {
w.addWithOptionalValuePrefix(
key, value, len(key.UserKey), false, 0, false)
key, false, value, len(key.UserKey), false, 0, false)
}
// Callers that always set addValuePrefix to false should use add() instead.
//
// isObsolete indicates whether this key-value pair is obsolete in this
// sstable (only applicable when writing data blocks) -- see the comment in
// table.go and the longer one in format.go. addValuePrefix adds a 1 byte
// prefix to the value, specified in valuePrefix -- this is used for data
// blocks in TableFormatPebblev3 onwards for SETs (see the comment in
// format.go, with more details in value_block.go). setHasSameKeyPrefix is
// also used in TableFormatPebblev3 onwards for SETs.
func (w *blockWriter) addWithOptionalValuePrefix(
key InternalKey,
isObsolete bool,
value []byte,
maxSharedKeyLen int,
addValuePrefix bool,
@@ -210,6 +235,9 @@ func (w *blockWriter) addWithOptionalValuePrefix(
w.curKey = make([]byte, 0, size*2)
}
w.curKey = w.curKey[:size]
if isObsolete {
key.Trailer = key.Trailer | trailerObsoleteBit
}
key.Encode(w.curKey)
w.storeWithOptionalValuePrefix(
@@ -371,16 +399,16 @@ type blockIter struct {
// For a block encoded with a restart interval of 1, cached and cachedBuf
// will not be used as there are no prefix compressed entries between the
// restart points.
cached []blockEntry
cachedBuf []byte
cacheHandle cache.Handle
// The first key in the block. This is used by the caller to set bounds
cached []blockEntry
cachedBuf []byte
handle bufferHandle
// for block iteration for already loaded blocks.
firstKey InternalKey
firstUserKey []byte
lazyValueHandling struct {
vbr *valueBlockReader
hasValuePrefix bool
}
hideObsoletePoints bool
}
// blockIter implements the base.InternalIterator interface.
@@ -388,14 +416,16 @@ var _ base.InternalIterator = (*blockIter)(nil)
func newBlockIter(cmp Compare, block block) (*blockIter, error) {
i := &blockIter{}
return i, i.init(cmp, block, 0)
return i, i.init(cmp, block, 0, false)
}
func (i *blockIter) String() string {
return "block"
}
func (i *blockIter) init(cmp Compare, block block, globalSeqNum uint64) error {
func (i *blockIter) init(
cmp Compare, block block, globalSeqNum uint64, hideObsoletePoints bool,
) error {
numRestarts := int32(binary.LittleEndian.Uint32(block[len(block)-4:]))
if numRestarts == 0 {
return base.CorruptionErrorf("pebble/table: invalid table (block has no restart points)")
@@ -408,6 +438,7 @@ func (i *blockIter) init(cmp Compare, block block, globalSeqNum uint64) error {
i.data = block
i.fullKey = i.fullKey[:0]
i.val = nil
i.hideObsoletePoints = hideObsoletePoints
i.clearCache()
if i.restarts > 0 {
if err := i.readFirstKey(); err != nil {
@@ -415,15 +446,21 @@ func (i *blockIter) init(cmp Compare, block block, globalSeqNum uint64) error {
}
} else {
// Block is empty.
i.firstKey = InternalKey{}
i.firstUserKey = nil
}
return nil
}
func (i *blockIter) initHandle(cmp Compare, block cache.Handle, globalSeqNum uint64) error {
i.cacheHandle.Release()
i.cacheHandle = block
return i.init(cmp, block.Get(), globalSeqNum)
// NB: two cases of hideObsoletePoints:
// - Local sstable iteration: globalSeqNum will be set iff the sstable was
// ingested.
// - Foreign sstable iteration: globalSeqNum is always set.
func (i *blockIter) initHandle(
cmp Compare, block bufferHandle, globalSeqNum uint64, hideObsoletePoints bool,
) error {
i.handle.Release()
i.handle = block
return i.init(cmp, block.Get(), globalSeqNum, hideObsoletePoints)
}
func (i *blockIter) invalidate() {
@@ -590,24 +627,27 @@ func (i *blockIter) readFirstKey() error {
// Manually inlining base.DecodeInternalKey provides a 5-10% speedup on
// BlockIter benchmarks.
if n := len(firstKey) - 8; n >= 0 {
i.firstKey.Trailer = binary.LittleEndian.Uint64(firstKey[n:])
i.firstKey.UserKey = firstKey[:n:n]
if i.globalSeqNum != 0 {
i.firstKey.SetSeqNum(i.globalSeqNum)
}
i.firstUserKey = firstKey[:n:n]
} else {
i.firstKey.Trailer = uint64(InternalKeyKindInvalid)
i.firstKey.UserKey = nil
i.firstUserKey = nil
return base.CorruptionErrorf("pebble/table: invalid firstKey in block")
}
return nil
}
func (i *blockIter) decodeInternalKey(key []byte) {
// The sstable internal obsolete bit is set when writing a block and unset by
// blockIter, so no code outside block writing/reading code ever sees it.
const trailerObsoleteBit = uint64(base.InternalKeyKindSSTableInternalObsoleteBit)
const trailerObsoleteMask = (InternalKeySeqNumMax << 8) | uint64(base.InternalKeyKindSSTableInternalObsoleteMask)
func (i *blockIter) decodeInternalKey(key []byte) (hiddenPoint bool) {
// Manually inlining base.DecodeInternalKey provides a 5-10% speedup on
// BlockIter benchmarks.
if n := len(key) - 8; n >= 0 {
i.ikey.Trailer = binary.LittleEndian.Uint64(key[n:])
trailer := binary.LittleEndian.Uint64(key[n:])
hiddenPoint = i.hideObsoletePoints &&
(trailer&trailerObsoleteBit != 0)
i.ikey.Trailer = trailer & trailerObsoleteMask
i.ikey.UserKey = key[:n:n]
if i.globalSeqNum != 0 {
i.ikey.SetSeqNum(i.globalSeqNum)
@@ -616,6 +656,7 @@ func (i *blockIter) decodeInternalKey(key []byte) {
i.ikey.Trailer = uint64(InternalKeyKindInvalid)
i.ikey.UserKey = nil
}
return hiddenPoint
}
func (i *blockIter) clearCache() {
@@ -640,6 +681,10 @@ func (i *blockIter) cacheEntry() {
i.cachedBuf = append(i.cachedBuf, i.key...)
}
func (i *blockIter) getFirstUserKey() []byte {
return i.firstUserKey
}
// SeekGE implements internalIterator.SeekGE, as documented in the pebble
// package.
func (i *blockIter) SeekGE(key []byte, flags base.SeekGEFlags) (*InternalKey, base.LazyValue) {
@@ -647,7 +692,6 @@ func (i *blockIter) SeekGE(key []byte, flags base.SeekGEFlags) (*InternalKey, ba
panic(errors.AssertionFailedf("invalidated blockIter used"))
}
ikey := base.MakeSearchKey(key)
i.clearCache()
// Find the index of the smallest restart point whose key is > the key
// sought; index will be numRestarts if there is no such restart point.
@@ -702,23 +746,23 @@ func (i *blockIter) SeekGE(key []byte, flags base.SeekGEFlags) (*InternalKey, ba
ptr = unsafe.Pointer(uintptr(ptr) + 5)
}
// Manually inlining base.DecodeInternalKey provides a 5-10% speedup on
// BlockIter benchmarks.
// Manually inlining part of base.DecodeInternalKey provides a 5-10%
// speedup on BlockIter benchmarks.
s := getBytes(ptr, int(v1))
var k InternalKey
var k []byte
if n := len(s) - 8; n >= 0 {
k.Trailer = binary.LittleEndian.Uint64(s[n:])
k.UserKey = s[:n:n]
// NB: We can't have duplicate keys if the globalSeqNum != 0, so we
// leave the seqnum on this key as 0 as it won't affect our search
// since ikey has the maximum seqnum.
} else {
k.Trailer = uint64(InternalKeyKindInvalid)
k = s[:n:n]
}
// Else k is invalid, and left as nil
if base.InternalCompare(i.cmp, ikey, k) >= 0 {
if i.cmp(key, k) > 0 {
// The search key is greater than the user key at this restart point.
// Search beyond this restart point, since we are trying to find the
// first restart point with a user key >= the search key.
index = h + 1 // preserves f(i-1) == false
} else {
// k >= search key, so prune everything after index (since index
// satisfies the property we are looking for).
upper = h // preserves f(j) == true
}
}
@@ -726,21 +770,25 @@ func (i *blockIter) SeekGE(key []byte, flags base.SeekGEFlags) (*InternalKey, ba
// => answer is index.
}
// Since keys are strictly increasing, if index > 0 then the restart point at
// index-1 will be the largest whose key is <= the key sought. If index ==
// 0, then all keys in this block are larger than the key sought, and offset
// remains at zero.
// index is the first restart point with key >= search key. Define the keys
// between a restart point and the next restart point as belonging to that
// restart point.
//
// Since keys are strictly increasing, if index > 0 then the restart point
// at index-1 will be the first one that has some keys belonging to it that
// could be equal to the search key. If index == 0, then all keys in this
// block are larger than the key sought, and offset remains at zero.
if index > 0 {
i.offset = decodeRestart(i.data[i.restarts+4*(index-1):])
}
i.readEntry()
i.decodeInternalKey(i.key)
hiddenPoint := i.decodeInternalKey(i.key)
// Iterate from that restart point to somewhere >= the key sought.
if !i.valid() {
return nil, base.LazyValue{}
}
if base.InternalCompare(i.cmp, i.ikey, ikey) >= 0 {
if !hiddenPoint && i.cmp(i.ikey.UserKey, key) >= 0 {
// Initialize i.lazyValue
if !i.lazyValueHandling.hasValuePrefix ||
base.TrailerKind(i.ikey.Trailer) != InternalKeyKindSet {
@@ -753,7 +801,7 @@ func (i *blockIter) SeekGE(key []byte, flags base.SeekGEFlags) (*InternalKey, ba
return &i.ikey, i.lazyValue
}
for i.Next(); i.valid(); i.Next() {
if base.InternalCompare(i.cmp, i.ikey, ikey) >= 0 {
if i.cmp(i.ikey.UserKey, key) >= 0 {
// i.Next() has already initialized i.lazyValue.
return &i.ikey, i.lazyValue
}
@@ -777,8 +825,6 @@ func (i *blockIter) SeekLT(key []byte, flags base.SeekLTFlags) (*InternalKey, ba
panic(errors.AssertionFailedf("invalidated blockIter used"))
}
ikey := base.MakeSearchKey(key)
i.clearCache()
// Find the index of the smallest restart point whose key is >= the key
// sought; index will be numRestarts if there is no such restart point.
@@ -833,23 +879,23 @@ func (i *blockIter) SeekLT(key []byte, flags base.SeekLTFlags) (*InternalKey, ba
ptr = unsafe.Pointer(uintptr(ptr) + 5)
}
// Manually inlining base.DecodeInternalKey provides a 5-10% speedup on
// BlockIter benchmarks.
// Manually inlining part of base.DecodeInternalKey provides a 5-10%
// speedup on BlockIter benchmarks.
s := getBytes(ptr, int(v1))
var k InternalKey
var k []byte
if n := len(s) - 8; n >= 0 {
k.Trailer = binary.LittleEndian.Uint64(s[n:])
k.UserKey = s[:n:n]
// NB: We can't have duplicate keys if the globalSeqNum != 0, so we
// leave the seqnum on this key as 0 as it won't affect our search
// since ikey has the maximum seqnum.
} else {
k.Trailer = uint64(InternalKeyKindInvalid)
k = s[:n:n]
}
// Else k is invalid, and left as nil
if base.InternalCompare(i.cmp, ikey, k) > 0 {
if i.cmp(key, k) > 0 {
// The search key is greater than the user key at this restart point.
// Search beyond this restart point, since we are trying to find the
// first restart point with a user key >= the search key.
index = h + 1 // preserves f(i-1) == false
} else {
// k >= search key, so prune everything after index (since index
// satisfies the property we are looking for).
upper = h // preserves f(j) == true
}
}
@@ -857,8 +903,15 @@ func (i *blockIter) SeekLT(key []byte, flags base.SeekLTFlags) (*InternalKey, ba
// => answer is index.
}
// Since keys are strictly increasing, if index > 0 then the restart point at
// index-1 will be the largest whose key is < the key sought.
// index is the first restart point with key >= search key. Define the keys
// between a restart point and the next restart point as belonging to that
// restart point. Note that index could be equal to i.numRestarts, i.e., we
// are past the last restart.
//
// Since keys are strictly increasing, if index > 0 then the restart point
// at index-1 will be the first one that has some keys belonging to it that
// are less than the search key. If index == 0, then all keys in this block
// are larger than the search key, so there is no match.
targetOffset := i.restarts
if index > 0 {
i.offset = decodeRestart(i.data[i.restarts+4*(index-1):])
@@ -881,24 +934,41 @@ func (i *blockIter) SeekLT(key []byte, flags base.SeekLTFlags) (*InternalKey, ba
for {
i.offset = i.nextOffset
i.readEntry()
i.decodeInternalKey(i.key)
// When hidden keys are common, there is additional optimization possible
// by not caching entries that are hidden (note that some calls to
// cacheEntry don't decode the internal key before caching, but checking
// whether a key is hidden does not require full decoding). However, we do
// need to use the blockEntry.offset in the cache for the first entry at
// the reset point to do the binary search when the cache is empty -- so
// we would need to cache that first entry (though not the key) even if
// was hidden. Our current assumption is that if there are large numbers
// of hidden keys we will be able to skip whole blocks (using block
// property filters) so we don't bother optimizing.
hiddenPoint := i.decodeInternalKey(i.key)
if i.cmp(i.ikey.UserKey, ikey.UserKey) >= 0 {
// NB: we don't use the hiddenPoint return value of decodeInternalKey
// since we want to stop as soon as we reach a key >= ikey.UserKey, so
// that we can reverse.
if i.cmp(i.ikey.UserKey, key) >= 0 {
// The current key is greater than or equal to our search key. Back up to
// the previous key which was less than our search key. Note that this for
// loop will execute at least once with this if-block not being true, so
// the key we are backing up to is the last one this loop cached.
i.Prev()
// i.Prev() has already initialized i.lazyValue.
return &i.ikey, i.lazyValue
return i.Prev()
}
if i.nextOffset >= targetOffset {
// We've reached the end of the current restart block. Return the current
// key. When the restart interval is 1, the first iteration of the for
// loop will bring us here. In that case ikey is backed by the block so
// we get the desired key stability guarantee for the lifetime of the
// blockIter.
// We've reached the end of the current restart block. Return the
// current key if not hidden, else call Prev().
//
// When the restart interval is 1, the first iteration of the for loop
// will bring us here. In that case ikey is backed by the block so we
// get the desired key stability guarantee for the lifetime of the
// blockIter. That is, we never cache anything and therefore never
// return a key backed by cachedBuf.
if hiddenPoint {
return i.Prev()
}
break
}
@@ -932,7 +1002,10 @@ func (i *blockIter) First() (*InternalKey, base.LazyValue) {
}
i.clearCache()
i.readEntry()
i.decodeInternalKey(i.key)
hiddenPoint := i.decodeInternalKey(i.key)
if hiddenPoint {
return i.Next()
}
if !i.lazyValueHandling.hasValuePrefix ||
base.TrailerKind(i.ikey.Trailer) != InternalKeyKindSet {
i.lazyValue = base.MakeInPlaceValue(i.val)
@@ -971,7 +1044,10 @@ func (i *blockIter) Last() (*InternalKey, base.LazyValue) {
i.readEntry()
}
i.decodeInternalKey(i.key)
hiddenPoint := i.decodeInternalKey(i.key)
if hiddenPoint {
return i.Prev()
}
if !i.lazyValueHandling.hasValuePrefix ||
base.TrailerKind(i.ikey.Trailer) != InternalKeyKindSet {
i.lazyValue = base.MakeInPlaceValue(i.val)
@@ -1001,6 +1077,7 @@ func (i *blockIter) Next() (*InternalKey, base.LazyValue) {
i.clearCache()
}
start:
i.offset = i.nextOffset
if !i.valid() {
return nil, base.LazyValue{}
@@ -1008,11 +1085,17 @@ func (i *blockIter) Next() (*InternalKey, base.LazyValue) {
i.readEntry()
// Manually inlined version of i.decodeInternalKey(i.key).
if n := len(i.key) - 8; n >= 0 {
i.ikey.Trailer = binary.LittleEndian.Uint64(i.key[n:])
trailer := binary.LittleEndian.Uint64(i.key[n:])
hiddenPoint := i.hideObsoletePoints &&
(trailer&trailerObsoleteBit != 0)
i.ikey.Trailer = trailer & trailerObsoleteMask
i.ikey.UserKey = i.key[:n:n]
if i.globalSeqNum != 0 {
i.ikey.SetSeqNum(i.globalSeqNum)
}
if hiddenPoint {
goto start
}
} else {
i.ikey.Trailer = uint64(InternalKeyKindInvalid)
i.ikey.UserKey = nil
@@ -1028,8 +1111,8 @@ func (i *blockIter) Next() (*InternalKey, base.LazyValue) {
return &i.ikey, i.lazyValue
}
// nextPrefix is used for implementing NPrefix.
func (i *blockIter) nextPrefix(succKey []byte) (*InternalKey, base.LazyValue) {
// NextPrefix implements (base.InternalIterator).NextPrefix.
func (i *blockIter) NextPrefix(succKey []byte) (*InternalKey, base.LazyValue) {
if i.lazyValueHandling.hasValuePrefix {
return i.nextPrefixV3(succKey)
}
@@ -1154,6 +1237,7 @@ func (i *blockIter) nextPrefixV3(succKey []byte) (*InternalKey, base.LazyValue)
// The trailer is written in little endian, so the key kind is the first
// byte in the trailer that is encoded in the slice [unshared-8:unshared].
keyKind := InternalKeyKind((*[manual.MaxArrayLen]byte)(ptr)[unshared-8])
keyKind = keyKind & base.InternalKeyKindSSTableInternalObsoleteMask
prefixChanged := false
if keyKind == InternalKeyKindSet {
if invariants.Enabled && value == 0 {
@@ -1269,8 +1353,12 @@ func (i *blockIter) nextPrefixV3(succKey []byte) (*InternalKey, base.LazyValue)
i.key = i.fullKey
}
// Manually inlined version of i.decodeInternalKey(i.key).
hiddenPoint := false
if n := len(i.key) - 8; n >= 0 {
i.ikey.Trailer = binary.LittleEndian.Uint64(i.key[n:])
trailer := binary.LittleEndian.Uint64(i.key[n:])
hiddenPoint = i.hideObsoletePoints &&
(trailer&trailerObsoleteBit != 0)
i.ikey.Trailer = trailer & trailerObsoleteMask
i.ikey.UserKey = i.key[:n:n]
if i.globalSeqNum != 0 {
i.ikey.SetSeqNum(i.globalSeqNum)
@@ -1286,6 +1374,9 @@ func (i *blockIter) nextPrefixV3(succKey []byte) (*InternalKey, base.LazyValue)
}
if prefixChanged || i.cmp(i.ikey.UserKey, succKey) >= 0 {
// Prefix has changed.
if hiddenPoint {
return i.Next()
}
if invariants.Enabled && !i.lazyValueHandling.hasValuePrefix {
panic(errors.AssertionFailedf("nextPrefixV3 being run for non-v3 sstable"))
}
@@ -1307,23 +1398,11 @@ func (i *blockIter) nextPrefixV3(succKey []byte) (*InternalKey, base.LazyValue)
return i.SeekGE(succKey, base.SeekGEFlagsNone)
}
// NextPrefix implements (base.InternalIterator).NextPrefix
func (i *blockIter) NextPrefix(succKey []byte) (*InternalKey, base.LazyValue) {
const nextsBeforeSeek = 3
k, v := i.Next()
for j := 1; k != nil && i.cmp(k.UserKey, succKey) < 0; j++ {
if j >= nextsBeforeSeek {
return i.SeekGE(succKey, base.SeekGEFlagsNone)
}
k, v = i.Next()
}
return k, v
}
// Prev implements internalIterator.Prev, as documented in the pebble
// package.
func (i *blockIter) Prev() (*InternalKey, base.LazyValue) {
if n := len(i.cached) - 1; n >= 0 {
start:
for n := len(i.cached) - 1; n >= 0; n-- {
i.nextOffset = i.offset
e := &i.cached[n]
i.offset = e.offset
@@ -1331,7 +1410,13 @@ func (i *blockIter) Prev() (*InternalKey, base.LazyValue) {
// Manually inlined version of i.decodeInternalKey(i.key).
i.key = i.cachedBuf[e.keyStart:e.keyEnd]
if n := len(i.key) - 8; n >= 0 {
i.ikey.Trailer = binary.LittleEndian.Uint64(i.key[n:])
trailer := binary.LittleEndian.Uint64(i.key[n:])
hiddenPoint := i.hideObsoletePoints &&
(trailer&trailerObsoleteBit != 0)
if hiddenPoint {
continue
}
i.ikey.Trailer = trailer & trailerObsoleteMask
i.ikey.UserKey = i.key[:n:n]
if i.globalSeqNum != 0 {
i.ikey.SetSeqNum(i.globalSeqNum)
@@ -1373,6 +1458,8 @@ func (i *blockIter) Prev() (*InternalKey, base.LazyValue) {
// index ≤ h < upper
offset := decodeRestart(i.data[i.restarts+4*h:])
if offset < targetOffset {
// Looking for the first restart that has offset >= targetOffset, so
// ignore h and earlier.
index = h + 1 // preserves f(i-1) == false
} else {
upper = h // preserves f(j) == true
@@ -1382,20 +1469,35 @@ func (i *blockIter) Prev() (*InternalKey, base.LazyValue) {
// => answer is index.
}
// index is first restart with offset >= targetOffset. Note that
// targetOffset may not be at a restart point since one can call Prev()
// after Next() (so the cache was not populated) and targetOffset refers to
// the current entry. index-1 must have an offset < targetOffset (it can't
// be equal to targetOffset since the binary search would have selected that
// as the index).
i.offset = 0
if index > 0 {
i.offset = decodeRestart(i.data[i.restarts+4*(index-1):])
}
// TODO(sumeer): why is the else case not an error given targetOffset is a
// valid offset.
i.readEntry()
// We stop when i.nextOffset == targetOffset since the targetOffset is the
// entry we are stepping back from, and we don't need to cache the entry
// before it, since it is the candidate to return.
for i.nextOffset < targetOffset {
i.cacheEntry()
i.offset = i.nextOffset
i.readEntry()
}
i.decodeInternalKey(i.key)
hiddenPoint := i.decodeInternalKey(i.key)
if hiddenPoint {
// Use the cache.
goto start
}
if !i.lazyValueHandling.hasValuePrefix ||
base.TrailerKind(i.ikey.Trailer) != InternalKeyKindSet {
i.lazyValue = base.MakeInPlaceValue(i.val)
@@ -1425,8 +1527,8 @@ func (i *blockIter) Error() error {
// Close implements internalIterator.Close, as documented in the pebble
// package.
func (i *blockIter) Close() error {
i.cacheHandle.Release()
i.cacheHandle = cache.Handle{}
i.handle.Release()
i.handle = bufferHandle{}
i.val = nil
i.lazyValue = base.LazyValue{}
i.lazyValueHandling.vbr = nil
@@ -1468,6 +1570,10 @@ type fragmentBlockIter struct {
err error
dir int8
closeHook func(i keyspan.FragmentIterator) error
// elideSameSeqnum, if true, returns only the first-occurring (in forward
// order) Key for each sequence number.
elideSameSeqnum bool
}
func (i *fragmentBlockIter) resetForReuse() fragmentBlockIter {
@@ -1496,6 +1602,24 @@ func (i *fragmentBlockIter) decodeSpanKeys(k *InternalKey, internalValue []byte)
}
}
func (i *fragmentBlockIter) elideKeysOfSameSeqNum() {
if invariants.Enabled {
if !i.elideSameSeqnum || len(i.span.Keys) == 0 {
panic("elideKeysOfSameSeqNum called when it should not be")
}
}
lastSeqNum := i.span.Keys[0].SeqNum()
k := 1
for j := 1; j < len(i.span.Keys); j++ {
if lastSeqNum != i.span.Keys[j].SeqNum() {
lastSeqNum = i.span.Keys[j].SeqNum()
i.span.Keys[k] = i.span.Keys[j]
k++
}
}
i.span.Keys = i.span.Keys[:k]
}
// gatherForward gathers internal keys with identical bounds. Keys defined over
// spans of the keyspace are fragmented such that any overlapping key spans have
// identical bounds. When these spans are persisted to a range deletion or range
@@ -1545,6 +1669,9 @@ func (i *fragmentBlockIter) gatherForward(k *InternalKey, lazyValue base.LazyVal
k, lazyValue = i.blockIter.Next()
internalValue = lazyValue.InPlaceValue()
}
if i.elideSameSeqnum && len(i.span.Keys) > 0 {
i.elideKeysOfSameSeqNum()
}
// i.blockIter is positioned over the first internal key for the next span.
return &i.span
}
@@ -1604,6 +1731,9 @@ func (i *fragmentBlockIter) gatherBackward(k *InternalKey, lazyValue base.LazyVa
// Backwards iteration encounters internal keys in the wrong order.
keyspan.SortKeysByTrailer(&i.span.Keys)
if i.elideSameSeqnum && len(i.span.Keys) > 0 {
i.elideKeysOfSameSeqNum()
}
return &i.span
}

View File

@@ -9,6 +9,7 @@ import (
"fmt"
"math"
"sync"
"unsafe"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/internal/rangekey"
@@ -117,7 +118,7 @@ type BlockPropertyCollector interface {
}
// SuffixReplaceableBlockCollector is an extension to the BlockPropertyCollector
// interface that allows a block property collector to indicate the it supports
// interface that allows a block property collector to indicate that it supports
// being *updated* during suffix replacement, i.e. when an existing SST in which
// all keys have the same key suffix is updated to have a new suffix.
//
@@ -189,13 +190,13 @@ type BoundLimitedBlockPropertyFilter interface {
// indicates that the filter may be used to filter blocks that exclusively
// contain keys ≥ `key`, so long as the blocks' keys also satisfy the upper
// bound.
KeyIsWithinLowerBound(key *InternalKey) bool
KeyIsWithinLowerBound(key []byte) bool
// KeyIsWithinUpperBound tests whether the provided internal key falls
// within the current upper bound of the filter. A true return value
// indicates that the filter may be used to filter blocks that exclusively
// contain keys ≤ `key`, so long as the blocks' keys also satisfy the lower
// bound.
KeyIsWithinUpperBound(key *InternalKey) bool
KeyIsWithinUpperBound(key []byte) bool
}
// BlockIntervalCollector is a helper implementation of BlockPropertyCollector
@@ -643,15 +644,21 @@ func (f *BlockPropertiesFilterer) intersectsUserPropsAndFinishInit(
// considered intersecting.
continue
}
byteProps := []byte(props)
if len(byteProps) < 1 {
if len(props) < 1 {
return false, base.CorruptionErrorf(
"block properties for %s is corrupted", f.filters[i].Name())
}
shortID := shortID(byteProps[0])
intersects, err := f.filters[i].Intersects(byteProps[1:])
if err != nil || !intersects {
return false, err
shortID := shortID(props[0])
{
// Use an unsafe conversion to avoid allocating. Intersects() is not
// supposed to modify the given slice.
// Note that unsafe.StringData only works if the string is not empty
// (which we already checked).
byteProps := unsafe.Slice(unsafe.StringData(props), len(props))
intersects, err := f.filters[i].Intersects(byteProps[1:])
if err != nil || !intersects {
return false, err
}
}
// Intersects the sstable, so need to use this filter when
// deciding whether to read blocks.
@@ -684,12 +691,11 @@ func (f *BlockPropertiesFilterer) intersectsUserPropsAndFinishInit(
// be unused within this file.
return true, nil
}
byteProps := []byte(props)
if len(byteProps) < 1 {
if len(props) < 1 {
return false, base.CorruptionErrorf(
"block properties for %s is corrupted", f.boundLimitedFilter.Name())
}
f.boundLimitedShortID = int(byteProps[0])
f.boundLimitedShortID = int(props[0])
// We don't check for table-level intersection for the bound-limited filter.
// The bound-limited filter is treated as vacuously intersecting.

View File

@@ -0,0 +1,148 @@
// Copyright 2023 The LevelDB-Go and Pebble Authors. All rights reserved. Use
// of this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
package sstable
import (
"github.com/cockroachdb/errors"
"github.com/cockroachdb/pebble/internal/cache"
)
// A bufferHandle is a handle to manually-managed memory. The handle may point
// to a block in the block cache (h.Get() != nil), or a buffer that exists
// outside the block cache allocated from a BufferPool (b.Valid()).
type bufferHandle struct {
h cache.Handle
b Buf
}
// Get retrieves the underlying buffer referenced by the handle.
func (bh bufferHandle) Get() []byte {
if v := bh.h.Get(); v != nil {
return v
} else if bh.b.p != nil {
return bh.b.p.pool[bh.b.i].b
}
return nil
}
// Release releases the buffer, either back to the block cache or BufferPool.
func (bh bufferHandle) Release() {
bh.h.Release()
bh.b.Release()
}
// A BufferPool holds a pool of buffers for holding sstable blocks. An initial
// size of the pool is provided on Init, but a BufferPool will grow to meet the
// largest working set size. It'll never shrink. When a buffer is released, the
// BufferPool recycles the buffer for future allocations.
//
// A BufferPool should only be used for short-lived allocations with
// well-understood working set sizes to avoid excessive memory consumption.
//
// BufferPool is not thread-safe.
type BufferPool struct {
// pool contains all the buffers held by the pool, including buffers that
// are in-use. For every i < len(pool): pool[i].v is non-nil.
pool []allocedBuffer
}
type allocedBuffer struct {
v *cache.Value
// b holds the current byte slice. It's backed by v, but may be a subslice
// of v's memory while the buffer is in-use [ len(b) ≤ len(v.Buf()) ].
//
// If the buffer is not currently in-use, b is nil. When being recycled, the
// BufferPool.Alloc will reset b to be a subslice of v.Buf().
b []byte
}
// Init initializes the pool with an initial working set buffer size of
// `initialSize`.
func (p *BufferPool) Init(initialSize int) {
*p = BufferPool{
pool: make([]allocedBuffer, 0, initialSize),
}
}
// initPreallocated is like Init but for internal sstable package use in
// instances where a pre-allocated slice of []allocedBuffer already exists. It's
// used to avoid an extra allocation initializing BufferPool.pool.
func (p *BufferPool) initPreallocated(pool []allocedBuffer) {
*p = BufferPool{
pool: pool[:0],
}
}
// Release releases all buffers held by the pool and resets the pool to an
// uninitialized state.
func (p *BufferPool) Release() {
for i := range p.pool {
if p.pool[i].b != nil {
panic(errors.AssertionFailedf("Release called on a BufferPool with in-use buffers"))
}
cache.Free(p.pool[i].v)
}
*p = BufferPool{}
}
// Alloc allocates a new buffer of size n. If the pool already holds a buffer at
// least as large as n, the pooled buffer is used instead.
//
// Alloc is O(MAX(N,M)) where N is the largest number of concurrently in-use
// buffers allocated and M is the initialSize passed to Init.
func (p *BufferPool) Alloc(n int) Buf {
unusableBufferIdx := -1
for i := 0; i < len(p.pool); i++ {
if p.pool[i].b == nil {
if len(p.pool[i].v.Buf()) >= n {
p.pool[i].b = p.pool[i].v.Buf()[:n]
return Buf{p: p, i: i}
}
unusableBufferIdx = i
}
}
// If we would need to grow the size of the pool to allocate another buffer,
// but there was a slot available occupied by a buffer that's just too
// small, replace the too-small buffer.
if len(p.pool) == cap(p.pool) && unusableBufferIdx >= 0 {
i := unusableBufferIdx
cache.Free(p.pool[i].v)
p.pool[i].v = cache.Alloc(n)
p.pool[i].b = p.pool[i].v.Buf()
return Buf{p: p, i: i}
}
// Allocate a new buffer.
v := cache.Alloc(n)
p.pool = append(p.pool, allocedBuffer{v: v, b: v.Buf()[:n]})
return Buf{p: p, i: len(p.pool) - 1}
}
// A Buf holds a reference to a manually-managed, pooled byte buffer.
type Buf struct {
p *BufferPool
// i holds the index into p.pool where the buffer may be found. This scheme
// avoids needing to allocate the handle to the buffer on the heap at the
// cost of copying two words instead of one.
i int
}
// Valid returns true if the buf holds a valid buffer.
func (b Buf) Valid() bool {
return b.p != nil
}
// Release releases the buffer back to the pool.
func (b *Buf) Release() {
if b.p == nil {
return
}
// Clear the allocedBuffer's byte slice. This signals the allocated buffer
// is no longer in use and a future call to BufferPool.Alloc may reuse this
// buffer.
b.p.pool[b.i].b = nil
b.p = nil
}

View File

@@ -52,10 +52,10 @@ func decompressInto(blockType blockType, compressed []byte, buf []byte) ([]byte,
return result, nil
}
// decompressBlock decompresses an SST block, with space allocated from a cache.
// decompressBlock decompresses an SST block, with manually-allocated space.
// NB: If decompressBlock returns (nil, nil), no decompression was necessary and
// the caller may use `b` directly.
func decompressBlock(cache *cache.Cache, blockType blockType, b []byte) (*cache.Value, error) {
func decompressBlock(blockType blockType, b []byte) (*cache.Value, error) {
if blockType == noCompressionBlockType {
return nil, nil
}

View File

@@ -18,14 +18,32 @@ type FilterMetrics struct {
Misses int64
}
var dummyFilterMetrics FilterMetrics
// FilterMetricsTracker is used to keep track of filter metrics. It contains the
// same metrics as FilterMetrics, but they can be updated atomically. An
// instance of FilterMetricsTracker can be passed to a Reader as a ReaderOption.
type FilterMetricsTracker struct {
// See FilterMetrics.Hits.
hits atomic.Int64
// See FilterMetrics.Misses.
misses atomic.Int64
}
func (m *FilterMetrics) readerApply(r *Reader) {
var _ ReaderOption = (*FilterMetricsTracker)(nil)
func (m *FilterMetricsTracker) readerApply(r *Reader) {
if r.tableFilter != nil {
r.tableFilter.metrics = m
}
}
// Load returns the current values as FilterMetrics.
func (m *FilterMetricsTracker) Load() FilterMetrics {
return FilterMetrics{
Hits: m.hits.Load(),
Misses: m.misses.Load(),
}
}
// BlockHandle is the file offset and length of a block.
type BlockHandle struct {
Offset, Length uint64
@@ -47,22 +65,24 @@ type filterWriter interface {
type tableFilterReader struct {
policy FilterPolicy
metrics *FilterMetrics
metrics *FilterMetricsTracker
}
func newTableFilterReader(policy FilterPolicy) *tableFilterReader {
return &tableFilterReader{
policy: policy,
metrics: &dummyFilterMetrics,
metrics: nil,
}
}
func (f *tableFilterReader) mayContain(data, key []byte) bool {
mayContain := f.policy.MayContain(TableFilter, data, key)
if mayContain {
atomic.AddInt64(&f.metrics.Misses, 1)
} else {
atomic.AddInt64(&f.metrics.Hits, 1)
if f.metrics != nil {
if mayContain {
f.metrics.misses.Add(1)
} else {
f.metrics.hits.Add(1)
}
}
return mayContain
}

View File

@@ -23,15 +23,164 @@ const (
TableFormatRocksDBv2
TableFormatPebblev1 // Block properties.
TableFormatPebblev2 // Range keys.
// TableFormatPebblev3 is not currently intended to subsume v2, as
// supporting value blocks adds a 1 byte prefix to each value. After
// thorough experimentation and some production experience, this may change.
TableFormatPebblev3 // Value blocks.
TableFormatPebblev4 // DELSIZED tombstones.
NumTableFormats
TableFormatMax = TableFormatPebblev3
TableFormatMax = NumTableFormats - 1
)
// TableFormatPebblev4, in addition to DELSIZED, introduces the use of
// InternalKeyKindSSTableInternalObsoleteBit.
//
// 1. Motivation
//
// We have various related problems caused by Pebble snapshots:
//
// - P1: RANGEDELs that delete points in the same sstable, but the points
// happen to not get deleted during compactions because of an open snapshot.
// This causes very expensive iteration, that has been observed in
// production deployments
//
// - P2: When iterating over a foreign sstable (in disaggregated storage), we
// need to do (a) point collapsing to expose at most one point per user key,
// (b) apply RANGEDELs in the sstable to hide deleted points in the same
// sstable. This per-sstable point collapsing iteration needs to be very
// efficient (ideally as efficient from a CPU perspective as iteration over
// regular sstables) since foreign sstables can be very long-lived -- one of
// the goals of disaggregated storage is to scale compute and disk bandwidth
// resources as a function of the hot (from a write perspective) data and
// not the whole data, so we don't want to have to rewrite foreign sstables
// solely to improve read performance.
//
// The ideal solution for P2 would allow user-facing reads to utilize the
// existing SST iterators (with slight modifications) and with no loss of
// efficiency. And for P1 and P2 we would like to skip whole blocks of
// overwritten/deleted points. Even when we can't skip whole blocks, avoiding
// key comparisons at iteration time to discover what points are deleted is
// very desirable, since keys can be long.
//
// We observe that:
//
// - Reads:
// - All user-facing reads in CockroachDB use iterators over the DB, hence
// have a higher read seqnum than all sstables (there are some rare cases
// that can violate this, but those are not important from a performance
// optimization perspective).
//
// - Certain internal-facing reads in CockroachDB use snapshots, but the
// snapshots are shortlived enough that most L5 and L6 sstables will have
// all seqnums lower than the snapshot seqnum.
//
// - Writes:
// - We already do key comparisons between points when writing the sstable
// to ensure that the sstable invariant (monotonically increasing internal
// keys) is not violated. So we know which points share the same userkey,
// and thereby which points are obsolete because there is a more recent
// point in the same sstable.
//
// - The compactionIter knows which point id deleted by a RANGEDEL even if
// the point does need to be written because of a snapshot.
//
// So this known information can be encoded in the sstable at write time and
// utilized for optimized reading.
//
// 2. Solution
//
// We primarily scope the solution to the following point kinds: SET,
// SETWITHDEL, DEL, DELSIZED, SINGLEDEL. These are the ones marked locally
// obsolete, i.e., obsolete within the sstable, and we can guarantee that at
// most one point will be exposed per user key. MERGE keys create more
// complexity: MERGE followed by MERGE causes multiple keys to not be
// obsolete. Same applies for MERGE followed by SET/SETWITHDEL/DEL*. Note
// that:
//
// - For regular sst iteration, the obsolete marking is a performance
// optimization, and multiple keys for the same userkey can be handled by
// higher layers in the iterator tree (specifically pebble.Iterator).
//
// - For foreign sst iteration, we disallow MERGEs to be written to such
// shared ssts (details below).
//
// The key kinds are marked with an obsolete bit
// (InternalKeyKindSSTableInternalObsoleteBit) when the key-value pair is
// obsolete. This marking is done within blockWriter, based on information
// passed to it by Writer. In turn, Writer uses a combination of key
// comparisons, and information provided by compactionIter to decide whether a
// key-value pair is obsolete. Additionally, a Pebble-internal
// BlockPropertyCollector (obsoleteKeyBlockPropertyCollector) is used to mark
// blocks where all key-value pairs are obsolete. Since the common case is
// non-obsolete blocks, this block property collector uses the empty byte
// slice to represent a non-obsolete block, which consumes no space in
// BlockHandleWithProperties.Props.
//
// At read time, the obsolete bit is only visible to the blockIter, which can
// be optionally configured to hide obsolete points. This hiding is only
// configured for data block iterators for sstables being read by user-facing
// iterators at a seqnum greater than the max seqnum in the sstable.
// Additionally, when this hiding is configured, a Pebble-internal block
// property filter (obsoleteKeyBlockPropertyFilter), is used to skip whole
// blocks that are obsolete.
//
// 2.1 Correctness
//
// Due to the level invariant, the sequence of seqnums for a user key in a
// sstable represents a contiguous subsequence of the seqnums for the userkey
// across the whole LSM, and is more recent than the seqnums in a sstable in a
// lower level. So exposing exactly one point from a sstable for a userkey
// will also mask the points for the userkey in lower levels. If we expose no
// point, because of RANGEDELs, that RANGEDEL will also mask the points in
// lower levels.
//
// Note that we do not need to do anything special at write time for
// SETWITHDEL and SINGLEDEL. This is because these key kinds are treated
// specially only by compactions, which do not hide obsolete points. For
// regular reads, SETWITHDEL behaves the same as SET and SINGLEDEL behaves the
// same as DEL.
//
// 2.2 Strictness and MERGE
//
// Setting the obsolete bit on point keys is advanced usage, so we support two
// modes, both of which must be truthful when setting the obsolete bit, but
// vary in when they don't set the obsolete bit.
//
// - Non-strict: In this mode, the bit does not need to be set for keys that
// are obsolete. Additionally, any sstable containing MERGE keys can only
// use this mode. An iterator over such an sstable, when configured to
// hideObsoletePoints, can expose multiple internal keys per user key, and
// can expose keys that are deleted by rangedels in the same sstable. This
// is the mode that non-advanced users should use. Pebble without
// disaggregated storage will also use this mode and will best-effort set
// the obsolete bit, to optimize iteration when snapshots have retained many
// obsolete keys.
//
// - Strict: In this mode, every obsolete key must have the obsolete bit set,
// and no MERGE keys are permitted. An iterator over such an sstable, when
// configured to hideObsoletePoints satisfies two properties:
// - S1: will expose at most one internal key per user key, which is the
// most recent one.
// - S2: will never expose keys that are deleted by rangedels in the same
// sstable.
//
// This is the mode for two use cases in disaggregated storage (which will
// exclude parts of the key space that has MERGEs), for levels that contain
// sstables that can become foreign sstables:
// - Pebble compaction output to these levels that can become foreign
// sstables.
//
// - CockroachDB ingest operations that can ingest into the levels that can
// become foreign sstables. Note, these are not sstables corresponding to
// copied data for CockroachDB range snapshots. This case occurs for
// operations like index backfills: these trivially satisfy the strictness
// criteria since they only write one key per userkey.
//
// TODO(sumeer): this latter case is not currently supported, since only
// Writer.AddWithForceObsolete calls are permitted for writing strict
// obsolete sstables. This is done to reduce the likelihood of bugs. One
// simple way to lift this limitation would be to disallow adding any
// RANGEDELs when a Pebble-external writer is trying to construct a strict
// obsolete sstable.
// ParseTableFormat parses the given magic bytes and version into its
// corresponding internal TableFormat.
func ParseTableFormat(magic []byte, version uint32) (TableFormat, error) {
@@ -53,6 +202,8 @@ func ParseTableFormat(magic []byte, version uint32) (TableFormat, error) {
return TableFormatPebblev2, nil
case 3:
return TableFormatPebblev3, nil
case 4:
return TableFormatPebblev4, nil
default:
return TableFormatUnspecified, base.CorruptionErrorf(
"pebble/table: unsupported pebble format version %d", errors.Safe(version),
@@ -78,6 +229,8 @@ func (f TableFormat) AsTuple() (string, uint32) {
return pebbleDBMagic, 2
case TableFormatPebblev3:
return pebbleDBMagic, 3
case TableFormatPebblev4:
return pebbleDBMagic, 4
default:
panic("sstable: unknown table format version tuple")
}
@@ -96,6 +249,8 @@ func (f TableFormat) String() string {
return "(Pebble,v2)"
case TableFormatPebblev3:
return "(Pebble,v3)"
case TableFormatPebblev4:
return "(Pebble,v4)"
default:
panic("sstable: unknown table format version tuple")
}

View File

@@ -23,6 +23,8 @@ const (
InternalKeyKindLogData = base.InternalKeyKindLogData
InternalKeyKindSingleDelete = base.InternalKeyKindSingleDelete
InternalKeyKindRangeDelete = base.InternalKeyKindRangeDelete
InternalKeyKindSetWithDelete = base.InternalKeyKindSetWithDelete
InternalKeyKindDeleteSized = base.InternalKeyKindDeleteSized
InternalKeyKindMax = base.InternalKeyKindMax
InternalKeyKindInvalid = base.InternalKeyKindInvalid
InternalKeySeqNumBatch = base.InternalKeySeqNumBatch

306
vendor/github.com/cockroachdb/pebble/sstable/layout.go generated vendored Normal file
View File

@@ -0,0 +1,306 @@
// Copyright 2011 The LevelDB-Go and Pebble Authors. All rights reserved. Use
// of this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
package sstable
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"io"
"sort"
"unsafe"
"github.com/cockroachdb/pebble/internal/base"
)
// Layout describes the block organization of an sstable.
type Layout struct {
// NOTE: changes to fields in this struct should also be reflected in
// ValidateBlockChecksums, which validates a static list of BlockHandles
// referenced in this struct.
Data []BlockHandleWithProperties
Index []BlockHandle
TopIndex BlockHandle
Filter BlockHandle
RangeDel BlockHandle
RangeKey BlockHandle
ValueBlock []BlockHandle
ValueIndex BlockHandle
Properties BlockHandle
MetaIndex BlockHandle
Footer BlockHandle
Format TableFormat
}
// Describe returns a description of the layout. If the verbose parameter is
// true, details of the structure of each block are returned as well.
func (l *Layout) Describe(
w io.Writer, verbose bool, r *Reader, fmtRecord func(key *base.InternalKey, value []byte),
) {
ctx := context.TODO()
type block struct {
BlockHandle
name string
}
var blocks []block
for i := range l.Data {
blocks = append(blocks, block{l.Data[i].BlockHandle, "data"})
}
for i := range l.Index {
blocks = append(blocks, block{l.Index[i], "index"})
}
if l.TopIndex.Length != 0 {
blocks = append(blocks, block{l.TopIndex, "top-index"})
}
if l.Filter.Length != 0 {
blocks = append(blocks, block{l.Filter, "filter"})
}
if l.RangeDel.Length != 0 {
blocks = append(blocks, block{l.RangeDel, "range-del"})
}
if l.RangeKey.Length != 0 {
blocks = append(blocks, block{l.RangeKey, "range-key"})
}
for i := range l.ValueBlock {
blocks = append(blocks, block{l.ValueBlock[i], "value-block"})
}
if l.ValueIndex.Length != 0 {
blocks = append(blocks, block{l.ValueIndex, "value-index"})
}
if l.Properties.Length != 0 {
blocks = append(blocks, block{l.Properties, "properties"})
}
if l.MetaIndex.Length != 0 {
blocks = append(blocks, block{l.MetaIndex, "meta-index"})
}
if l.Footer.Length != 0 {
if l.Footer.Length == levelDBFooterLen {
blocks = append(blocks, block{l.Footer, "leveldb-footer"})
} else {
blocks = append(blocks, block{l.Footer, "footer"})
}
}
sort.Slice(blocks, func(i, j int) bool {
return blocks[i].Offset < blocks[j].Offset
})
for i := range blocks {
b := &blocks[i]
fmt.Fprintf(w, "%10d %s (%d)\n", b.Offset, b.name, b.Length)
if !verbose {
continue
}
if b.name == "filter" {
continue
}
if b.name == "footer" || b.name == "leveldb-footer" {
trailer, offset := make([]byte, b.Length), b.Offset
_ = r.readable.ReadAt(ctx, trailer, int64(offset))
if b.name == "footer" {
checksumType := ChecksumType(trailer[0])
fmt.Fprintf(w, "%10d checksum type: %s\n", offset, checksumType)
trailer, offset = trailer[1:], offset+1
}
metaHandle, n := binary.Uvarint(trailer)
metaLen, m := binary.Uvarint(trailer[n:])
fmt.Fprintf(w, "%10d meta: offset=%d, length=%d\n", offset, metaHandle, metaLen)
trailer, offset = trailer[n+m:], offset+uint64(n+m)
indexHandle, n := binary.Uvarint(trailer)
indexLen, m := binary.Uvarint(trailer[n:])
fmt.Fprintf(w, "%10d index: offset=%d, length=%d\n", offset, indexHandle, indexLen)
trailer, offset = trailer[n+m:], offset+uint64(n+m)
fmt.Fprintf(w, "%10d [padding]\n", offset)
trailing := 12
if b.name == "leveldb-footer" {
trailing = 8
}
offset += uint64(len(trailer) - trailing)
trailer = trailer[len(trailer)-trailing:]
if b.name == "footer" {
version := trailer[:4]
fmt.Fprintf(w, "%10d version: %d\n", offset, binary.LittleEndian.Uint32(version))
trailer, offset = trailer[4:], offset+4
}
magicNumber := trailer
fmt.Fprintf(w, "%10d magic number: 0x%x\n", offset, magicNumber)
continue
}
h, err := r.readBlock(
context.Background(), b.BlockHandle, nil /* transform */, nil /* readHandle */, nil /* stats */, nil /* buffer pool */)
if err != nil {
fmt.Fprintf(w, " [err: %s]\n", err)
continue
}
getRestart := func(data []byte, restarts, i int32) int32 {
return decodeRestart(data[restarts+4*i:])
}
formatIsRestart := func(data []byte, restarts, numRestarts, offset int32) {
i := sort.Search(int(numRestarts), func(i int) bool {
return getRestart(data, restarts, int32(i)) >= offset
})
if i < int(numRestarts) && getRestart(data, restarts, int32(i)) == offset {
fmt.Fprintf(w, " [restart]\n")
} else {
fmt.Fprintf(w, "\n")
}
}
formatRestarts := func(data []byte, restarts, numRestarts int32) {
for i := int32(0); i < numRestarts; i++ {
offset := getRestart(data, restarts, i)
fmt.Fprintf(w, "%10d [restart %d]\n",
b.Offset+uint64(restarts+4*i), b.Offset+uint64(offset))
}
}
formatTrailer := func() {
trailer := make([]byte, blockTrailerLen)
offset := int64(b.Offset + b.Length)
_ = r.readable.ReadAt(ctx, trailer, offset)
bt := blockType(trailer[0])
checksum := binary.LittleEndian.Uint32(trailer[1:])
fmt.Fprintf(w, "%10d [trailer compression=%s checksum=0x%04x]\n", offset, bt, checksum)
}
var lastKey InternalKey
switch b.name {
case "data", "range-del", "range-key":
iter, _ := newBlockIter(r.Compare, h.Get())
for key, value := iter.First(); key != nil; key, value = iter.Next() {
ptr := unsafe.Pointer(uintptr(iter.ptr) + uintptr(iter.offset))
shared, ptr := decodeVarint(ptr)
unshared, ptr := decodeVarint(ptr)
value2, _ := decodeVarint(ptr)
total := iter.nextOffset - iter.offset
// The format of the numbers in the record line is:
//
// (<total> = <length> [<shared>] + <unshared> + <value>)
//
// <total> is the total number of bytes for the record.
// <length> is the size of the 3 varint encoded integers for <shared>,
// <unshared>, and <value>.
// <shared> is the number of key bytes shared with the previous key.
// <unshared> is the number of unshared key bytes.
// <value> is the number of value bytes.
fmt.Fprintf(w, "%10d record (%d = %d [%d] + %d + %d)",
b.Offset+uint64(iter.offset), total,
total-int32(unshared+value2), shared, unshared, value2)
formatIsRestart(iter.data, iter.restarts, iter.numRestarts, iter.offset)
if fmtRecord != nil {
fmt.Fprintf(w, " ")
if l.Format < TableFormatPebblev3 {
fmtRecord(key, value.InPlaceValue())
} else {
// InPlaceValue() will succeed even for data blocks where the
// actual value is in a different location, since this value was
// fetched from a blockIter which does not know about value
// blocks.
v := value.InPlaceValue()
if base.TrailerKind(key.Trailer) != InternalKeyKindSet {
fmtRecord(key, v)
} else if !isValueHandle(valuePrefix(v[0])) {
fmtRecord(key, v[1:])
} else {
vh := decodeValueHandle(v[1:])
fmtRecord(key, []byte(fmt.Sprintf("value handle %+v", vh)))
}
}
}
if base.InternalCompare(r.Compare, lastKey, *key) >= 0 {
fmt.Fprintf(w, " WARNING: OUT OF ORDER KEYS!\n")
}
lastKey.Trailer = key.Trailer
lastKey.UserKey = append(lastKey.UserKey[:0], key.UserKey...)
}
formatRestarts(iter.data, iter.restarts, iter.numRestarts)
formatTrailer()
case "index", "top-index":
iter, _ := newBlockIter(r.Compare, h.Get())
for key, value := iter.First(); key != nil; key, value = iter.Next() {
bh, err := decodeBlockHandleWithProperties(value.InPlaceValue())
if err != nil {
fmt.Fprintf(w, "%10d [err: %s]\n", b.Offset+uint64(iter.offset), err)
continue
}
fmt.Fprintf(w, "%10d block:%d/%d",
b.Offset+uint64(iter.offset), bh.Offset, bh.Length)
formatIsRestart(iter.data, iter.restarts, iter.numRestarts, iter.offset)
}
formatRestarts(iter.data, iter.restarts, iter.numRestarts)
formatTrailer()
case "properties":
iter, _ := newRawBlockIter(r.Compare, h.Get())
for valid := iter.First(); valid; valid = iter.Next() {
fmt.Fprintf(w, "%10d %s (%d)",
b.Offset+uint64(iter.offset), iter.Key().UserKey, iter.nextOffset-iter.offset)
formatIsRestart(iter.data, iter.restarts, iter.numRestarts, iter.offset)
}
formatRestarts(iter.data, iter.restarts, iter.numRestarts)
formatTrailer()
case "meta-index":
iter, _ := newRawBlockIter(r.Compare, h.Get())
for valid := iter.First(); valid; valid = iter.Next() {
value := iter.Value()
var bh BlockHandle
var n int
var vbih valueBlocksIndexHandle
isValueBlocksIndexHandle := false
if bytes.Equal(iter.Key().UserKey, []byte(metaValueIndexName)) {
vbih, n, err = decodeValueBlocksIndexHandle(value)
bh = vbih.h
isValueBlocksIndexHandle = true
} else {
bh, n = decodeBlockHandle(value)
}
if n == 0 || n != len(value) {
fmt.Fprintf(w, "%10d [err: %s]\n", b.Offset+uint64(iter.offset), err)
continue
}
var vbihStr string
if isValueBlocksIndexHandle {
vbihStr = fmt.Sprintf(" value-blocks-index-lengths: %d(num), %d(offset), %d(length)",
vbih.blockNumByteLength, vbih.blockOffsetByteLength, vbih.blockLengthByteLength)
}
fmt.Fprintf(w, "%10d %s block:%d/%d%s",
b.Offset+uint64(iter.offset), iter.Key().UserKey,
bh.Offset, bh.Length, vbihStr)
formatIsRestart(iter.data, iter.restarts, iter.numRestarts, iter.offset)
}
formatRestarts(iter.data, iter.restarts, iter.numRestarts)
formatTrailer()
case "value-block":
// We don't peer into the value-block since it can't be interpreted
// without the valueHandles.
case "value-index":
// We have already read the value-index to construct the list of
// value-blocks, so no need to do it again.
}
h.Release()
}
last := blocks[len(blocks)-1]
fmt.Fprintf(w, "%10d EOF\n", last.Offset+last.Length)
}

View File

@@ -21,6 +21,17 @@ const (
NCompression
)
var ignoredInternalProperties = map[string]struct{}{
"rocksdb.column.family.id": {},
"rocksdb.fixed.key.length": {},
"rocksdb.index.key.is.user.key": {},
"rocksdb.index.value.is.delta.encoded": {},
"rocksdb.oldest.key.time": {},
"rocksdb.creation.time": {},
"rocksdb.file.creation.time": {},
"rocksdb.format.version": {},
}
func (c Compression) String() string {
switch c {
case DefaultCompression:
@@ -100,6 +111,9 @@ type ReaderOptions struct {
// The default cache size is a zero-size cache.
Cache *cache.Cache
// User properties specified in this map will not be added to sst.Properties.UserProperties.
DeniedUserProperties map[string]struct{}
// Comparer defines a total ordering over the space of []byte keys: a 'less
// than' relationship. The same comparison algorithm must be used for reads
// and writes over the lifetime of the DB.
@@ -107,6 +121,9 @@ type ReaderOptions struct {
// The default value uses the same ordering as bytes.Compare.
Comparer *Comparer
// Merge defines the Merge function in use for this keyspace.
Merge base.Merge
// Filters is a map from filter policy name to filter policy. It is used for
// debugging tools which may be used on multiple databases configured with
// different filter policies. It is not necessary to populate this filters
@@ -126,12 +143,18 @@ func (o ReaderOptions) ensureDefaults() ReaderOptions {
if o.Comparer == nil {
o.Comparer = base.DefaultComparer
}
if o.Merge == nil {
o.Merge = base.DefaultMerger.Merge
}
if o.MergerName == "" {
o.MergerName = base.DefaultMerger.Name
}
if o.LoggerAndTracer == nil {
o.LoggerAndTracer = base.NoopLoggerAndTracer{}
}
if o.DeniedUserProperties == nil {
o.DeniedUserProperties = ignoredInternalProperties
}
return o
}
@@ -210,6 +233,17 @@ type WriterOptions struct {
// by a wider range of tools and libraries.
TableFormat TableFormat
// IsStrictObsolete is only relevant for >= TableFormatPebblev4. See comment
// in format.go. Must be false if format < TableFormatPebblev4.
//
// TODO(bilal): set this when writing shared ssts.
IsStrictObsolete bool
// WritingToLowestLevel is only relevant for >= TableFormatPebblev4. It is
// used to set the obsolete bit on DEL/DELSIZED/SINGLEDEL if they are the
// youngest for a userkey.
WritingToLowestLevel bool
// TablePropertyCollectors is a list of TablePropertyCollector creation
// functions. A new TablePropertyCollector is created for each sstable built
// and lives for the lifetime of the table.

View File

@@ -23,20 +23,22 @@ var propTagMap = make(map[string]reflect.StructField)
var propBoolTrue = []byte{'1'}
var propBoolFalse = []byte{'0'}
var columnFamilyIDField = func() reflect.StructField {
f, ok := reflect.TypeOf(Properties{}).FieldByName("ColumnFamilyID")
if !ok {
panic("Properties.ColumnFamilyID field not found")
}
return f
}()
var propOffsetTagMap = make(map[uintptr]string)
func init() {
t := reflect.TypeOf(Properties{})
func generateTagMaps(t reflect.Type, indexPrefix []int) {
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if f.Type.Kind() == reflect.Struct {
if tag := f.Tag.Get("prop"); i == 0 && tag == "pebble.embbeded_common_properties" {
// CommonProperties struct embedded in Properties. Note that since
// CommonProperties is placed at the top of properties we can use
// the offsets of the fields within CommonProperties to determine
// the offsets of those fields within Properties.
generateTagMaps(f.Type, []int{i})
continue
}
panic("pebble: unknown struct type in Properties")
}
if tag := f.Tag.Get("prop"); tag != "" {
switch f.Type.Kind() {
case reflect.Bool:
@@ -46,86 +48,125 @@ func init() {
default:
panic(fmt.Sprintf("unsupported property field type: %s %s", f.Name, f.Type))
}
if len(indexPrefix) > 0 {
// Prepend the index prefix so that we can use FieldByIndex on the top-level struct.
f.Index = append(indexPrefix[:len(indexPrefix):len(indexPrefix)], f.Index...)
}
propTagMap[tag] = f
propOffsetTagMap[f.Offset] = tag
}
}
}
// Properties holds the sstable property values. The properties are
// automatically populated during sstable creation and load from the properties
// meta block when an sstable is opened.
type Properties struct {
// ID of column family for this SST file, corresponding to the CF identified
// by column_family_name.
ColumnFamilyID uint64 `prop:"rocksdb.column.family.id"`
// Name of the column family with which this SST file is associated. Empty if
// the column family is unknown.
ColumnFamilyName string `prop:"rocksdb.column.family.name"`
// The name of the comparer used in this table.
ComparerName string `prop:"rocksdb.comparator"`
// The compression algorithm used to compress blocks.
CompressionName string `prop:"rocksdb.compression"`
// The compression options used to compress blocks.
CompressionOptions string `prop:"rocksdb.compression_options"`
// The time when the SST file was created. Since SST files are immutable,
// this is equivalent to last modified time.
CreationTime uint64 `prop:"rocksdb.creation.time"`
// The total size of all data blocks.
DataSize uint64 `prop:"rocksdb.data.size"`
// The external sstable version format. Version 2 is the one RocksDB has been
// using since 5.13. RocksDB only uses the global sequence number for an
// sstable if this property has been set.
ExternalFormatVersion uint32 `prop:"rocksdb.external_sst_file.version"`
// Actual SST file creation time. 0 means unknown.
FileCreationTime uint64 `prop:"rocksdb.file.creation.time"`
// The name of the filter policy used in this table. Empty if no filter
// policy is used.
FilterPolicyName string `prop:"rocksdb.filter.policy"`
// The size of filter block.
FilterSize uint64 `prop:"rocksdb.filter.size"`
// If 0, key is variable length. Otherwise number of bytes for each key.
FixedKeyLen uint64 `prop:"rocksdb.fixed.key.length"`
// Format version, reserved for backward compatibility.
FormatVersion uint64 `prop:"rocksdb.format.version"`
// The global sequence number to use for all entries in the table. Present if
// the table was created externally and ingested whole.
GlobalSeqNum uint64 `prop:"rocksdb.external_sst_file.global_seqno"`
// Whether the index key is user key or an internal key.
IndexKeyIsUserKey uint64 `prop:"rocksdb.index.key.is.user.key"`
// Total number of index partitions if kTwoLevelIndexSearch is used.
IndexPartitions uint64 `prop:"rocksdb.index.partitions"`
// The size of index block.
IndexSize uint64 `prop:"rocksdb.index.size"`
// The index type. TODO(peter): add a more detailed description.
IndexType uint32 `prop:"rocksdb.block.based.table.index.type"`
// Whether delta encoding is used to encode the index values.
IndexValueIsDeltaEncoded uint64 `prop:"rocksdb.index.value.is.delta.encoded"`
// The name of the merger used in this table. Empty if no merger is used.
MergerName string `prop:"rocksdb.merge.operator"`
// The number of blocks in this table.
NumDataBlocks uint64 `prop:"rocksdb.num.data.blocks"`
func init() {
generateTagMaps(reflect.TypeOf(Properties{}), nil)
}
// CommonProperties holds properties for either a virtual or a physical sstable. This
// can be used by code which doesn't care to make the distinction between physical
// and virtual sstables properties.
//
// For virtual sstables, fields are constructed through extrapolation upon virtual
// reader construction. See MakeVirtualReader for implementation details.
//
// NB: The values of these properties can affect correctness. For example,
// if NumRangeKeySets == 0, but the sstable actually contains range keys, then
// the iterators will behave incorrectly.
type CommonProperties struct {
// The number of entries in this table.
NumEntries uint64 `prop:"rocksdb.num.entries"`
// Total raw key size.
RawKeySize uint64 `prop:"rocksdb.raw.key.size"`
// Total raw value size.
RawValueSize uint64 `prop:"rocksdb.raw.value.size"`
// Total raw key size of point deletion tombstones. This value is comparable
// to RawKeySize.
RawPointTombstoneKeySize uint64 `prop:"pebble.raw.point-tombstone.key.size"`
// Sum of the raw value sizes carried by point deletion tombstones
// containing size estimates. See the DeleteSized key kind. This value is
// comparable to Raw{Key,Value}Size.
RawPointTombstoneValueSize uint64 `prop:"pebble.raw.point-tombstone.value.size"`
// The number of point deletion entries ("tombstones") in this table that
// carry a size hint indicating the size of the value the tombstone deletes.
NumSizedDeletions uint64 `prop:"pebble.num.deletions.sized"`
// The number of deletion entries in this table, including both point and
// range deletions.
NumDeletions uint64 `prop:"rocksdb.deleted.keys"`
// The number of entries in this table.
NumEntries uint64 `prop:"rocksdb.num.entries"`
// The number of merge operands in the table.
NumMergeOperands uint64 `prop:"rocksdb.merge.operands"`
// The number of range deletions in this table.
NumRangeDeletions uint64 `prop:"rocksdb.num.range-deletions"`
// The number of RANGEKEYDELs in this table.
NumRangeKeyDels uint64 `prop:"pebble.num.range-key-dels"`
// The number of RANGEKEYSETs in this table.
NumRangeKeySets uint64 `prop:"pebble.num.range-key-sets"`
// Total size of value blocks and value index block. Only serialized if > 0.
ValueBlocksSize uint64 `prop:"pebble.value-blocks.size"`
}
// String is only used for testing purposes.
func (c *CommonProperties) String() string {
var buf bytes.Buffer
v := reflect.ValueOf(*c)
loaded := make(map[uintptr]struct{})
writeProperties(loaded, v, &buf)
return buf.String()
}
// NumPointDeletions is the number of point deletions in the sstable. For virtual
// sstables, this is an estimate.
func (c *CommonProperties) NumPointDeletions() uint64 {
return c.NumDeletions - c.NumRangeDeletions
}
// Properties holds the sstable property values. The properties are
// automatically populated during sstable creation and load from the properties
// meta block when an sstable is opened.
type Properties struct {
// CommonProperties needs to be at the top of the Properties struct so that the
// offsets of the fields in CommonProperties match the offsets of the embedded
// fields of CommonProperties in Properties.
CommonProperties `prop:"pebble.embbeded_common_properties"`
// The name of the comparer used in this table.
ComparerName string `prop:"rocksdb.comparator"`
// The compression algorithm used to compress blocks.
CompressionName string `prop:"rocksdb.compression"`
// The compression options used to compress blocks.
CompressionOptions string `prop:"rocksdb.compression_options"`
// The total size of all data blocks.
DataSize uint64 `prop:"rocksdb.data.size"`
// The external sstable version format. Version 2 is the one RocksDB has been
// using since 5.13. RocksDB only uses the global sequence number for an
// sstable if this property has been set.
ExternalFormatVersion uint32 `prop:"rocksdb.external_sst_file.version"`
// The name of the filter policy used in this table. Empty if no filter
// policy is used.
FilterPolicyName string `prop:"rocksdb.filter.policy"`
// The size of filter block.
FilterSize uint64 `prop:"rocksdb.filter.size"`
// The global sequence number to use for all entries in the table. Present if
// the table was created externally and ingested whole.
GlobalSeqNum uint64 `prop:"rocksdb.external_sst_file.global_seqno"`
// Total number of index partitions if kTwoLevelIndexSearch is used.
IndexPartitions uint64 `prop:"rocksdb.index.partitions"`
// The size of index block.
IndexSize uint64 `prop:"rocksdb.index.size"`
// The index type. TODO(peter): add a more detailed description.
IndexType uint32 `prop:"rocksdb.block.based.table.index.type"`
// For formats >= TableFormatPebblev4, this is set to true if the obsolete
// bit is strict for all the point keys.
IsStrictObsolete bool `prop:"pebble.obsolete.is_strict"`
// The name of the merger used in this table. Empty if no merger is used.
MergerName string `prop:"rocksdb.merge.operator"`
// The number of blocks in this table.
NumDataBlocks uint64 `prop:"rocksdb.num.data.blocks"`
// The number of merge operands in the table.
NumMergeOperands uint64 `prop:"rocksdb.merge.operands"`
// The number of RANGEKEYUNSETs in this table.
NumRangeKeyUnsets uint64 `prop:"pebble.num.range-key-unsets"`
// The number of value blocks in this table. Only serialized if > 0.
NumValueBlocks uint64 `prop:"pebble.num.value-blocks"`
// The number of values stored in value blocks. Only serialized if > 0.
NumValuesInValueBlocks uint64 `prop:"pebble.num.values.in.value-blocks"`
// Timestamp of the earliest key. 0 if unknown.
OldestKeyTime uint64 `prop:"rocksdb.oldest.key.time"`
// The name of the prefix extractor used in this table. Empty if no prefix
// extractor is used.
PrefixExtractorName string `prop:"rocksdb.prefix.extractor.name"`
@@ -134,14 +175,10 @@ type Properties struct {
// A comma separated list of names of the property collectors used in this
// table.
PropertyCollectorNames string `prop:"rocksdb.property.collectors"`
// Total raw key size.
RawKeySize uint64 `prop:"rocksdb.raw.key.size"`
// Total raw rangekey key size.
RawRangeKeyKeySize uint64 `prop:"pebble.raw.range-key.key.size"`
// Total raw rangekey value size.
RawRangeKeyValueSize uint64 `prop:"pebble.raw.range-key.value.size"`
// Total raw value size.
RawValueSize uint64 `prop:"rocksdb.raw.value.size"`
// The total number of keys in this table that were pinned by open snapshots.
SnapshotPinnedKeys uint64 `prop:"pebble.num.snapshot-pinned-keys"`
// The cumulative bytes of keys in this table that were pinned by
@@ -154,8 +191,6 @@ type Properties struct {
TopLevelIndexSize uint64 `prop:"rocksdb.top-level.index.size"`
// User collected properties.
UserProperties map[string]string
// Total size of value blocks and value index block. Only serialized if > 0.
ValueBlocksSize uint64 `prop:"pebble.value-blocks.size"`
// If filtering is enabled, was the filter created on the whole key.
WholeKeyFiltering bool `prop:"rocksdb.block.based.table.whole.key.filtering"`
@@ -176,12 +211,15 @@ func (p *Properties) NumRangeKeys() uint64 {
return p.NumRangeKeyDels + p.NumRangeKeySets + p.NumRangeKeyUnsets
}
func (p *Properties) String() string {
var buf bytes.Buffer
v := reflect.ValueOf(*p)
func writeProperties(loaded map[uintptr]struct{}, v reflect.Value, buf *bytes.Buffer) {
vt := v.Type()
for i := 0; i < v.NumField(); i++ {
ft := vt.Field(i)
if ft.Type.Kind() == reflect.Struct {
// Embedded struct within the properties.
writeProperties(loaded, v.Field(i), buf)
continue
}
tag := ft.Tag.Get("prop")
if tag == "" {
continue
@@ -191,30 +229,33 @@ func (p *Properties) String() string {
// TODO(peter): Use f.IsZero() when we can rely on go1.13.
if zero := reflect.Zero(f.Type()); zero.Interface() == f.Interface() {
// Skip printing of zero values which were not loaded from disk.
if _, ok := p.Loaded[ft.Offset]; !ok {
if _, ok := loaded[ft.Offset]; !ok {
continue
}
}
fmt.Fprintf(&buf, "%s: ", tag)
fmt.Fprintf(buf, "%s: ", tag)
switch ft.Type.Kind() {
case reflect.Bool:
fmt.Fprintf(&buf, "%t\n", f.Bool())
fmt.Fprintf(buf, "%t\n", f.Bool())
case reflect.Uint32:
fmt.Fprintf(&buf, "%d\n", f.Uint())
fmt.Fprintf(buf, "%d\n", f.Uint())
case reflect.Uint64:
u := f.Uint()
if ft.Offset == columnFamilyIDField.Offset && u == math.MaxInt32 {
fmt.Fprintf(&buf, "-\n")
} else {
fmt.Fprintf(&buf, "%d\n", f.Uint())
}
fmt.Fprintf(buf, "%d\n", f.Uint())
case reflect.String:
fmt.Fprintf(&buf, "%s\n", f.String())
fmt.Fprintf(buf, "%s\n", f.String())
default:
panic("not reached")
}
}
}
func (p *Properties) String() string {
var buf bytes.Buffer
v := reflect.ValueOf(*p)
writeProperties(p.Loaded, v, &buf)
// Write the UserProperties.
keys := make([]string, 0, len(p.UserProperties))
for key := range p.UserProperties {
keys = append(keys, key)
@@ -226,16 +267,18 @@ func (p *Properties) String() string {
return buf.String()
}
func (p *Properties) load(b block, blockOffset uint64) error {
func (p *Properties) load(
b block, blockOffset uint64, deniedUserProperties map[string]struct{},
) error {
i, err := newRawBlockIter(bytes.Compare, b)
if err != nil {
return err
}
p.Loaded = make(map[uintptr]struct{})
v := reflect.ValueOf(p).Elem()
for valid := i.First(); valid; valid = i.Next() {
tag := intern.Bytes(i.Key().UserKey)
if f, ok := propTagMap[tag]; ok {
if f, ok := propTagMap[string(i.Key().UserKey)]; ok {
p.Loaded[f.Offset] = struct{}{}
field := v.FieldByIndex(f.Index)
switch f.Type.Kind() {
@@ -245,7 +288,7 @@ func (p *Properties) load(b block, blockOffset uint64) error {
field.SetUint(uint64(binary.LittleEndian.Uint32(i.Value())))
case reflect.Uint64:
var n uint64
if tag == propGlobalSeqnumName {
if string(i.Key().UserKey) == propGlobalSeqnumName {
n = binary.LittleEndian.Uint64(i.Value())
} else {
n, _ = binary.Uvarint(i.Value())
@@ -261,7 +304,10 @@ func (p *Properties) load(b block, blockOffset uint64) error {
if p.UserProperties == nil {
p.UserProperties = make(map[string]string)
}
p.UserProperties[tag] = string(i.Value())
if _, denied := deniedUserProperties[string(i.Key().UserKey)]; !denied {
p.UserProperties[intern.Bytes(i.Key().UserKey)] = string(i.Value())
}
}
return nil
}
@@ -297,16 +343,12 @@ func (p *Properties) saveString(m map[string][]byte, offset uintptr, value strin
m[propOffsetTagMap[offset]] = []byte(value)
}
func (p *Properties) save(w *rawBlockWriter) {
func (p *Properties) save(tblFormat TableFormat, w *rawBlockWriter) {
m := make(map[string][]byte)
for k, v := range p.UserProperties {
m[k] = []byte(v)
}
p.saveUvarint(m, unsafe.Offsetof(p.ColumnFamilyID), p.ColumnFamilyID)
if p.ColumnFamilyName != "" {
p.saveString(m, unsafe.Offsetof(p.ColumnFamilyName), p.ColumnFamilyName)
}
if p.ComparerName != "" {
p.saveString(m, unsafe.Offsetof(p.ComparerName), p.ComparerName)
}
@@ -316,37 +358,45 @@ func (p *Properties) save(w *rawBlockWriter) {
if p.CompressionOptions != "" {
p.saveString(m, unsafe.Offsetof(p.CompressionOptions), p.CompressionOptions)
}
p.saveUvarint(m, unsafe.Offsetof(p.CreationTime), p.CreationTime)
p.saveUvarint(m, unsafe.Offsetof(p.DataSize), p.DataSize)
if p.ExternalFormatVersion != 0 {
p.saveUint32(m, unsafe.Offsetof(p.ExternalFormatVersion), p.ExternalFormatVersion)
p.saveUint64(m, unsafe.Offsetof(p.GlobalSeqNum), p.GlobalSeqNum)
}
if p.FileCreationTime > 0 {
p.saveUvarint(m, unsafe.Offsetof(p.FileCreationTime), p.FileCreationTime)
}
if p.FilterPolicyName != "" {
p.saveString(m, unsafe.Offsetof(p.FilterPolicyName), p.FilterPolicyName)
}
p.saveUvarint(m, unsafe.Offsetof(p.FilterSize), p.FilterSize)
p.saveUvarint(m, unsafe.Offsetof(p.FixedKeyLen), p.FixedKeyLen)
p.saveUvarint(m, unsafe.Offsetof(p.FormatVersion), p.FormatVersion)
p.saveUvarint(m, unsafe.Offsetof(p.IndexKeyIsUserKey), p.IndexKeyIsUserKey)
if p.IndexPartitions != 0 {
p.saveUvarint(m, unsafe.Offsetof(p.IndexPartitions), p.IndexPartitions)
p.saveUvarint(m, unsafe.Offsetof(p.TopLevelIndexSize), p.TopLevelIndexSize)
}
p.saveUvarint(m, unsafe.Offsetof(p.IndexSize), p.IndexSize)
p.saveUint32(m, unsafe.Offsetof(p.IndexType), p.IndexType)
p.saveUvarint(m, unsafe.Offsetof(p.IndexValueIsDeltaEncoded), p.IndexValueIsDeltaEncoded)
if p.IsStrictObsolete {
p.saveBool(m, unsafe.Offsetof(p.IsStrictObsolete), p.IsStrictObsolete)
}
if p.MergerName != "" {
p.saveString(m, unsafe.Offsetof(p.MergerName), p.MergerName)
}
p.saveUvarint(m, unsafe.Offsetof(p.NumDataBlocks), p.NumDataBlocks)
p.saveUvarint(m, unsafe.Offsetof(p.NumEntries), p.NumEntries)
p.saveUvarint(m, unsafe.Offsetof(p.NumDeletions), p.NumDeletions)
if p.NumSizedDeletions > 0 {
p.saveUvarint(m, unsafe.Offsetof(p.NumSizedDeletions), p.NumSizedDeletions)
}
p.saveUvarint(m, unsafe.Offsetof(p.NumMergeOperands), p.NumMergeOperands)
p.saveUvarint(m, unsafe.Offsetof(p.NumRangeDeletions), p.NumRangeDeletions)
// NB: We only write out some properties for Pebble formats. This isn't
// strictly necessary because unrecognized properties are interpreted as
// user-defined properties, however writing them prevents byte-for-byte
// equivalence with RocksDB files that some of our testing requires.
if p.RawPointTombstoneKeySize > 0 && tblFormat >= TableFormatPebblev1 {
p.saveUvarint(m, unsafe.Offsetof(p.RawPointTombstoneKeySize), p.RawPointTombstoneKeySize)
}
if p.RawPointTombstoneValueSize > 0 {
p.saveUvarint(m, unsafe.Offsetof(p.RawPointTombstoneValueSize), p.RawPointTombstoneValueSize)
}
if p.NumRangeKeys() > 0 {
p.saveUvarint(m, unsafe.Offsetof(p.NumRangeKeyDels), p.NumRangeKeyDels)
p.saveUvarint(m, unsafe.Offsetof(p.NumRangeKeySets), p.NumRangeKeySets)
@@ -360,7 +410,6 @@ func (p *Properties) save(w *rawBlockWriter) {
if p.NumValuesInValueBlocks > 0 {
p.saveUvarint(m, unsafe.Offsetof(p.NumValuesInValueBlocks), p.NumValuesInValueBlocks)
}
p.saveUvarint(m, unsafe.Offsetof(p.OldestKeyTime), p.OldestKeyTime)
if p.PrefixExtractorName != "" {
p.saveString(m, unsafe.Offsetof(p.PrefixExtractorName), p.PrefixExtractorName)
}
@@ -380,6 +429,16 @@ func (p *Properties) save(w *rawBlockWriter) {
}
p.saveBool(m, unsafe.Offsetof(p.WholeKeyFiltering), p.WholeKeyFiltering)
if tblFormat < TableFormatPebblev1 {
m["rocksdb.column.family.id"] = binary.AppendUvarint([]byte(nil), math.MaxInt32)
m["rocksdb.fixed.key.length"] = []byte{0x00}
m["rocksdb.index.key.is.user.key"] = []byte{0x00}
m["rocksdb.index.value.is.delta.encoded"] = []byte{0x00}
m["rocksdb.oldest.key.time"] = []byte{0x00}
m["rocksdb.creation.time"] = []byte{0x00}
m["rocksdb.format.version"] = []byte{0x00}
}
keys := make([]string, 0, len(m))
for key := range m {
keys = append(keys, key)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,291 @@
// Copyright 2011 The LevelDB-Go and Pebble Authors. All rights reserved. Use
// of this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
package sstable
import (
"fmt"
"os"
"sync"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/internal/invariants"
)
// Iterator iterates over an entire table of data.
type Iterator interface {
base.InternalIterator
// NextPrefix implements (base.InternalIterator).NextPrefix.
NextPrefix(succKey []byte) (*InternalKey, base.LazyValue)
// MaybeFilteredKeys may be called when an iterator is exhausted to indicate
// whether or not the last positioning method may have skipped any keys due
// to block-property filters. This is used by the Pebble levelIter to
// control when an iterator steps to the next sstable.
//
// MaybeFilteredKeys may always return false positives, that is it may
// return true when no keys were filtered. It should only be called when the
// iterator is exhausted. It must never return false negatives when the
// iterator is exhausted.
MaybeFilteredKeys() bool
SetCloseHook(fn func(i Iterator) error)
}
// Iterator positioning optimizations and singleLevelIterator and
// twoLevelIterator:
//
// An iterator is absolute positioned using one of the Seek or First or Last
// calls. After absolute positioning, there can be relative positioning done
// by stepping using Prev or Next.
//
// We implement optimizations below where an absolute positioning call can in
// some cases use the current position to do less work. To understand these,
// we first define some terms. An iterator is bounds-exhausted if the bounds
// (upper of lower) have been reached. An iterator is data-exhausted if it has
// the reached the end of the data (forward or reverse) in the sstable. A
// singleLevelIterator only knows a local-data-exhausted property since when
// it is used as part of a twoLevelIterator, the twoLevelIterator can step to
// the next lower-level index block.
//
// The bounds-exhausted property is tracked by
// singleLevelIterator.exhaustedBounds being +1 (upper bound reached) or -1
// (lower bound reached). The same field is reused by twoLevelIterator. Either
// may notice the exhaustion of the bound and set it. Note that if
// singleLevelIterator sets this property, it is not a local property (since
// the bound has been reached regardless of whether this is in the context of
// the twoLevelIterator or not).
//
// The data-exhausted property is tracked in a more subtle manner. We define
// two predicates:
// - partial-local-data-exhausted (PLDE):
// i.data.isDataInvalidated() || !i.data.valid()
// - partial-global-data-exhausted (PGDE):
// i.index.isDataInvalidated() || !i.index.valid() || i.data.isDataInvalidated() ||
// !i.data.valid()
//
// PLDE is defined for a singleLevelIterator. PGDE is defined for a
// twoLevelIterator. Oddly, in our code below the singleLevelIterator does not
// know when it is part of a twoLevelIterator so it does not know when its
// property is local or global.
//
// Now to define data-exhausted:
// - Prerequisite: we must know that the iterator has been positioned and
// i.err is nil.
// - bounds-exhausted must not be true:
// If bounds-exhausted is true, we have incomplete knowledge of
// data-exhausted since PLDE or PGDE could be true because we could have
// chosen not to load index block or data block and figured out that the
// bound is exhausted (due to block property filters filtering out index and
// data blocks and going past the bound on the top level index block). Note
// that if we tried to separate out the BPF case from others we could
// develop more knowledge here.
// - PGDE is true for twoLevelIterator. PLDE is true if it is a standalone
// singleLevelIterator. !PLDE or !PGDE of course imply that data-exhausted
// is not true.
//
// An implication of the above is that if we are going to somehow utilize
// knowledge of data-exhausted in an optimization, we must not forget the
// existing value of bounds-exhausted since by forgetting the latter we can
// erroneously think that data-exhausted is true. Bug #2036 was due to this
// forgetting.
//
// Now to the two categories of optimizations we currently have:
// - Monotonic bounds optimization that reuse prior iterator position when
// doing seek: These only work with !data-exhausted. We could choose to make
// these work with data-exhausted but have not bothered because in the
// context of a DB if data-exhausted were true, the DB would move to the
// next file in the level. Note that this behavior of moving to the next
// file is not necessarily true for L0 files, so there could be some benefit
// in the future in this optimization. See the WARNING-data-exhausted
// comments if trying to optimize this in the future.
// - TrySeekUsingNext optimizations: these work regardless of exhaustion
// state.
//
// Implementation detail: In the code PLDE only checks that
// i.data.isDataInvalidated(). This narrower check is safe, since this is a
// subset of the set expressed by the OR expression. Also, it is not a
// de-optimization since whenever we exhaust the iterator we explicitly call
// i.data.invalidate(). PGDE checks i.index.isDataInvalidated() &&
// i.data.isDataInvalidated(). Again, this narrower check is safe, and not a
// de-optimization since whenever we exhaust the iterator we explicitly call
// i.index.invalidate() and i.data.invalidate(). The && is questionable -- for
// now this is a bit of defensive code. We should seriously consider removing
// it, since defensive code suggests we are not confident about our invariants
// (and if we are not confident, we need more invariant assertions, not
// defensive code).
//
// TODO(sumeer): remove the aforementioned defensive code.
var singleLevelIterPool = sync.Pool{
New: func() interface{} {
i := &singleLevelIterator{}
// Note: this is a no-op if invariants are disabled or race is enabled.
invariants.SetFinalizer(i, checkSingleLevelIterator)
return i
},
}
var twoLevelIterPool = sync.Pool{
New: func() interface{} {
i := &twoLevelIterator{}
// Note: this is a no-op if invariants are disabled or race is enabled.
invariants.SetFinalizer(i, checkTwoLevelIterator)
return i
},
}
// TODO(jackson): rangedel fragmentBlockIters can't be pooled because of some
// code paths that double Close the iters. Fix the double close and pool the
// *fragmentBlockIter type directly.
var rangeKeyFragmentBlockIterPool = sync.Pool{
New: func() interface{} {
i := &rangeKeyFragmentBlockIter{}
// Note: this is a no-op if invariants are disabled or race is enabled.
invariants.SetFinalizer(i, checkRangeKeyFragmentBlockIterator)
return i
},
}
func checkSingleLevelIterator(obj interface{}) {
i := obj.(*singleLevelIterator)
if p := i.data.handle.Get(); p != nil {
fmt.Fprintf(os.Stderr, "singleLevelIterator.data.handle is not nil: %p\n", p)
os.Exit(1)
}
if p := i.index.handle.Get(); p != nil {
fmt.Fprintf(os.Stderr, "singleLevelIterator.index.handle is not nil: %p\n", p)
os.Exit(1)
}
}
func checkTwoLevelIterator(obj interface{}) {
i := obj.(*twoLevelIterator)
if p := i.data.handle.Get(); p != nil {
fmt.Fprintf(os.Stderr, "singleLevelIterator.data.handle is not nil: %p\n", p)
os.Exit(1)
}
if p := i.index.handle.Get(); p != nil {
fmt.Fprintf(os.Stderr, "singleLevelIterator.index.handle is not nil: %p\n", p)
os.Exit(1)
}
}
func checkRangeKeyFragmentBlockIterator(obj interface{}) {
i := obj.(*rangeKeyFragmentBlockIter)
if p := i.blockIter.handle.Get(); p != nil {
fmt.Fprintf(os.Stderr, "fragmentBlockIter.blockIter.handle is not nil: %p\n", p)
os.Exit(1)
}
}
// compactionIterator is similar to Iterator but it increments the number of
// bytes that have been iterated through.
type compactionIterator struct {
*singleLevelIterator
bytesIterated *uint64
prevOffset uint64
}
// compactionIterator implements the base.InternalIterator interface.
var _ base.InternalIterator = (*compactionIterator)(nil)
func (i *compactionIterator) String() string {
if i.vState != nil {
return i.vState.fileNum.String()
}
return i.reader.fileNum.String()
}
func (i *compactionIterator) SeekGE(
key []byte, flags base.SeekGEFlags,
) (*InternalKey, base.LazyValue) {
panic("pebble: SeekGE unimplemented")
}
func (i *compactionIterator) SeekPrefixGE(
prefix, key []byte, flags base.SeekGEFlags,
) (*base.InternalKey, base.LazyValue) {
panic("pebble: SeekPrefixGE unimplemented")
}
func (i *compactionIterator) SeekLT(
key []byte, flags base.SeekLTFlags,
) (*InternalKey, base.LazyValue) {
panic("pebble: SeekLT unimplemented")
}
func (i *compactionIterator) First() (*InternalKey, base.LazyValue) {
i.err = nil // clear cached iteration error
return i.skipForward(i.singleLevelIterator.First())
}
func (i *compactionIterator) Last() (*InternalKey, base.LazyValue) {
panic("pebble: Last unimplemented")
}
// Note: compactionIterator.Next mirrors the implementation of Iterator.Next
// due to performance. Keep the two in sync.
func (i *compactionIterator) Next() (*InternalKey, base.LazyValue) {
if i.err != nil {
return nil, base.LazyValue{}
}
return i.skipForward(i.data.Next())
}
func (i *compactionIterator) NextPrefix(succKey []byte) (*InternalKey, base.LazyValue) {
panic("pebble: NextPrefix unimplemented")
}
func (i *compactionIterator) Prev() (*InternalKey, base.LazyValue) {
panic("pebble: Prev unimplemented")
}
func (i *compactionIterator) skipForward(
key *InternalKey, val base.LazyValue,
) (*InternalKey, base.LazyValue) {
if key == nil {
for {
if key, _ := i.index.Next(); key == nil {
break
}
result := i.loadBlock(+1)
if result != loadBlockOK {
if i.err != nil {
break
}
switch result {
case loadBlockFailed:
// We checked that i.index was at a valid entry, so
// loadBlockFailed could not have happened due to to i.index
// being exhausted, and must be due to an error.
panic("loadBlock should not have failed with no error")
case loadBlockIrrelevant:
panic("compactionIter should not be using block intervals for skipping")
default:
panic(fmt.Sprintf("unexpected case %d", result))
}
}
// result == loadBlockOK
if key, val = i.data.First(); key != nil {
break
}
}
}
curOffset := i.recordOffset()
*i.bytesIterated += uint64(curOffset - i.prevOffset)
i.prevOffset = curOffset
if i.vState != nil && key != nil {
cmp := i.cmp(key.UserKey, i.vState.upper.UserKey)
if cmp > 0 || (i.vState.upper.IsExclusiveSentinel() && cmp == 0) {
return nil, base.LazyValue{}
}
}
return key, val
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,206 @@
// Copyright 2011 The LevelDB-Go and Pebble Authors. All rights reserved. Use
// of this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
package sstable
import (
"context"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/internal/keyspan"
"github.com/cockroachdb/pebble/internal/manifest"
)
// VirtualReader wraps Reader. Its purpose is to restrict functionality of the
// Reader which should be inaccessible to virtual sstables, and enforce bounds
// invariants associated with virtual sstables. All reads on virtual sstables
// should go through a VirtualReader.
//
// INVARIANT: Any iterators created through a virtual reader will guarantee that
// they don't expose keys outside the virtual sstable bounds.
type VirtualReader struct {
vState virtualState
reader *Reader
Properties CommonProperties
}
// Lightweight virtual sstable state which can be passed to sstable iterators.
type virtualState struct {
lower InternalKey
upper InternalKey
fileNum base.FileNum
Compare Compare
isForeign bool
}
func ceilDiv(a, b uint64) uint64 {
return (a + b - 1) / b
}
// MakeVirtualReader is used to contruct a reader which can read from virtual
// sstables.
func MakeVirtualReader(
reader *Reader, meta manifest.VirtualFileMeta, isForeign bool,
) VirtualReader {
if reader.fileNum != meta.FileBacking.DiskFileNum {
panic("pebble: invalid call to MakeVirtualReader")
}
vState := virtualState{
lower: meta.Smallest,
upper: meta.Largest,
fileNum: meta.FileNum,
Compare: reader.Compare,
isForeign: isForeign,
}
v := VirtualReader{
vState: vState,
reader: reader,
}
v.Properties.RawKeySize = ceilDiv(reader.Properties.RawKeySize*meta.Size, meta.FileBacking.Size)
v.Properties.RawValueSize = ceilDiv(reader.Properties.RawValueSize*meta.Size, meta.FileBacking.Size)
v.Properties.NumEntries = ceilDiv(reader.Properties.NumEntries*meta.Size, meta.FileBacking.Size)
v.Properties.NumDeletions = ceilDiv(reader.Properties.NumDeletions*meta.Size, meta.FileBacking.Size)
v.Properties.NumRangeDeletions = ceilDiv(reader.Properties.NumRangeDeletions*meta.Size, meta.FileBacking.Size)
v.Properties.NumRangeKeyDels = ceilDiv(reader.Properties.NumRangeKeyDels*meta.Size, meta.FileBacking.Size)
// Note that we rely on NumRangeKeySets for correctness. If the sstable may
// contain range keys, then NumRangeKeySets must be > 0. ceilDiv works because
// meta.Size will not be 0 for virtual sstables.
v.Properties.NumRangeKeySets = ceilDiv(reader.Properties.NumRangeKeySets*meta.Size, meta.FileBacking.Size)
v.Properties.ValueBlocksSize = ceilDiv(reader.Properties.ValueBlocksSize*meta.Size, meta.FileBacking.Size)
v.Properties.NumSizedDeletions = ceilDiv(reader.Properties.NumSizedDeletions*meta.Size, meta.FileBacking.Size)
v.Properties.RawPointTombstoneKeySize = ceilDiv(reader.Properties.RawPointTombstoneKeySize*meta.Size, meta.FileBacking.Size)
v.Properties.RawPointTombstoneValueSize = ceilDiv(reader.Properties.RawPointTombstoneValueSize*meta.Size, meta.FileBacking.Size)
return v
}
// NewCompactionIter is the compaction iterator function for virtual readers.
func (v *VirtualReader) NewCompactionIter(
bytesIterated *uint64, rp ReaderProvider, bufferPool *BufferPool,
) (Iterator, error) {
return v.reader.newCompactionIter(bytesIterated, rp, &v.vState, bufferPool)
}
// NewIterWithBlockPropertyFiltersAndContextEtc wraps
// Reader.NewIterWithBlockPropertyFiltersAndContext. We assume that the passed
// in [lower, upper) bounds will have at least some overlap with the virtual
// sstable bounds. No overlap is not currently supported in the iterator.
func (v *VirtualReader) NewIterWithBlockPropertyFiltersAndContextEtc(
ctx context.Context,
lower, upper []byte,
filterer *BlockPropertiesFilterer,
hideObsoletePoints, useFilterBlock bool,
stats *base.InternalIteratorStats,
rp ReaderProvider,
) (Iterator, error) {
return v.reader.newIterWithBlockPropertyFiltersAndContext(
ctx, lower, upper, filterer, hideObsoletePoints, useFilterBlock, stats, rp, &v.vState,
)
}
// ValidateBlockChecksumsOnBacking will call ValidateBlockChecksumsOnBacking on the underlying reader.
// Note that block checksum validation is NOT restricted to virtual sstable bounds.
func (v *VirtualReader) ValidateBlockChecksumsOnBacking() error {
return v.reader.ValidateBlockChecksums()
}
// NewRawRangeDelIter wraps Reader.NewRawRangeDelIter.
func (v *VirtualReader) NewRawRangeDelIter() (keyspan.FragmentIterator, error) {
iter, err := v.reader.NewRawRangeDelIter()
if err != nil {
return nil, err
}
if iter == nil {
return nil, nil
}
// Truncation of spans isn't allowed at a user key that also contains points
// in the same virtual sstable, as it would lead to covered points getting
// uncovered. Set panicOnUpperTruncate to true if the file's upper bound
// is not an exclusive sentinel.
//
// As an example, if an sstable contains a rangedel a-c and point keys at
// a.SET.2 and b.SET.3, the file bounds [a#2,SET-b#RANGEDELSENTINEL] are
// allowed (as they exclude b.SET.3), or [a#2,SET-c#RANGEDELSENTINEL] (as it
// includes both point keys), but not [a#2,SET-b#3,SET] (as it would truncate
// the rangedel at b and lead to the point being uncovered).
return keyspan.Truncate(
v.reader.Compare, iter, v.vState.lower.UserKey, v.vState.upper.UserKey,
&v.vState.lower, &v.vState.upper, !v.vState.upper.IsExclusiveSentinel(), /* panicOnUpperTruncate */
), nil
}
// NewRawRangeKeyIter wraps Reader.NewRawRangeKeyIter.
func (v *VirtualReader) NewRawRangeKeyIter() (keyspan.FragmentIterator, error) {
iter, err := v.reader.NewRawRangeKeyIter()
if err != nil {
return nil, err
}
if iter == nil {
return nil, nil
}
// Truncation of spans isn't allowed at a user key that also contains points
// in the same virtual sstable, as it would lead to covered points getting
// uncovered. Set panicOnUpperTruncate to true if the file's upper bound
// is not an exclusive sentinel.
//
// As an example, if an sstable contains a range key a-c and point keys at
// a.SET.2 and b.SET.3, the file bounds [a#2,SET-b#RANGEKEYSENTINEL] are
// allowed (as they exclude b.SET.3), or [a#2,SET-c#RANGEKEYSENTINEL] (as it
// includes both point keys), but not [a#2,SET-b#3,SET] (as it would truncate
// the range key at b and lead to the point being uncovered).
return keyspan.Truncate(
v.reader.Compare, iter, v.vState.lower.UserKey, v.vState.upper.UserKey,
&v.vState.lower, &v.vState.upper, !v.vState.upper.IsExclusiveSentinel(), /* panicOnUpperTruncate */
), nil
}
// Constrain bounds will narrow the start, end bounds if they do not fit within
// the virtual sstable. The function will return if the new end key is
// inclusive.
func (v *virtualState) constrainBounds(
start, end []byte, endInclusive bool,
) (lastKeyInclusive bool, first []byte, last []byte) {
first = start
if start == nil || v.Compare(start, v.lower.UserKey) < 0 {
first = v.lower.UserKey
}
// Note that we assume that start, end has some overlap with the virtual
// sstable bounds.
last = v.upper.UserKey
lastKeyInclusive = !v.upper.IsExclusiveSentinel()
if end != nil {
cmp := v.Compare(end, v.upper.UserKey)
switch {
case cmp == 0:
lastKeyInclusive = !v.upper.IsExclusiveSentinel() && endInclusive
last = v.upper.UserKey
case cmp > 0:
lastKeyInclusive = !v.upper.IsExclusiveSentinel()
last = v.upper.UserKey
default:
lastKeyInclusive = endInclusive
last = end
}
}
// TODO(bananabrick): What if someone passes in bounds completely outside of
// virtual sstable bounds?
return lastKeyInclusive, first, last
}
// EstimateDiskUsage just calls VirtualReader.reader.EstimateDiskUsage after
// enforcing the virtual sstable bounds.
func (v *VirtualReader) EstimateDiskUsage(start, end []byte) (uint64, error) {
_, f, l := v.vState.constrainBounds(start, end, true /* endInclusive */)
return v.reader.EstimateDiskUsage(f, l)
}
// CommonProperties implements the CommonReader interface.
func (v *VirtualReader) CommonProperties() *CommonProperties {
return &v.Properties
}

View File

@@ -2,6 +2,7 @@ package sstable
import (
"bytes"
"context"
"math"
"sync"
@@ -42,6 +43,9 @@ func RewriteKeySuffixes(
// modification as they are not affected by the suffix, while block and table
// properties are only minimally recomputed.
//
// TODO(sumeer): document limitations, if any, due to this limited
// re-computation of properties (is there any loss of fidelity?).
//
// Any block and table property collectors configured in the WriterOptions must
// implement SuffixReplaceableTableCollector/SuffixReplaceableBlockCollector.
//
@@ -49,6 +53,26 @@ func RewriteKeySuffixes(
// same TableFormat as the input, which is returned in case the caller wants
// to do some error checking. Suffix rewriting is meant to be efficient, and
// allowing changes in the TableFormat detracts from that efficiency.
//
// Any obsolete bits that key-value pairs may be annotated with are ignored
// and lost during the rewrite. Additionally, the output sstable has the
// pebble.obsolete.is_strict property set to false. These limitations could be
// removed if needed. The current use case for
// RewriteKeySuffixesAndReturnFormat in CockroachDB is for MVCC-compliant file
// ingestion, where these files do not contain RANGEDELs and have one
// key-value pair per userkey -- so they trivially satisfy the strict
// criteria, and we don't need the obsolete bit as a performance optimization.
// For disaggregated storage, strict obsolete sstables are needed for L5 and
// L6, but at the time of writing, we expect such MVCC-compliant file
// ingestion to only ingest into levels L4 and higher. If this changes, we can
// do one of two things to get rid of this limitation:
// - Validate that there are no duplicate userkeys and no RANGEDELs/MERGEs
// in the sstable to be rewritten. Validating no duplicate userkeys is
// non-trivial when rewriting blocks in parallel, so we could encode the
// pre-existing condition in the (existing) SnapshotPinnedKeys property --
// we need to update the external sst writer to calculate and encode this
// property.
// - Preserve the obsolete bit (with changes to the blockIter).
func RewriteKeySuffixesAndReturnFormat(
sst []byte,
rOpts ReaderOptions,
@@ -183,7 +207,7 @@ func rewriteBlocks(
if err != nil {
return err
}
if err := iter.init(r.Compare, inputBlock, r.Properties.GlobalSeqNum); err != nil {
if err := iter.init(r.Compare, inputBlock, r.Properties.GlobalSeqNum, false); err != nil {
return err
}
@@ -217,12 +241,12 @@ func rewriteBlocks(
copy(scratch.UserKey, key.UserKey[:si])
copy(scratch.UserKey[si:], to)
// NB: for TableFormatPebblev3, since
// NB: for TableFormatPebblev3 and higher, since
// !iter.lazyValueHandling.hasValuePrefix, it will return the raw value
// in the block, which includes the 1-byte prefix. This is fine since bw
// also does not know about the prefix and will preserve it in bw.add.
v := val.InPlaceValue()
if invariants.Enabled && r.tableFormat == TableFormatPebblev3 &&
if invariants.Enabled && r.tableFormat >= TableFormatPebblev3 &&
key.Kind() == InternalKeyKindSet {
if len(v) < 1 {
return errors.Errorf("value has no prefix")
@@ -432,8 +456,13 @@ func (c copyFilterWriter) policyName() string { return c.origPolicyName }
// RewriteKeySuffixesViaWriter is similar to RewriteKeySuffixes but uses just a
// single loop over the Reader that writes each key to the Writer with the new
// suffix. The is significantly slower than the parallelized rewriter, and does
// more work to rederive filters, props, etc, however re-doing that work makes
// it less restrictive -- props no longer need to
// more work to rederive filters, props, etc.
//
// Any obsolete bits that key-value pairs may be annotated with are ignored
// and lost during the rewrite. Some of the obsolete bits may be recreated --
// specifically when there are multiple keys with the same user key.
// Additionally, the output sstable has the pebble.obsolete.is_strict property
// set to false. See the longer comment at RewriteKeySuffixesAndReturnFormat.
func RewriteKeySuffixesViaWriter(
r *Reader, out objstorage.Writable, o WriterOptions, from, to []byte,
) (*WriterMetadata, error) {
@@ -441,6 +470,7 @@ func RewriteKeySuffixesViaWriter(
return nil, errors.New("a valid splitter is required to rewrite suffixes")
}
o.IsStrictObsolete = false
w := NewWriter(out, o)
defer func() {
if w != nil {
@@ -471,7 +501,7 @@ func RewriteKeySuffixesViaWriter(
if err != nil {
return nil, err
}
if w.addPoint(scratch, val); err != nil {
if w.addPoint(scratch, val, false); err != nil {
return nil, err
}
k, v = i.Next()
@@ -514,7 +544,7 @@ func readBlockBuf(r *Reader, bh BlockHandle, buf []byte) ([]byte, []byte, error)
return res, buf, err
}
// memReader is a thin wrapper around a []byte such that it can be passed to an
// memReader is a thin wrapper around a []byte such that it can be passed to
// sstable.Reader. It supports concurrent use, and does so without locking in
// contrast to the heavier read/write vfs.MemFile.
type memReader struct {
@@ -534,22 +564,26 @@ func newMemReader(b []byte) *memReader {
return r
}
// ReadAt implements io.ReaderAt.
func (m *memReader) ReadAt(p []byte, off int64) (n int, err error) {
return m.r.ReadAt(p, off)
// ReadAt is part of objstorage.Readable.
func (m *memReader) ReadAt(_ context.Context, p []byte, off int64) error {
n, err := m.r.ReadAt(p, off)
if invariants.Enabled && err == nil && n != len(p) {
panic("short read")
}
return err
}
// Close implements io.Closer.
// Close is part of objstorage.Readable.
func (*memReader) Close() error {
return nil
}
// Stat implements objstorage.Readable.
// Stat is part of objstorage.Readable.
func (m *memReader) Size() int64 {
return int64(len(m.b))
}
// NewReadHandle implements objstorage.Readable.
func (m *memReader) NewReadHandle() objstorage.ReadHandle {
// NewReadHandle is part of objstorage.Readable.
func (m *memReader) NewReadHandle(_ context.Context) objstorage.ReadHandle {
return &m.rh
}

View File

@@ -68,8 +68,8 @@
package sstable // import "github.com/cockroachdb/pebble/sstable"
import (
"context"
"encoding/binary"
"io"
"github.com/cockroachdb/errors"
"github.com/cockroachdb/pebble/internal/base"
@@ -160,8 +160,27 @@ The metaindex block also contains block handles as values, with keys being
the names of the meta blocks.
For a description of value blocks and the meta value index block, see
value_block.go
value_block.go.
Data blocks have some additional features:
- For TableFormatPebblev3 onwards:
- For SETs, the value has a 1 byte value prefix, which indicates whether the
value is inline, or in a separate value block, and indicates whether the
prefix of the userkey (as defined by split) has changed or not. See
value_block.go for details.
- The most significant bit of the restart points is used to indicate whether
userkey prefix has changed since the last restart point. See the detailed
comment in blockWriter.
- The maximum length of the "shared prefix" when encoding the key, is the
length of the prefix of the userkey (as defined by split) of the previous
key.
- For TableFormatPebblev4 onwards:
- The key kinds may be altered to set the
InternalKeyKindSSTableInternalObsoleteBit if the key-value pair is obsolete
in the context of that sstable (for a reader that reads at a higher seqnum
than the highest seqnum in the sstable). For details, see the comment in
format.go.
*/
const (
@@ -316,12 +335,11 @@ func readFooter(f objstorage.Readable) (footer, error) {
off := size - maxFooterLen
if off < 0 {
off = 0
buf = buf[:size]
}
n, err := f.ReadAt(buf, off)
if err != nil && err != io.EOF {
if err := f.ReadAt(context.TODO(), buf, off); err != nil {
return footer, errors.Wrap(err, "pebble/table: invalid table (could not read footer)")
}
buf = buf[:n]
switch magic := buf[len(buf)-len(rocksDBMagic):]; string(magic) {
case levelDBMagic:
@@ -429,7 +447,7 @@ func supportsTwoLevelIndex(format TableFormat) bool {
switch format {
case TableFormatLevelDB:
return false
case TableFormatRocksDBv2, TableFormatPebblev1, TableFormatPebblev2, TableFormatPebblev3:
case TableFormatRocksDBv2, TableFormatPebblev1, TableFormatPebblev2, TableFormatPebblev3, TableFormatPebblev4:
return true
default:
panic("sstable: unspecified table format version")

View File

@@ -13,17 +13,83 @@ import (
"github.com/cockroachdb/errors"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/internal/cache"
"github.com/cockroachdb/pebble/internal/invariants"
"github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing"
"golang.org/x/exp/rand"
)
// Value blocks are supported in TableFormatPebblev3.
//
// 1. Motivation and overview
//
// Value blocks are a mechanism designed for sstables storing MVCC data, where
// there can be many versions of a key that need to be kept, but only the
// latest value is typically read. See the documentation for Comparer.Split
// regarding MVCC keys.
// latest value is typically read (see the documentation for Comparer.Split
// regarding MVCC keys). The goal is faster reads. Unlike Pebble versions,
// which can be eagerly thrown away (except when there are snapshots), MVCC
// versions are long-lived (e.g. default CockroachDB garbage collection
// threshold for older versions is 24 hours) and can significantly slow down
// reads. We have seen CockroachDB production workloads with very slow reads
// due to:
// - 100s of versions for each key in a table.
//
// - Tables with mostly MVCC garbage consisting of 2 versions per key -- a
// real key-value pair, followed by a key-value pair whose value (usually
// with zero byte length) indicates it is an MVCC tombstone.
//
// The value blocks mechanism attempts to improve read throughput in these
// cases when the key size is smaller than the value sizes of older versions.
// This is done by moving the value of an older version to a value block in a
// different part of the sstable. This improves spatial locality of the data
// being read by the workload, which increases caching effectiveness.
//
// Additionally, even when the key size is not smaller than the value of older
// versions (e.g. secondary indexes in CockroachDB), TableFormatPebblev3
// stores the result of key comparisons done at write time inside the sstable,
// which makes stepping from one key prefix to the next prefix (i.e., skipping
// over older versions of a MVCC key) more efficient by avoiding key
// comparisons and key decoding. See the results in
// https://github.com/cockroachdb/pebble/pull/2149 and more details in the
// comment inside BenchmarkIteratorScanNextPrefix. These improvements are also
// visible in end-to-end CockroachDB tests, as outlined in
// https://github.com/cockroachdb/cockroach/pull/96652.
//
// In TableFormatPebblev3, each SET has a one byte value prefix that tells us
// whether the value is in-place or in a value block. This 1 byte prefix
// encodes additional information:
//
// - ShortAttribute: This is an attribute of the value. Currently, CockroachDB
// uses it to represent whether the value is a tombstone or not. This avoids
// the need to fetch a value from the value block if the caller only wants
// to figure out whether it is an MVCC tombstone. The length of the value is
// another attribute that the caller can be interested in, and it is also
// accessible without reading the value in the value block (see the value
// handle in the details section).
//
// - SET-same-prefix: this enables the aforementioned optimization when
// stepping from one key prefix to the next key prefix.
//
// We further optimize this iteration over prefixes by using the restart
// points in a block to encode whether the SET at a restart point has the same
// prefix since the last restart point. This allows us to skip over restart
// points within the same block. See the comment in blockWriter, and how both
// SET-same-prefix and the restart point information is used in
// blockIter.nextPrefixV3.
//
// This flexibility of values that are in-place or in value blocks requires
// flexibility in the iterator interface. The InternalIterator interface
// returns a LazyValue instead of a byte slice. Additionally, pebble.Iterator
// allows the caller to ask for a LazyValue. See lazy_value.go for details,
// including the memory lifetime management.
//
// For historical discussions about this feature, see the issue
// https://github.com/cockroachdb/pebble/issues/1170 and the prototype in
// https://github.com/cockroachdb/pebble/pull/1443.
//
// The code in this file mainly covers value block and related encodings. We
// discuss these in the next section.
//
// 2. Details
//
// Note that the notion of the latest value is local to the sstable. It is
// possible that that latest value has been deleted by a sstable in a higher
@@ -34,7 +100,7 @@ import (
// read them. The code in this file is agnostic to the policy regarding what
// should be stored in value blocks -- it allows even the latest MVCC version
// to be stored in a value block. The policy decision in made in the
// sstable.Writer. See Writer.makeAddPointDecision.
// sstable.Writer. See Writer.makeAddPointDecisionV3.
//
// Data blocks contain two kinds of SET keys: those with in-place values and
// those with a value handle. To distinguish these two cases we use a single
@@ -661,7 +727,7 @@ func (ukb *UserKeyPrefixBound) IsEmpty() bool {
type blockProviderWhenOpen interface {
readBlockForVBR(
ctx context.Context, h BlockHandle, stats *base.InternalIteratorStats,
) (cache.Handle, error)
) (bufferHandle, error)
}
type blockProviderWhenClosed struct {
@@ -682,8 +748,12 @@ func (bpwc *blockProviderWhenClosed) close() {
func (bpwc blockProviderWhenClosed) readBlockForVBR(
ctx context.Context, h BlockHandle, stats *base.InternalIteratorStats,
) (cache.Handle, error) {
return bpwc.r.readBlock(ctx, h, nil, nil, stats)
) (bufferHandle, error) {
ctx = objiotracing.WithBlockType(ctx, objiotracing.ValueBlock)
// TODO(jackson,sumeer): Consider whether to use a buffer pool in this case.
// The bpwc is not allowed to outlive the iterator tree, so it cannot
// outlive the buffer pool.
return bpwc.r.readBlock(ctx, h, nil, nil, stats, nil /* buffer pool */)
}
// ReaderProvider supports the implementation of blockProviderWhenClosed.
@@ -722,16 +792,16 @@ type valueBlockReader struct {
// The value blocks index is lazily retrieved the first time the reader
// needs to read a value that resides in a value block.
vbiBlock []byte
vbiCache cache.Handle
vbiCache bufferHandle
// When sequentially iterating through all key-value pairs, the cost of
// repeatedly getting a block that is already in the cache and releasing the
// cache.Handle can be ~40% of the cpu overhead. So the reader remembers the
// bufferHandle can be ~40% of the cpu overhead. So the reader remembers the
// last value block it retrieved, in case there is locality of access, and
// this value block can be used for the next value retrieval.
valueBlockNum uint32
valueBlock []byte
valueBlockPtr unsafe.Pointer
valueCache cache.Handle
valueCache bufferHandle
lazyFetcher base.LazyFetcher
closed bool
bufToMangle []byte
@@ -765,12 +835,12 @@ func (r *valueBlockReader) close() {
// we were to reopen this valueBlockReader and retrieve the same
// Handle.value from the cache, we don't want to accidentally unref it when
// attempting to unref the old handle.
r.vbiCache = cache.Handle{}
r.vbiCache = bufferHandle{}
r.valueBlock = nil
r.valueBlockPtr = nil
r.valueCache.Release()
// See comment above.
r.valueCache = cache.Handle{}
r.valueCache = bufferHandle{}
r.closed = true
// rp, vbih, stats remain valid, so that LazyFetcher.ValueFetcher can be
// implemented.

View File

@@ -119,7 +119,7 @@ type Writer struct {
// the cache, providing a defense in depth against bugs which cause cache
// collisions.
cacheID uint64
fileNum base.FileNum
fileNum base.DiskFileNum
// The following fields are copied from Options.
blockSize int
blockSizeThreshold int
@@ -132,6 +132,8 @@ type Writer struct {
separator Separator
successor Successor
tableFormat TableFormat
isStrictObsolete bool
writingToLowestLevel bool
cache *cache.Cache
restartInterval int
checksumType ChecksumType
@@ -166,6 +168,7 @@ type Writer struct {
props Properties
propCollectors []TablePropertyCollector
blockPropCollectors []BlockPropertyCollector
obsoleteCollector obsoleteKeyBlockPropertyCollector
blockPropsEncoder blockPropertiesEncoder
// filter accumulates the filter block. If populated, the filter ingests
// either the output of w.split (i.e. a prefix extractor) if w.split is not
@@ -215,6 +218,8 @@ type pointKeyInfo struct {
// prefixLen uses w.split, if not nil. Only computed when w.valueBlockWriter
// is not nil.
prefixLen int
// True iff the point was marked obsolete.
isObsolete bool
}
type coordinationState struct {
@@ -673,7 +678,12 @@ func (w *Writer) Set(key, value []byte) error {
if w.err != nil {
return w.err
}
return w.addPoint(base.MakeInternalKey(key, 0, InternalKeyKindSet), value)
if w.isStrictObsolete {
return errors.Errorf("use AddWithForceObsolete")
}
// forceObsolete is false based on the assumption that no RANGEDELs in the
// sstable delete the added points.
return w.addPoint(base.MakeInternalKey(key, 0, InternalKeyKindSet), value, false)
}
// Delete deletes the value for the given key. The sequence number is set to
@@ -685,7 +695,12 @@ func (w *Writer) Delete(key []byte) error {
if w.err != nil {
return w.err
}
return w.addPoint(base.MakeInternalKey(key, 0, InternalKeyKindDelete), nil)
if w.isStrictObsolete {
return errors.Errorf("use AddWithForceObsolete")
}
// forceObsolete is false based on the assumption that no RANGEDELs in the
// sstable delete the added points.
return w.addPoint(base.MakeInternalKey(key, 0, InternalKeyKindDelete), nil, false)
}
// DeleteRange deletes all of the keys (and values) in the range [start,end)
@@ -711,7 +726,13 @@ func (w *Writer) Merge(key, value []byte) error {
if w.err != nil {
return w.err
}
return w.addPoint(base.MakeInternalKey(key, 0, InternalKeyKindMerge), value)
if w.isStrictObsolete {
return errors.Errorf("use AddWithForceObsolete")
}
// forceObsolete is false based on the assumption that no RANGEDELs in the
// sstable that delete the added points. If the user configured this writer
// to be strict-obsolete, addPoint will reject the addition of this MERGE.
return w.addPoint(base.MakeInternalKey(key, 0, InternalKeyKindMerge), value, false)
}
// Add adds a key/value pair to the table being written. For a given Writer,
@@ -721,6 +742,24 @@ func (w *Writer) Merge(key, value []byte) error {
// point entries. Additionally, range deletion tombstones must be fragmented
// (i.e. by keyspan.Fragmenter).
func (w *Writer) Add(key InternalKey, value []byte) error {
if w.isStrictObsolete {
return errors.Errorf("use AddWithForceObsolete")
}
return w.AddWithForceObsolete(key, value, false)
}
// AddWithForceObsolete must be used when writing a strict-obsolete sstable.
//
// forceObsolete indicates whether the caller has determined that this key is
// obsolete even though it may be the latest point key for this userkey. This
// should be set to true for keys obsoleted by RANGEDELs, and is required for
// strict-obsolete sstables.
//
// Note that there are two properties, S1 and S2 (see comment in format.go)
// that strict-obsolete ssts must satisfy. S2, due to RANGEDELs, is solely the
// responsibility of the caller. S1 is solely the responsibility of the
// callee.
func (w *Writer) AddWithForceObsolete(key InternalKey, value []byte, forceObsolete bool) error {
if w.err != nil {
return w.err
}
@@ -735,7 +774,7 @@ func (w *Writer) Add(key InternalKey, value []byte) error {
"pebble: range keys must be added via one of the RangeKey* functions")
return w.err
}
return w.addPoint(key, value)
return w.addPoint(key, value, forceObsolete)
}
func (w *Writer) makeAddPointDecisionV2(key InternalKey) error {
@@ -745,12 +784,7 @@ func (w *Writer) makeAddPointDecisionV2(key InternalKey) error {
return nil
}
if !w.disableKeyOrderChecks {
prevPointUserKey := w.dataBlockBuf.dataBlock.curKey
n := len(prevPointUserKey) - base.InternalTrailerLen
if n < 0 {
panic("corrupt key in blockWriter buffer")
}
prevPointUserKey = prevPointUserKey[:n:n]
prevPointUserKey := w.dataBlockBuf.dataBlock.getCurUserKey()
cmpUser := w.compare(prevPointUserKey, key.UserKey)
if cmpUser > 0 || (cmpUser == 0 && prevTrailer <= key.Trailer) {
return errors.Errorf(
@@ -762,9 +796,17 @@ func (w *Writer) makeAddPointDecisionV2(key InternalKey) error {
return nil
}
// REQUIRES: at least one point has been written to the Writer.
func (w *Writer) getLastPointUserKey() []byte {
if w.dataBlockBuf.dataBlock.nEntries == 0 {
panic(errors.AssertionFailedf("no point keys added to writer"))
}
return w.dataBlockBuf.dataBlock.getCurUserKey()
}
func (w *Writer) makeAddPointDecisionV3(
key InternalKey, valueLen int,
) (setHasSamePrefix bool, writeToValueBlock bool, err error) {
) (setHasSamePrefix bool, writeToValueBlock bool, isObsolete bool, err error) {
prevPointKeyInfo := w.lastPointKeyInfo
w.lastPointKeyInfo.userKeyLen = len(key.UserKey)
w.lastPointKeyInfo.prefixLen = w.lastPointKeyInfo.userKeyLen
@@ -772,18 +814,16 @@ func (w *Writer) makeAddPointDecisionV3(
w.lastPointKeyInfo.prefixLen = w.split(key.UserKey)
}
w.lastPointKeyInfo.trailer = key.Trailer
if w.dataBlockBuf.dataBlock.nEntries == 0 {
return false, false, nil
w.lastPointKeyInfo.isObsolete = false
if !w.meta.HasPointKeys {
return false, false, false, nil
}
prevPointUserKey := w.dataBlockBuf.dataBlock.curKey
n := len(prevPointUserKey) - base.InternalTrailerLen
if n < 0 {
panic("corrupt key in blockWriter buffer")
}
prevPointUserKey = prevPointUserKey[:n:n]
keyKind := base.TrailerKind(key.Trailer)
prevPointUserKey := w.getLastPointUserKey()
prevPointKey := InternalKey{UserKey: prevPointUserKey, Trailer: prevPointKeyInfo.trailer}
considerWriteToValueBlock := base.TrailerKind(prevPointKeyInfo.trailer) == InternalKeyKindSet &&
base.TrailerKind(key.Trailer) == InternalKeyKindSet
prevKeyKind := base.TrailerKind(prevPointKeyInfo.trailer)
considerWriteToValueBlock := prevKeyKind == InternalKeyKindSet &&
keyKind == InternalKeyKindSet
if considerWriteToValueBlock && !w.requiredInPlaceValueBound.IsEmpty() {
keyPrefix := key.UserKey[:w.lastPointKeyInfo.prefixLen]
cmpUpper := w.compare(
@@ -812,14 +852,63 @@ func (w *Writer) makeAddPointDecisionV3(
} else {
cmpUser = w.compare(prevPointUserKey, key.UserKey)
}
// Ensure that no one adds a point key kind without considering the obsolete
// handling for that kind.
switch keyKind {
case InternalKeyKindSet, InternalKeyKindSetWithDelete, InternalKeyKindMerge,
InternalKeyKindDelete, InternalKeyKindSingleDelete, InternalKeyKindDeleteSized:
default:
panic(errors.AssertionFailedf("unexpected key kind %s", keyKind.String()))
}
// If same user key, then the current key is obsolete if any of the
// following is true:
// C1 The prev key was obsolete.
// C2 The prev key was not a MERGE. When the previous key is a MERGE we must
// preserve SET* and MERGE since their values will be merged into the
// previous key. We also must preserve DEL* since there may be an older
// SET*/MERGE in a lower level that must not be merged with the MERGE --
// if we omit the DEL* that lower SET*/MERGE will become visible.
//
// Regardless of whether it is the same user key or not
// C3 The current key is some kind of point delete, and we are writing to
// the lowest level, then it is also obsolete. The correctness of this
// relies on the same user key not spanning multiple sstables in a level.
//
// C1 ensures that for a user key there is at most one transition from
// !obsolete to obsolete. Consider a user key k, for which the first n keys
// are not obsolete. We consider the various value of n:
//
// n = 0: This happens due to forceObsolete being set by the caller, or due
// to C3. forceObsolete must only be set due a RANGEDEL, and that RANGEDEL
// must also delete all the lower seqnums for the same user key. C3 triggers
// due to a point delete and that deletes all the lower seqnums for the same
// user key.
//
// n = 1: This is the common case. It happens when the first key is not a
// MERGE, or the current key is some kind of point delete.
//
// n > 1: This is due to a sequence of MERGE keys, potentially followed by a
// single non-MERGE key.
isObsoleteC1AndC2 := cmpUser == 0 &&
(prevPointKeyInfo.isObsolete || prevKeyKind != InternalKeyKindMerge)
isObsoleteC3 := w.writingToLowestLevel &&
(keyKind == InternalKeyKindDelete || keyKind == InternalKeyKindSingleDelete ||
keyKind == InternalKeyKindDeleteSized)
isObsolete = isObsoleteC1AndC2 || isObsoleteC3
// TODO(sumeer): storing isObsolete SET and SETWITHDEL in value blocks is
// possible, but requires some care in documenting and checking invariants.
// There is code that assumes nothing in value blocks because of single MVCC
// version (those should be ok). We have to ensure setHasSamePrefix is
// correctly initialized here etc.
if !w.disableKeyOrderChecks &&
(cmpUser > 0 || (cmpUser == 0 && prevPointKeyInfo.trailer <= key.Trailer)) {
return false, false, errors.Errorf(
return false, false, false, errors.Errorf(
"pebble: keys must be added in strictly increasing order: %s, %s",
prevPointKey.Pretty(w.formatKey), key.Pretty(w.formatKey))
}
if !considerWriteToValueBlock {
return false, false, nil
return false, false, isObsolete, nil
}
// NB: it is possible that cmpUser == 0, i.e., these two SETs have identical
// user keys (because of an open snapshot). This should be the rare case.
@@ -832,19 +921,24 @@ func (w *Writer) makeAddPointDecisionV3(
if considerWriteToValueBlock && valueLen <= tinyValueThreshold {
considerWriteToValueBlock = false
}
return setHasSamePrefix, considerWriteToValueBlock, nil
return setHasSamePrefix, considerWriteToValueBlock, isObsolete, nil
}
func (w *Writer) addPoint(key InternalKey, value []byte) error {
func (w *Writer) addPoint(key InternalKey, value []byte, forceObsolete bool) error {
if w.isStrictObsolete && key.Kind() == InternalKeyKindMerge {
return errors.Errorf("MERGE not supported in a strict-obsolete sstable")
}
var err error
var setHasSameKeyPrefix, writeToValueBlock, addPrefixToValueStoredWithKey bool
var isObsolete bool
maxSharedKeyLen := len(key.UserKey)
if w.valueBlockWriter != nil {
// maxSharedKeyLen is limited to the prefix of the preceding key. If the
// preceding key was in a different block, then the blockWriter will
// ignore this maxSharedKeyLen.
maxSharedKeyLen = w.lastPointKeyInfo.prefixLen
setHasSameKeyPrefix, writeToValueBlock, err = w.makeAddPointDecisionV3(key, len(value))
setHasSameKeyPrefix, writeToValueBlock, isObsolete, err =
w.makeAddPointDecisionV3(key, len(value))
addPrefixToValueStoredWithKey = base.TrailerKind(key.Trailer) == InternalKeyKindSet
} else {
err = w.makeAddPointDecisionV2(key)
@@ -852,6 +946,8 @@ func (w *Writer) addPoint(key InternalKey, value []byte) error {
if err != nil {
return err
}
isObsolete = w.tableFormat >= TableFormatPebblev4 && (isObsolete || forceObsolete)
w.lastPointKeyInfo.isObsolete = isObsolete
var valueStoredWithKey []byte
var prefix valuePrefix
var valueStoredWithKeyLen int
@@ -907,16 +1003,19 @@ func (w *Writer) addPoint(key InternalKey, value []byte) error {
return err
}
}
if w.tableFormat >= TableFormatPebblev4 {
w.obsoleteCollector.AddPoint(isObsolete)
}
w.maybeAddToFilter(key.UserKey)
w.dataBlockBuf.dataBlock.addWithOptionalValuePrefix(
key, valueStoredWithKey, maxSharedKeyLen, addPrefixToValueStoredWithKey, prefix,
key, isObsolete, valueStoredWithKey, maxSharedKeyLen, addPrefixToValueStoredWithKey, prefix,
setHasSameKeyPrefix)
w.meta.updateSeqNum(key.SeqNum())
if !w.meta.HasPointKeys {
k := base.DecodeInternalKey(w.dataBlockBuf.dataBlock.curKey)
k := w.dataBlockBuf.dataBlock.getCurKey()
// NB: We need to ensure that SmallestPoint.UserKey is set, so we create
// an InternalKey which is semantically identical to the key, but won't
// have a nil UserKey. We do this, because key.UserKey could be nil, and
@@ -932,6 +1031,22 @@ func (w *Writer) addPoint(key InternalKey, value []byte) error {
switch key.Kind() {
case InternalKeyKindDelete, InternalKeyKindSingleDelete:
w.props.NumDeletions++
w.props.RawPointTombstoneKeySize += uint64(len(key.UserKey))
case InternalKeyKindDeleteSized:
var size uint64
if len(value) > 0 {
var n int
size, n = binary.Uvarint(value)
if n <= 0 {
w.err = errors.Newf("%s key's value (%x) does not parse as uvarint",
errors.Safe(key.Kind().String()), value)
return w.err
}
}
w.props.NumDeletions++
w.props.NumSizedDeletions++
w.props.RawPointTombstoneKeySize += uint64(len(key.UserKey))
w.props.RawPointTombstoneValueSize += size
case InternalKeyKindMerge:
w.props.NumMergeOperands++
}
@@ -952,7 +1067,7 @@ func (w *Writer) addTombstone(key InternalKey, value []byte) error {
if !w.disableKeyOrderChecks && !w.rangeDelV1Format && w.rangeDelBlock.nEntries > 0 {
// Check that tombstones are being added in fragmented order. If the two
// tombstones overlap, their start and end keys must be identical.
prevKey := base.DecodeInternalKey(w.rangeDelBlock.curKey)
prevKey := w.rangeDelBlock.getCurKey()
switch c := w.compare(prevKey.UserKey, key.UserKey); {
case c > 0:
w.err = errors.Errorf("pebble: keys must be added in order: %s, %s",
@@ -1148,7 +1263,7 @@ func (w *Writer) encodeRangeKeySpan(span keyspan.Span) {
func (w *Writer) addRangeKey(key InternalKey, value []byte) error {
if !w.disableKeyOrderChecks && w.rangeKeyBlock.nEntries > 0 {
prevStartKey := base.DecodeInternalKey(w.rangeKeyBlock.curKey)
prevStartKey := w.rangeKeyBlock.getCurKey()
prevEndKey, _, ok := rangekey.DecodeEndKey(prevStartKey.Kind(), w.rangeKeyBlock.curValue)
if !ok {
// We panic here as we should have previously decoded and validated this
@@ -1294,7 +1409,7 @@ func (w *Writer) flush(key InternalKey) error {
// dataBlockBuf is added back to a sync.Pool. In this particular case, the
// byte slice which supports "sep" will eventually be copied when "sep" is
// added to the index block.
prevKey := base.DecodeInternalKey(w.dataBlockBuf.dataBlock.curKey)
prevKey := w.dataBlockBuf.dataBlock.getCurKey()
sep := w.indexEntrySep(prevKey, key, w.dataBlockBuf)
// We determine that we should flush an index block from the Writer client
// goroutine, but we actually finish the index block from the writeQueue.
@@ -1584,7 +1699,7 @@ func (w *Writer) finishIndexBlock(indexBuf *indexBlockBuf, props []byte) error {
nEntries: indexBuf.block.nEntries, properties: props,
}
w.indexSepAlloc, part.sep = cloneKeyWithBuf(
base.DecodeInternalKey(indexBuf.block.curKey), w.indexSepAlloc,
indexBuf.block.getCurKey(), w.indexSepAlloc,
)
bk := indexBuf.finish()
if len(w.indexBlockAlloc) < len(bk) {
@@ -1660,7 +1775,7 @@ func compressAndChecksum(b []byte, compression Compression, blockBuf *blockBuf)
func (w *Writer) writeCompressedBlock(block []byte, blockTrailerBuf []byte) (BlockHandle, error) {
bh := BlockHandle{Offset: w.meta.Size, Length: uint64(len(block))}
if w.cacheID != 0 && w.fileNum != 0 {
if w.cacheID != 0 && w.fileNum.FileNum() != 0 {
// Remove the block being written from the cache. This provides defense in
// depth against bugs which cause cache collisions.
//
@@ -1687,7 +1802,7 @@ func (w *Writer) writeCompressedBlock(block []byte, blockTrailerBuf []byte) (Blo
// return a BlockHandle.
func (w *Writer) Write(blockWithTrailer []byte) (n int, err error) {
offset := w.meta.Size
if w.cacheID != 0 && w.fileNum != 0 {
if w.cacheID != 0 && w.fileNum.FileNum() != 0 {
// Remove the block being written from the cache. This provides defense in
// depth against bugs which cause cache collisions.
//
@@ -1728,12 +1843,20 @@ func (w *Writer) assertFormatCompatibility() error {
)
}
// PebbleDBv3: value blocks.
if (w.props.NumValueBlocks > 0 || w.props.NumValuesInValueBlocks > 0 ||
w.props.ValueBlocksSize > 0) && w.tableFormat < TableFormatPebblev3 {
return errors.Newf(
"table format version %s is less than the minimum required version %s for value blocks",
w.tableFormat, TableFormatPebblev3)
}
// PebbleDBv4: DELSIZED tombstones.
if w.props.NumSizedDeletions > 0 && w.tableFormat < TableFormatPebblev4 {
return errors.Newf(
"table format version %s is less than the minimum required version %s for sized deletion tombstones",
w.tableFormat, TableFormatPebblev4)
}
return nil
}
@@ -1780,7 +1903,7 @@ func (w *Writer) Close() (err error) {
// however, if a dataBlock is flushed, then we add a key to the new w.dataBlockBuf in the
// addPoint function after the flush occurs.
if w.dataBlockBuf.dataBlock.nEntries >= 1 {
w.meta.SetLargestPointKey(base.DecodeInternalKey(w.dataBlockBuf.dataBlock.curKey).Clone())
w.meta.SetLargestPointKey(w.dataBlockBuf.dataBlock.getCurKey().Clone())
}
// Finish the last data block, or force an empty data block if there
@@ -1794,7 +1917,7 @@ func (w *Writer) Close() (err error) {
if err != nil {
return err
}
prevKey := base.DecodeInternalKey(w.dataBlockBuf.dataBlock.curKey)
prevKey := w.dataBlockBuf.dataBlock.getCurKey()
if err := w.addIndexEntrySync(prevKey, InternalKey{}, bhp, w.dataBlockBuf.tmp[:]); err != nil {
return err
}
@@ -1873,7 +1996,7 @@ func (w *Writer) Close() (err error) {
var rangeKeyBH BlockHandle
if w.props.NumRangeKeys() > 0 {
key := base.DecodeInternalKey(w.rangeKeyBlock.curKey)
key := w.rangeKeyBlock.getCurKey()
kind := key.Kind()
endKey, _, ok := rangekey.DecodeEndKey(kind, w.rangeKeyBlock.curValue)
if !ok {
@@ -1948,7 +2071,7 @@ func (w *Writer) Close() (err error) {
// reduces table size without a significant impact on performance.
raw.restartInterval = propertiesBlockRestartInterval
w.props.CompressionOptions = rocksDBCompressionOptions
w.props.save(&raw)
w.props.save(w.tableFormat, &raw)
bh, err := w.writeBlock(raw.finish(), NoCompression, &w.blockBuf)
if err != nil {
return err
@@ -2062,7 +2185,7 @@ func (o PreviousPointKeyOpt) UnsafeKey() base.InternalKey {
if o.w.dataBlockBuf.dataBlock.nEntries >= 1 {
// o.w.dataBlockBuf.dataBlock.curKey is guaranteed to point to the last point key
// which was added to the Writer.
return base.DecodeInternalKey(o.w.dataBlockBuf.dataBlock.curKey)
return o.w.dataBlockBuf.dataBlock.getCurKey()
}
return base.InternalKey{}
}
@@ -2091,6 +2214,8 @@ func NewWriter(writable objstorage.Writable, o WriterOptions, extraOpts ...Write
separator: o.Comparer.Separator,
successor: o.Comparer.Successor,
tableFormat: o.TableFormat,
isStrictObsolete: o.IsStrictObsolete,
writingToLowestLevel: o.WritingToLowestLevel,
cache: o.Cache,
restartInterval: o.BlockRestartInterval,
checksumType: o.Checksum,
@@ -2109,7 +2234,7 @@ func NewWriter(writable objstorage.Writable, o WriterOptions, extraOpts ...Write
Format: o.Comparer.FormatKey,
},
}
if w.tableFormat == TableFormatPebblev3 {
if w.tableFormat >= TableFormatPebblev3 {
w.shortAttributeExtractor = o.ShortAttributeExtractor
w.requiredInPlaceValueBound = o.RequiredInPlaceValueBound
w.valueBlockWriter = newValueBlockWriter(
@@ -2157,14 +2282,14 @@ func NewWriter(writable objstorage.Writable, o WriterOptions, extraOpts ...Write
}
}
w.props.ColumnFamilyID = math.MaxInt32
w.props.ComparerName = o.Comparer.Name
w.props.CompressionName = o.Compression.String()
w.props.MergerName = o.MergerName
w.props.PropertyCollectorNames = "[]"
w.props.ExternalFormatVersion = rocksDBExternalFormatVersion
if len(o.TablePropertyCollectors) > 0 || len(o.BlockPropertyCollectors) > 0 {
if len(o.TablePropertyCollectors) > 0 || len(o.BlockPropertyCollectors) > 0 ||
w.tableFormat >= TableFormatPebblev4 {
var buf bytes.Buffer
buf.WriteString("[")
if len(o.TablePropertyCollectors) > 0 {
@@ -2177,16 +2302,22 @@ func NewWriter(writable objstorage.Writable, o WriterOptions, extraOpts ...Write
buf.WriteString(w.propCollectors[i].Name())
}
}
numBlockPropertyCollectors := len(o.BlockPropertyCollectors)
if w.tableFormat >= TableFormatPebblev4 {
numBlockPropertyCollectors++
}
// shortID is a uint8, so we cannot exceed that number of block
// property collectors.
if numBlockPropertyCollectors > math.MaxUint8 {
w.err = errors.New("pebble: too many block property collectors")
return w
}
if numBlockPropertyCollectors > 0 {
w.blockPropCollectors = make([]BlockPropertyCollector, numBlockPropertyCollectors)
}
if len(o.BlockPropertyCollectors) > 0 {
// shortID is a uint8, so we cannot exceed that number of block
// property collectors.
if len(o.BlockPropertyCollectors) > math.MaxUint8 {
w.err = errors.New("pebble: too many block property collectors")
return w
}
// The shortID assigned to a collector is the same as its index in
// this slice.
w.blockPropCollectors = make([]BlockPropertyCollector, len(o.BlockPropertyCollectors))
for i := range o.BlockPropertyCollectors {
w.blockPropCollectors[i] = o.BlockPropertyCollectors[i]()
if i > 0 || len(o.TablePropertyCollectors) > 0 {
@@ -2195,6 +2326,13 @@ func NewWriter(writable objstorage.Writable, o WriterOptions, extraOpts ...Write
buf.WriteString(w.blockPropCollectors[i].Name())
}
}
if w.tableFormat >= TableFormatPebblev4 {
if numBlockPropertyCollectors > 1 || len(o.TablePropertyCollectors) > 0 {
buf.WriteString(",")
}
w.blockPropCollectors[numBlockPropertyCollectors-1] = &w.obsoleteCollector
buf.WriteString(w.obsoleteCollector.Name())
}
buf.WriteString("]")
w.props.PropertyCollectorNames = buf.String()
}
@@ -2228,3 +2366,85 @@ func init() {
}
private.SSTableInternalProperties = internalGetProperties
}
type obsoleteKeyBlockPropertyCollector struct {
blockIsNonObsolete bool
indexIsNonObsolete bool
tableIsNonObsolete bool
}
func encodeNonObsolete(isNonObsolete bool, buf []byte) []byte {
if isNonObsolete {
return buf
}
return append(buf, 't')
}
func (o *obsoleteKeyBlockPropertyCollector) Name() string {
return "obsolete-key"
}
func (o *obsoleteKeyBlockPropertyCollector) Add(key InternalKey, value []byte) error {
// Ignore.
return nil
}
func (o *obsoleteKeyBlockPropertyCollector) AddPoint(isObsolete bool) {
o.blockIsNonObsolete = o.blockIsNonObsolete || !isObsolete
}
func (o *obsoleteKeyBlockPropertyCollector) FinishDataBlock(buf []byte) ([]byte, error) {
o.tableIsNonObsolete = o.tableIsNonObsolete || o.blockIsNonObsolete
return encodeNonObsolete(o.blockIsNonObsolete, buf), nil
}
func (o *obsoleteKeyBlockPropertyCollector) AddPrevDataBlockToIndexBlock() {
o.indexIsNonObsolete = o.indexIsNonObsolete || o.blockIsNonObsolete
o.blockIsNonObsolete = false
}
func (o *obsoleteKeyBlockPropertyCollector) FinishIndexBlock(buf []byte) ([]byte, error) {
indexIsNonObsolete := o.indexIsNonObsolete
o.indexIsNonObsolete = false
return encodeNonObsolete(indexIsNonObsolete, buf), nil
}
func (o *obsoleteKeyBlockPropertyCollector) FinishTable(buf []byte) ([]byte, error) {
return encodeNonObsolete(o.tableIsNonObsolete, buf), nil
}
func (o *obsoleteKeyBlockPropertyCollector) UpdateKeySuffixes(
oldProp []byte, oldSuffix, newSuffix []byte,
) error {
_, err := propToIsObsolete(oldProp)
if err != nil {
return err
}
// Suffix rewriting currently loses the obsolete bit.
o.blockIsNonObsolete = true
return nil
}
// NB: obsoleteKeyBlockPropertyFilter is stateless. This aspect of the filter
// is used in table_cache.go for in-place modification of a filters slice.
type obsoleteKeyBlockPropertyFilter struct{}
func (o obsoleteKeyBlockPropertyFilter) Name() string {
return "obsolete-key"
}
// Intersects returns true if the set represented by prop intersects with
// the set in the filter.
func (o obsoleteKeyBlockPropertyFilter) Intersects(prop []byte) (bool, error) {
return propToIsObsolete(prop)
}
func propToIsObsolete(prop []byte) (bool, error) {
if len(prop) == 0 {
return true, nil
}
if len(prop) > 1 || prop[0] != 't' {
return false, errors.Errorf("unexpected property %x", prop)
}
return false, nil
}