github.com/grafana/pyroscope@v1.18.0/pkg/phlaredb/block/fetcher_test.go (about)

     1  // SPDX-License-Identifier: AGPL-3.0-only
     2  
     3  package block_test
     4  
     5  import (
     6  	"context"
     7  	"path"
     8  	"path/filepath"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/go-kit/log"
    13  	"github.com/oklog/ulid/v2"
    14  	"github.com/prometheus/client_golang/prometheus"
    15  	"github.com/prometheus/client_golang/prometheus/promauto"
    16  	"github.com/prometheus/client_golang/prometheus/testutil"
    17  	"github.com/stretchr/testify/assert"
    18  	"github.com/stretchr/testify/require"
    19  
    20  	objstore_testutil "github.com/grafana/pyroscope/pkg/objstore/testutil"
    21  	"github.com/grafana/pyroscope/pkg/phlaredb/block"
    22  	block_testutil "github.com/grafana/pyroscope/pkg/phlaredb/block/testutil"
    23  	"github.com/grafana/pyroscope/pkg/pprof/testhelper"
    24  	phlarecontext "github.com/grafana/pyroscope/pkg/pyroscope/context"
    25  )
    26  
    27  func TestMetaFetcher_Fetch_ShouldReturnDiscoveredBlocksIncludingMarkedForDeletion(t *testing.T) {
    28  	var (
    29  		ctx    = context.Background()
    30  		reg    = prometheus.NewPedanticRegistry()
    31  		logger = log.NewNopLogger()
    32  	)
    33  
    34  	// Create a bucket client with global markers.
    35  	bkt, _ := objstore_testutil.NewFilesystemBucket(t, ctx, t.TempDir())
    36  	bkt = block.BucketWithGlobalMarkers(bkt)
    37  
    38  	f, err := block.NewMetaFetcher(logger, 10, bkt, t.TempDir(), reg, nil)
    39  	require.NoError(t, err)
    40  
    41  	t.Run("should return no metas and no partials on no block in the storage", func(t *testing.T) {
    42  		actualMetas, actualPartials, actualErr := f.Fetch(ctx)
    43  		require.NoError(t, actualErr)
    44  		require.Empty(t, actualMetas)
    45  		require.Empty(t, actualPartials)
    46  
    47  		assert.NoError(t, testutil.GatherAndCompare(reg, strings.NewReader(`
    48  			# HELP blocks_meta_sync_failures_total Total blocks metadata synchronization failures
    49  			# TYPE blocks_meta_sync_failures_total counter
    50  			blocks_meta_sync_failures_total 0
    51  
    52  			# HELP blocks_meta_synced Number of block metadata synced
    53  			# TYPE blocks_meta_synced gauge
    54  			blocks_meta_synced{state="corrupted-meta-json"} 0
    55  			blocks_meta_synced{state="duplicate"} 0
    56  			blocks_meta_synced{state="failed"} 0
    57  			blocks_meta_synced{state="label-excluded"} 0
    58  			blocks_meta_synced{state="loaded"} 0
    59  			blocks_meta_synced{state="marked-for-deletion"} 0
    60  			blocks_meta_synced{state="marked-for-no-compact"} 0
    61  			blocks_meta_synced{state="no-meta-json"} 0
    62  			blocks_meta_synced{state="time-excluded"} 0
    63  
    64  			# HELP blocks_meta_syncs_total Total blocks metadata synchronization attempts
    65  			# TYPE blocks_meta_syncs_total counter
    66  			blocks_meta_syncs_total 1
    67  		`), "blocks_meta_syncs_total", "blocks_meta_sync_failures_total", "blocks_meta_synced"))
    68  	})
    69  
    70  	// Upload a block.
    71  	block1ID, block1Dir := createTestBlock(t)
    72  	require.NoError(t, block.Upload(ctx, logger, bkt, block1Dir))
    73  
    74  	// Upload a partial block.
    75  	block2ID, block2Dir := createTestBlock(t)
    76  	require.NoError(t, block.Upload(ctx, logger, bkt, block2Dir))
    77  	require.NoError(t, bkt.Delete(ctx, path.Join(block2ID.String(), block.MetaFilename)))
    78  
    79  	t.Run("should return metas and partials on some blocks in the storage", func(t *testing.T) {
    80  		actualMetas, actualPartials, actualErr := f.Fetch(ctx)
    81  		require.NoError(t, actualErr)
    82  		require.Len(t, actualMetas, 1)
    83  		require.Contains(t, actualMetas, block1ID)
    84  		require.Len(t, actualPartials, 1)
    85  		require.Contains(t, actualPartials, block2ID)
    86  
    87  		assert.NoError(t, testutil.GatherAndCompare(reg, strings.NewReader(`
    88  			# HELP blocks_meta_sync_failures_total Total blocks metadata synchronization failures
    89  			# TYPE blocks_meta_sync_failures_total counter
    90  			blocks_meta_sync_failures_total 0
    91  
    92  			# HELP blocks_meta_synced Number of block metadata synced
    93  			# TYPE blocks_meta_synced gauge
    94  			blocks_meta_synced{state="corrupted-meta-json"} 0
    95  			blocks_meta_synced{state="duplicate"} 0
    96  			blocks_meta_synced{state="failed"} 0
    97  			blocks_meta_synced{state="label-excluded"} 0
    98  			blocks_meta_synced{state="loaded"} 1
    99  			blocks_meta_synced{state="marked-for-deletion"} 0
   100  			blocks_meta_synced{state="marked-for-no-compact"} 0
   101  			blocks_meta_synced{state="no-meta-json"} 1
   102  			blocks_meta_synced{state="time-excluded"} 0
   103  
   104  			# HELP blocks_meta_syncs_total Total blocks metadata synchronization attempts
   105  			# TYPE blocks_meta_syncs_total counter
   106  			blocks_meta_syncs_total 2
   107  		`), "blocks_meta_syncs_total", "blocks_meta_sync_failures_total", "blocks_meta_synced"))
   108  	})
   109  
   110  	// Upload a block and mark it for deletion.
   111  	block3ID, block3Dir := createTestBlock(t)
   112  	require.NoError(t, block.Upload(ctx, logger, bkt, block3Dir))
   113  	require.NoError(t, block.MarkForDeletion(ctx, logger, bkt, block3ID, "", false, promauto.With(nil).NewCounter(prometheus.CounterOpts{})))
   114  
   115  	t.Run("should include blocks marked for deletion", func(t *testing.T) {
   116  		actualMetas, actualPartials, actualErr := f.Fetch(ctx)
   117  		require.NoError(t, actualErr)
   118  		require.Len(t, actualMetas, 2)
   119  		require.Contains(t, actualMetas, block1ID)
   120  		require.Contains(t, actualMetas, block3ID)
   121  		require.Len(t, actualPartials, 1)
   122  		require.Contains(t, actualPartials, block2ID)
   123  
   124  		assert.NoError(t, testutil.GatherAndCompare(reg, strings.NewReader(`
   125  			# HELP blocks_meta_sync_failures_total Total blocks metadata synchronization failures
   126  			# TYPE blocks_meta_sync_failures_total counter
   127  			blocks_meta_sync_failures_total 0
   128  
   129  			# HELP blocks_meta_synced Number of block metadata synced
   130  			# TYPE blocks_meta_synced gauge
   131  			blocks_meta_synced{state="corrupted-meta-json"} 0
   132  			blocks_meta_synced{state="duplicate"} 0
   133  			blocks_meta_synced{state="failed"} 0
   134  			blocks_meta_synced{state="label-excluded"} 0
   135  			blocks_meta_synced{state="loaded"} 2
   136  			blocks_meta_synced{state="marked-for-deletion"} 0
   137  			blocks_meta_synced{state="marked-for-no-compact"} 0
   138  			blocks_meta_synced{state="no-meta-json"} 1
   139  			blocks_meta_synced{state="time-excluded"} 0
   140  
   141  			# HELP blocks_meta_syncs_total Total blocks metadata synchronization attempts
   142  			# TYPE blocks_meta_syncs_total counter
   143  			blocks_meta_syncs_total 3
   144  		`), "blocks_meta_syncs_total", "blocks_meta_sync_failures_total", "blocks_meta_synced"))
   145  	})
   146  }
   147  
   148  func TestMetaFetcher_FetchWithoutMarkedForDeletion_ShouldReturnDiscoveredBlocksExcludingMarkedForDeletion(t *testing.T) {
   149  	var (
   150  		ctx    = context.Background()
   151  		reg    = prometheus.NewPedanticRegistry()
   152  		logger = log.NewNopLogger()
   153  	)
   154  
   155  	// Create a bucket client with global markers.
   156  	bkt, _ := objstore_testutil.NewFilesystemBucket(t, ctx, t.TempDir())
   157  	bkt = block.BucketWithGlobalMarkers(bkt)
   158  
   159  	f, err := block.NewMetaFetcher(logger, 10, bkt, t.TempDir(), reg, nil)
   160  	require.NoError(t, err)
   161  
   162  	t.Run("should return no metas and no partials on no block in the storage", func(t *testing.T) {
   163  		actualMetas, actualPartials, actualErr := f.FetchWithoutMarkedForDeletion(ctx)
   164  		require.NoError(t, actualErr)
   165  		require.Empty(t, actualMetas)
   166  		require.Empty(t, actualPartials)
   167  
   168  		assert.NoError(t, testutil.GatherAndCompare(reg, strings.NewReader(`
   169  			# HELP blocks_meta_sync_failures_total Total blocks metadata synchronization failures
   170  			# TYPE blocks_meta_sync_failures_total counter
   171  			blocks_meta_sync_failures_total 0
   172  
   173  			# HELP blocks_meta_synced Number of block metadata synced
   174  			# TYPE blocks_meta_synced gauge
   175  			blocks_meta_synced{state="corrupted-meta-json"} 0
   176  			blocks_meta_synced{state="duplicate"} 0
   177  			blocks_meta_synced{state="failed"} 0
   178  			blocks_meta_synced{state="label-excluded"} 0
   179  			blocks_meta_synced{state="loaded"} 0
   180  			blocks_meta_synced{state="marked-for-deletion"} 0
   181  			blocks_meta_synced{state="marked-for-no-compact"} 0
   182  			blocks_meta_synced{state="no-meta-json"} 0
   183  			blocks_meta_synced{state="time-excluded"} 0
   184  
   185  			# HELP blocks_meta_syncs_total Total blocks metadata synchronization attempts
   186  			# TYPE blocks_meta_syncs_total counter
   187  			blocks_meta_syncs_total 1
   188  		`), "blocks_meta_syncs_total", "blocks_meta_sync_failures_total", "blocks_meta_synced"))
   189  	})
   190  
   191  	// Upload a block.
   192  	block1ID, block1Dir := createTestBlock(t)
   193  	require.NoError(t, block.Upload(ctx, logger, bkt, block1Dir))
   194  
   195  	// Upload a partial block.
   196  	block2ID, block2Dir := createTestBlock(t)
   197  	require.NoError(t, block.Upload(ctx, logger, bkt, block2Dir))
   198  	require.NoError(t, bkt.Delete(ctx, path.Join(block2ID.String(), block.MetaFilename)))
   199  
   200  	t.Run("should return metas and partials on some blocks in the storage", func(t *testing.T) {
   201  		actualMetas, actualPartials, actualErr := f.FetchWithoutMarkedForDeletion(ctx)
   202  		require.NoError(t, actualErr)
   203  		require.Len(t, actualMetas, 1)
   204  		require.Contains(t, actualMetas, block1ID)
   205  		require.Len(t, actualPartials, 1)
   206  		require.Contains(t, actualPartials, block2ID)
   207  
   208  		assert.NoError(t, testutil.GatherAndCompare(reg, strings.NewReader(`
   209  			# HELP blocks_meta_sync_failures_total Total blocks metadata synchronization failures
   210  			# TYPE blocks_meta_sync_failures_total counter
   211  			blocks_meta_sync_failures_total 0
   212  
   213  			# HELP blocks_meta_synced Number of block metadata synced
   214  			# TYPE blocks_meta_synced gauge
   215  			blocks_meta_synced{state="corrupted-meta-json"} 0
   216  			blocks_meta_synced{state="duplicate"} 0
   217  			blocks_meta_synced{state="failed"} 0
   218  			blocks_meta_synced{state="label-excluded"} 0
   219  			blocks_meta_synced{state="loaded"} 1
   220  			blocks_meta_synced{state="marked-for-deletion"} 0
   221  			blocks_meta_synced{state="marked-for-no-compact"} 0
   222  			blocks_meta_synced{state="no-meta-json"} 1
   223  			blocks_meta_synced{state="time-excluded"} 0
   224  
   225  			# HELP blocks_meta_syncs_total Total blocks metadata synchronization attempts
   226  			# TYPE blocks_meta_syncs_total counter
   227  			blocks_meta_syncs_total 2
   228  		`), "blocks_meta_syncs_total", "blocks_meta_sync_failures_total", "blocks_meta_synced"))
   229  	})
   230  
   231  	// Upload a block and mark it for deletion.
   232  	block3ID, block3Dir := createTestBlock(t)
   233  	require.NoError(t, block.Upload(ctx, logger, bkt, block3Dir))
   234  	require.NoError(t, block.MarkForDeletion(ctx, logger, bkt, block3ID, "", false, promauto.With(nil).NewCounter(prometheus.CounterOpts{})))
   235  
   236  	t.Run("should include blocks marked for deletion", func(t *testing.T) {
   237  		actualMetas, actualPartials, actualErr := f.FetchWithoutMarkedForDeletion(ctx)
   238  		require.NoError(t, actualErr)
   239  		require.Len(t, actualMetas, 1)
   240  		require.Contains(t, actualMetas, block1ID)
   241  		require.Len(t, actualPartials, 1)
   242  		require.Contains(t, actualPartials, block2ID)
   243  
   244  		assert.NoError(t, testutil.GatherAndCompare(reg, strings.NewReader(`
   245  			# HELP blocks_meta_sync_failures_total Total blocks metadata synchronization failures
   246  			# TYPE blocks_meta_sync_failures_total counter
   247  			blocks_meta_sync_failures_total 0
   248  
   249  			# HELP blocks_meta_synced Number of block metadata synced
   250  			# TYPE blocks_meta_synced gauge
   251  			blocks_meta_synced{state="corrupted-meta-json"} 0
   252  			blocks_meta_synced{state="duplicate"} 0
   253  			blocks_meta_synced{state="failed"} 0
   254  			blocks_meta_synced{state="label-excluded"} 0
   255  			blocks_meta_synced{state="loaded"} 1
   256  			blocks_meta_synced{state="marked-for-deletion"} 1
   257  			blocks_meta_synced{state="marked-for-no-compact"} 0
   258  			blocks_meta_synced{state="no-meta-json"} 1
   259  			blocks_meta_synced{state="time-excluded"} 0
   260  
   261  			# HELP blocks_meta_syncs_total Total blocks metadata synchronization attempts
   262  			# TYPE blocks_meta_syncs_total counter
   263  			blocks_meta_syncs_total 3
   264  		`), "blocks_meta_syncs_total", "blocks_meta_sync_failures_total", "blocks_meta_synced"))
   265  	})
   266  }
   267  
   268  func TestMetaFetcher_ShouldNotIssueAnyAPICallToObjectStorageIfAllBlockMetasAreCached(t *testing.T) {
   269  	var (
   270  		ctx        = context.Background()
   271  		logger     = log.NewNopLogger()
   272  		fetcherDir = t.TempDir()
   273  	)
   274  	bkt, _ := objstore_testutil.NewFilesystemBucket(t, ctx, fetcherDir)
   275  
   276  	// Upload few blocks.
   277  	block1ID, block1Dir := createTestBlock(t)
   278  	require.NoError(t, block.Upload(ctx, logger, bkt, block1Dir))
   279  	block2ID, block2Dir := createTestBlock(t)
   280  	require.NoError(t, block.Upload(ctx, logger, bkt, block2Dir))
   281  
   282  	// Create a fetcher and fetch block metas to populate the cache on disk.
   283  	reg1 := prometheus.NewPedanticRegistry()
   284  	ctx = phlarecontext.WithRegistry(ctx, reg1)
   285  	bkt, _ = objstore_testutil.NewFilesystemBucket(t, ctx, fetcherDir)
   286  	fetcher1, err := block.NewMetaFetcher(logger, 10, bkt, fetcherDir, nil, nil)
   287  	require.NoError(t, err)
   288  	actualMetas, _, actualErr := fetcher1.Fetch(ctx)
   289  	require.NoError(t, actualErr)
   290  	require.Len(t, actualMetas, 2)
   291  	require.Contains(t, actualMetas, block1ID)
   292  	require.Contains(t, actualMetas, block2ID)
   293  
   294  	assert.NoError(t, testutil.GatherAndCompare(reg1, strings.NewReader(`
   295  		# HELP objstore_bucket_operations_total Total number of all attempted operations against a bucket.
   296  		# TYPE objstore_bucket_operations_total counter
   297  		objstore_bucket_operations_total{bucket="test",operation="attributes"} 0
   298  		objstore_bucket_operations_total{bucket="test",operation="exists"} 0
   299  		objstore_bucket_operations_total{bucket="test",operation="delete"} 0
   300  		objstore_bucket_operations_total{bucket="test",operation="upload"} 0
   301  		objstore_bucket_operations_total{bucket="test",operation="get"} 2
   302  		objstore_bucket_operations_total{bucket="test",operation="get_range"} 0
   303  		objstore_bucket_operations_total{bucket="test",operation="iter"} 1
   304  	`), "objstore_bucket_operations_total"))
   305  
   306  	// Create a new fetcher and fetch blocks again. This time we expect all meta.json to be loaded from cache.
   307  	reg2 := prometheus.NewPedanticRegistry()
   308  	ctx = phlarecontext.WithRegistry(ctx, reg2)
   309  	bkt, _ = objstore_testutil.NewFilesystemBucket(t, ctx, fetcherDir)
   310  	fetcher2, err := block.NewMetaFetcher(logger, 10, bkt, fetcherDir, nil, nil)
   311  	require.NoError(t, err)
   312  	actualMetas, _, actualErr = fetcher2.Fetch(ctx)
   313  	require.NoError(t, actualErr)
   314  	require.Len(t, actualMetas, 2)
   315  	require.Contains(t, actualMetas, block1ID)
   316  	require.Contains(t, actualMetas, block2ID)
   317  
   318  	assert.NoError(t, testutil.GatherAndCompare(reg2, strings.NewReader(`
   319  		# HELP objstore_bucket_operations_total Total number of all attempted operations against a bucket.
   320  		# TYPE objstore_bucket_operations_total counter
   321  		objstore_bucket_operations_total{bucket="test",operation="attributes"} 0
   322  		objstore_bucket_operations_total{bucket="test",operation="delete"} 0
   323  		objstore_bucket_operations_total{bucket="test",operation="exists"} 0
   324  		objstore_bucket_operations_total{bucket="test",operation="get"} 0
   325  		objstore_bucket_operations_total{bucket="test",operation="get_range"} 0
   326  		objstore_bucket_operations_total{bucket="test",operation="iter"} 1
   327  		objstore_bucket_operations_total{bucket="test",operation="upload"} 0
   328  	`), "objstore_bucket_operations_total"))
   329  }
   330  
   331  func createTestBlock(t *testing.T) (blockID ulid.ULID, blockDir string) {
   332  	meta, dir := block_testutil.CreateBlock(t, func() []*testhelper.ProfileBuilder {
   333  		return []*testhelper.ProfileBuilder{
   334  			testhelper.NewProfileBuilder(int64(1)).
   335  				CPUProfile().
   336  				WithLabels(
   337  					"job", "a",
   338  				).ForStacktraceString("foo", "bar", "baz").AddSamples(1),
   339  		}
   340  	})
   341  	blockID = meta.ULID
   342  	blockDir = filepath.Join(dir, blockID.String())
   343  	return
   344  }