Skip to content

Commit ba2c2b6

Browse files
authored
feat: Adds property based testing framework for compaction (#26783) (#26865)
1 parent 12b46eb commit ba2c2b6

File tree

3 files changed

+239
-77
lines changed

3 files changed

+239
-77
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package tsm1_test
2+
3+
import (
4+
"github.com/influxdata/influxdb/tsdb/engine/tsm1"
5+
"time"
6+
)
7+
8+
type TestLevelResults struct {
9+
level1Groups []tsm1.PlannedCompactionGroup
10+
level2Groups []tsm1.PlannedCompactionGroup
11+
level3Groups []tsm1.PlannedCompactionGroup
12+
level4Groups []tsm1.PlannedCompactionGroup
13+
level5Groups []tsm1.PlannedCompactionGroup
14+
}
15+
16+
type TestEnginePlanCompactionsRunner struct {
17+
name string
18+
files []tsm1.ExtFileStat
19+
defaultBlockCount int // Default block count if member of files has FirstBlockCount of 0.
20+
// This is specifically used to adjust the modification time
21+
// so we can simulate the passage of time in tests
22+
testShardTime time.Duration
23+
// Each result is for the different plantypes
24+
expectedResult func() TestLevelResults
25+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package tsm1_test
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"golang.org/x/exp/slices"
7+
8+
"github.com/influxdata/influxdb/tsdb/engine/tsm1"
9+
)
10+
11+
// CompactionProperty represents a property that compaction groups should satisfy
12+
type CompactionProperty struct {
13+
Name string
14+
Description string
15+
Validator func(allFiles []string, groups []tsm1.CompactionGroup) error
16+
}
17+
18+
// AdjacentFileProperty validates that compaction groups don't create gaps
19+
var AdjacentFileProperty = CompactionProperty{
20+
Name: "Adjacency Rule",
21+
Description: "Files should not have non-adjacent files within the same compaction level - if files A and C are in the same level group, any file B between them should also be in a group at the same level or higher",
22+
Validator: validateFileAdjacency,
23+
}
24+
25+
type fileInfo struct {
26+
filename string
27+
generation int
28+
sequence int
29+
index int
30+
}
31+
32+
// validateFileAdjacency checks that there are no adjacency violations between TSM files
33+
// The following example will highlight an adjacency violation:
34+
// Given the following list of files [01-01.tsm, 02-02.tsm, 03-03.tsm] let's say we have the following compaction plans created
35+
// Group 1: [01-01.tsm, 03-03.tsm] & Group 2: [02-02.tsm]
36+
// This violates file adjacency as the first compaction group sees [01-01.tsm, X, 03-03.tsm] and the second group: [X, 02-02.tsm, X]
37+
// these are non-contiguous blocks, when the first group performs compaction we will have two files that are out of order compacted together.
38+
// This rule is important to maintain the ordering of files, with improper ordering we cannot determine which point is the newest point when overwrites occur.
39+
// We always want the newest write to win.
40+
func validateFileAdjacency(allFiles []string, groups []tsm1.CompactionGroup) error {
41+
var fileInfos []fileInfo
42+
for _, file := range allFiles {
43+
gen, seq, err := tsm1.DefaultParseFileName(file)
44+
if err != nil {
45+
return fmt.Errorf("failed to parse file %s: %v", file, err)
46+
}
47+
fileInfos = append(fileInfos, fileInfo{
48+
filename: file,
49+
generation: gen,
50+
sequence: seq,
51+
})
52+
}
53+
54+
slices.SortFunc(fileInfos, func(a, b fileInfo) int {
55+
if a.generation != b.generation {
56+
return a.generation - b.generation
57+
}
58+
59+
return a.sequence - b.sequence
60+
})
61+
62+
var fileMap = make(map[string]fileInfo, len(allFiles))
63+
for i, file := range allFiles {
64+
fileMap[file] = fileInfo{
65+
index: i,
66+
}
67+
}
68+
69+
for groupIndex, group := range groups {
70+
lastIndex := -1
71+
for _, file := range group {
72+
f, ok := fileMap[file]
73+
if !ok {
74+
return fmt.Errorf("file %s not found in group %d", file, groupIndex)
75+
}
76+
77+
if lastIndex == -1 {
78+
lastIndex = f.index
79+
} else {
80+
// Check lastIndex wrt f.index
81+
if lastIndex+1 != f.index {
82+
return fmt.Errorf("file %s in compaction group %d violates adjacency policy", file, groupIndex+1)
83+
}
84+
lastIndex = f.index
85+
}
86+
}
87+
}
88+
89+
return nil
90+
}
91+
92+
// ValidateCompactionProperties validates that compaction results satisfy all properties
93+
func ValidateCompactionProperties(allFiles []string, results TestLevelResults, properties ...CompactionProperty) error {
94+
var errs []error
95+
96+
// Collect all compaction groups from results
97+
var allGroups []tsm1.CompactionGroup
98+
allGroups = append(allGroups, extractGroups(results.level1Groups)...)
99+
allGroups = append(allGroups, extractGroups(results.level2Groups)...)
100+
allGroups = append(allGroups, extractGroups(results.level3Groups)...)
101+
allGroups = append(allGroups, extractGroups(results.level4Groups)...)
102+
allGroups = append(allGroups, extractGroups(results.level5Groups)...)
103+
104+
// Validate each property
105+
for _, property := range properties {
106+
if err := property.Validator(allFiles, allGroups); err != nil {
107+
errs = append(errs, fmt.Errorf("%s violation: %v", property.Name, err))
108+
}
109+
}
110+
111+
return errors.Join(errs...)
112+
}
113+
114+
// extractGroups extracts CompactionGroup from PlannedCompactionGroup
115+
func extractGroups(plannedGroups []tsm1.PlannedCompactionGroup) []tsm1.CompactionGroup {
116+
var groups []tsm1.CompactionGroup
117+
for _, planned := range plannedGroups {
118+
groups = append(groups, planned.Group)
119+
}
120+
return groups
121+
}
122+
123+
// ValidateTestCase validates both expected results and actual planner output
124+
func ValidateTestCase(testCase TestEnginePlanCompactionsRunner, actualResults TestLevelResults) error {
125+
var errs []error
126+
127+
// Extract all filenames from test case
128+
var allFiles = make([]string, len(testCase.files))
129+
for i, file := range testCase.files {
130+
allFiles[i] = file.Path
131+
}
132+
133+
// Validate expected results
134+
expectedResults := testCase.expectedResult()
135+
if expectedErr := ValidateCompactionProperties(allFiles, expectedResults, AdjacentFileProperty); expectedErr != nil {
136+
errs = append(errs, fmt.Errorf("expected results: %v", expectedErr))
137+
}
138+
139+
// Validate actual results
140+
if actualErr := ValidateCompactionProperties(allFiles, actualResults, AdjacentFileProperty); actualErr != nil {
141+
errs = append(errs, fmt.Errorf("actual results: %v", actualErr))
142+
}
143+
144+
return errors.Join(errs...)
145+
}

0 commit comments

Comments
 (0)