github.com/grafana/pyroscope@v1.18.0/pkg/compactor/bucket_compactor_test.go (about) 1 // SPDX-License-Identifier: AGPL-3.0-only 2 // Provenance-includes-location: https://github.com/grafana/mimir/blob/main/pkg/compactor/bucket_compactor_test.go 3 // Provenance-includes-license: Apache-2.0 4 // Provenance-includes-copyright: The Cortex Authors. 5 6 package compactor 7 8 import ( 9 "context" 10 "fmt" 11 "path/filepath" 12 "strings" 13 "testing" 14 "time" 15 16 "github.com/go-kit/log" 17 "github.com/oklog/ulid/v2" 18 "github.com/prometheus/client_golang/prometheus" 19 "github.com/prometheus/client_golang/prometheus/promauto" 20 "github.com/prometheus/client_golang/prometheus/testutil" 21 "github.com/prometheus/prometheus/model/labels" 22 "github.com/stretchr/testify/assert" 23 "github.com/stretchr/testify/require" 24 "github.com/thanos-io/objstore" 25 26 phlareobj "github.com/grafana/pyroscope/pkg/objstore" 27 objstore_testutil "github.com/grafana/pyroscope/pkg/objstore/testutil" 28 "github.com/grafana/pyroscope/pkg/phlaredb/block" 29 "github.com/grafana/pyroscope/pkg/util/extprom" 30 ) 31 32 func TestGroupKey(t *testing.T) { 33 for _, tcase := range []struct { 34 input block.Meta 35 expected string 36 }{ 37 { 38 input: block.Meta{}, 39 expected: "0@17241709254077376921", 40 }, 41 { 42 input: block.Meta{ 43 Labels: map[string]string{}, 44 Downsample: block.Downsample{Resolution: 0}, 45 }, 46 expected: "0@17241709254077376921", 47 }, 48 { 49 input: block.Meta{ 50 Labels: map[string]string{"foo": "bar", "foo1": "bar2"}, 51 Downsample: block.Downsample{Resolution: 0}, 52 }, 53 expected: "0@2124638872457683483", 54 }, 55 { 56 input: block.Meta{ 57 Labels: map[string]string{`foo/some..thing/some.thing/../`: `a_b_c/bar-something-a\metric/a\x`}, 58 Downsample: block.Downsample{Resolution: 0}, 59 }, 60 expected: "0@16590761456214576373", 61 }, 62 } { 63 if ok := t.Run("", func(t *testing.T) { 64 assert.Equal(t, tcase.expected, DefaultGroupKey(tcase.input)) 65 }); !ok { 66 return 67 } 68 } 69 } 70 71 func TestGroupMaxMinTime(t *testing.T) { 72 g := &Job{ 73 metasByMinTime: []*block.Meta{ 74 {MinTime: 0, MaxTime: 10}, 75 {MinTime: 1, MaxTime: 20}, 76 {MinTime: 2, MaxTime: 30}, 77 }, 78 } 79 80 assert.Equal(t, int64(0), g.MinTime()) 81 assert.Equal(t, int64(30), g.MaxTime()) 82 } 83 84 func TestBucketCompactor_FilterOwnJobs(t *testing.T) { 85 jobsFn := func() []*Job { 86 return []*Job{ 87 NewJob("user", "key1", labels.EmptyLabels(), 0, false, 0, 0, ""), 88 NewJob("user", "key2", labels.EmptyLabels(), 0, false, 0, 0, ""), 89 NewJob("user", "key3", labels.EmptyLabels(), 0, false, 0, 0, ""), 90 NewJob("user", "key4", labels.EmptyLabels(), 0, false, 0, 0, ""), 91 } 92 } 93 94 tests := map[string]struct { 95 ownJob ownCompactionJobFunc 96 expectedJobs int 97 }{ 98 "should return all planned jobs if the compactor instance owns all of them": { 99 ownJob: func(job *Job) (bool, error) { 100 return true, nil 101 }, 102 expectedJobs: 4, 103 }, 104 "should return no jobs if the compactor instance owns none of them": { 105 ownJob: func(job *Job) (bool, error) { 106 return false, nil 107 }, 108 expectedJobs: 0, 109 }, 110 "should return some jobs if the compactor instance owns some of them": { 111 ownJob: func() ownCompactionJobFunc { 112 count := 0 113 return func(job *Job) (bool, error) { 114 count++ 115 return count%2 == 0, nil 116 } 117 }(), 118 expectedJobs: 2, 119 }, 120 } 121 122 m := NewBucketCompactorMetrics(promauto.With(nil).NewCounter(prometheus.CounterOpts{}), nil) 123 for testName, testCase := range tests { 124 t.Run(testName, func(t *testing.T) { 125 bc, err := NewBucketCompactor(log.NewNopLogger(), nil, nil, nil, nil, "", nil, 2, testCase.ownJob, nil, 0, 4, m) 126 require.NoError(t, err) 127 128 res, err := bc.filterOwnJobs(jobsFn()) 129 130 require.NoError(t, err) 131 assert.Len(t, res, testCase.expectedJobs) 132 }) 133 } 134 } 135 136 func TestBlockMaxTimeDeltas(t *testing.T) { 137 j1 := NewJob("user", "key1", labels.EmptyLabels(), 0, false, 0, 0, "") 138 require.NoError(t, j1.AppendMeta(&block.Meta{ 139 MinTime: 1500002700159, 140 MaxTime: 1500002800159, 141 })) 142 143 j2 := NewJob("user", "key2", labels.EmptyLabels(), 0, false, 0, 0, "") 144 require.NoError(t, j2.AppendMeta(&block.Meta{ 145 MinTime: 1500002600159, 146 MaxTime: 1500002700159, 147 })) 148 require.NoError(t, j2.AppendMeta(&block.Meta{ 149 MinTime: 1500002700159, 150 MaxTime: 1500002800159, 151 })) 152 153 metrics := NewBucketCompactorMetrics(promauto.With(nil).NewCounter(prometheus.CounterOpts{}), nil) 154 now := time.UnixMilli(1500002900159) 155 bc, err := NewBucketCompactor(log.NewNopLogger(), nil, nil, nil, nil, "", nil, 2, nil, nil, 0, 4, metrics) 156 require.NoError(t, err) 157 158 deltas := bc.blockMaxTimeDeltas(now, []*Job{j1, j2}) 159 assert.Equal(t, []float64{100, 200, 100}, deltas) 160 } 161 162 func TestNoCompactionMarkFilter(t *testing.T) { 163 ctx := context.Background() 164 165 // Use bucket with global markers to make sure that our custom filters work correctly. 166 bkt := block.BucketWithGlobalMarkers(phlareobj.NewBucket(objstore.NewInMemBucket())) 167 168 block1 := ulid.MustParse("01DTVP434PA9VFXSW2JK000001") // No mark file. 169 block2 := ulid.MustParse("01DTVP434PA9VFXSW2JK000002") // Marked for no-compaction 170 block3 := ulid.MustParse("01DTVP434PA9VFXSW2JK000003") // Has wrong version of marker file. 171 block4 := ulid.MustParse("01DTVP434PA9VFXSW2JK000004") // Has invalid marker file. 172 block5 := ulid.MustParse("01DTVP434PA9VFXSW2JK000005") // No mark file. 173 174 for name, testFn := range map[string]func(t *testing.T, synced block.GaugeVec){ 175 "filter with no deletion of blocks marked for no-compaction": func(t *testing.T, synced block.GaugeVec) { 176 metas := map[ulid.ULID]*block.Meta{ 177 block1: blockMeta(block1.String(), 100, 200, nil), 178 block2: blockMeta(block2.String(), 200, 300, nil), // Has no-compaction marker. 179 block4: blockMeta(block4.String(), 400, 500, nil), // Invalid marker is still a marker, and block will be in NoCompactMarkedBlocks. 180 block5: blockMeta(block5.String(), 500, 600, nil), 181 } 182 183 f := NewNoCompactionMarkFilter(phlareobj.NewBucket(objstore.WithNoopInstr(bkt)), false) 184 require.NoError(t, f.Filter(ctx, metas, synced)) 185 186 require.Contains(t, metas, block1) 187 require.Contains(t, metas, block2) 188 require.Contains(t, metas, block4) 189 require.Contains(t, metas, block5) 190 191 require.Len(t, f.NoCompactMarkedBlocks(), 2) 192 require.Contains(t, f.NoCompactMarkedBlocks(), block2, block4) 193 194 assert.Equal(t, 2.0, testutil.ToFloat64(synced.WithLabelValues(block.MarkedForNoCompactionMeta))) 195 }, 196 "filter with deletion enabled": func(t *testing.T, synced block.GaugeVec) { 197 metas := map[ulid.ULID]*block.Meta{ 198 block1: blockMeta(block1.String(), 100, 200, nil), 199 block2: blockMeta(block2.String(), 300, 300, nil), // Has no-compaction marker. 200 block4: blockMeta(block4.String(), 400, 500, nil), // Marker with invalid syntax is ignored. 201 block5: blockMeta(block5.String(), 500, 600, nil), 202 } 203 204 f := NewNoCompactionMarkFilter(phlareobj.NewBucket(objstore.WithNoopInstr(bkt)), true) 205 require.NoError(t, f.Filter(ctx, metas, synced)) 206 207 require.Contains(t, metas, block1) 208 require.NotContains(t, metas, block2) // block2 was removed from metas. 209 require.NotContains(t, metas, block4) // block4 has invalid marker, but we don't check for marker content. 210 require.Contains(t, metas, block5) 211 212 require.Len(t, f.NoCompactMarkedBlocks(), 2) 213 require.Contains(t, f.NoCompactMarkedBlocks(), block2) 214 require.Contains(t, f.NoCompactMarkedBlocks(), block4) 215 216 assert.Equal(t, 2.0, testutil.ToFloat64(synced.WithLabelValues(block.MarkedForNoCompactionMeta))) 217 }, 218 "filter with deletion enabled, but canceled context": func(t *testing.T, synced block.GaugeVec) { 219 metas := map[ulid.ULID]*block.Meta{ 220 block1: blockMeta(block1.String(), 100, 200, nil), 221 block2: blockMeta(block2.String(), 200, 300, nil), 222 block3: blockMeta(block3.String(), 300, 400, nil), 223 block4: blockMeta(block4.String(), 400, 500, nil), 224 block5: blockMeta(block5.String(), 500, 600, nil), 225 } 226 227 canceledCtx, cancel := context.WithCancel(context.Background()) 228 cancel() 229 230 f := NewNoCompactionMarkFilter(phlareobj.NewBucket(objstore.WithNoopInstr(bkt)), true) 231 require.Error(t, f.Filter(canceledCtx, metas, synced)) 232 233 require.Contains(t, metas, block1) 234 require.Contains(t, metas, block2) 235 require.Contains(t, metas, block3) 236 require.Contains(t, metas, block4) 237 require.Contains(t, metas, block5) 238 239 require.Empty(t, f.NoCompactMarkedBlocks()) 240 assert.Equal(t, 0.0, testutil.ToFloat64(synced.WithLabelValues(block.MarkedForNoCompactionMeta))) 241 }, 242 "filtering block with wrong marker version": func(t *testing.T, synced block.GaugeVec) { 243 metas := map[ulid.ULID]*block.Meta{ 244 block3: blockMeta(block3.String(), 300, 300, nil), // Has compaction marker with invalid version, but Filter doesn't check for that. 245 } 246 247 f := NewNoCompactionMarkFilter(phlareobj.NewBucket(objstore.WithNoopInstr(bkt)), true) 248 err := f.Filter(ctx, metas, synced) 249 require.NoError(t, err) 250 require.Empty(t, metas) 251 252 assert.Equal(t, 1.0, testutil.ToFloat64(synced.WithLabelValues(block.MarkedForNoCompactionMeta))) 253 }, 254 } { 255 t.Run(name, func(t *testing.T) { 256 // Block 2 is marked for no-compaction. 257 require.NoError(t, block.MarkForNoCompact(ctx, log.NewNopLogger(), bkt, block2, block.OutOfOrderChunksNoCompactReason, "details...", promauto.With(nil).NewCounter(prometheus.CounterOpts{}))) 258 // Block 3 has marker with invalid version 259 require.NoError(t, bkt.Upload(ctx, block3.String()+"/no-compact-mark.json", strings.NewReader(`{"id":"`+block3.String()+`","version":100,"details":"details","no_compact_time":1637757932,"reason":"reason"}`))) 260 // Block 4 has marker with invalid JSON syntax 261 require.NoError(t, bkt.Upload(ctx, block4.String()+"/no-compact-mark.json", strings.NewReader(`invalid json`))) 262 263 synced := extprom.NewTxGaugeVec(nil, prometheus.GaugeOpts{Name: "synced", Help: "Number of block metadata synced"}, 264 []string{"state"}, []string{block.MarkedForNoCompactionMeta}, 265 ) 266 267 testFn(t, synced) 268 }) 269 } 270 } 271 272 func TestCompactedBlocksTimeRangeVerification(t *testing.T) { 273 const ( 274 sourceMinTime = 1000 275 sourceMaxTime = 2500 276 ) 277 278 tests := map[string]struct { 279 compactedBlockMinTime int64 280 compactedBlockMaxTime int64 281 shouldErr bool 282 expectedErrMsg string 283 }{ 284 "should pass with minTime and maxTime matching the source blocks": { 285 compactedBlockMinTime: sourceMinTime, 286 compactedBlockMaxTime: sourceMaxTime, 287 shouldErr: false, 288 }, 289 "should fail with compacted block minTime < source minTime": { 290 compactedBlockMinTime: sourceMinTime - 500, 291 compactedBlockMaxTime: sourceMaxTime, 292 shouldErr: true, 293 expectedErrMsg: fmt.Sprintf("compacted block minTime %d is before source minTime %d", sourceMinTime-500, sourceMinTime), 294 }, 295 "should fail with compacted block maxTime > source maxTime": { 296 compactedBlockMinTime: sourceMinTime, 297 compactedBlockMaxTime: sourceMaxTime + 500, 298 shouldErr: true, 299 expectedErrMsg: fmt.Sprintf("compacted block maxTime %d is after source maxTime %d", sourceMaxTime+500, sourceMaxTime), 300 }, 301 "should fail due to minTime and maxTime not found": { 302 compactedBlockMinTime: sourceMinTime + 250, 303 compactedBlockMaxTime: sourceMaxTime - 250, 304 shouldErr: true, 305 expectedErrMsg: fmt.Sprintf("compacted block(s) do not contain minTime %d and maxTime %d from the source blocks", sourceMinTime, sourceMaxTime), 306 }, 307 } 308 309 for testName, testData := range tests { 310 testData := testData // Prevent loop variable being captured by func literal 311 t.Run(testName, func(t *testing.T) { 312 t.Parallel() 313 tempDir := t.TempDir() 314 315 bucketClient, _ := objstore_testutil.NewFilesystemBucket(t, context.Background(), tempDir) 316 317 compactedBlock1 := createDBBlock(t, bucketClient, "foo", testData.compactedBlockMinTime, testData.compactedBlockMinTime+500, 10, nil) 318 compactedBlock2 := createDBBlock(t, bucketClient, "foo", testData.compactedBlockMaxTime-500, testData.compactedBlockMaxTime, 10, nil) 319 320 err := verifyCompactedBlocksTimeRanges([]ulid.ULID{compactedBlock1, compactedBlock2}, sourceMinTime, sourceMaxTime, filepath.Join(tempDir, "foo", "phlaredb/")) 321 if testData.shouldErr { 322 require.ErrorContains(t, err, testData.expectedErrMsg) 323 } else { 324 require.NoError(t, err) 325 } 326 }) 327 } 328 }