github.com/grafana/pyroscope@v1.18.0/pkg/metastore/index/store/index_store_test.go (about)

     1  package store
     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  	"github.com/grafana/pyroscope/pkg/test"
    13  )
    14  
    15  const testTenant = "test-tenant"
    16  
    17  func TestShard_Overlaps(t *testing.T) {
    18  	db := test.BoltDB(t)
    19  
    20  	store := NewIndexStore()
    21  	require.NoError(t, db.Update(func(tx *bbolt.Tx) error {
    22  		return store.CreateBuckets(tx)
    23  	}))
    24  
    25  	partitionKey := NewPartition(test.Time("2024-09-11T06:00:00.000Z"), 6*time.Hour)
    26  	shardID := uint32(1)
    27  
    28  	blockMinTime := test.UnixMilli("2024-09-11T07:00:00.000Z")
    29  	blockMaxTime := test.UnixMilli("2024-09-11T09:00:00.000Z")
    30  
    31  	blockMeta := &metastorev1.BlockMeta{
    32  		FormatVersion: 1,
    33  		Id:            "test-block-123",
    34  		Tenant:        1, // Index 1 in StringTable ("test-tenant")
    35  		Shard:         shardID,
    36  		MinTime:       blockMinTime,
    37  		MaxTime:       blockMaxTime,
    38  		Datasets: []*metastorev1.Dataset{
    39  			{
    40  				Tenant:  1, // Index 1 in StringTable ("test-tenant")
    41  				Name:    3, // Index 3 in StringTable ("test-dataset")
    42  				MinTime: blockMinTime,
    43  				MaxTime: blockMaxTime,
    44  				// Labels format: [count, name_idx, value_idx, name_idx, value_idx, ...]
    45  				// 2 labels: service_name="service", __profile_type__="cpu"
    46  				Labels: []int32{2, 3, 5, 4, 6},
    47  			},
    48  		},
    49  		StringTable: []string{
    50  			"",                 // Index 0
    51  			"test-tenant",      // Index 1
    52  			"test-dataset",     // Index 2
    53  			"service_name",     // Index 3
    54  			"__profile_type__", // Index 4
    55  			"service",          // Index 5
    56  			"cpu",              // Index 6
    57  		},
    58  	}
    59  
    60  	// store a block
    61  	require.NoError(t, db.Update(func(tx *bbolt.Tx) error {
    62  		return NewShard(partitionKey, testTenant, shardID).Store(tx, blockMeta)
    63  	}))
    64  
    65  	require.NoError(t, db.View(func(tx *bbolt.Tx) error {
    66  		shard, err := store.LoadShard(tx, partitionKey, testTenant, shardID)
    67  		require.NoError(t, err)
    68  		require.NotNil(t, shard)
    69  
    70  		assert.Equal(t, blockMinTime, shard.ShardIndex.MinTime)
    71  		assert.Equal(t, blockMaxTime, shard.ShardIndex.MaxTime)
    72  
    73  		testCases := []struct {
    74  			name      string
    75  			startTime time.Time
    76  			endTime   time.Time
    77  			expected  bool
    78  		}{
    79  			{
    80  				name:      "complete overlap - query contains block range",
    81  				startTime: test.Time("2024-09-11T06:30:00.000Z"),
    82  				endTime:   test.Time("2024-09-11T10:00:00.000Z"),
    83  				expected:  true,
    84  			},
    85  			{
    86  				name:      "block contains query range",
    87  				startTime: test.Time("2024-09-11T07:30:00.000Z"),
    88  				endTime:   test.Time("2024-09-11T08:30:00.000Z"),
    89  				expected:  true,
    90  			},
    91  			{
    92  				name:      "partial overlap - start before block, end within block",
    93  				startTime: test.Time("2024-09-11T06:30:00.000Z"),
    94  				endTime:   test.Time("2024-09-11T08:00:00.000Z"),
    95  				expected:  true,
    96  			},
    97  			{
    98  				name:      "partial overlap - start within block, end after block",
    99  				startTime: test.Time("2024-09-11T08:00:00.000Z"),
   100  				endTime:   test.Time("2024-09-11T10:00:00.000Z"),
   101  				expected:  true,
   102  			},
   103  			{
   104  				name:      "edge case - query ends exactly at block start",
   105  				startTime: test.Time("2024-09-11T06:00:00.000Z"),
   106  				endTime:   test.Time("2024-09-11T07:00:00.000Z"),
   107  				expected:  true, // Inclusive boundary check
   108  			},
   109  			{
   110  				name:      "edge case - query starts exactly at block end",
   111  				startTime: test.Time("2024-09-11T09:00:00.000Z"),
   112  				endTime:   test.Time("2024-09-11T10:00:00.000Z"),
   113  				expected:  true, // Inclusive boundary check
   114  			},
   115  			{
   116  				name:      "no overlap - query before block",
   117  				startTime: test.Time("2024-09-11T05:00:00.000Z"),
   118  				endTime:   test.Time("2024-09-11T06:59:58.999Z"),
   119  				expected:  false,
   120  			},
   121  			{
   122  				name:      "no overlap - query after block",
   123  				startTime: test.Time("2024-09-11T09:00:00.001Z"),
   124  				endTime:   test.Time("2024-09-11T11:00:00.000Z"),
   125  				expected:  false,
   126  			},
   127  			{
   128  				name:      "exact match - same start and end times",
   129  				startTime: test.Time("2024-09-11T07:00:00.000Z"),
   130  				endTime:   test.Time("2024-09-11T09:00:00.000Z"),
   131  				expected:  true,
   132  			},
   133  		}
   134  
   135  		for _, tc := range testCases {
   136  			t.Run(tc.name, func(t *testing.T) {
   137  				result := shard.ShardIndex.Overlaps(tc.startTime, tc.endTime)
   138  				assert.Equal(t, tc.expected, result,
   139  					"Overlaps(%v, %v) = %v, expected %v",
   140  					tc.startTime, tc.endTime, result, tc.expected)
   141  			})
   142  		}
   143  
   144  		return nil
   145  	}))
   146  }
   147  
   148  func TestIndexStore_DeleteShard(t *testing.T) {
   149  	createBlock := func(id, tenant string, shard uint32) *metastorev1.BlockMeta {
   150  		return &metastorev1.BlockMeta{
   151  			Id:          id,
   152  			Tenant:      1,
   153  			Shard:       shard,
   154  			MinTime:     test.UnixMilli("2024-01-01T10:00:00.000Z"),
   155  			MaxTime:     test.UnixMilli("2024-01-01T11:00:00.000Z"),
   156  			StringTable: []string{"", tenant},
   157  		}
   158  	}
   159  
   160  	storeBlock := func(t *testing.T, db *bbolt.DB, p Partition, tenant string, shard uint32, block *metastorev1.BlockMeta) {
   161  		require.NoError(t, db.Update(func(tx *bbolt.Tx) error {
   162  			return NewShard(p, tenant, shard).Store(tx, block)
   163  		}))
   164  	}
   165  
   166  	assertShard := func(t *testing.T, db *bbolt.DB, store *IndexStore, p Partition, tenant string, shard uint32, exists bool) {
   167  		require.NoError(t, db.View(func(tx *bbolt.Tx) error {
   168  			s, err := store.LoadShard(tx, p, tenant, shard)
   169  			if exists {
   170  				assert.NoError(t, err)
   171  				assert.NotNil(t, s)
   172  			} else {
   173  				assert.Nil(t, s)
   174  			}
   175  			return nil
   176  		}))
   177  	}
   178  
   179  	assertPartition := func(t *testing.T, db *bbolt.DB, _ *IndexStore, p Partition, exists bool) {
   180  		require.NoError(t, db.View(func(tx *bbolt.Tx) error {
   181  			q := p.Query(tx)
   182  			if exists {
   183  				assert.NotNil(t, q)
   184  			} else {
   185  				assert.Nil(t, q)
   186  			}
   187  			return nil
   188  		}))
   189  	}
   190  
   191  	t.Run("basic deletion", func(t *testing.T) {
   192  		db := test.BoltDB(t)
   193  		store := NewIndexStore()
   194  		require.NoError(t, db.Update(store.CreateBuckets))
   195  
   196  		p := NewPartition(test.Time("2024-01-01T10:00:00.000Z"), 6*time.Hour)
   197  
   198  		storeBlock(t, db, p, testTenant, 1, createBlock("block1", testTenant, 1))
   199  		storeBlock(t, db, p, testTenant, 2, createBlock("block2", testTenant, 2))
   200  
   201  		assertShard(t, db, store, p, testTenant, 1, true)
   202  		assertShard(t, db, store, p, testTenant, 2, true)
   203  
   204  		require.NoError(t, db.Update(func(tx *bbolt.Tx) error {
   205  			return store.DeleteShard(tx, p, testTenant, 1)
   206  		}))
   207  
   208  		assertShard(t, db, store, p, testTenant, 1, false)
   209  		assertShard(t, db, store, p, testTenant, 2, true)
   210  	})
   211  
   212  	t.Run("delete non-existent shard", func(t *testing.T) {
   213  		db := test.BoltDB(t)
   214  		store := NewIndexStore()
   215  		require.NoError(t, db.Update(store.CreateBuckets))
   216  
   217  		p := NewPartition(test.Time("2024-01-01T10:00:00.000Z"), 6*time.Hour)
   218  
   219  		err := db.Update(func(tx *bbolt.Tx) error {
   220  			return store.DeleteShard(tx, p, "non-existent", 999)
   221  		})
   222  		assert.NoError(t, err)
   223  	})
   224  
   225  	t.Run("tenant bucket cleanup", func(t *testing.T) {
   226  		db := test.BoltDB(t)
   227  		store := NewIndexStore()
   228  		require.NoError(t, db.Update(store.CreateBuckets))
   229  
   230  		p := NewPartition(test.Time("2024-01-01T10:00:00.000Z"), 6*time.Hour)
   231  
   232  		storeBlock(t, db, p, testTenant, 1, createBlock("block1", testTenant, 1))
   233  
   234  		assertShard(t, db, store, p, testTenant, 1, true)
   235  		assertPartition(t, db, store, p, true)
   236  
   237  		require.NoError(t, db.Update(func(tx *bbolt.Tx) error {
   238  			return store.DeleteShard(tx, p, testTenant, 1)
   239  		}))
   240  
   241  		assertShard(t, db, store, p, testTenant, 1, false)
   242  		assertPartition(t, db, store, p, false)
   243  	})
   244  
   245  	t.Run("partition bucket cleanup with multiple tenants", func(t *testing.T) {
   246  		db := test.BoltDB(t)
   247  		store := NewIndexStore()
   248  		require.NoError(t, db.Update(store.CreateBuckets))
   249  
   250  		p := NewPartition(test.Time("2024-01-01T10:00:00.000Z"), 6*time.Hour)
   251  		tenant1, tenant2 := "tenant-1", "tenant-2"
   252  
   253  		storeBlock(t, db, p, tenant1, 1, createBlock("block1", tenant1, 1))
   254  		storeBlock(t, db, p, tenant2, 1, createBlock("block2", tenant2, 1))
   255  
   256  		assertShard(t, db, store, p, tenant1, 1, true)
   257  		assertShard(t, db, store, p, tenant2, 1, true)
   258  		assertPartition(t, db, store, p, true)
   259  
   260  		require.NoError(t, db.Update(func(tx *bbolt.Tx) error {
   261  			return store.DeleteShard(tx, p, tenant1, 1)
   262  		}))
   263  
   264  		assertShard(t, db, store, p, tenant1, 1, false)
   265  		assertShard(t, db, store, p, tenant2, 1, true)
   266  		assertPartition(t, db, store, p, true)
   267  
   268  		require.NoError(t, db.Update(func(tx *bbolt.Tx) error {
   269  			return store.DeleteShard(tx, p, tenant2, 1)
   270  		}))
   271  
   272  		assertShard(t, db, store, p, tenant1, 1, false)
   273  		assertShard(t, db, store, p, tenant2, 1, false)
   274  		assertPartition(t, db, store, p, false)
   275  	})
   276  
   277  	t.Run("multiple shards same tenant", func(t *testing.T) {
   278  		db := test.BoltDB(t)
   279  		store := NewIndexStore()
   280  		require.NoError(t, db.Update(store.CreateBuckets))
   281  
   282  		p := NewPartition(test.Time("2024-01-01T10:00:00.000Z"), 6*time.Hour)
   283  
   284  		storeBlock(t, db, p, testTenant, 1, createBlock("block1", testTenant, 1))
   285  		storeBlock(t, db, p, testTenant, 2, createBlock("block2", testTenant, 2))
   286  		storeBlock(t, db, p, testTenant, 3, createBlock("block3", testTenant, 3))
   287  
   288  		require.NoError(t, db.Update(func(tx *bbolt.Tx) error {
   289  			return store.DeleteShard(tx, p, testTenant, 2)
   290  		}))
   291  
   292  		assertShard(t, db, store, p, testTenant, 1, true)
   293  		assertShard(t, db, store, p, testTenant, 2, false)
   294  		assertShard(t, db, store, p, testTenant, 3, true)
   295  		assertPartition(t, db, store, p, true)
   296  
   297  		require.NoError(t, db.Update(func(tx *bbolt.Tx) error {
   298  			return store.DeleteShard(tx, p, testTenant, 1)
   299  		}))
   300  		require.NoError(t, db.Update(func(tx *bbolt.Tx) error {
   301  			return store.DeleteShard(tx, p, testTenant, 3)
   302  		}))
   303  
   304  		assertShard(t, db, store, p, testTenant, 1, false)
   305  		assertShard(t, db, store, p, testTenant, 2, false)
   306  		assertShard(t, db, store, p, testTenant, 3, false)
   307  		assertPartition(t, db, store, p, false)
   308  	})
   309  
   310  	t.Run("multiple partitions isolation", func(t *testing.T) {
   311  		db := test.BoltDB(t)
   312  		store := NewIndexStore()
   313  		require.NoError(t, db.Update(store.CreateBuckets))
   314  
   315  		p1 := NewPartition(test.Time("2024-01-01T10:00:00.000Z"), 6*time.Hour)
   316  		p2 := NewPartition(test.Time("2024-01-01T16:00:00.000Z"), 6*time.Hour)
   317  
   318  		storeBlock(t, db, p1, testTenant, 1, createBlock("block1", testTenant, 1))
   319  		storeBlock(t, db, p2, testTenant, 1, createBlock("block2", testTenant, 1))
   320  
   321  		assertShard(t, db, store, p1, testTenant, 1, true)
   322  		assertShard(t, db, store, p2, testTenant, 1, true)
   323  
   324  		require.NoError(t, db.Update(func(tx *bbolt.Tx) error {
   325  			return store.DeleteShard(tx, p1, testTenant, 1)
   326  		}))
   327  
   328  		assertShard(t, db, store, p1, testTenant, 1, false)
   329  		assertShard(t, db, store, p2, testTenant, 1, true)
   330  		assertPartition(t, db, store, p1, false)
   331  		assertPartition(t, db, store, p2, true)
   332  	})
   333  }