Skip to content

Commit ae524f9

Browse files
committed
manifest: break up addL0Files
This change pulls out a `canUseL0Files` method from `addL0Files`. If this method succeeds, we can assume that `addL0Files` will succeed.
1 parent de903d9 commit ae524f9

File tree

2 files changed

+56
-70
lines changed

2 files changed

+56
-70
lines changed

internal/manifest/l0_sublevels.go

Lines changed: 45 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@ import (
1818
"github.com/cockroachdb/pebble/internal/invariants"
1919
)
2020

21-
// errInvalidL0SublevelsOpt is for use in addL0Files when the incremental
22-
// sublevel generation optimization failed, and newL0Sublevels must be called.
23-
var errInvalidL0SublevelsOpt = errors.New("pebble: L0 sublevel generation optimization cannot be used")
24-
2521
// Intervals are of the form [start, end) with no gap between intervals. Each
2622
// file overlaps perfectly with a sequence of intervals. This perfect overlap
2723
// occurs because the union of file boundary keys is used to pick intervals.
@@ -314,9 +310,7 @@ func newL0Sublevels(
314310
// Initialize minIntervalIndex and maxIntervalIndex for each file, and use that
315311
// to update intervals.
316312
for f := iter.First(); f != nil; f = iter.Next() {
317-
if err := s.addFileToSublevels(f, false /* checkInvariant */); err != nil {
318-
return nil, err
319-
}
313+
s.addFileToSublevels(f)
320314
}
321315
// Sort each sublevel in increasing key order.
322316
for i := range s.levelFiles {
@@ -409,51 +403,20 @@ func mergeIntervals(
409403
return result, oldToNewMap
410404
}
411405

412-
// addL0Files incrementally builds a new l0Sublevels for when the only change
413-
// since the receiver l0Sublevels was an addition of the specified tables, with
414-
// no L0 deletions. The common case of this is an ingestion or a flush. These
415-
// files can "sit on top" of existing sublevels, creating at most one new
416-
// sublevel for a flush (and possibly multiple for an ingestion), and at most
417-
// 2*len(files) additions to s.orderedIntervals. No files must have been deleted
418-
// from L0, and the added files must all be newer in sequence numbers than
419-
// existing files in l0Sublevels. The levelMetadata parameter corresponds to the
420-
// new L0 post addition of files. This method is meant to be significantly more
421-
// performant than newL0Sublevels.
422-
//
423-
// Note that this function can only be called once on a given receiver; it
424-
// appends to some slices in s which is only safe when done once. This is okay,
425-
// as the common case (generating a new l0Sublevels after a flush/ingestion) is
426-
// only going to necessitate one call of this method on a given receiver. The
427-
// returned value, if non-nil, can then have [*l0Sublevels.addL0Files] called on
428-
// it again, and so on. If [errInvalidL0SublevelsOpt] is returned as an error,
429-
// it means the optimization could not be applied (i.e. files added were older
430-
// than files already in the sublevels, which is possible around ingestions and
431-
// in tests). Eg. it can happen when an ingested file was ingested without
432-
// queueing a flush since it did not actually overlap with any keys in the
433-
// memtable. Later on the memtable was flushed, and the memtable had keys
434-
// spanning around the ingested file, producing a flushed file that overlapped
435-
// with the ingested file in file bounds but not in keys. It's possible for that
436-
// flushed file to have a lower LargestSeqNum than the ingested file if all the
437-
// additions after the ingestion were to another flushed file that was split
438-
// into a separate sstable during flush. Any other non-nil error means
439-
// [l0Sublevels] generation failed in the same way as [newL0Sublevels] would
440-
// likely fail.
441-
func (s *l0Sublevels) addL0Files(
442-
addedTables map[base.FileNum]*TableMetadata,
443-
flushSplitMaxBytes int64,
444-
levelMetadata *LevelMetadata,
445-
) (*l0Sublevels, error) {
406+
func (s *l0Sublevels) canUseAddL0Files(
407+
addedTables map[base.FileNum]*TableMetadata, levelMetadata *LevelMetadata,
408+
) (filesToAddInOrder []*TableMetadata, ok bool) {
446409
if s.addL0FilesCalled {
447410
if invariants.Enabled {
448411
panic("addL0Files called twice on the same receiver")
449412
}
450-
return nil, errInvalidL0SublevelsOpt
413+
return nil, false
451414
}
452415
if s.levelMetadata.Len()+len(addedTables) != levelMetadata.Len() {
453416
if invariants.Enabled {
454417
panic("levelMetadata mismatch")
455418
}
456-
return nil, errInvalidL0SublevelsOpt
419+
return nil, false
457420
}
458421

459422
// addL0Files only works when the files we are adding match exactly the last
@@ -465,12 +428,40 @@ func (s *l0Sublevels) addL0Files(
465428
if addedTables[t.FileNum] == nil {
466429
// t is an existing table that sorts after some of the new tables
467430
// (specifically the ones we haven't yet seen).
468-
return nil, errInvalidL0SublevelsOpt
431+
return nil, false
469432
}
470433
files[i] = t
471434
t = iter.Prev()
472435
}
473-
// Note: files now contains addedTables in increasing seqnum order.
436+
return files, true
437+
}
438+
439+
// addL0Files incrementally builds a new l0Sublevels for when the only change
440+
// since the receiver l0Sublevels was an addition of the specified tables, with
441+
// no L0 deletions. The common case of this is an ingestion or a flush. These
442+
// files can "sit on top" of existing sublevels, creating at most one new
443+
// sublevel for a flush (and possibly multiple for an ingestion), and at most
444+
// 2*len(files) additions to s.orderedIntervals. No files must have been deleted
445+
// from L0, and the added files must all be newer in sequence numbers than
446+
// existing files in l0Sublevels. The levelMetadata parameter corresponds to the
447+
// new L0 post addition of files. This method is meant to be significantly more
448+
// performant than newL0Sublevels.
449+
//
450+
// This function is intended to be called with the result of canUseAddL0Files(),
451+
// which is the list of new L0 tables in increasing L0 order.
452+
//
453+
// Note that this function can only be called once on a given receiver; it
454+
// appends to some slices in s which is only safe when done once. This is okay,
455+
// as the common case (generating a new l0Sublevels after a flush/ingestion) is
456+
// only going to necessitate one call of this method on a given receiver. The
457+
// returned value, if non-nil, can then have [*l0Sublevels.addL0Files] called on
458+
// it again, and so on.
459+
func (s *l0Sublevels) addL0Files(
460+
files []*TableMetadata, flushSplitMaxBytes int64, levelMetadata *LevelMetadata,
461+
) *l0Sublevels {
462+
if s.addL0FilesCalled {
463+
panic("addL0Files called twice on the same receiver")
464+
}
474465
s.addL0FilesCalled = true
475466

476467
// Start with a shallow copy of s.
@@ -618,9 +609,7 @@ func (s *l0Sublevels) addL0Files(
618609
// Update interval indices for new files.
619610
for i, f := range files {
620611
f.L0Index = s.levelMetadata.Len() + i
621-
if err := newVal.addFileToSublevels(f, true /* checkInvariant */); err != nil {
622-
return nil, err
623-
}
612+
newVal.addFileToSublevels(f)
624613
updatedSublevels = append(updatedSublevels, f.SubLevel)
625614
}
626615

@@ -658,16 +647,14 @@ func (s *l0Sublevels) addL0Files(
658647
newVal.flushSplitUserKeys = nil
659648
newVal.calculateFlushSplitKeys(flushSplitMaxBytes)
660649
newVal.Check()
661-
return newVal, nil
650+
return newVal
662651
}
663652

664653
// addFileToSublevels is called during l0Sublevels generation, and adds f to the
665654
// correct sublevel's levelFiles, the relevant intervals' files slices, and sets
666655
// interval indices on f. This method, if called successively on multiple files,
667-
// _must_ be called on successively newer files (by seqnum). If checkInvariant
668-
// is true, it could check for this in some cases and return
669-
// [errInvalidL0SublevelsOpt] if that invariant isn't held.
670-
func (s *l0Sublevels) addFileToSublevels(f *TableMetadata, checkInvariant bool) error {
656+
// _must_ be called on successively newer files (by seqnum).
657+
func (s *l0Sublevels) addFileToSublevels(f *TableMetadata) {
671658
// This is a simple and not very accurate estimate of the number of
672659
// bytes this SSTable contributes to the intervals it is a part of.
673660
//
@@ -680,10 +667,8 @@ func (s *l0Sublevels) addFileToSublevels(f *TableMetadata, checkInvariant bool)
680667
for i := f.minIntervalIndex; i <= f.maxIntervalIndex; i++ {
681668
interval := &s.orderedIntervals[i]
682669
if len(interval.files) > 0 {
683-
if checkInvariant && interval.files[len(interval.files)-1].LargestSeqNum > f.LargestSeqNum {
684-
// We are sliding this file "underneath" an existing file. Throw away
685-
// and start over in newL0Sublevels.
686-
return errInvalidL0SublevelsOpt
670+
if interval.files[len(interval.files)-1].LargestSeqNum > f.LargestSeqNum {
671+
panic(errors.AssertionFailedf("addFileToSublevels found existing newer file"))
687672
}
688673
// interval.files is sorted by sublevels, from lowest to highest.
689674
// addL0Files can only add files at sublevels higher than existing files
@@ -703,14 +688,13 @@ func (s *l0Sublevels) addFileToSublevels(f *TableMetadata, checkInvariant bool)
703688
}
704689
f.SubLevel = subLevel
705690
if subLevel > len(s.levelFiles) {
706-
return errors.Errorf("chose a sublevel beyond allowed range of sublevels: %d vs 0-%d", subLevel, len(s.levelFiles))
691+
panic(errors.AssertionFailedf("chose a sublevel beyond allowed range of sublevels: %d vs 0-%d", subLevel, len(s.levelFiles)))
707692
}
708693
if subLevel == len(s.levelFiles) {
709694
s.levelFiles = append(s.levelFiles, []*TableMetadata{f})
710695
} else {
711696
s.levelFiles[subLevel] = append(s.levelFiles[subLevel], f)
712697
}
713-
return nil
714698
}
715699

716700
func (s *l0Sublevels) calculateFlushSplitKeys(flushSplitMaxBytes int64) {
@@ -2147,8 +2131,8 @@ func (o *L0Organizer) Update(
21472131
}
21482132
// If we only added tables, try to use addL0Files.
21492133
if len(deletedL0Tables) == 0 {
2150-
newSublevels, err := o.l0Sublevels.addL0Files(addedL0Tables, o.flushSplitBytes, newLevelMeta)
2151-
if err == nil {
2134+
if files, ok := o.l0Sublevels.canUseAddL0Files(addedL0Tables, newLevelMeta); ok {
2135+
newSublevels := o.l0Sublevels.addL0Files(files, o.flushSplitBytes, newLevelMeta)
21522136
// In invariants mode, sometimes rebuild from scratch to verify that
21532137
// AddL0Files did the right thing. Note that NewL0Sublevels updates
21542138
// fields in TableMetadata like L0Index, so we don't want to do this
@@ -2170,9 +2154,6 @@ func (o *L0Organizer) Update(
21702154
o.l0Sublevels = newSublevels
21712155
return
21722156
}
2173-
if !errors.Is(err, errInvalidL0SublevelsOpt) {
2174-
panic(errors.AssertionFailedf("error generating L0Sublevels: %s", err))
2175-
}
21762157
}
21772158
var err error
21782159
o.l0Sublevels, err = newL0Sublevels(newLevelMeta, o.cmp, o.formatKey, o.flushSplitBytes)

internal/manifest/l0_sublevels_test.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"time"
2020

2121
"github.com/cockroachdb/datadriven"
22+
"github.com/cockroachdb/errors"
2223
"github.com/cockroachdb/pebble/internal/base"
2324
"github.com/cockroachdb/pebble/internal/testkeys"
2425
"github.com/cockroachdb/pebble/record"
@@ -294,10 +295,13 @@ func TestL0Sublevels(t *testing.T) {
294295
levelMetadata := MakeLevelMetadata(base.DefaultComparer.Compare, 0, fileMetas[0])
295296
if initialize {
296297
if addL0FilesOpt {
297-
sublevels, err = sublevels.addL0Files(addedL0Files, int64(flushSplitMaxBytes), &levelMetadata)
298-
// Check if the output matches a full initialization.
299-
sublevels2, _ := newL0Sublevels(&levelMetadata, base.DefaultComparer.Compare, base.DefaultFormatter, int64(flushSplitMaxBytes))
300-
if sublevels != nil && sublevels2 != nil {
298+
files, ok := sublevels.canUseAddL0Files(addedL0Files, &levelMetadata)
299+
if !ok {
300+
err = errors.Newf("pebble: L0 sublevel generation optimization cannot be used")
301+
} else {
302+
sublevels = sublevels.addL0Files(files, int64(flushSplitMaxBytes), &levelMetadata)
303+
// Check if the output matches a full initialization.
304+
sublevels2, _ := newL0Sublevels(&levelMetadata, base.DefaultComparer.Compare, base.DefaultFormatter, int64(flushSplitMaxBytes))
301305
require.Equal(t, sublevels.flushSplitUserKeys, sublevels2.flushSplitUserKeys)
302306
require.Equal(t, sublevels.levelFiles, sublevels2.levelFiles)
303307
}
@@ -555,8 +559,9 @@ func TestAddL0FilesEquivalence(t *testing.T) {
555559
// that of the previous L0Sublevels. So it must be called before
556560
// newL0Sublevels; calling it the other way around results in
557561
// out-of-bounds panics.
558-
s2, err = s2.addL0Files(filesToAdd, flushSplitMaxBytes, &levelMetadata)
559-
require.NoError(t, err)
562+
files, ok := s2.canUseAddL0Files(filesToAdd, &levelMetadata)
563+
require.True(t, ok)
564+
s2 = s2.addL0Files(files, flushSplitMaxBytes, &levelMetadata)
560565
}
561566

562567
s, err = newL0Sublevels(&levelMetadata, testkeys.Comparer.Compare, testkeys.Comparer.FormatKey, flushSplitMaxBytes)

0 commit comments

Comments
 (0)