github.com/grafana/pyroscope@v1.18.0/pkg/phlaredb/bucketindex/updater_test.go (about)

     1  // SPDX-License-Identifier: AGPL-3.0-only
     2  // Provenance-includes-location: https://github.com/cortexproject/cortex/blob/master/pkg/storage/tsdb/bucketindex/updater_test.go
     3  // Provenance-includes-license: Apache-2.0
     4  // Provenance-includes-copyright: The Cortex Authors.
     5  
     6  package bucketindex
     7  
     8  import (
     9  	"bytes"
    10  	"context"
    11  	"path"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/go-kit/log"
    16  	"github.com/oklog/ulid/v2"
    17  	"github.com/pkg/errors"
    18  	"github.com/stretchr/testify/assert"
    19  	"github.com/stretchr/testify/require"
    20  
    21  	"github.com/grafana/pyroscope/pkg/objstore"
    22  	objstore_testutil "github.com/grafana/pyroscope/pkg/objstore/testutil"
    23  	"github.com/grafana/pyroscope/pkg/phlaredb/block"
    24  	block_testutil "github.com/grafana/pyroscope/pkg/phlaredb/block/testutil"
    25  	"github.com/grafana/pyroscope/pkg/phlaredb/sharding"
    26  )
    27  
    28  func TestUpdater_UpdateIndex(t *testing.T) {
    29  	const userID = "user-1"
    30  
    31  	ctx := context.Background()
    32  	logger := log.NewNopLogger()
    33  	bkt, _ := objstore_testutil.NewFilesystemBucket(t, ctx, t.TempDir())
    34  
    35  	// Generate the initial index.
    36  	bkt = block.BucketWithGlobalMarkers(bkt)
    37  	block1 := block_testutil.MockStorageBlockWithExtLabels(t, bkt, userID, 10, 20, nil)
    38  	block_testutil.MockNoCompactMark(t, bkt, userID, block1) // no-compact mark is ignored by bucket index updater.
    39  	block2 := block_testutil.MockStorageBlockWithExtLabels(t, bkt, userID, 20, 30, map[string]string{sharding.CompactorShardIDLabel: "1_of_5"})
    40  	block2Mark := block_testutil.MockStorageDeletionMark(t, bkt, userID, block2)
    41  
    42  	w := NewUpdater(bkt, userID, nil, logger)
    43  	returnedIdx, _, err := w.UpdateIndex(ctx, nil)
    44  	require.NoError(t, err)
    45  	assertBucketIndexEqual(t, returnedIdx, bkt, userID,
    46  		[]block.Meta{block1, block2},
    47  		[]*block.DeletionMark{block2Mark})
    48  
    49  	// Create new blocks, and update the index.
    50  	block3 := block_testutil.MockStorageBlockWithExtLabels(t, bkt, userID, 30, 40, map[string]string{"aaa": "bbb"})
    51  	block4 := block_testutil.MockStorageBlockWithExtLabels(t, bkt, userID, 40, 50, map[string]string{sharding.CompactorShardIDLabel: "2_of_5"})
    52  	block4Mark := block_testutil.MockStorageDeletionMark(t, bkt, userID, block4)
    53  
    54  	returnedIdx, _, err = w.UpdateIndex(ctx, returnedIdx)
    55  	require.NoError(t, err)
    56  	assertBucketIndexEqual(t, returnedIdx, bkt, userID,
    57  		[]block.Meta{block1, block2, block3, block4},
    58  		[]*block.DeletionMark{block2Mark, block4Mark})
    59  
    60  	// Hard delete a block and update the index.
    61  	require.NoError(t, block.Delete(ctx, log.NewNopLogger(), objstore.NewTenantBucketClient(userID, bkt, nil), block2.ULID))
    62  
    63  	returnedIdx, _, err = w.UpdateIndex(ctx, returnedIdx)
    64  	require.NoError(t, err)
    65  	assertBucketIndexEqual(t, returnedIdx, bkt, userID,
    66  		[]block.Meta{block1, block3, block4},
    67  		[]*block.DeletionMark{block4Mark})
    68  }
    69  
    70  func TestUpdater_UpdateIndex_ShouldSkipPartialBlocks(t *testing.T) {
    71  	const userID = "user-1"
    72  
    73  	ctx := context.Background()
    74  	logger := log.NewNopLogger()
    75  
    76  	bkt, _ := objstore_testutil.NewFilesystemBucket(t, ctx, t.TempDir())
    77  
    78  	// Mock some blocks in the storage.
    79  	bkt = block.BucketWithGlobalMarkers(bkt)
    80  	block1 := block_testutil.MockStorageBlockWithExtLabels(t, bkt, userID, 10, 20, map[string]string{"hello": "world"})
    81  	block2 := block_testutil.MockStorageBlockWithExtLabels(t, bkt, userID, 20, 30, map[string]string{sharding.CompactorShardIDLabel: "3_of_10"})
    82  	block3 := block_testutil.MockStorageBlockWithExtLabels(t, bkt, userID, 30, 40, nil)
    83  	block2Mark := block_testutil.MockStorageDeletionMark(t, bkt, userID, block2)
    84  
    85  	// No compact marks are ignored by bucket index.
    86  	block_testutil.MockNoCompactMark(t, bkt, userID, block3)
    87  
    88  	// Delete a block's meta.json to simulate a partial block.
    89  	require.NoError(t, bkt.Delete(ctx, path.Join(userID, "phlaredb/", block3.ULID.String(), block.MetaFilename)))
    90  
    91  	w := NewUpdater(bkt, userID, nil, logger)
    92  	idx, partials, err := w.UpdateIndex(ctx, nil)
    93  	require.NoError(t, err)
    94  	assertBucketIndexEqual(t, idx, bkt, userID,
    95  		[]block.Meta{block1, block2},
    96  		[]*block.DeletionMark{block2Mark})
    97  
    98  	assert.Len(t, partials, 1)
    99  	assert.True(t, errors.Is(partials[block3.ULID], ErrBlockMetaNotFound))
   100  }
   101  
   102  func TestUpdater_UpdateIndex_ShouldSkipBlocksWithCorruptedMeta(t *testing.T) {
   103  	const userID = "user-1"
   104  
   105  	ctx := context.Background()
   106  	logger := log.NewNopLogger()
   107  
   108  	bkt, _ := objstore_testutil.NewFilesystemBucket(t, ctx, t.TempDir())
   109  
   110  	// Mock some blocks in the storage.
   111  	bkt = block.BucketWithGlobalMarkers(bkt)
   112  	block1 := block_testutil.MockStorageBlockWithExtLabels(t, bkt, userID, 10, 20, nil)
   113  	block2 := block_testutil.MockStorageBlockWithExtLabels(t, bkt, userID, 20, 30, map[string]string{sharding.CompactorShardIDLabel: "55_of_64"})
   114  	block3 := block_testutil.MockStorageBlockWithExtLabels(t, bkt, userID, 30, 40, nil)
   115  	block2Mark := block_testutil.MockStorageDeletionMark(t, bkt, userID, block2)
   116  
   117  	// Overwrite a block's meta.json with invalid data.
   118  	require.NoError(t, bkt.Upload(ctx, path.Join(userID, "phlaredb/", block3.ULID.String(), block.MetaFilename), bytes.NewReader([]byte("invalid!}"))))
   119  
   120  	w := NewUpdater(bkt, userID, nil, logger)
   121  	idx, partials, err := w.UpdateIndex(ctx, nil)
   122  	require.NoError(t, err)
   123  	assertBucketIndexEqual(t, idx, bkt, userID,
   124  		[]block.Meta{block1, block2},
   125  		[]*block.DeletionMark{block2Mark})
   126  
   127  	assert.Len(t, partials, 1)
   128  	assert.True(t, errors.Is(partials[block3.ULID], ErrBlockMetaCorrupted))
   129  }
   130  
   131  func TestUpdater_UpdateIndex_ShouldSkipCorruptedDeletionMarks(t *testing.T) {
   132  	const userID = "user-1"
   133  
   134  	ctx := context.Background()
   135  	logger := log.NewNopLogger()
   136  
   137  	bkt, _ := objstore_testutil.NewFilesystemBucket(t, ctx, t.TempDir())
   138  
   139  	// Mock some blocks in the storage.
   140  	bkt = block.BucketWithGlobalMarkers(bkt)
   141  	block1 := block_testutil.MockStorageBlockWithExtLabels(t, bkt, userID, 10, 20, nil)
   142  	block2 := block_testutil.MockStorageBlockWithExtLabels(t, bkt, userID, 20, 30, nil)
   143  	block3 := block_testutil.MockStorageBlockWithExtLabels(t, bkt, userID, 30, 40, map[string]string{sharding.CompactorShardIDLabel: "2_of_7"})
   144  	block2Mark := block_testutil.MockStorageDeletionMark(t, bkt, userID, block2)
   145  
   146  	// Overwrite a block's deletion-mark.json with invalid data.
   147  	require.NoError(t, bkt.Upload(ctx, path.Join(userID, "phlaredb/", block2Mark.ID.String(), block.DeletionMarkFilename), bytes.NewReader([]byte("invalid!}"))))
   148  
   149  	w := NewUpdater(bkt, userID, nil, logger)
   150  	idx, partials, err := w.UpdateIndex(ctx, nil)
   151  	require.NoError(t, err)
   152  	assertBucketIndexEqual(t, idx, bkt, userID,
   153  		[]block.Meta{block1, block2, block3},
   154  		[]*block.DeletionMark{})
   155  	assert.Empty(t, partials)
   156  }
   157  
   158  func TestUpdater_UpdateIndex_NoTenantInTheBucket(t *testing.T) {
   159  	const userID = "user-1"
   160  
   161  	ctx := context.Background()
   162  	bkt, _ := objstore_testutil.NewFilesystemBucket(t, ctx, t.TempDir())
   163  
   164  	for _, oldIdx := range []*Index{nil, {}} {
   165  		w := NewUpdater(bkt, userID, nil, log.NewNopLogger())
   166  		idx, partials, err := w.UpdateIndex(ctx, oldIdx)
   167  
   168  		require.NoError(t, err)
   169  		assert.Equal(t, IndexVersion3, idx.Version)
   170  		assert.InDelta(t, time.Now().Unix(), idx.UpdatedAt, 2)
   171  		assert.Len(t, idx.Blocks, 0)
   172  		assert.Len(t, idx.BlockDeletionMarks, 0)
   173  		assert.Empty(t, partials)
   174  	}
   175  }
   176  
   177  func TestUpdater_UpdateIndexFromVersion1ToVersion2(t *testing.T) {
   178  	const userID = "user-2"
   179  
   180  	ctx := context.Background()
   181  	logger := log.NewNopLogger()
   182  
   183  	bkt, _ := objstore_testutil.NewFilesystemBucket(t, ctx, t.TempDir())
   184  
   185  	// Generate blocks with compactor shard ID.
   186  	bkt = block.BucketWithGlobalMarkers(bkt)
   187  	block1 := block_testutil.MockStorageBlockWithExtLabels(t, bkt, userID, 10, 20, map[string]string{sharding.CompactorShardIDLabel: "1_of_4"})
   188  	block2 := block_testutil.MockStorageBlockWithExtLabels(t, bkt, userID, 20, 30, map[string]string{sharding.CompactorShardIDLabel: "3_of_4"})
   189  
   190  	block1WithoutCompactorShardID := block1
   191  	block1WithoutCompactorShardID.Labels = nil
   192  
   193  	block2WithoutCompactorShardID := block2
   194  	block2WithoutCompactorShardID.Labels = nil
   195  
   196  	// Double check that original block1 and block2 still have compactor shards set.
   197  	require.Equal(t, "1_of_4", block1.Labels[sharding.CompactorShardIDLabel])
   198  	require.Equal(t, "3_of_4", block2.Labels[sharding.CompactorShardIDLabel])
   199  
   200  	// Generate index (this produces V2 index, with compactor shard IDs).
   201  	w := NewUpdater(bkt, userID, nil, logger)
   202  	returnedIdx, _, err := w.UpdateIndex(ctx, nil)
   203  	require.NoError(t, err)
   204  	assertBucketIndexEqual(t, returnedIdx, bkt, userID,
   205  		[]block.Meta{block1, block2},
   206  		[]*block.DeletionMark{})
   207  
   208  	// Now remove Compactor Shard ID from index.
   209  	for _, b := range returnedIdx.Blocks {
   210  		b.CompactorShardID = ""
   211  	}
   212  
   213  	// Try to update existing index. Since we didn't change the version, updater will reuse the index, and not update CompactorShardID field.
   214  	returnedIdx, _, err = w.UpdateIndex(ctx, returnedIdx)
   215  	require.NoError(t, err)
   216  	assertBucketIndexEqual(t, returnedIdx, bkt, userID,
   217  		[]block.Meta{block1WithoutCompactorShardID, block2WithoutCompactorShardID}, // No compactor shards in bucket index.
   218  		[]*block.DeletionMark{})
   219  
   220  	// Now set index version to old version 1. Rerunning updater should rebuild index from scratch.
   221  	returnedIdx.Version = IndexVersion1
   222  
   223  	returnedIdx, _, err = w.UpdateIndex(ctx, returnedIdx)
   224  	require.NoError(t, err)
   225  	assertBucketIndexEqual(t, returnedIdx, bkt, userID,
   226  		[]block.Meta{block1, block2}, // Compactor shards are back.
   227  		[]*block.DeletionMark{})
   228  }
   229  
   230  func TestUpdater_UpdateIndexFromVersion2ToVersion3(t *testing.T) {
   231  	const userID = "user-2"
   232  
   233  	ctx := context.Background()
   234  	logger := log.NewNopLogger()
   235  
   236  	bkt, _ := objstore_testutil.NewFilesystemBucket(t, ctx, t.TempDir())
   237  
   238  	// Generate blocks with compactor shard ID.
   239  	bkt = block.BucketWithGlobalMarkers(bkt)
   240  	block1 := block_testutil.MockStorageBlockWithExtLabels(t, bkt, userID, 10, 20, map[string]string{sharding.CompactorShardIDLabel: "1_of_4"})
   241  	block2 := block_testutil.MockStorageBlockWithExtLabels(t, bkt, userID, 20, 30, map[string]string{sharding.CompactorShardIDLabel: "3_of_4"})
   242  
   243  	block1WithoutCompactionLevel := block1
   244  	block1WithoutCompactionLevel.Compaction.Level = 0
   245  
   246  	block2WithoutCompactionLevel := block2
   247  	block2WithoutCompactionLevel.Compaction.Level = 0
   248  
   249  	// Double check that original block1 and block2 still have compactor shards set.
   250  	require.Equal(t, 1, block1.Compaction.Level)
   251  	require.Equal(t, 1, block2.Compaction.Level)
   252  
   253  	// Generate index (this produces V3 index, with compaction levels).
   254  	w := NewUpdater(bkt, userID, nil, logger)
   255  	returnedIdx, _, err := w.UpdateIndex(ctx, nil)
   256  	require.NoError(t, err)
   257  	assertBucketIndexEqual(t, returnedIdx, bkt, userID,
   258  		[]block.Meta{block1, block2},
   259  		[]*block.DeletionMark{})
   260  
   261  	// Now remove compaction levels from index.
   262  	for _, b := range returnedIdx.Blocks {
   263  		b.CompactionLevel = 0
   264  	}
   265  
   266  	// Try to update existing index. Since we didn't change the version, updater will reuse the index, and not update compaction levels.
   267  	returnedIdx, _, err = w.UpdateIndex(ctx, returnedIdx)
   268  	require.NoError(t, err)
   269  	assertBucketIndexEqual(t, returnedIdx, bkt, userID,
   270  		[]block.Meta{block1WithoutCompactionLevel, block2WithoutCompactionLevel}, // No compactor shards in bucket index.
   271  		[]*block.DeletionMark{})
   272  
   273  	// Now set index version to old version 2. Rerunning updater should rebuild index from scratch.
   274  	returnedIdx.Version = IndexVersion2
   275  
   276  	returnedIdx, _, err = w.UpdateIndex(ctx, returnedIdx)
   277  	require.NoError(t, err)
   278  	assertBucketIndexEqual(t, returnedIdx, bkt, userID,
   279  		[]block.Meta{block1, block2}, // Compaction levels are back.
   280  		[]*block.DeletionMark{})
   281  }
   282  
   283  func getBlockUploadedAt(t testing.TB, bkt objstore.Bucket, userID string, blockID ulid.ULID) int64 {
   284  	metaFile := path.Join(userID, "phlaredb/", blockID.String(), block.MetaFilename)
   285  
   286  	attrs, err := bkt.Attributes(context.Background(), metaFile)
   287  	require.NoError(t, err)
   288  
   289  	return attrs.LastModified.Unix()
   290  }
   291  
   292  func assertBucketIndexEqual(t testing.TB, idx *Index, bkt objstore.Bucket, userID string, expectedBlocks []block.Meta, expectedDeletionMarks []*block.DeletionMark) {
   293  	assert.Equal(t, IndexVersion3, idx.Version)
   294  	assert.InDelta(t, time.Now().Unix(), idx.UpdatedAt, 2)
   295  
   296  	// Build the list of expected block index entries.
   297  	var expectedBlockEntries []*Block
   298  	for _, b := range expectedBlocks {
   299  		expectedBlockEntries = append(expectedBlockEntries, &Block{
   300  			ID:               b.ULID,
   301  			MinTime:          b.MinTime,
   302  			MaxTime:          b.MaxTime,
   303  			UploadedAt:       getBlockUploadedAt(t, bkt, userID, b.ULID),
   304  			CompactorShardID: b.Labels[sharding.CompactorShardIDLabel],
   305  			CompactionLevel:  b.Compaction.Level,
   306  		})
   307  	}
   308  
   309  	assert.ElementsMatch(t, expectedBlockEntries, idx.Blocks)
   310  
   311  	// Build the list of expected block deletion mark index entries.
   312  	var expectedMarkEntries []*BlockDeletionMark
   313  	for _, m := range expectedDeletionMarks {
   314  		expectedMarkEntries = append(expectedMarkEntries, &BlockDeletionMark{
   315  			ID:           m.ID,
   316  			DeletionTime: m.DeletionTime,
   317  		})
   318  	}
   319  
   320  	assert.ElementsMatch(t, expectedMarkEntries, idx.BlockDeletionMarks)
   321  }