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  }