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

     1  // SPDX-License-Identifier: AGPL-3.0-only
     2  // Provenance-includes-location: https://github.com/cortexproject/cortex/blob/master/pkg/storage/tsdb/bucketindex/markers_bucket_client_test.go
     3  // Provenance-includes-license: Apache-2.0
     4  // Provenance-includes-copyright: The Cortex Authors.
     5  
     6  package block
     7  
     8  import (
     9  	"bytes"
    10  	"context"
    11  	"os"
    12  	"path"
    13  	"strings"
    14  	"testing"
    15  
    16  	"github.com/go-kit/log"
    17  	"github.com/oklog/ulid/v2"
    18  	"github.com/prometheus/client_golang/prometheus"
    19  	"github.com/prometheus/client_golang/prometheus/testutil"
    20  	"github.com/stretchr/testify/assert"
    21  	"github.com/stretchr/testify/require"
    22  
    23  	"github.com/grafana/pyroscope/pkg/objstore"
    24  	objstore_testutil "github.com/grafana/pyroscope/pkg/objstore/testutil"
    25  	phlarecontext "github.com/grafana/pyroscope/pkg/pyroscope/context"
    26  )
    27  
    28  func TestGlobalMarkersBucket_Delete_ShouldSucceedIfDeletionMarkDoesNotExistInTheBlockButExistInTheGlobalLocation(t *testing.T) {
    29  	ctx := context.Background()
    30  
    31  	// Create a mocked block deletion mark in the global location.
    32  	blockID := ulid.MustNew(1, nil)
    33  	for _, globalPath := range []string{DeletionMarkFilepath(blockID), NoCompactMarkFilepath(blockID)} {
    34  		bkt, _ := objstore_testutil.NewFilesystemBucket(t, ctx, t.TempDir())
    35  		bkt = BucketWithGlobalMarkers(bkt)
    36  
    37  		require.NoError(t, bkt.Upload(ctx, globalPath, strings.NewReader("{}")))
    38  
    39  		// Ensure it exists before deleting it.
    40  		ok, err := bkt.Exists(ctx, globalPath)
    41  		require.NoError(t, err)
    42  		require.True(t, ok)
    43  
    44  		require.NoError(t, bkt.Delete(ctx, globalPath))
    45  
    46  		// Ensure has been actually deleted.
    47  		ok, err = bkt.Exists(ctx, globalPath)
    48  		require.NoError(t, err)
    49  		require.False(t, ok)
    50  	}
    51  }
    52  
    53  func TestGlobalMarkersBucket_DeleteShouldDeleteGlobalMarkIfBlockMarkerDoesntExist(t *testing.T) {
    54  	ctx := context.Background()
    55  
    56  	blockID := ulid.MustNew(1, nil)
    57  
    58  	for name, tc := range map[string]struct {
    59  		blockMarker  string
    60  		globalMarker string
    61  	}{
    62  		"deletion mark": {
    63  			blockMarker:  path.Join(blockID.String(), DeletionMarkFilename),
    64  			globalMarker: DeletionMarkFilepath(blockID),
    65  		},
    66  		"no compact": {
    67  			blockMarker:  path.Join(blockID.String(), NoCompactMarkFilename),
    68  			globalMarker: NoCompactMarkFilepath(blockID),
    69  		},
    70  	} {
    71  		t.Run(name, func(t *testing.T) {
    72  			// Create a mocked block deletion mark in the global location.
    73  			bkt, _ := objstore_testutil.NewFilesystemBucket(t, ctx, t.TempDir())
    74  			bkt = BucketWithGlobalMarkers(bkt)
    75  
    76  			// Upload global only
    77  			require.NoError(t, bkt.Upload(ctx, tc.globalMarker, strings.NewReader("{}")))
    78  
    79  			// Verify global exists.
    80  			verifyPathExists(t, bkt, tc.globalMarker, true)
    81  
    82  			// Delete block marker.
    83  			err := bkt.Delete(ctx, tc.blockMarker)
    84  			require.NoError(t, err)
    85  
    86  			// Ensure global one been actually deleted.
    87  			verifyPathExists(t, bkt, tc.globalMarker, false)
    88  		})
    89  	}
    90  }
    91  
    92  func TestUploadToGlobalMarkerPath(t *testing.T) {
    93  	blockID := ulid.MustNew(1, nil)
    94  	for name, tc := range map[string]struct {
    95  		blockMarker  string
    96  		globalMarker string
    97  	}{
    98  		"deletion mark": {
    99  			blockMarker:  path.Join(blockID.String(), DeletionMarkFilename),
   100  			globalMarker: DeletionMarkFilepath(blockID),
   101  		},
   102  		"no compact": {
   103  			blockMarker:  path.Join(blockID.String(), NoCompactMarkFilename),
   104  			globalMarker: NoCompactMarkFilepath(blockID),
   105  		},
   106  	} {
   107  		t.Run(name, func(t *testing.T) {
   108  			bkt, _ := objstore_testutil.NewFilesystemBucket(t, context.Background(), t.TempDir())
   109  			bkt = BucketWithGlobalMarkers(bkt)
   110  
   111  			// Verify that uploading block mark file uploads it to the global markers location too.
   112  			require.NoError(t, bkt.Upload(context.Background(), tc.blockMarker, strings.NewReader("mark file")))
   113  
   114  			verifyPathExists(t, bkt, tc.globalMarker, true)
   115  		})
   116  	}
   117  }
   118  
   119  func TestGlobalMarkersBucket_ExistShouldReportTrueOnlyIfBothExist(t *testing.T) {
   120  	blockID := ulid.MustNew(1, nil)
   121  
   122  	for name, tc := range map[string]struct {
   123  		blockMarker  string
   124  		globalMarker string
   125  	}{
   126  		"deletion mark": {
   127  			blockMarker:  path.Join(blockID.String(), DeletionMarkFilename),
   128  			globalMarker: DeletionMarkFilepath(blockID),
   129  		},
   130  		"no compact": {
   131  			blockMarker:  path.Join(blockID.String(), NoCompactMarkFilename),
   132  			globalMarker: NoCompactMarkFilepath(blockID),
   133  		},
   134  	} {
   135  		t.Run(name, func(t *testing.T) {
   136  			bkt, _ := objstore_testutil.NewFilesystemBucket(t, context.Background(), t.TempDir())
   137  			bkt = BucketWithGlobalMarkers(bkt)
   138  
   139  			// Upload to global marker only
   140  			require.NoError(t, bkt.Upload(context.Background(), tc.globalMarker, strings.NewReader("mark file")))
   141  
   142  			// Verify global exists, but block marker doesn't.
   143  			verifyPathExists(t, bkt, tc.globalMarker, true)
   144  			verifyPathExists(t, bkt, tc.blockMarker, false)
   145  
   146  			// Now upload to block marker (also overwrites global)
   147  			require.NoError(t, bkt.Upload(context.Background(), tc.blockMarker, strings.NewReader("mark file")))
   148  
   149  			// Verify global exists and block marker does too.
   150  			verifyPathExists(t, bkt, tc.globalMarker, true)
   151  			verifyPathExists(t, bkt, tc.blockMarker, true)
   152  
   153  			// Now delete global file, and only keep block.
   154  			require.NoError(t, bkt.Delete(context.Background(), tc.globalMarker))
   155  
   156  			// Verify global doesn't exist anymore. Block marker also returns false, even though it *does* exist.
   157  			verifyPathExists(t, bkt, tc.globalMarker, false)
   158  			verifyPathExists(t, bkt, tc.blockMarker, false)
   159  		})
   160  	}
   161  }
   162  
   163  func verifyPathExists(t *testing.T, bkt objstore.Bucket, name string, expected bool) {
   164  	t.Helper()
   165  
   166  	ok, err := bkt.Exists(context.Background(), name)
   167  	require.NoError(t, err)
   168  	require.Equal(t, expected, ok)
   169  }
   170  
   171  func TestGlobalMarkersBucket_getGlobalMarkPathFromBlockMark(t *testing.T) {
   172  	type testCase struct {
   173  		name     string
   174  		expected string
   175  	}
   176  
   177  	tests := []testCase{
   178  		{name: "", expected: ""},
   179  		{name: "01FV060K6XXCS8BCD2CH6C3GBR/index", expected: ""},
   180  	}
   181  
   182  	for _, marker := range []string{DeletionMarkFilename, NoCompactMarkFilename} {
   183  		tests = append(tests, testCase{name: marker, expected: ""})
   184  		tests = append(tests, testCase{name: "01FV060K6XXCS8BCD2CH6C3GBR/" + marker, expected: "markers/01FV060K6XXCS8BCD2CH6C3GBR-" + marker})
   185  		tests = append(tests, testCase{name: "/path/to/01FV060K6XXCS8BCD2CH6C3GBR/" + marker, expected: "/path/to/markers/01FV060K6XXCS8BCD2CH6C3GBR-" + marker})
   186  		tests = append(tests, testCase{name: "invalid-block-id/" + marker, expected: ""})
   187  	}
   188  
   189  	for _, tc := range tests {
   190  		t.Run(tc.name, func(t *testing.T) {
   191  			result := getGlobalMarkPathFromBlockMark(tc.name)
   192  			assert.Equal(t, tc.expected, result)
   193  		})
   194  	}
   195  }
   196  
   197  func TestGlobalMarkersBucket_isDeletionMark(t *testing.T) {
   198  	block1 := ulid.MustNew(1, nil)
   199  
   200  	tests := []struct {
   201  		name       string
   202  		expectedOk bool
   203  		expectedID ulid.ULID
   204  	}{
   205  		{
   206  			name:       "",
   207  			expectedOk: false,
   208  		}, {
   209  			name:       "deletion-mark.json",
   210  			expectedOk: false,
   211  		}, {
   212  			name:       block1.String() + "/index",
   213  			expectedOk: false,
   214  		}, {
   215  			name:       block1.String() + "/deletion-mark.json",
   216  			expectedOk: true,
   217  			expectedID: block1,
   218  		}, {
   219  			name:       "/path/to/" + block1.String() + "/deletion-mark.json",
   220  			expectedOk: true,
   221  			expectedID: block1,
   222  		},
   223  	}
   224  
   225  	for _, tc := range tests {
   226  		t.Run(tc.name, func(t *testing.T) {
   227  			actualID, actualOk := isDeletionMark(tc.name)
   228  			assert.Equal(t, tc.expectedOk, actualOk)
   229  			assert.Equal(t, tc.expectedID, actualID)
   230  		})
   231  	}
   232  }
   233  
   234  func TestGlobalMarkersBucket_isNoCompactMark(t *testing.T) {
   235  	block1 := ulid.MustNew(1, nil)
   236  
   237  	tests := []struct {
   238  		name       string
   239  		expectedOk bool
   240  		expectedID ulid.ULID
   241  	}{
   242  		{
   243  			name:       "",
   244  			expectedOk: false,
   245  		}, {
   246  			name:       "no-compact-mark.json",
   247  			expectedOk: false,
   248  		}, {
   249  			name:       block1.String() + "/index",
   250  			expectedOk: false,
   251  		}, {
   252  			name:       block1.String() + "/no-compact-mark.json",
   253  			expectedOk: true,
   254  			expectedID: block1,
   255  		}, {
   256  			name:       "/path/to/" + block1.String() + "/no-compact-mark.json",
   257  			expectedOk: true,
   258  			expectedID: block1,
   259  		},
   260  	}
   261  
   262  	for _, tc := range tests {
   263  		t.Run(tc.name, func(t *testing.T) {
   264  			actualID, actualOk := isNoCompactMark(tc.name)
   265  			assert.Equal(t, tc.expectedOk, actualOk)
   266  			assert.Equal(t, tc.expectedID, actualID)
   267  		})
   268  	}
   269  }
   270  
   271  func TestBucketWithGlobalMarkers_ShouldWorkCorrectlyWithBucketMetrics(t *testing.T) {
   272  	reg := prometheus.NewPedanticRegistry()
   273  	ctx := phlarecontext.WithRegistry(context.Background(), reg)
   274  	// We wrap the underlying filesystem bucket client with metrics,
   275  	// global markers (intentionally in the middle of the chain) and
   276  	// user prefix.
   277  	bkt, _ := objstore_testutil.NewFilesystemBucket(t, ctx, t.TempDir())
   278  	bkt = BucketWithGlobalMarkers(bkt)
   279  	userBkt := objstore.NewTenantBucketClient("user-1", bkt, nil)
   280  
   281  	reader, err := userBkt.Get(ctx, "does-not-exist")
   282  	require.Error(t, err)
   283  	require.Nil(t, reader)
   284  	assert.True(t, bkt.IsObjNotFoundErr(err))
   285  
   286  	// Should track the failure.
   287  	assert.NoError(t, testutil.GatherAndCompare(reg, bytes.NewBufferString(`
   288  		# HELP objstore_bucket_operation_failures_total Total number of operations against a bucket that failed, but were not expected to fail in certain way from caller perspective. Those errors have to be investigated.
   289  		# TYPE objstore_bucket_operation_failures_total counter
   290  		objstore_bucket_operation_failures_total{bucket="test",operation="attributes"} 0
   291  		objstore_bucket_operation_failures_total{bucket="test",operation="delete"} 0
   292  		objstore_bucket_operation_failures_total{bucket="test",operation="exists"} 0
   293  		objstore_bucket_operation_failures_total{bucket="test",operation="get"} 1
   294  		objstore_bucket_operation_failures_total{bucket="test",operation="get_range"} 0
   295  		objstore_bucket_operation_failures_total{bucket="test",operation="iter"} 0
   296  		objstore_bucket_operation_failures_total{bucket="test",operation="upload"} 0
   297  		# HELP objstore_bucket_operations_total Total number of all attempted operations against a bucket.
   298  		# TYPE objstore_bucket_operations_total counter
   299  		objstore_bucket_operations_total{bucket="test",operation="attributes"} 0
   300  		objstore_bucket_operations_total{bucket="test",operation="delete"} 0
   301  		objstore_bucket_operations_total{bucket="test",operation="exists"} 0
   302  		objstore_bucket_operations_total{bucket="test",operation="get"} 1
   303  		objstore_bucket_operations_total{bucket="test",operation="get_range"} 0
   304  		objstore_bucket_operations_total{bucket="test",operation="iter"} 0
   305  		objstore_bucket_operations_total{bucket="test",operation="upload"} 0
   306  	`),
   307  		"objstore_bucket_operations_total",
   308  		"objstore_bucket_operation_failures_total",
   309  	))
   310  
   311  	reader, err = userBkt.ReaderWithExpectedErrs(userBkt.IsObjNotFoundErr).Get(ctx, "does-not-exist")
   312  	require.Error(t, err)
   313  	require.Nil(t, reader)
   314  	assert.True(t, bkt.IsObjNotFoundErr(err))
   315  
   316  	// Should not track the failure.
   317  	assert.NoError(t, testutil.GatherAndCompare(reg, bytes.NewBufferString(`
   318  		# HELP objstore_bucket_operation_failures_total Total number of operations against a bucket that failed, but were not expected to fail in certain way from caller perspective. Those errors have to be investigated.
   319  		# TYPE objstore_bucket_operation_failures_total counter
   320  		objstore_bucket_operation_failures_total{bucket="test",operation="attributes"} 0
   321  		objstore_bucket_operation_failures_total{bucket="test",operation="delete"} 0
   322  		objstore_bucket_operation_failures_total{bucket="test",operation="exists"} 0
   323  		objstore_bucket_operation_failures_total{bucket="test",operation="get"} 1
   324  		objstore_bucket_operation_failures_total{bucket="test",operation="get_range"} 0
   325  		objstore_bucket_operation_failures_total{bucket="test",operation="iter"} 0
   326  		objstore_bucket_operation_failures_total{bucket="test",operation="upload"} 0
   327  		# HELP objstore_bucket_operations_total Total number of all attempted operations against a bucket.
   328  		# TYPE objstore_bucket_operations_total counter
   329  		objstore_bucket_operations_total{bucket="test",operation="attributes"} 0
   330  		objstore_bucket_operations_total{bucket="test",operation="delete"} 0
   331  		objstore_bucket_operations_total{bucket="test",operation="exists"} 0
   332  		objstore_bucket_operations_total{bucket="test",operation="get"} 2
   333  		objstore_bucket_operations_total{bucket="test",operation="get_range"} 0
   334  		objstore_bucket_operations_total{bucket="test",operation="iter"} 0
   335  		objstore_bucket_operations_total{bucket="test",operation="upload"} 0
   336  	`),
   337  		"objstore_bucket_operations_total",
   338  		"objstore_bucket_operation_failures_total",
   339  	))
   340  }
   341  
   342  func TestPhlareDBGlobalMarker(t *testing.T) {
   343  	// Create a mocked block deletion mark in the global location.
   344  	bkt, _ := objstore_testutil.NewFilesystemBucket(t, context.Background(), t.TempDir())
   345  	bkt = BucketWithGlobalMarkers(bkt)
   346  
   347  	bkt = objstore.NewTenantBucketClient("foo-1", bkt, nil)
   348  
   349  	id := generateULID()
   350  
   351  	err := MarkForDeletion(context.Background(), log.NewLogfmtLogger(os.Stderr), bkt, id, "foo", false, prometheus.NewCounter(prometheus.CounterOpts{}))
   352  	require.NoError(t, err)
   353  
   354  	ok, err := bkt.Exists(context.Background(), DeletionMarkFilepath(id))
   355  
   356  	require.NoError(t, err)
   357  	require.True(t, ok)
   358  
   359  	ok, err = bkt.Exists(context.Background(), path.Join(id.String(), DeletionMarkFilename))
   360  	require.NoError(t, err)
   361  	require.True(t, ok)
   362  }