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 }