github.com/grafana/pyroscope@v1.18.0/pkg/metastore/compaction/compactor/compaction_queue_test.go (about)

     1  package compactor
     2  
     3  import (
     4  	"fmt"
     5  	"math/rand"
     6  	"strconv"
     7  	"testing"
     8  
     9  	"github.com/stretchr/testify/assert"
    10  	"github.com/stretchr/testify/require"
    11  
    12  	"github.com/grafana/pyroscope/pkg/metastore/compaction"
    13  )
    14  
    15  func testBlockEntry(id int) blockEntry { return blockEntry{id: strconv.Itoa(id)} }
    16  
    17  func testBlockQueue(cfg Config) *blockQueue {
    18  	stats := newGlobalQueueStats(len(cfg.Levels))
    19  	return newBlockQueue(cfg, nil, stats)
    20  }
    21  
    22  func TestBlockQueue_Push(t *testing.T) {
    23  	q := testBlockQueue(Config{Levels: []LevelConfig{{MaxBlocks: 3}}})
    24  	key := compactionKey{tenant: "t", shard: 1}
    25  
    26  	result := q.stagedBlocks(key).push(testBlockEntry(1))
    27  	require.True(t, result)
    28  	require.Equal(t, 1, len(q.staged[key].batch.blocks))
    29  	assert.Equal(t, testBlockEntry(1), q.staged[key].batch.blocks[0])
    30  
    31  	q.stagedBlocks(key).push(testBlockEntry(2))
    32  	q.stagedBlocks(key).push(testBlockEntry(3)) // Staged blocks formed the first batch.
    33  	assert.Equal(t, 0, len(q.staged[key].batch.blocks))
    34  	assert.Equal(t, []blockEntry{testBlockEntry(1), testBlockEntry(2), testBlockEntry(3)}, q.head.blocks)
    35  
    36  	q.stagedBlocks(key).push(testBlockEntry(4))
    37  	q.stagedBlocks(key).push(testBlockEntry(5))
    38  	assert.Equal(t, 2, len(q.staged[key].batch.blocks))
    39  
    40  	remove(q, key, "1", "2") // Remove the first batch.
    41  	assert.Equal(t, []blockEntry{zeroBlockEntry, zeroBlockEntry, testBlockEntry(3)}, q.head.blocks)
    42  	remove(q, key, "3")
    43  	assert.Nil(t, q.head)
    44  
    45  	q.stagedBlocks(key).push(testBlockEntry(6)) // Complete the second batch.
    46  	assert.Equal(t, 0, len(q.staged[key].batch.blocks))
    47  
    48  	q.stagedBlocks(key).push(testBlockEntry(7))
    49  	assert.Equal(t, []blockEntry{testBlockEntry(4), testBlockEntry(5), testBlockEntry(6)}, q.head.blocks)
    50  	assert.Equal(t, 1, len(q.staged[key].batch.blocks))
    51  }
    52  
    53  func TestBlockQueue_DuplicateBlock(t *testing.T) {
    54  	q := testBlockQueue(Config{Levels: []LevelConfig{{MaxBlocks: 3}}})
    55  	key := compactionKey{tenant: "t", shard: 1}
    56  
    57  	require.True(t, q.stagedBlocks(key).push(testBlockEntry(1)))
    58  	require.False(t, q.stagedBlocks(key).push(testBlockEntry(1)))
    59  
    60  	assert.Equal(t, 1, len(q.staged[key].batch.blocks))
    61  }
    62  
    63  func TestBlockQueue_Remove(t *testing.T) {
    64  	q := testBlockQueue(Config{Levels: []LevelConfig{{MaxBlocks: 3}}})
    65  	key := compactionKey{tenant: "t", shard: 1}
    66  	q.stagedBlocks(key).push(testBlockEntry(1))
    67  	q.stagedBlocks(key).push(testBlockEntry(2))
    68  
    69  	remove(q, key, "1")
    70  	require.Empty(t, q.staged[key].batch.blocks[0])
    71  
    72  	_, exists := q.staged[key].refs["1"]
    73  	assert.False(t, exists)
    74  
    75  	remove(q, key, "2")
    76  	require.Nil(t, q.head)
    77  	require.Nil(t, q.tail)
    78  }
    79  
    80  func TestBlockQueue_RemoveNotFound(t *testing.T) {
    81  	q := testBlockQueue(Config{Levels: []LevelConfig{{MaxBlocks: 3}}})
    82  	key := compactionKey{tenant: "t", shard: 1}
    83  	remove(q, key, "1")
    84  	q.stagedBlocks(key).push(testBlockEntry(1))
    85  	remove(q, key, "2")
    86  	q.stagedBlocks(key).push(testBlockEntry(2))
    87  	q.stagedBlocks(key).push(testBlockEntry(3))
    88  
    89  	assert.Equal(t, []blockEntry{testBlockEntry(1), testBlockEntry(2), testBlockEntry(3)}, q.head.blocks)
    90  }
    91  
    92  func TestBlockQueue_Linking(t *testing.T) {
    93  	q := testBlockQueue(Config{Levels: []LevelConfig{{MaxBlocks: 2}}})
    94  	key := compactionKey{tenant: "t", shard: 1}
    95  
    96  	q.stagedBlocks(key).push(testBlockEntry(1))
    97  	q.stagedBlocks(key).push(testBlockEntry(2))
    98  	head, tail := q.head, q.tail
    99  	require.NotNil(t, head)
   100  	assert.Equal(t, head, tail)
   101  
   102  	q.stagedBlocks(key).push(testBlockEntry(3))
   103  	assert.NotNil(t, q.tail)
   104  	assert.Equal(t, head, q.head)
   105  	assert.Equal(t, tail, q.tail)
   106  	assert.Equal(t, []blockEntry{testBlockEntry(1), testBlockEntry(2)}, q.head.blocks)
   107  	assert.Equal(t, q.tail.blocks, q.head.blocks)
   108  
   109  	q.stagedBlocks(key).push(testBlockEntry(4))
   110  	assert.NotNil(t, q.tail.prevG)
   111  	assert.NotNil(t, q.head.nextG)
   112  
   113  	q.stagedBlocks(key).push(testBlockEntry(5))
   114  	q.stagedBlocks(key).push(testBlockEntry(6))
   115  	assert.NotNil(t, q.tail.prevG.prevG)
   116  	assert.NotNil(t, q.head.nextG.nextG)
   117  
   118  	t.Run("iterator does not affect the queue", func(t *testing.T) {
   119  		expected := []string{"1", "2", "3", "4", "5", "6"}
   120  		for i := 0; i < 3; i++ {
   121  			collected := make([]string, 0, len(expected))
   122  			iter := newBlockIter()
   123  			iter.setBatch(q.head)
   124  			for {
   125  				b, ok := iter.peek()
   126  				if !ok {
   127  					assert.Equal(t, expected, collected)
   128  					break
   129  				}
   130  				collected = append(collected, b)
   131  				iter.advance()
   132  			}
   133  		}
   134  	})
   135  
   136  	t.Run("remove staging batch from the queue", func(t *testing.T) {
   137  		q.stagedBlocks(key).push(testBlockEntry(7))
   138  		// Block 7 is still staged: test that it can be
   139  		// removed without affecting the queue.
   140  		head, tail = q.head, q.tail
   141  		remove(q, key, "7")
   142  		assert.Equal(t, head, q.head)
   143  		assert.Equal(t, tail, q.tail)
   144  	})
   145  
   146  	t.Run("empty queue", func(t *testing.T) {
   147  		remove(q, key, "3", "2")
   148  		remove(q, key, "4", "1")
   149  		remove(q, key, "6")
   150  		remove(q, key, "5")
   151  		assert.Nil(t, q.head)
   152  		assert.Nil(t, q.tail)
   153  	})
   154  }
   155  
   156  func TestBlockQueue_EmptyQueue(t *testing.T) {
   157  	const (
   158  		numKeys         = 50
   159  		numBlocksPerKey = 100
   160  	)
   161  
   162  	q := testBlockQueue(Config{Levels: []LevelConfig{{MaxBlocks: 3}}})
   163  	keys := make([]compactionKey, numKeys)
   164  	for i := 0; i < numKeys; i++ {
   165  		keys[i] = compactionKey{
   166  			tenant: fmt.Sprint(i),
   167  			shard:  uint32(i),
   168  		}
   169  	}
   170  
   171  	blocks := make(map[compactionKey][]string)
   172  	for _, key := range keys {
   173  		for j := 0; j < numBlocksPerKey; j++ {
   174  			block := testBlockEntry(j)
   175  			require.True(t, q.stagedBlocks(key).push(block))
   176  			blocks[key] = append(blocks[key], block.id)
   177  		}
   178  	}
   179  
   180  	for key, s := range blocks {
   181  		rand.Shuffle(len(s), func(i, j int) {
   182  			s[i], s[j] = s[j], s[i]
   183  		})
   184  		for _, b := range s {
   185  			staged, ok := q.staged[key]
   186  			if !ok {
   187  				return
   188  			}
   189  			assert.NotEmpty(t, staged.delete(b))
   190  		}
   191  	}
   192  
   193  	for key := range blocks {
   194  		require.Nil(t, q.staged[key])
   195  	}
   196  
   197  	assert.Nil(t, q.head)
   198  	assert.Nil(t, q.tail)
   199  }
   200  
   201  func TestBlockQueue_FlushByAge(t *testing.T) {
   202  	s := Config{
   203  		Levels: []LevelConfig{
   204  			{MaxBlocks: 3, MaxAge: 1},
   205  			{MaxBlocks: 5, MaxAge: 1},
   206  		},
   207  	}
   208  
   209  	c := newCompactionQueue(s, nil)
   210  	blocks := []compaction.BlockEntry{
   211  		{Tenant: "A", Shard: 1, Level: 1, Index: 1, AppendedAt: 5, ID: "1"},
   212  		{Tenant: "A", Shard: 1, Level: 1, Index: 2, AppendedAt: 15, ID: "2"},
   213  		{Tenant: "A", Shard: 0, Level: 1, Index: 3, AppendedAt: 30, ID: "3"},
   214  		{Tenant: "A", Shard: 0, Level: 1, Index: 4, AppendedAt: 35, ID: "4"},
   215  		{Tenant: "B", Shard: 0, Level: 1, Index: 5, AppendedAt: 40, ID: "5"},
   216  	}
   217  	for _, e := range blocks {
   218  		c.push(e)
   219  	}
   220  
   221  	batches := make([]blockEntry, 0, len(blocks))
   222  	q := c.blockQueue(1)
   223  	iter := newBatchIter(q)
   224  	for {
   225  		b, ok := iter.next()
   226  		if !ok {
   227  			break
   228  		}
   229  		batches = append(batches, b.blocks...)
   230  	}
   231  
   232  	expected := []blockEntry{{"1", 1}, {"2", 2}, {"3", 3}, {"4", 4}}
   233  	// "5" remains staged as we need another push to evict it.
   234  	assert.Equal(t, expected, batches)
   235  
   236  	// We have 3 compaction queues (staging batches):
   237  	// A/1/1, A/0/1, B/0/1.
   238  	assert.Equal(t, 3, q.updates.Len())
   239  
   240  	staged := q.stagedBlocks(compactionKey{tenant: "B", shard: 0, level: 1})
   241  	staged.delete("5")
   242  	// B/0/1 compaction queue is removed.
   243  	assert.Equal(t, 2, q.updates.Len())
   244  
   245  	staged = q.stagedBlocks(compactionKey{tenant: "A", shard: 0, level: 1})
   246  	staged.delete("4")
   247  	// A/0/1 is still here.
   248  	assert.Equal(t, 2, q.updates.Len())
   249  	staged.delete("3")
   250  	// A/0/1 compaction queue is removed.
   251  	assert.Equal(t, 1, q.updates.Len())
   252  
   253  	// A/1/1 compaction queue is removed.
   254  	staged = q.stagedBlocks(compactionKey{tenant: "A", shard: 1, level: 1})
   255  	assert.NotEmpty(t, staged.delete("1"))
   256  	assert.Equal(t, 1, q.updates.Len())
   257  	assert.NotEmpty(t, staged.delete("2"))
   258  	assert.Equal(t, 0, staged.queue.updates.Len())
   259  }
   260  
   261  func TestBlockQueue_BatchIterator(t *testing.T) {
   262  	q := testBlockQueue(Config{Levels: []LevelConfig{{MaxBlocks: 3}}})
   263  	keys := []compactionKey{
   264  		{tenant: "t-1", shard: 1},
   265  		{tenant: "t-2", shard: 2},
   266  	}
   267  
   268  	for j := 0; j < 20; j++ {
   269  		q.stagedBlocks(keys[j%len(keys)]).push(testBlockEntry(j))
   270  	}
   271  
   272  	iter := newBatchIter(q)
   273  	for _, expected := range []struct {
   274  		key    compactionKey
   275  		blocks []string
   276  	}{
   277  		{key: keys[0], blocks: []string{"0", "2", "4"}},
   278  		{key: keys[1], blocks: []string{"1", "3", "5"}},
   279  		{key: keys[0], blocks: []string{"6", "8", "10"}},
   280  		{key: keys[1], blocks: []string{"7", "9", "11"}},
   281  		{key: keys[0], blocks: []string{"12", "14", "16"}},
   282  		{key: keys[1], blocks: []string{"13", "15", "17"}},
   283  	} {
   284  		b, ok := iter.next()
   285  		require.True(t, ok)
   286  		assert.Equal(t, expected.key, b.staged.key)
   287  		actual := make([]string, len(b.blocks))
   288  		for i := range b.blocks {
   289  			actual[i] = b.blocks[i].id
   290  		}
   291  		assert.Equal(t, expected.blocks, actual)
   292  	}
   293  
   294  	_, ok := iter.next()
   295  	assert.False(t, ok)
   296  }
   297  
   298  func remove(q *blockQueue, key compactionKey, block ...string) {
   299  	staged, ok := q.staged[key]
   300  	if !ok {
   301  		return
   302  	}
   303  	for _, b := range block {
   304  		staged.delete(b)
   305  	}
   306  }