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  }