github.com/grafana/pyroscope@v1.18.0/pkg/metastore/index/index_test.go (about) 1 package index 2 3 import ( 4 "testing" 5 "time" 6 7 "github.com/stretchr/testify/assert" 8 "github.com/stretchr/testify/require" 9 "go.etcd.io/bbolt" 10 11 metastorev1 "github.com/grafana/pyroscope/api/gen/proto/go/metastore/v1" 12 indexstore "github.com/grafana/pyroscope/pkg/metastore/index/store" 13 "github.com/grafana/pyroscope/pkg/test" 14 "github.com/grafana/pyroscope/pkg/util" 15 ) 16 17 func TestIndex_PartitionList(t *testing.T) { 18 const testTenant = "tenant" 19 20 t.Run("new shard", func(t *testing.T) { 21 db := test.BoltDB(t) 22 idx := NewIndex(util.Logger, NewStore(), DefaultConfig) 23 require.NoError(t, db.Update(idx.Init)) 24 25 shardID := uint32(42) 26 blockMeta := &metastorev1.BlockMeta{ 27 Id: test.ULID("2024-09-11T07:00:00.001Z"), 28 Tenant: 1, 29 Shard: shardID, 30 MinTime: test.UnixMilli("2024-09-11T07:00:00.000Z"), 31 MaxTime: test.UnixMilli("2024-09-11T09:00:00.000Z"), 32 CreatedBy: 1, 33 StringTable: []string{"", testTenant, "ingester"}, 34 } 35 36 require.NoError(t, db.Update(func(tx *bbolt.Tx) error { 37 return idx.InsertBlock(tx, blockMeta.CloneVT()) 38 })) 39 40 p := indexstore.NewPartition(test.Time("2024-09-11T07:00:00.001Z"), idx.config.partitionDuration) 41 findPartition(t, db, idx, p) 42 shard := findShard(t, db, p, testTenant, shardID) 43 assert.Equal(t, blockMeta.MinTime, shard.ShardIndex.MinTime) 44 assert.Equal(t, blockMeta.MaxTime, shard.ShardIndex.MaxTime) 45 }) 46 47 t.Run("shard update", func(t *testing.T) { 48 db := test.BoltDB(t) 49 idx := NewIndex(util.Logger, NewStore(), DefaultConfig) 50 51 p := indexstore.NewPartition(test.Time("2024-09-11T06:00:00.000Z"), 6*time.Hour) 52 tenant := testTenant 53 shardID := uint32(1) 54 55 blockMeta := &metastorev1.BlockMeta{ 56 Id: test.ULID("2024-09-11T07:00:00.001Z"), 57 Tenant: 1, 58 Shard: shardID, 59 MinTime: test.UnixMilli("2024-09-11T07:00:00.000Z"), 60 MaxTime: test.UnixMilli("2024-09-11T09:00:00.000Z"), 61 CreatedBy: 1, 62 StringTable: []string{"", tenant, "ingester"}, 63 } 64 65 require.NoError(t, db.Update(idx.Init)) 66 require.NoError(t, db.Update(func(tx *bbolt.Tx) error { 67 return idx.InsertBlock(tx, blockMeta.CloneVT()) 68 })) 69 70 idx = NewIndex(util.Logger, NewStore(), DefaultConfig) 71 require.NoError(t, db.View(idx.Restore)) 72 73 findPartition(t, db, idx, p) 74 shard := findShard(t, db, p, tenant, shardID) 75 assert.Equal(t, blockMeta.MinTime, shard.ShardIndex.MinTime) 76 assert.Equal(t, blockMeta.MaxTime, shard.ShardIndex.MaxTime) 77 78 newBlockMeta := &metastorev1.BlockMeta{ 79 Id: test.ULID("2024-09-11T08:00:00.001Z"), 80 Tenant: 1, 81 Shard: shardID, 82 MinTime: test.UnixMilli("2024-09-11T06:30:00.000Z"), 83 MaxTime: test.UnixMilli("2024-09-11T10:00:00.000Z"), 84 CreatedBy: 1, 85 StringTable: []string{"", tenant, "ingester"}, 86 } 87 88 require.NoError(t, db.Update(func(tx *bbolt.Tx) error { 89 return idx.InsertBlock(tx, newBlockMeta.CloneVT()) 90 })) 91 92 updated := findShard(t, db, p, tenant, shardID) 93 assert.Equal(t, newBlockMeta.MinTime, updated.ShardIndex.MinTime) 94 assert.Equal(t, newBlockMeta.MaxTime, updated.ShardIndex.MaxTime) 95 96 require.NoError(t, db.View(func(tx *bbolt.Tx) error { 97 s, err := idx.shards.getForRead(tx, p, tenant, shardID) 98 if err != nil { 99 return err 100 } 101 require.NotNil(t, s) 102 assert.Equal(t, s.ShardIndex.MinTime, updated.ShardIndex.MinTime) 103 assert.Equal(t, s.ShardIndex.MaxTime, updated.ShardIndex.MaxTime) 104 return nil 105 })) 106 }) 107 } 108 109 func findPartition(t *testing.T, db *bbolt.DB, idx *Index, k indexstore.Partition) { 110 var p indexstore.Partition 111 require.NoError(t, db.View(func(tx *bbolt.Tx) error { 112 for partition := range idx.Partitions(tx) { 113 if partition.Equal(k) { 114 p = partition 115 break 116 } 117 } 118 return nil 119 })) 120 assert.NotZero(t, p) 121 } 122 123 func findShard(t *testing.T, db *bbolt.DB, partition indexstore.Partition, tenant string, shardID uint32) indexstore.Shard { 124 var s indexstore.Shard 125 require.NoError(t, db.View(func(tx *bbolt.Tx) error { 126 for shard := range partition.Query(tx).Shards(tenant) { 127 if shard.Shard == shardID { 128 s = shard 129 break 130 } 131 } 132 return nil 133 })) 134 assert.NotZero(t, s) 135 return s 136 } 137 138 func TestIndex_RestoreTimeBasedLoading(t *testing.T) { 139 db := test.BoltDB(t) 140 config := DefaultConfig 141 config.queryLookaroundPeriod = time.Hour 142 143 idx := NewIndex(util.Logger, NewStore(), config) 144 require.NoError(t, db.Update(idx.Init)) 145 146 now := time.Now() 147 const testTenant = "test-tenant" 148 149 t1 := now.Add(-30 * time.Minute) 150 t2 := now.Add(-25 * time.Hour) 151 t3 := now.Add(25 * time.Hour) 152 153 blocks := []*metastorev1.BlockMeta{ 154 { 155 Id: test.ULID(t1.Format(time.RFC3339)), 156 Tenant: 1, 157 Shard: 1, 158 MinTime: t1.UnixMilli(), 159 MaxTime: now.Add(time.Hour).UnixMilli(), 160 StringTable: []string{"", testTenant}, 161 }, 162 163 { 164 Id: test.ULID(t2.Format(time.RFC3339)), 165 Tenant: 1, 166 Shard: 2, 167 MinTime: t2.UnixMilli(), 168 MaxTime: t2.Add(time.Hour).UnixMilli(), 169 StringTable: []string{"", testTenant}, 170 }, 171 { 172 Id: test.ULID(t3.Format(time.RFC3339)), 173 Tenant: 1, 174 Shard: 3, 175 MinTime: t3.UnixMilli(), 176 MaxTime: t3.Add(time.Hour).UnixMilli(), 177 StringTable: []string{"", testTenant}, 178 }, 179 } 180 181 for _, block := range blocks { 182 require.NoError(t, db.Update(func(tx *bbolt.Tx) error { 183 return idx.InsertBlock(tx, block) 184 })) 185 } 186 187 idx = NewIndex(util.Logger, NewStore(), config) 188 require.NoError(t, db.Update(idx.Restore)) 189 require.NoError(t, db.View(func(tx *bbolt.Tx) error { 190 s, _ := idx.shards.cache.Get(shardCacheKey{indexstore.NewPartition(t1, config.partitionDuration), testTenant, 1}) 191 assert.NotNil(t, s) 192 s, _ = idx.shards.cache.Get(shardCacheKey{indexstore.NewPartition(t2, config.partitionDuration), testTenant, 2}) 193 assert.Nil(t, s) 194 s, _ = idx.shards.cache.Get(shardCacheKey{indexstore.NewPartition(t3, config.partitionDuration), testTenant, 3}) 195 assert.Nil(t, s) 196 return nil 197 })) 198 } 199 200 func TestShardIterator_TimeFiltering(t *testing.T) { 201 db := test.BoltDB(t) 202 config := DefaultConfig 203 config.queryLookaroundPeriod = 0 204 idx := NewIndex(util.Logger, NewStore(), config) 205 require.NoError(t, db.Update(idx.Init)) 206 207 tenant := "test" 208 blocks := []*metastorev1.BlockMeta{ 209 { 210 Id: test.ULID("2024-01-01T10:00:00.000Z"), 211 Tenant: 1, 212 Shard: 1, 213 MinTime: test.UnixMilli("2024-01-01T10:00:00.000Z"), 214 MaxTime: test.UnixMilli("2024-01-01T11:00:00.000Z"), 215 StringTable: []string{"", tenant}, 216 }, 217 { 218 Id: test.ULID("2024-01-01T12:00:00.000Z"), 219 Tenant: 1, 220 Shard: 2, 221 MinTime: test.UnixMilli("2024-01-01T12:00:00.000Z"), 222 MaxTime: test.UnixMilli("2024-01-01T13:00:00.000Z"), 223 StringTable: []string{"", tenant}, 224 }, 225 } 226 227 for _, block := range blocks { 228 require.NoError(t, db.Update(func(tx *bbolt.Tx) error { return idx.InsertBlock(tx, block) })) 229 } 230 231 testCases := []struct { 232 name string 233 startTime string 234 endTime string 235 expected []uint32 236 }{ 237 {"overlap first", "2024-01-01T10:30:00.000Z", "2024-01-01T10:45:00.000Z", []uint32{1}}, 238 {"overlap second", "2024-01-01T12:30:00.000Z", "2024-01-01T12:45:00.000Z", []uint32{2}}, 239 {"no overlap", "2024-01-01T15:00:00.000Z", "2024-01-01T16:00:00.000Z", []uint32{}}, 240 } 241 242 for _, tc := range testCases { 243 t.Run(tc.name, func(t *testing.T) { 244 var loaded []uint32 245 require.NoError(t, db.View(func(tx *bbolt.Tx) error { 246 iter := newShardIterator(tx, idx, test.Time(tc.startTime), test.Time(tc.endTime), tenant) 247 for iter.Next() { 248 loaded = append(loaded, iter.At().Shard) 249 } 250 return iter.Err() 251 })) 252 assert.ElementsMatch(t, tc.expected, loaded) 253 }) 254 } 255 } 256 257 func TestIndex_DeleteShard(t *testing.T) { 258 const baseTime = "2024-01-01T10:00:00.000Z" 259 260 createBlock := func(tenant string, shard uint32, offset time.Duration) *metastorev1.BlockMeta { 261 ts := test.Time(baseTime).Add(offset) 262 return &metastorev1.BlockMeta{ 263 Id: test.ULID(ts.Format(time.RFC3339)), 264 Tenant: 1, 265 Shard: shard, 266 MinTime: ts.UnixMilli(), 267 MaxTime: ts.Add(time.Hour).UnixMilli(), 268 StringTable: []string{"", tenant}, 269 } 270 } 271 272 insertBlocks := func(t *testing.T, db *bbolt.DB, idx *Index, blocks []*metastorev1.BlockMeta) { 273 for _, block := range blocks { 274 require.NoError(t, db.Update(func(tx *bbolt.Tx) error { 275 return idx.InsertBlock(tx, block) 276 })) 277 } 278 } 279 280 assertShard := func(t *testing.T, db *bbolt.DB, p indexstore.Partition, tenant string, shard uint32, exists bool) { 281 require.NoError(t, db.View(func(tx *bbolt.Tx) error { 282 q := p.Query(tx) 283 if q == nil && !exists { 284 return nil 285 } 286 require.NotNil(t, q) 287 288 var found bool 289 for s := range q.Shards(tenant) { 290 if s.Shard == shard { 291 found = true 292 break 293 } 294 } 295 296 assert.Equal(t, exists, found) 297 return nil 298 })) 299 } 300 301 t.Run("basic deletion", func(t *testing.T) { 302 db := test.BoltDB(t) 303 idx := NewIndex(util.Logger, NewStore(), DefaultConfig) 304 require.NoError(t, db.Update(idx.Init)) 305 306 tenant := "test-tenant" 307 blocks := []*metastorev1.BlockMeta{ 308 createBlock(tenant, 1, 0), 309 createBlock(tenant, 2, 30*time.Minute), 310 } 311 312 insertBlocks(t, db, idx, blocks) 313 p := indexstore.NewPartition(test.Time(baseTime), idx.config.partitionDuration) 314 315 assertShard(t, db, p, tenant, 1, true) 316 assertShard(t, db, p, tenant, 2, true) 317 318 require.NoError(t, db.Update(func(tx *bbolt.Tx) error { 319 return idx.DeleteShard(tx, p, tenant, 1) 320 })) 321 322 assertShard(t, db, p, tenant, 1, false) 323 assertShard(t, db, p, tenant, 2, true) 324 325 k := shardCacheKey{partition: p, tenant: tenant, shard: 1} 326 cached, found := idx.shards.cache.Get(k) 327 assert.False(t, found) 328 assert.Nil(t, cached) 329 }) 330 331 t.Run("delete non-existent shard", func(t *testing.T) { 332 db := test.BoltDB(t) 333 idx := NewIndex(util.Logger, NewStore(), DefaultConfig) 334 require.NoError(t, db.Update(idx.Init)) 335 336 p := indexstore.NewPartition(test.Time(baseTime), idx.config.partitionDuration) 337 err := db.Update(func(tx *bbolt.Tx) error { 338 return idx.DeleteShard(tx, p, "non-existent", 999) 339 }) 340 assert.NoError(t, err) 341 }) 342 343 t.Run("multiple tenants isolation", func(t *testing.T) { 344 db := test.BoltDB(t) 345 idx := NewIndex(util.Logger, NewStore(), DefaultConfig) 346 require.NoError(t, db.Update(idx.Init)) 347 348 tenant1, tenant2 := "tenant-1", "tenant-2" 349 blocks := []*metastorev1.BlockMeta{ 350 createBlock(tenant1, 1, 0), 351 createBlock(tenant2, 1, 30*time.Minute), 352 } 353 354 insertBlocks(t, db, idx, blocks) 355 p := indexstore.NewPartition(test.Time(baseTime), idx.config.partitionDuration) 356 357 assertShard(t, db, p, tenant1, 1, true) 358 assertShard(t, db, p, tenant2, 1, true) 359 360 require.NoError(t, db.Update(func(tx *bbolt.Tx) error { 361 return idx.DeleteShard(tx, p, tenant1, 1) 362 })) 363 364 assertShard(t, db, p, tenant1, 1, false) 365 assertShard(t, db, p, tenant2, 1, true) 366 }) 367 } 368 369 func TestIndex_GetTenantStats(t *testing.T) { 370 const ( 371 existingTenant = "tenant" 372 ) 373 var ( 374 minTime = test.UnixMilli("2024-09-11T07:00:00.000Z") 375 maxTime = test.UnixMilli("2024-09-11T09:00:00.000Z") 376 ) 377 378 db := test.BoltDB(t) 379 idx := NewIndex(util.Logger, NewStore(), DefaultConfig) 380 require.NoError(t, db.Update(idx.Init)) 381 382 shardID := uint32(42) 383 blockMeta := &metastorev1.BlockMeta{ 384 Id: test.ULID("2024-09-11T07:00:00.001Z"), 385 Tenant: 1, 386 Shard: shardID, 387 MinTime: minTime, 388 MaxTime: maxTime, 389 CreatedBy: 1, 390 StringTable: []string{"", existingTenant, "ingester"}, 391 } 392 393 require.NoError(t, db.Update(func(tx *bbolt.Tx) error { 394 return idx.InsertBlock(tx, blockMeta.CloneVT()) 395 })) 396 397 require.NoError(t, db.View(func(tx *bbolt.Tx) error { 398 stats := idx.GetTenantStats(tx, existingTenant) 399 assert.Equal(t, true, stats.GetDataIngested()) 400 assert.Equal(t, minTime, stats.GetOldestProfileTime()) 401 assert.Equal(t, maxTime, stats.GetNewestProfileTime()) 402 return nil 403 })) 404 405 require.NoError(t, db.View(func(tx *bbolt.Tx) error { 406 stats := idx.GetTenantStats(tx, "tenant-never-sent") 407 assert.Equal(t, false, stats.GetDataIngested()) 408 assert.Equal(t, int64(0), stats.GetOldestProfileTime()) 409 assert.Equal(t, int64(0), stats.GetNewestProfileTime()) 410 return nil 411 })) 412 413 }