/* * SPDX-FileCopyrightText: © Hypermode Inc. * SPDX-License-Identifier: Apache-2.0 */ package badger import ( "bytes" "fmt" "log" "math" "sync" "github.com/dgraph-io/badger/v4/table" "github.com/dgraph-io/badger/v4/y" ) type keyRange struct { left []byte right []byte inf bool size int64 // size is used for Key splits. } func (r keyRange) isEmpty() bool { return len(r.left) == 0 && len(r.right) == 0 && !r.inf } var infRange = keyRange{inf: true} func (r keyRange) String() string { return fmt.Sprintf("[left=%x, right=%x, inf=%v]", r.left, r.right, r.inf) } func (r keyRange) equals(dst keyRange) bool { return bytes.Equal(r.left, dst.left) && bytes.Equal(r.right, dst.right) && r.inf == dst.inf } func (r *keyRange) extend(kr keyRange) { // TODO(ibrahim): Is this needed? if kr.isEmpty() { return } if r.isEmpty() { *r = kr } if len(r.left) == 0 || y.CompareKeys(kr.left, r.left) < 0 { r.left = kr.left } if len(r.right) == 0 || y.CompareKeys(kr.right, r.right) > 0 { r.right = kr.right } if kr.inf { r.inf = true } } func (r keyRange) overlapsWith(dst keyRange) bool { // Empty keyRange always overlaps. if r.isEmpty() { return true } // TODO(ibrahim): Do you need this? // Empty dst doesn't overlap with anything. if dst.isEmpty() { return false } if r.inf || dst.inf { return true } // [dst.left, dst.right] ... [r.left, r.right] // If my left is greater than dst right, we have no overlap. if y.CompareKeys(r.left, dst.right) > 0 { return false } // [r.left, r.right] ... [dst.left, dst.right] // If my right is less than dst left, we have no overlap. if y.CompareKeys(r.right, dst.left) < 0 { return false } // We have overlap. return true } // getKeyRange returns the smallest and the biggest in the list of tables. // TODO(naman): Write a test for this. The smallest and the biggest should // be the smallest of the leftmost table and the biggest of the right most table. func getKeyRange(tables ...*table.Table) keyRange { if len(tables) == 0 { return keyRange{} } smallest := tables[0].Smallest() biggest := tables[0].Biggest() for i := 1; i < len(tables); i++ { if y.CompareKeys(tables[i].Smallest(), smallest) < 0 { smallest = tables[i].Smallest() } if y.CompareKeys(tables[i].Biggest(), biggest) > 0 { biggest = tables[i].Biggest() } } // We pick all the versions of the smallest and the biggest key. Note that version zero would // be the rightmost key, considering versions are default sorted in descending order. return keyRange{ left: y.KeyWithTs(y.ParseKey(smallest), math.MaxUint64), right: y.KeyWithTs(y.ParseKey(biggest), 0), } } type levelCompactStatus struct { ranges []keyRange delSize int64 } func (lcs *levelCompactStatus) debug() string { var b bytes.Buffer for _, r := range lcs.ranges { b.WriteString(r.String()) } return b.String() } func (lcs *levelCompactStatus) overlapsWith(dst keyRange) bool { for _, r := range lcs.ranges { if r.overlapsWith(dst) { return true } } return false } func (lcs *levelCompactStatus) remove(dst keyRange) bool { final := lcs.ranges[:0] var found bool for _, r := range lcs.ranges { if !r.equals(dst) { final = append(final, r) } else { found = true } } lcs.ranges = final return found } type compactStatus struct { sync.RWMutex levels []*levelCompactStatus tables map[uint64]struct{} } func (cs *compactStatus) overlapsWith(level int, this keyRange) bool { cs.RLock() defer cs.RUnlock() thisLevel := cs.levels[level] return thisLevel.overlapsWith(this) } func (cs *compactStatus) delSize(l int) int64 { cs.RLock() defer cs.RUnlock() return cs.levels[l].delSize } type thisAndNextLevelRLocked struct{} // compareAndAdd will check whether we can run this compactDef. That it doesn't overlap with any // other running compaction. If it can be run, it would store this run in the compactStatus state. func (cs *compactStatus) compareAndAdd(_ thisAndNextLevelRLocked, cd compactDef) bool { cs.Lock() defer cs.Unlock() tl := cd.thisLevel.level y.AssertTruef(tl < len(cs.levels), "Got level %d. Max levels: %d", tl, len(cs.levels)) thisLevel := cs.levels[cd.thisLevel.level] nextLevel := cs.levels[cd.nextLevel.level] if thisLevel.overlapsWith(cd.thisRange) { return false } if nextLevel.overlapsWith(cd.nextRange) { return false } // Check whether this level really needs compaction or not. Otherwise, we'll end up // running parallel compactions for the same level. // Update: We should not be checking size here. Compaction priority already did the size checks. // Here we should just be executing the wish of others. thisLevel.ranges = append(thisLevel.ranges, cd.thisRange) nextLevel.ranges = append(nextLevel.ranges, cd.nextRange) thisLevel.delSize += cd.thisSize for _, t := range append(cd.top, cd.bot...) { cs.tables[t.ID()] = struct{}{} } return true } func (cs *compactStatus) delete(cd compactDef) { cs.Lock() defer cs.Unlock() tl := cd.thisLevel.level y.AssertTruef(tl < len(cs.levels), "Got level %d. Max levels: %d", tl, len(cs.levels)) thisLevel := cs.levels[cd.thisLevel.level] nextLevel := cs.levels[cd.nextLevel.level] thisLevel.delSize -= cd.thisSize found := thisLevel.remove(cd.thisRange) // The following check makes sense only if we're compacting more than one // table. In case of the max level, we might rewrite a single table to // remove stale data. if cd.thisLevel != cd.nextLevel && !cd.nextRange.isEmpty() { found = nextLevel.remove(cd.nextRange) && found } if !found { this := cd.thisRange next := cd.nextRange fmt.Printf("Looking for: %s in this level %d.\n", this, tl) fmt.Printf("This Level:\n%s\n", thisLevel.debug()) fmt.Println() fmt.Printf("Looking for: %s in next level %d.\n", next, cd.nextLevel.level) fmt.Printf("Next Level:\n%s\n", nextLevel.debug()) log.Fatal("keyRange not found") } for _, t := range append(cd.top, cd.bot...) { _, ok := cs.tables[t.ID()] y.AssertTrue(ok) delete(cs.tables, t.ID()) } }