github.com/grafana/pyroscope@v1.18.0/pkg/compactionworker/worker_test.go (about) 1 package compactionworker 2 3 import ( 4 "context" 5 "errors" 6 "path/filepath" 7 "strings" 8 "sync" 9 "sync/atomic" 10 "testing" 11 "time" 12 13 "github.com/go-kit/log" 14 "github.com/prometheus/client_golang/prometheus" 15 "github.com/stretchr/testify/assert" 16 "github.com/stretchr/testify/mock" 17 "github.com/stretchr/testify/require" 18 thanosstore "github.com/thanos-io/objstore" 19 "google.golang.org/grpc" 20 21 metastorev1 "github.com/grafana/pyroscope/api/gen/proto/go/metastore/v1" 22 "github.com/grafana/pyroscope/pkg/block" 23 "github.com/grafana/pyroscope/pkg/objstore" 24 "github.com/grafana/pyroscope/pkg/test" 25 "github.com/grafana/pyroscope/pkg/test/mocks/mockmetastorev1" 26 "github.com/grafana/pyroscope/pkg/test/mocks/mockobjstore" 27 ) 28 29 type MetastoreClientMock struct { 30 *mockmetastorev1.MockCompactionServiceClient 31 *mockmetastorev1.MockIndexServiceClient 32 } 33 34 func createTestWorker(t *testing.T, client MetastoreClient, compactFn compactFunc, bucket objstore.Bucket) *Worker { 35 config := Config{ 36 JobConcurrency: 2, 37 JobPollInterval: 100 * time.Millisecond, 38 RequestTimeout: time.Second, 39 CleanupMaxDuration: time.Second, 40 TempDir: t.TempDir(), 41 } 42 43 worker, err := New( 44 log.NewNopLogger(), 45 config, 46 client, 47 bucket, 48 prometheus.NewRegistry(), 49 nil, // ruler 50 nil, // exporter 51 ) 52 53 require.NoError(t, err) 54 worker.compactFn = compactFn 55 return worker 56 } 57 58 func runWorker(w *Worker) { 59 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 60 defer cancel() 61 62 var wg sync.WaitGroup 63 wg.Add(1) 64 go func() { 65 defer wg.Done() 66 svc := w.Service() 67 _ = svc.StartAsync(ctx) 68 _ = svc.AwaitRunning(ctx) 69 time.Sleep(500 * time.Millisecond) 70 svc.StopAsync() 71 _ = svc.AwaitTerminated(ctx) 72 }() 73 74 wg.Wait() 75 } 76 77 func TestWorker_SuccessfulCompaction(t *testing.T) { 78 bucket := mockobjstore.NewMockBucket(t) 79 compactionClient := mockmetastorev1.NewMockCompactionServiceClient(t) 80 indexClient := mockmetastorev1.NewMockIndexServiceClient(t) 81 client := &MetastoreClientMock{ 82 MockCompactionServiceClient: compactionClient, 83 MockIndexServiceClient: indexClient, 84 } 85 86 block1ID := test.ULID("2024-01-01T10:00:00Z") 87 block2ID := test.ULID("2024-01-01T11:00:00Z") 88 compactedBlockID := test.ULID("2024-01-01T12:00:00Z") 89 90 compactFn := func(ctx context.Context, blocks []*metastorev1.BlockMeta, storage objstore.Bucket, options ...block.CompactionOption) ([]*metastorev1.BlockMeta, error) { 91 require.Len(t, blocks, 2) 92 assert.Equal(t, block1ID, blocks[0].Id) 93 assert.Equal(t, block2ID, blocks[1].Id) 94 return []*metastorev1.BlockMeta{{Id: compactedBlockID, Tenant: 1, Shard: 1, CompactionLevel: 2}}, nil 95 } 96 97 w := createTestWorker(t, client, compactFn, bucket) 98 99 job := &metastorev1.CompactionJob{ 100 Name: "test-job", 101 Tenant: "test-tenant", 102 Shard: 1, 103 CompactionLevel: 1, 104 SourceBlocks: []string{block1ID, block2ID}, 105 } 106 assignment := &metastorev1.CompactionJobAssignment{ 107 Name: "test-job", 108 Token: 12345, 109 } 110 111 metadata := []*metastorev1.BlockMeta{ 112 {Id: block1ID, Tenant: 1, Shard: 1}, 113 {Id: block2ID, Tenant: 1, Shard: 1}, 114 } 115 compactionClient.EXPECT().PollCompactionJobs(mock.Anything, mock.MatchedBy(func(req *metastorev1.PollCompactionJobsRequest) bool { 116 return req.JobCapacity > 0 117 }), mock.Anything).Return(&metastorev1.PollCompactionJobsResponse{ 118 CompactionJobs: []*metastorev1.CompactionJob{job}, 119 Assignments: []*metastorev1.CompactionJobAssignment{assignment}, 120 }, nil).Once() 121 122 indexClient.EXPECT().GetBlockMetadata(mock.Anything, mock.Anything, mock.Anything).Return(&metastorev1.GetBlockMetadataResponse{ 123 Blocks: metadata, 124 }, nil).Once() 125 126 compactionClient.EXPECT().PollCompactionJobs(mock.Anything, mock.MatchedBy(func(req *metastorev1.PollCompactionJobsRequest) bool { 127 return len(req.StatusUpdates) > 0 && req.StatusUpdates[0].Status == metastorev1.CompactionJobStatus_COMPACTION_STATUS_SUCCESS 128 }), mock.Anything).Return(&metastorev1.PollCompactionJobsResponse{}, nil).Once() 129 130 // Additional polls should return empty responses. 131 compactionClient.EXPECT().PollCompactionJobs(mock.Anything, mock.Anything, mock.Anything).Return(&metastorev1.PollCompactionJobsResponse{}, nil).Maybe() 132 133 runWorker(w) 134 } 135 136 func TestWorker_CompactionFailure(t *testing.T) { 137 bucket := mockobjstore.NewMockBucket(t) 138 compactionClient := mockmetastorev1.NewMockCompactionServiceClient(t) 139 indexClient := mockmetastorev1.NewMockIndexServiceClient(t) 140 client := &MetastoreClientMock{ 141 MockCompactionServiceClient: compactionClient, 142 MockIndexServiceClient: indexClient, 143 } 144 145 block1ID := test.ULID("2024-01-01T10:00:00Z") 146 147 compactFn := func(ctx context.Context, blocks []*metastorev1.BlockMeta, storage objstore.Bucket, options ...block.CompactionOption) ([]*metastorev1.BlockMeta, error) { 148 return nil, errors.New("compaction failed") 149 } 150 151 w := createTestWorker(t, client, compactFn, bucket) 152 153 job := &metastorev1.CompactionJob{ 154 Name: "test-job", 155 Tenant: "test-tenant", 156 Shard: 1, 157 CompactionLevel: 1, 158 SourceBlocks: []string{block1ID}, 159 } 160 assignment := &metastorev1.CompactionJobAssignment{ 161 Name: "test-job", 162 Token: 12345, 163 } 164 165 metadata := []*metastorev1.BlockMeta{ 166 {Id: block1ID, Tenant: 1, Shard: 1}, 167 } 168 compactionClient.EXPECT().PollCompactionJobs(mock.Anything, mock.MatchedBy(func(req *metastorev1.PollCompactionJobsRequest) bool { 169 return req.JobCapacity > 0 170 }), mock.Anything).Return(&metastorev1.PollCompactionJobsResponse{ 171 CompactionJobs: []*metastorev1.CompactionJob{job}, 172 Assignments: []*metastorev1.CompactionJobAssignment{assignment}, 173 }, nil).Once() 174 175 indexClient.EXPECT().GetBlockMetadata(mock.Anything, mock.Anything, mock.Anything).Return(&metastorev1.GetBlockMetadataResponse{ 176 Blocks: metadata, 177 }, nil).Once() 178 179 bucket.EXPECT().IsObjNotFoundErr(mock.Anything).Return(false).Maybe() 180 compactionClient.EXPECT().PollCompactionJobs(mock.Anything, mock.Anything, mock.Anything).Return(&metastorev1.PollCompactionJobsResponse{}, nil).Maybe() 181 182 runWorker(w) 183 } 184 185 func TestWorker_JobCancellation(t *testing.T) { 186 bucket := mockobjstore.NewMockBucket(t) 187 compactionClient := mockmetastorev1.NewMockCompactionServiceClient(t) 188 indexClient := mockmetastorev1.NewMockIndexServiceClient(t) 189 client := &MetastoreClientMock{ 190 MockCompactionServiceClient: compactionClient, 191 MockIndexServiceClient: indexClient, 192 } 193 194 block1ID := test.ULID("2024-01-01T10:00:00Z") 195 196 compactFn := func(ctx context.Context, blocks []*metastorev1.BlockMeta, storage objstore.Bucket, options ...block.CompactionOption) ([]*metastorev1.BlockMeta, error) { 197 return nil, context.Canceled 198 } 199 200 w := createTestWorker(t, client, compactFn, bucket) 201 202 job := &metastorev1.CompactionJob{ 203 Name: "test-job", 204 Tenant: "test-tenant", 205 Shard: 1, 206 CompactionLevel: 1, 207 SourceBlocks: []string{block1ID}, 208 } 209 assignment := &metastorev1.CompactionJobAssignment{ 210 Name: "test-job", 211 Token: 12345, 212 } 213 214 compactionClient.EXPECT().PollCompactionJobs(mock.Anything, mock.MatchedBy(func(req *metastorev1.PollCompactionJobsRequest) bool { 215 return req.JobCapacity > 0 216 }), mock.Anything).Return(&metastorev1.PollCompactionJobsResponse{ 217 CompactionJobs: []*metastorev1.CompactionJob{job}, 218 Assignments: []*metastorev1.CompactionJobAssignment{assignment}, 219 }, nil).Once() 220 221 indexClient.EXPECT().GetBlockMetadata(mock.Anything, mock.Anything, mock.Anything).Return(&metastorev1.GetBlockMetadataResponse{ 222 Blocks: []*metastorev1.BlockMeta{{Id: block1ID, Tenant: 1, Shard: 1}}, 223 }, nil).Maybe() 224 225 compactionClient.EXPECT().PollCompactionJobs(mock.Anything, mock.Anything, mock.Anything).Return(&metastorev1.PollCompactionJobsResponse{}, nil).Maybe() 226 227 runWorker(w) 228 } 229 230 func TestWorker_TombstoneHandling(t *testing.T) { 231 bucket := mockobjstore.NewMockBucket(t) 232 compactionClient := mockmetastorev1.NewMockCompactionServiceClient(t) 233 indexClient := mockmetastorev1.NewMockIndexServiceClient(t) 234 client := &MetastoreClientMock{ 235 MockCompactionServiceClient: compactionClient, 236 MockIndexServiceClient: indexClient, 237 } 238 239 sourceBlockID := test.ULID("2024-01-01T11:00:00Z") 240 compactedBlockID := test.ULID("2024-01-01T12:00:00Z") 241 oldBlock1ID := test.ULID("2024-01-01T08:00:00Z") 242 oldBlock2ID := test.ULID("2024-01-01T09:00:00Z") 243 244 compactFn := func(ctx context.Context, blocks []*metastorev1.BlockMeta, storage objstore.Bucket, options ...block.CompactionOption) ([]*metastorev1.BlockMeta, error) { 245 return []*metastorev1.BlockMeta{{Id: compactedBlockID, Tenant: 1, Shard: 1, CompactionLevel: 2}}, nil 246 } 247 248 w := createTestWorker(t, client, compactFn, bucket) 249 250 tombstones := []*metastorev1.Tombstones{{ 251 Blocks: &metastorev1.BlockTombstones{ 252 Name: "test-tombstone", 253 Tenant: "test-tenant", 254 Shard: 1, 255 CompactionLevel: 1, 256 Blocks: []string{oldBlock1ID, oldBlock2ID}, 257 }, 258 }} 259 260 job := &metastorev1.CompactionJob{ 261 Name: "test-job", 262 Tenant: "test-tenant", 263 Shard: 1, 264 CompactionLevel: 1, 265 SourceBlocks: []string{sourceBlockID}, 266 Tombstones: tombstones, 267 } 268 assignment := &metastorev1.CompactionJobAssignment{ 269 Name: "test-job", 270 Token: 12345, 271 } 272 273 metadata := []*metastorev1.BlockMeta{ 274 {Id: sourceBlockID, Tenant: 1, Shard: 1}, 275 } 276 277 compactionClient.EXPECT().PollCompactionJobs(mock.Anything, mock.MatchedBy(func(req *metastorev1.PollCompactionJobsRequest) bool { 278 return req.JobCapacity > 0 279 }), mock.Anything).Return(&metastorev1.PollCompactionJobsResponse{ 280 CompactionJobs: []*metastorev1.CompactionJob{job}, 281 Assignments: []*metastorev1.CompactionJobAssignment{assignment}, 282 }, nil).Once() 283 284 indexClient.EXPECT().GetBlockMetadata(mock.Anything, mock.Anything, mock.Anything).Return(&metastorev1.GetBlockMetadataResponse{ 285 Blocks: metadata, 286 }, nil).Once() 287 288 bucket.EXPECT().Delete(mock.Anything, mock.MatchedBy(func(path string) bool { 289 return (strings.Contains(path, oldBlock1ID) || strings.Contains(path, oldBlock2ID)) && 290 strings.Contains(path, "test-tenant") 291 })).Return(nil).Times(2) 292 293 compactionClient.EXPECT().PollCompactionJobs(mock.Anything, mock.MatchedBy(func(req *metastorev1.PollCompactionJobsRequest) bool { 294 return len(req.StatusUpdates) > 0 && req.StatusUpdates[0].Status == metastorev1.CompactionJobStatus_COMPACTION_STATUS_SUCCESS 295 }), mock.Anything).Return(&metastorev1.PollCompactionJobsResponse{}, nil).Once() 296 297 compactionClient.EXPECT().PollCompactionJobs(mock.Anything, mock.Anything, mock.Anything).Return(&metastorev1.PollCompactionJobsResponse{}, nil).Maybe() 298 299 runWorker(w) 300 } 301 302 func TestWorker_MetadataNotFound(t *testing.T) { 303 bucket := mockobjstore.NewMockBucket(t) 304 compactionClient := mockmetastorev1.NewMockCompactionServiceClient(t) 305 indexClient := mockmetastorev1.NewMockIndexServiceClient(t) 306 client := &MetastoreClientMock{ 307 MockCompactionServiceClient: compactionClient, 308 MockIndexServiceClient: indexClient, 309 } 310 311 missingBlockID := test.ULID("2024-01-01T10:00:00Z") 312 313 compactFn := func(ctx context.Context, blocks []*metastorev1.BlockMeta, storage objstore.Bucket, options ...block.CompactionOption) ([]*metastorev1.BlockMeta, error) { 314 t.Error("compactFn should not be called when metadata is not found") 315 return nil, errors.New("should not be called") 316 } 317 318 w := createTestWorker(t, client, compactFn, bucket) 319 320 job := &metastorev1.CompactionJob{ 321 Name: "test-job", 322 Tenant: "test-tenant", 323 Shard: 1, 324 CompactionLevel: 1, 325 SourceBlocks: []string{missingBlockID}, 326 } 327 assignment := &metastorev1.CompactionJobAssignment{ 328 Name: "test-job", 329 Token: 12345, 330 } 331 332 compactionClient.EXPECT().PollCompactionJobs(mock.Anything, mock.MatchedBy(func(req *metastorev1.PollCompactionJobsRequest) bool { 333 return req.JobCapacity > 0 334 }), mock.Anything).Return(&metastorev1.PollCompactionJobsResponse{ 335 CompactionJobs: []*metastorev1.CompactionJob{job}, 336 Assignments: []*metastorev1.CompactionJobAssignment{assignment}, 337 }, nil).Once() 338 339 indexClient.EXPECT().GetBlockMetadata(mock.Anything, mock.Anything, mock.Anything).Return((*metastorev1.GetBlockMetadataResponse)(nil), errors.New("metadata not found")).Once() 340 341 compactionClient.EXPECT().PollCompactionJobs(mock.Anything, mock.Anything, mock.Anything).Return(&metastorev1.PollCompactionJobsResponse{}, nil).Maybe() 342 343 runWorker(w) 344 } 345 346 func TestWorker_ShardTombstoneHandling(t *testing.T) { 347 bucket := mockobjstore.NewMockBucket(t) 348 compactionClient := mockmetastorev1.NewMockCompactionServiceClient(t) 349 indexClient := mockmetastorev1.NewMockIndexServiceClient(t) 350 client := &MetastoreClientMock{ 351 MockCompactionServiceClient: compactionClient, 352 MockIndexServiceClient: indexClient, 353 } 354 355 sourceBlockID := test.ULID("2024-01-01T11:00:00Z") 356 compactedBlockID := test.ULID("2024-01-01T12:00:00Z") 357 oldBlock1ID := test.ULID("2024-01-01T08:00:00Z") 358 oldBlock2ID := test.ULID("2024-01-01T09:00:00Z") 359 newBlock1ID := test.ULID("2024-01-01T10:30:00Z") 360 newBlock2ID := test.ULID("2024-01-01T11:30:00Z") 361 362 compactFn := func(ctx context.Context, blocks []*metastorev1.BlockMeta, storage objstore.Bucket, options ...block.CompactionOption) ([]*metastorev1.BlockMeta, error) { 363 return []*metastorev1.BlockMeta{ 364 {Id: compactedBlockID, Tenant: 1, Shard: 1, CompactionLevel: 2}, 365 }, nil 366 } 367 368 w := createTestWorker(t, client, compactFn, bucket) 369 370 tombstoneTime := test.Time("2024-01-01T09:00:00Z") 371 duration := time.Hour 372 shardTombstone := &metastorev1.Tombstones{ 373 Shard: &metastorev1.ShardTombstone{ 374 Name: "test-shard-tombstone", 375 Tenant: "test-tenant", 376 Shard: 1, 377 Timestamp: tombstoneTime.UnixNano(), 378 Duration: int64(duration), 379 }, 380 } 381 382 job := &metastorev1.CompactionJob{ 383 Name: "test-job", 384 Tenant: "test-tenant", 385 Shard: 1, 386 CompactionLevel: 1, 387 SourceBlocks: []string{sourceBlockID}, 388 Tombstones: []*metastorev1.Tombstones{shardTombstone}, 389 } 390 assignment := &metastorev1.CompactionJobAssignment{ 391 Name: "test-job", 392 Token: 12345, 393 } 394 395 metadata := []*metastorev1.BlockMeta{ 396 {Id: sourceBlockID, Tenant: 1, Shard: 1}, 397 } 398 399 compactionClient.EXPECT().PollCompactionJobs(mock.Anything, mock.MatchedBy(func(req *metastorev1.PollCompactionJobsRequest) bool { 400 return req.JobCapacity > 0 401 }), mock.Anything).Return(&metastorev1.PollCompactionJobsResponse{ 402 CompactionJobs: []*metastorev1.CompactionJob{job}, 403 Assignments: []*metastorev1.CompactionJobAssignment{assignment}, 404 }, nil).Once() 405 406 indexClient.EXPECT().GetBlockMetadata(mock.Anything, mock.Anything, mock.Anything).Return(&metastorev1.GetBlockMetadataResponse{ 407 Blocks: metadata, 408 }, nil).Once() 409 410 expectedDir := block.BuildObjectDir("test-tenant", 1) 411 bucket.EXPECT().Iter(mock.Anything, expectedDir, mock.Anything, mock.Anything).Run( 412 func(ctx context.Context, dir string, fn func(string) error, options ...thanosstore.IterOption) { 413 blockPaths := []string{ 414 block.BuildObjectPath("test-tenant", 1, 1, oldBlock1ID), // Should be deleted 415 block.BuildObjectPath("test-tenant", 1, 1, oldBlock2ID), // Should be deleted 416 block.BuildObjectPath("test-tenant", 1, 1, newBlock1ID), // SkipAll 417 block.BuildObjectPath("test-tenant", 1, 1, newBlock2ID), // 418 block.BuildObjectPath("test-tenant", 1, 1, sourceBlockID), // 419 } 420 for _, path := range blockPaths { 421 if err := fn(path); err != nil { 422 return // Return(filepath.SkipAll).Once() 423 } 424 } 425 }).Return(filepath.SkipAll).Once() 426 427 bucket.EXPECT().Delete(mock.Anything, block.BuildObjectPath("test-tenant", 1, 1, oldBlock1ID)).Return(nil).Once() 428 bucket.EXPECT().Delete(mock.Anything, block.BuildObjectPath("test-tenant", 1, 1, oldBlock2ID)).Return(nil).Once() 429 compactionClient.EXPECT().PollCompactionJobs(mock.Anything, mock.MatchedBy(func(req *metastorev1.PollCompactionJobsRequest) bool { 430 return len(req.StatusUpdates) > 0 && req.StatusUpdates[0].Status == metastorev1.CompactionJobStatus_COMPACTION_STATUS_SUCCESS 431 }), mock.Anything).Return(&metastorev1.PollCompactionJobsResponse{}, nil).Once() 432 433 compactionClient.EXPECT().PollCompactionJobs(mock.Anything, mock.Anything, mock.Anything).Return(&metastorev1.PollCompactionJobsResponse{}, nil).Maybe() 434 435 runWorker(w) 436 } 437 438 var skipCompactionFn = func(context.Context, []*metastorev1.BlockMeta, objstore.Bucket, ...block.CompactionOption) ([]*metastorev1.BlockMeta, error) { 439 return nil, nil 440 } 441 442 func TestWorker_CleanupMaxDurationAtShutdown(t *testing.T) { 443 bucket := mockobjstore.NewMockBucket(t) 444 compactionClient := mockmetastorev1.NewMockCompactionServiceClient(t) 445 indexClient := mockmetastorev1.NewMockIndexServiceClient(t) 446 client := &MetastoreClientMock{ 447 MockCompactionServiceClient: compactionClient, 448 MockIndexServiceClient: indexClient, 449 } 450 451 config := Config{ 452 JobConcurrency: 1, 453 JobPollInterval: 100 * time.Millisecond, 454 RequestTimeout: time.Second, 455 CleanupMaxDuration: 15 * time.Second, 456 TempDir: t.TempDir(), 457 } 458 459 worker, err := New( 460 log.NewNopLogger(), 461 config, 462 client, 463 bucket, 464 nil, // registry 465 nil, // ruler 466 nil, // exporter 467 ) 468 require.NoError(t, err) 469 worker.compactFn = skipCompactionFn 470 471 job := &metastorev1.CompactionJob{Name: "test-job"} 472 assignment := &metastorev1.CompactionJobAssignment{Name: job.Name, Token: 12345} 473 job.Tombstones = []*metastorev1.Tombstones{{ 474 Blocks: &metastorev1.BlockTombstones{ 475 Name: "test-tombstone", 476 Tenant: "test-tenant", 477 Shard: 1, 478 CompactionLevel: 1, 479 Blocks: []string{"a", "b"}, 480 }, 481 }} 482 483 var once sync.Once 484 done := make(chan struct{}) 485 triggerShutdown := func(context.Context, *metastorev1.PollCompactionJobsRequest, ...grpc.CallOption) { 486 once.Do(func() { close(done) }) 487 } 488 489 compactionClient.EXPECT(). 490 PollCompactionJobs(mock.Anything, mock.Anything, mock.Anything). 491 Run(triggerShutdown). 492 Return(&metastorev1.PollCompactionJobsResponse{ 493 CompactionJobs: []*metastorev1.CompactionJob{job}, 494 Assignments: []*metastorev1.CompactionJobAssignment{assignment}, 495 }, nil).Once() 496 497 indexClient.EXPECT(). 498 GetBlockMetadata(mock.Anything, mock.Anything, mock.Anything). 499 Return(&metastorev1.GetBlockMetadataResponse{}, nil). 500 Once() 501 502 var blocksDeleted atomic.Int32 503 bucket.EXPECT(). 504 Delete(mock.Anything, mock.Anything). 505 Run(func(context.Context, string) { 506 blocksDeleted.Add(1) 507 time.Sleep(100 * time.Millisecond) 508 }).Return(nil).Times(2) 509 510 compactionClient.EXPECT(). 511 PollCompactionJobs(mock.Anything, mock.Anything, mock.Anything). 512 Return(&metastorev1.PollCompactionJobsResponse{}, nil).Maybe() 513 514 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 515 defer cancel() 516 517 svc := worker.Service() 518 assert.NoError(t, svc.StartAsync(ctx)) 519 assert.NoError(t, svc.AwaitRunning(ctx)) 520 521 // Wait for the job to be polled and shutdown immediately. 522 <-done 523 svc.StopAsync() 524 assert.NoError(t, svc.AwaitTerminated(ctx)) 525 526 require.Equal(t, 2, int(blocksDeleted.Load())) 527 }