Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
cabcdd1
feat: Adds detection for orphaned TSM files while running PlanOptimize
devanbenz Aug 30, 2025
ff08f6e
feat: Adds 4 more tests
devanbenz Sep 2, 2025
b9a9667
feat: Make sure tests are level4
devanbenz Sep 2, 2025
b3f1b2d
feat: Let's use less then default max points per blocks, taking a loo…
devanbenz Sep 2, 2025
2c178fd
feat: Sets the planning logic for Plan() to include checks to ensure …
devanbenz Sep 2, 2025
4425ded
feat: Adds logic where we will only compact lower level files
devanbenz Sep 2, 2025
4ee4278
feat: Only push up tests to show failures
devanbenz Sep 3, 2025
86ad19a
feat: Adds test generation file
devanbenz Sep 3, 2025
84db100
feat: Adds static test case file
devanbenz Sep 3, 2025
3020f03
feat: Set up lower first block count tests
devanbenz Sep 3, 2025
0cbf752
feat: Move other tests in to static test file
devanbenz Sep 3, 2025
003f303
feat: Update check for last file index
devanbenz Sep 3, 2025
8eb2aec
feat: Adds additional tests
devanbenz Sep 3, 2025
886f593
feat: Use same generation for l1 and l2 file
devanbenz Sep 3, 2025
e715375
feat: Set check to use greater than 3 for the lower level files
devanbenz Sep 3, 2025
b81d67f
feat: Remove setting end inside break check
devanbenz Sep 4, 2025
e7d6038
feat: Adds an additional test
devanbenz Sep 5, 2025
3056c24
feat: Adds a option for setting nested compactor on or off
devanbenz Sep 5, 2025
0030fb0
feat: Reword nested compactor config
devanbenz Sep 5, 2025
397236e
feat: Run tests for both ff off and on
devanbenz Sep 5, 2025
97db9f3
feat: Adjust logic so we will not pick up files out of order
devanbenz Sep 7, 2025
42f6ec8
feat: This commit adds a check to see if we have a group of level 4 f…
devanbenz Sep 8, 2025
d19d19d
feat: Simplify logic for nested PlanLevel files
devanbenz Sep 8, 2025
0c556e2
feat: Trying to make sure generations are not compacted out of order
devanbenz Sep 8, 2025
065d359
feat: Adds property based testing framework for compaction tests
devanbenz Sep 8, 2025
a697635
feat: Working on property based testing framework
devanbenz Sep 8, 2025
2a73c97
feat: Adds property based testing framework for compaction
devanbenz Sep 8, 2025
b708182
feat: Adds common for compact tests
devanbenz Sep 8, 2025
6896118
feat: Adjusts tests to remove file gaps
devanbenz Sep 8, 2025
4d03abe
feat: Remove comments
devanbenz Sep 8, 2025
8d241d9
feat: Update with results
devanbenz Sep 8, 2025
c14a416
feat: Adjust AdjacentFileProperty to improve algorithm
devanbenz Sep 9, 2025
a149a65
feat: Adds a log instead of requirement for violations in adjacency
devanbenz Sep 9, 2025
f3498c0
feat: I DO NOT think that I need to sort
devanbenz Sep 9, 2025
5ace584
feat: Use assert for testing
devanbenz Sep 9, 2025
40affaa
feat: Merge branch 'db/compaction-property-based-testing' into db/bad…
devanbenz Sep 9, 2025
061ade9
feat: rename method to validateFileAdjacency
devanbenz Sep 9, 2025
cd61e6d
feat: Merge branch 'db/compaction-property-based-testing' into db/bad…
devanbenz Sep 9, 2025
36e8106
feat: Just build the map and use that
devanbenz Sep 9, 2025
2e11b6f
feat: Merge branch 'db/compaction-property-based-testing' into db/bad…
devanbenz Sep 9, 2025
bbcfa47
feat: Adding fixAdjacencyViolations
devanbenz Sep 9, 2025
f1042fa
feat: cleanup
devanbenz Sep 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions tsdb/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ const (

// MaxTSMFileSize is the maximum size of TSM files.
MaxTSMFileSize = uint32(2048 * 1024 * 1024) // 2GB

// DefaultNestedCompactorEnabled is the default value on whether to enable the nested
// compaction level checking feature flag. This rectifies an issue where TSM files
// Have 1-3 rogue lower level file(s) nested within larger level files. This state
// is not frequent but has been seen. Enable this flag to capture these files during full compaction.
DefaultNestedCompactorEnabled = false
)

var SingleGenerationReasonText string = SingleGenerationReason()
Expand Down Expand Up @@ -186,6 +192,12 @@ type Config struct {
// been found to be problematic in some cases. It may help users who have
// slow disks.
TSMWillNeed bool `toml:"tsm-use-madv-willneed"`

// NestedCompactorEnabled controls whether we enable the nested
// compaction level checking feature flag. This rectifies an issue where TSM files
// Have 1-3 rogue lower level file(s) nested within larger level files. This state
// is not frequent but has been seen. Enable this flag to capture these files during full compaction.
NestedCompactorEnabled bool `toml:"nested-compactor-enabled"`
}

// NewConfig returns the default configuration for tsdb.
Expand Down Expand Up @@ -217,6 +229,8 @@ func NewConfig() Config {

TraceLoggingEnabled: false,
TSMWillNeed: false,

NestedCompactorEnabled: DefaultNestedCompactorEnabled,
}
}

Expand Down
133 changes: 112 additions & 21 deletions tsdb/engine/tsm1/compact.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,13 @@ type CompactionPlanner interface {
SetAggressiveCompactionPointsPerBlock(aggressiveCompactionPointsPerBlock int)

GetAggressiveCompactionPointsPerBlock() int

// SetNestedCompactor controls whether we enable the nested
// compaction level checking feature flag. This rectifies an issue where TSM files
// Have 1-3 rogue lower level file(s) nested within larger level files. This state
// is not frequent but has been seen. Enable this flag to capture these files during full compaction.
SetNestedCompactor(enabled bool)
GetNestedCompactorEnabled() bool
}

// DefaultPlanner implements CompactionPlanner using a strategy to roll up
Expand Down Expand Up @@ -179,6 +186,12 @@ type DefaultPlanner struct {
// aggressiveCompactionPointsPerBlock is the amount of points that should be
// packed in to a TSM file block during aggressive compaction
aggressiveCompactionPointsPerBlock int

// enableNestedCompactor controls whether we enable the nested
// compaction level checking feature flag. This rectifies an issue where TSM files
// Have 1-3 rogue lower level file(s) nested within larger level files. This state
// is not frequent but has been seen. Enable this flag to capture these files during full compaction.
enableNestedCompactor bool
}

type fileStore interface {
Expand Down Expand Up @@ -256,6 +269,14 @@ func (c *DefaultPlanner) SetAggressiveCompactionPointsPerBlock(aggressiveCompact
c.aggressiveCompactionPointsPerBlock = aggressiveCompactionPointsPerBlock
}

func (c *DefaultPlanner) SetNestedCompactor(enableNestedCompactor bool) {
c.enableNestedCompactor = enableNestedCompactor
}

func (c *DefaultPlanner) GetNestedCompactorEnabled() bool {
return c.enableNestedCompactor
}

func (c *DefaultPlanner) GetAggressiveCompactionPointsPerBlock() int {
return c.aggressiveCompactionPointsPerBlock
}
Expand Down Expand Up @@ -375,9 +396,29 @@ func (c *DefaultPlanner) PlanLevel(level int) ([]CompactionGroup, int64) {

// Remove any groups in the wrong level
var levelGroups []tsmGenerations
for _, cur := range groups {
if cur.level() == level {
levelGroups = append(levelGroups, cur)
if c.enableNestedCompactor {
// When nested compactor is enabled and planning lower levels (1-3),
// check if there are higher level files (4+) both BEFORE and AFTER this group
// Only skip if the lower-level files are truly nested between higher-level files
for i, cur := range groups {
if len(groups) > i+1 {
nextLevel := groups[i+1]
if cur.level() < nextLevel.level() {
Copy link
Contributor

@philjb philjb Sep 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is handling the groups as we want.

concern these generations:
old 4, 4, 4, 2, 2, 2, 2, 1, 1, 1, 1, 4, 4, 4, 3, 3, 3, 2, 2, 1, 1, 1, 1, 1, 1 young
They will group into:
old G4, G2, G1, G4', G3, G2', G1 young
This conditional will "skip/continue" G1 but not G2 even though both G1 and G2 are nested within L4 generations.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need to scan ahead. I don't think you can "only" look at the level of an immediately subsequent group.

Perhaps this code:

	// Remove any groups in the wrong level
	var levelGroups []tsmGenerations
	if c.enableNestedCompactor {
		// search for runs of groups nested between level 4+ groups
		// e.g. G4, G4, G1, G4, G2 ... = G1
		// e.g. G4, G4, G2, G4, G1, G4 ... = (G2), (G1)
		// e.g. G4, G4, G2, G1, G3, G1, G4, G1, G4 ... = (G2, G1, G3, G1), (G1)
		skip := make([]bool, len(groups))
		lastG4 := 0
		for i, cur := range groups {
			lvl := cur.level()
			if  lvl >= 4 {
				if i > 0 && lastG4 != i - 1 {
					// we have returned to L4+ after seeing L3-
					// skip all the groups in between
					for j := lastG4+1; j < i; j++ {
						skip[j] = true
					}
				}
				lastG4 = i
			}
			
			if lvl != level {
				// normal (non-nested) behavior: group's level doesn't match the requested planning level
				skip[i] = true
			}
		}
		// process skip list
		for i, cur := range groups {
			if !skip[i] {
				levelGroups = append(levelGroups, cur)
			}
		}
	} else {
		for _, cur := range groups {
			if cur.level() == level {
				levelGroups = append(levelGroups, cur)
			}
		}
	}

continue
} else if cur.level() == level {
levelGroups = append(levelGroups, cur)
}
} else {
if cur.level() == level {
levelGroups = append(levelGroups, cur)
}
}
}
} else {
for _, cur := range groups {
if cur.level() == level {
levelGroups = append(levelGroups, cur)
}
}
}

Expand Down Expand Up @@ -580,11 +621,26 @@ func (c *DefaultPlanner) Plan(lastWrite time.Time) ([]CompactionGroup, int64) {
// each generation in descending break once we see a file less than 4.
end := 0
start := 0
lastHighLevelIndex := -1

for i, g := range generations {
if g.level() <= 3 {
break
if c.enableNestedCompactor {
// Track the last high-level generation
if g.level() > 3 {
lastHighLevelIndex = i
}
} else {
if g.level() <= 3 {
break
}

end = i + 1
}
end = i + 1
}

// If we have high-level files, only include generations up to the last high-level file
if c.enableNestedCompactor && lastHighLevelIndex >= 0 {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may not be needed since we set lastHighLevelIndex to -1 so if the feature flag is disabled it will just always be -1.. I want to remain consistent and indicate that this code is only supposed to be enabled when the feature flag is on so I decided to keep it.

end = lastHighLevelIndex + 1
}

// As compactions run, the oldest files get bigger. We don't want to re-compact them during
Expand All @@ -610,9 +666,16 @@ func (c *DefaultPlanner) Plan(lastWrite time.Time) ([]CompactionGroup, int64) {
// can become larger faster than ones after them. We want to skip those really big ones and just
// compact the smaller ones until they are closer in size.
if i > 0 {
if g.size()*2 < generations[i-1].size() {
start = i
break
if c.enableNestedCompactor {
if g.size()*2 < generations[i-1].size() && generations[i-1].level() >= generations[i].level() {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to add a comment about why this check needs to be different with the feature flag on. It's a bit confusing to just look at without knowing context.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment would be great.

I still think that this logic will "leave behind" a non fully compacted file, but i think this is likely ok. For example

gen-lvl size index
6-6 6.1 0
5-5 5.1 1
4-4 4.1 2
3-2 2 3
2-2 .9 4
1-7 .9 5
0-1 .9 6

start will be 4, end will be 6. The 3-2 file won't be picked up.

start = i
break
}
} else {
if g.size()*2 < generations[i-1].size() {
start = i
break
}
}
}
}
Expand All @@ -636,13 +699,15 @@ func (c *DefaultPlanner) Plan(lastWrite time.Time) ([]CompactionGroup, int64) {

for j := i; j < i+step && j < len(generations); j++ {
gen := generations[j]
lvl := gen.level()
if !c.enableNestedCompactor {
lvl := gen.level()

// Skip compacting this group if there happens to be any lower level files in the
// middle. These will get picked up by the level compactors.
if lvl <= 3 {
skipGroup = true
break
// Skip compacting this group if there happens to be lower level
// files in the middle.
if lvl <= 3 {
skipGroup = true
break
}
}

// Skip the file if it's over the max size and it contains a full block
Expand All @@ -652,7 +717,7 @@ func (c *DefaultPlanner) Plan(lastWrite time.Time) ([]CompactionGroup, int64) {
}
}

if skipGroup {
if !c.enableNestedCompactor && skipGroup {
continue
}

Expand All @@ -671,12 +736,27 @@ func (c *DefaultPlanner) Plan(lastWrite time.Time) ([]CompactionGroup, int64) {

// With the groups, we need to evaluate whether the group as a whole can be compacted
compactable := []tsmGenerations{}
for _, group := range groups {
// if we don't have enough generations to compact, skip it
if len(group) < 4 && !group.hasTombstones() {
continue

if c.enableNestedCompactor {
lastGroupLevel := 4
for _, group := range groups {
// If we have the nested compactor flag enabled we need to try our best to not ever
// skip over lower lever files. This will ensure that if there are a few higher level files
// BUT over 4 lower level files nested we will compact them all together.
if len(group) < 4 && !group.hasTombstones() && lastGroupLevel == group.level() {
continue
}
compactable = append(compactable, group)
lastGroupLevel = group.level()
}
} else {
for _, group := range groups {
// if we don't have enough generations to compact, skip it
if len(group) < 4 && !group.hasTombstones() {
continue
}
compactable = append(compactable, group)
}
compactable = append(compactable, group)
}

// All the files to be compacted must be compacted in order. We need to convert each
Expand Down Expand Up @@ -1946,6 +2026,17 @@ func (a tsmGenerations) hasTombstones() bool {
return false
}

func (a tsmGenerations) lowestLevel() int {
var level int
for _, g := range a {
lev := g.level()
if lev < level {
level = lev
}
}
return level
}

func (a tsmGenerations) level() int {
var level int
for _, g := range a {
Expand Down
Loading