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 }