github.com/grafana/pyroscope@v1.18.0/pkg/ingester/retention_test.go (about) 1 package ingester 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io/fs" 8 "math/rand" 9 "os" 10 "path/filepath" 11 "slices" 12 "sort" 13 "strings" 14 "testing" 15 "time" 16 17 "github.com/go-kit/log" 18 "github.com/oklog/ulid/v2" 19 "github.com/samber/lo" 20 "github.com/spf13/afero" 21 "github.com/stretchr/testify/mock" 22 "github.com/stretchr/testify/require" 23 24 "github.com/grafana/pyroscope/pkg/phlaredb" 25 "github.com/grafana/pyroscope/pkg/phlaredb/shipper" 26 diskutil "github.com/grafana/pyroscope/pkg/util/disk" 27 ) 28 29 func TestDiskCleaner_DeleteUploadedBlocks(t *testing.T) { 30 t.Run("multi_tenant_blocks", func(t *testing.T) { 31 const anonTenantID = "anonymous" 32 const tenantID = "1234" 33 34 e := &mockBlockEvictor{} 35 36 bm := &mockBlockManager{} 37 bm.On("GetTenantIDs", mock.Anything). 38 Return([]string{anonTenantID, tenantID}, nil). 39 Once() 40 bm.On("GetBlocksForTenant", mock.Anything, anonTenantID). 41 Return([]*tenantBlock{{ 42 ID: ulid.MustParse(generateBlockID(t, "01AC")), 43 TenantID: anonTenantID, 44 Path: fmt.Sprintf("./data/%s/%s", anonTenantID, generateBlockID(t, "01AC")), 45 Uploaded: true, 46 }}, nil). 47 Once() 48 bm.On("GetBlocksForTenant", mock.Anything, tenantID). 49 Return([]*tenantBlock{{ 50 ID: ulid.MustParse(generateBlockID(t, "01AB")), 51 TenantID: anonTenantID, 52 Path: fmt.Sprintf("./data/%s/%s", anonTenantID, generateBlockID(t, "01AB")), 53 Uploaded: false, 54 }}, nil) 55 bm.On("DeleteBlock", mock.Anything, mock.Anything). 56 Return(nil). 57 Once() 58 59 dc := newDiskCleaner(log.NewNopLogger(), e, defaultRetentionPolicy(), phlaredb.Config{ 60 DataPath: "./data", 61 }) 62 dc.blockManager = bm 63 64 want := 1 65 got := dc.DeleteUploadedBlocks(context.Background()) 66 require.Equal(t, want, got) 67 }) 68 69 t.Run("delete_blocks_past_expiry", func(t *testing.T) { 70 // Two blocks are created and marked as "uploaded", but only one is past 71 // the expiry window. Only the expired one should be deleted. 72 73 const anonTenantID = "anonymous" 74 entropy := rand.New(rand.NewSource(0)) 75 expiry := 10 * time.Minute 76 77 nowMS := ulid.Timestamp(time.Now()) 78 nowID := ulid.MustNew(nowMS, entropy) 79 80 expiredMS := ulid.Timestamp(time.Now().Add(-(2 * expiry))) // Twice as long ago as the expiry. 81 expiredID := ulid.MustNew(expiredMS, entropy) 82 83 e := &mockBlockEvictor{} 84 85 bm := &mockBlockManager{} 86 bm.On("GetTenantIDs", mock.Anything). 87 Return([]string{anonTenantID}, nil). 88 Once() 89 bm.On("GetBlocksForTenant", mock.Anything, anonTenantID). 90 Return([]*tenantBlock{ 91 { 92 ID: nowID, 93 TenantID: anonTenantID, 94 Path: fmt.Sprintf("./data/%s/%s", anonTenantID, nowID.String()), 95 Uploaded: true, 96 }, 97 { 98 ID: expiredID, 99 TenantID: anonTenantID, 100 Path: fmt.Sprintf("./data/%s/%s", anonTenantID, expiredID.String()), 101 Uploaded: true, 102 }, 103 }, nil). 104 Once() 105 bm.On("DeleteBlock", mock.Anything, mock.Anything). 106 Return(nil). 107 Once() 108 109 policy := defaultRetentionPolicy() 110 policy.Expiry = expiry 111 112 dc := newDiskCleaner(log.NewNopLogger(), e, policy, phlaredb.Config{ 113 DataPath: "./data", 114 }) 115 dc.blockManager = bm 116 117 want := 1 118 got := dc.DeleteUploadedBlocks(context.Background()) 119 require.Equal(t, want, got) 120 }) 121 122 t.Run("no_tenant_dirs", func(t *testing.T) { 123 e := &mockBlockEvictor{} 124 125 bm := &mockBlockManager{} 126 bm.On("GetTenantIDs", mock.Anything). 127 Return([]string{}, nil). 128 Once() 129 130 dc := newDiskCleaner(log.NewNopLogger(), e, defaultRetentionPolicy(), phlaredb.Config{ 131 DataPath: "./data", 132 }) 133 dc.blockManager = bm 134 135 want := 0 136 got := dc.DeleteUploadedBlocks(context.Background()) 137 require.Equal(t, want, got) 138 }) 139 140 t.Run("no_block_dirs", func(t *testing.T) { 141 const tenantID = "anonymous" 142 143 e := &mockBlockEvictor{} 144 145 bm := &mockBlockManager{} 146 bm.On("GetTenantIDs", mock.Anything). 147 Return([]string{tenantID}, nil). 148 Once() 149 bm.On("GetBlocksForTenant", mock.Anything, tenantID). 150 Return([]*tenantBlock{}, nil). 151 Once() 152 153 dc := newDiskCleaner(log.NewNopLogger(), e, defaultRetentionPolicy(), phlaredb.Config{ 154 DataPath: "./data", 155 }) 156 dc.blockManager = bm 157 158 want := 0 159 got := dc.DeleteUploadedBlocks(context.Background()) 160 require.Equal(t, want, got) 161 }) 162 } 163 164 func TestDiskCleaner_EnforceHighDiskUtilization(t *testing.T) { 165 t.Run("no_high_disk", func(t *testing.T) { 166 const anonTenantID = "anonymous" 167 e := &mockBlockEvictor{} 168 169 bm := &mockBlockManager{} 170 bm.On("GetTenantIDs", mock.Anything). 171 Return([]string{anonTenantID}, nil). 172 Once() 173 bm.On("GetBlocksForTenant", mock.Anything, anonTenantID). 174 Return([]*tenantBlock{ 175 { 176 ID: ulid.MustParse(generateBlockID(t, "01AC")), 177 TenantID: anonTenantID, 178 Path: fmt.Sprintf("/data/%s/local/%s", anonTenantID, generateBlockID(t, "01AC")), 179 Uploaded: true, 180 }, 181 }, nil). 182 Once() 183 bm.On("DeleteBlock", mock.Anything, mock.Anything). 184 Return(nil) 185 186 vc := &mockVolumeChecker{} 187 vc.On("HasHighDiskUtilization", mock.Anything). 188 Return(&diskutil.VolumeStats{ 189 HighDiskUtilization: false, 190 BytesAvailable: 100, 191 BytesTotal: 200, 192 }, nil). 193 Once() 194 195 dc := newDiskCleaner(log.NewNopLogger(), e, defaultRetentionPolicy(), phlaredb.Config{ 196 DataPath: "./data", 197 }) 198 dc.blockManager = bm 199 dc.volumeChecker = vc 200 201 deleted, bytesFreed, hadHighDisk := dc.CleanupBlocksWhenHighDiskUtilization(context.Background()) 202 require.Equal(t, 0, deleted) 203 require.Equal(t, 0, bytesFreed) 204 require.False(t, hadHighDisk) 205 }) 206 207 t.Run("has_high_disk", func(t *testing.T) { 208 const anonTenantID = "anonymous" 209 210 e := &mockBlockEvictor{} 211 212 bm := &mockBlockManager{} 213 bm.On("GetTenantIDs", mock.Anything). 214 Return([]string{anonTenantID}, nil). 215 Once() 216 bm.On("GetBlocksForTenant", mock.Anything, anonTenantID). 217 Return([]*tenantBlock{ 218 { 219 ID: ulid.MustParse(generateBlockID(t, "01AC")), 220 TenantID: anonTenantID, 221 Path: fmt.Sprintf("/data/%s/local/%s", anonTenantID, generateBlockID(t, "01AC")), 222 Uploaded: true, 223 }, 224 { 225 ID: ulid.MustParse(generateBlockID(t, "01AD")), 226 TenantID: anonTenantID, 227 Path: fmt.Sprintf("/data/%s/local/%s", anonTenantID, generateBlockID(t, "01AD")), 228 Uploaded: false, 229 }, 230 { 231 ID: ulid.MustParse(generateBlockID(t, "01AE")), 232 TenantID: anonTenantID, 233 Path: fmt.Sprintf("/data/%s/local/%s", anonTenantID, generateBlockID(t, "01AE")), 234 Uploaded: false, 235 }, 236 }, nil). 237 Once() 238 bm.On("DeleteBlock", mock.Anything, mock.Anything). 239 Return(nil) 240 241 vc := &mockVolumeChecker{} 242 vc.On("HasHighDiskUtilization", mock.Anything). 243 Return(&diskutil.VolumeStats{ 244 HighDiskUtilization: true, 245 BytesAvailable: 0, 246 BytesTotal: 200, 247 }, nil). 248 Once() 249 vc.On("HasHighDiskUtilization", mock.Anything). 250 Return(&diskutil.VolumeStats{ 251 HighDiskUtilization: true, 252 BytesAvailable: 100, 253 BytesTotal: 200, 254 }, nil). 255 Once() 256 vc.On("HasHighDiskUtilization", mock.Anything). 257 Return(&diskutil.VolumeStats{ 258 HighDiskUtilization: false, 259 BytesAvailable: 100, 260 BytesTotal: 200, 261 }, nil). 262 Once() 263 264 dc := newDiskCleaner(log.NewNopLogger(), e, defaultRetentionPolicy(), phlaredb.Config{ 265 DataPath: "./data", 266 }) 267 dc.blockManager = bm 268 dc.volumeChecker = vc 269 270 deleted, bytesFreed, hadHighDisk := dc.CleanupBlocksWhenHighDiskUtilization(context.Background()) 271 require.Equal(t, 2, deleted) 272 require.Equal(t, 100, bytesFreed) 273 require.True(t, hadHighDisk) 274 }) 275 276 t.Run("has_high_disk_with_delayed_volume_checker_stats", func(t *testing.T) { 277 const anonTenantID = "anonymous" 278 279 e := &mockBlockEvictor{} 280 281 bm := &mockBlockManager{} 282 bm.On("GetTenantIDs", mock.Anything). 283 Return([]string{anonTenantID}, nil). 284 Once() 285 bm.On("GetBlocksForTenant", mock.Anything, anonTenantID). 286 Return([]*tenantBlock{ 287 { 288 ID: ulid.MustParse(generateBlockID(t, "01AC")), 289 TenantID: anonTenantID, 290 Path: fmt.Sprintf("/data/%s/local/%s", anonTenantID, generateBlockID(t, "01AC")), 291 Uploaded: true, 292 }, 293 { 294 ID: ulid.MustParse(generateBlockID(t, "01AD")), 295 TenantID: anonTenantID, 296 Path: fmt.Sprintf("/data/%s/local/%s", anonTenantID, generateBlockID(t, "01AD")), 297 Uploaded: false, 298 }, 299 }, nil). 300 Once() 301 bm.On("DeleteBlock", mock.Anything, mock.Anything). 302 Return(nil) 303 304 vc := &mockVolumeChecker{} 305 vc.On("HasHighDiskUtilization", mock.Anything). 306 Return(&diskutil.VolumeStats{ 307 HighDiskUtilization: true, 308 BytesAvailable: 100, 309 BytesTotal: 200, 310 }, nil). 311 Twice() // Report the same result twice, causing the loop to break. 312 313 dc := newDiskCleaner(log.NewNopLogger(), e, defaultRetentionPolicy(), phlaredb.Config{ 314 DataPath: "./data", 315 }) 316 dc.blockManager = bm 317 dc.volumeChecker = vc 318 319 deleted, bytesFreed, hadHighDisk := dc.CleanupBlocksWhenHighDiskUtilization(context.Background()) 320 require.Equal(t, 1, deleted) 321 require.Equal(t, 0, bytesFreed) 322 require.True(t, hadHighDisk) 323 }) 324 } 325 326 func TestDiskCleaner_isBlockDeletableForUploadedBlocks(t *testing.T) { 327 tests := []struct { 328 Name string 329 Expiry time.Duration 330 Block *tenantBlock 331 Want bool 332 }{ 333 { 334 Name: "uploaded_and_expired", 335 Expiry: 10 * time.Minute, 336 Block: &tenantBlock{ 337 ID: generateBlockIDFromTS(t, time.Now().Add(-(11 * time.Minute))), 338 Uploaded: true, 339 }, 340 Want: true, 341 }, 342 { 343 Name: "not_uploaded", 344 Expiry: 10 * time.Minute, 345 Block: &tenantBlock{ 346 ID: generateBlockIDFromTS(t, time.Now().Add(-(11 * time.Minute))), 347 Uploaded: false, 348 }, 349 Want: false, 350 }, 351 { 352 Name: "not_expired", 353 Expiry: 10 * time.Minute, 354 Block: &tenantBlock{ 355 ID: generateBlockIDFromTS(t, time.Now().Add(-(9 * time.Minute))), 356 Uploaded: true, 357 }, 358 Want: false, 359 }, 360 { 361 Name: "not_uploaded_and_not_expired", 362 Expiry: 10 * time.Minute, 363 Block: &tenantBlock{ 364 ID: generateBlockIDFromTS(t, time.Now().Add(-(9 * time.Minute))), 365 Uploaded: false, 366 }, 367 Want: false, 368 }, 369 } 370 371 dc := &diskCleaner{ 372 policy: defaultRetentionPolicy(), 373 } 374 375 for _, tt := range tests { 376 t.Run(tt.Name, func(t *testing.T) { 377 dc.policy.Expiry = tt.Expiry 378 379 got := tt.Block.Uploaded && dc.isExpired(tt.Block) 380 require.Equal(t, tt.Want, got) 381 }) 382 } 383 } 384 385 func TestFSBlockManager(t *testing.T) { 386 const root = "/data" 387 blocksByTenant := map[string][]*tenantBlock{ 388 "anonymous": { 389 { 390 ID: ulid.MustParse(generateBlockID(t, "01AC")), 391 TenantID: "anonymous", 392 Path: "/data/anonymous/local/" + generateBlockID(t, "01AC"), 393 Uploaded: false, 394 }, 395 { 396 ID: ulid.MustParse(generateBlockID(t, "01AD")), 397 TenantID: "anonymous", 398 Path: "/data/anonymous/local/" + generateBlockID(t, "01AD"), 399 Uploaded: true, 400 }, 401 }, 402 "1218": { 403 { 404 ID: ulid.MustParse(generateBlockID(t, "11AC")), 405 TenantID: "1218", 406 Path: "/data/1218/local/" + generateBlockID(t, "11AC"), 407 Uploaded: false, 408 }, 409 { 410 ID: ulid.MustParse(generateBlockID(t, "11AD")), 411 TenantID: "1218", 412 Path: "/data/1218/local/" + generateBlockID(t, "11AD"), 413 Uploaded: true, 414 }, 415 }, 416 } 417 418 e := &mockBlockEvictor{} 419 420 fs := &mockFS{ 421 Fs: afero.NewMemMapFs(), 422 Root: root, 423 } 424 for tenantID, blocks := range blocksByTenant { 425 blockIDs := lo.Map(blocks, func(block *tenantBlock, _ int) string { 426 return block.ID.String() 427 }) 428 fs.createBlocksForTenant(t, tenantID, blockIDs...) 429 430 uploadedBlockIDs := lo.Map(lo.Filter(blocks, func(block *tenantBlock, _ int) bool { 431 return block.Uploaded 432 }), func(block *tenantBlock, _ int) string { 433 return block.ID.String() 434 }) 435 fs.markBlocksShippedForTenant(t, tenantID, uploadedBlockIDs...) 436 } 437 438 // Create a lost+found directory. 439 fs.createDirectories(t, "lost+found") 440 441 t.Run("GetTenantIDs", func(t *testing.T) { 442 bm := newFSBlockManager(root, e, fs) 443 tenantIDs, err := bm.GetTenantIDs(context.Background()) 444 require.NoError(t, err) 445 require.Equal(t, []string{"1218", "anonymous"}, tenantIDs) 446 // Explicitly check lost+found isn't in tenant id list. 447 require.NotContains(t, tenantIDs, "lost+found") 448 }) 449 450 t.Run("GetBlocksForTenant", func(t *testing.T) { 451 bm := newFSBlockManager(root, e, fs) 452 blocks, err := bm.GetBlocksForTenant(context.Background(), "anonymous") 453 require.NoError(t, err) 454 require.Equal(t, blocksByTenant["anonymous"], blocks) 455 456 blocks, err = bm.GetBlocksForTenant(context.Background(), "1218") 457 require.NoError(t, err) 458 require.Equal(t, blocksByTenant["1218"], blocks) 459 460 _, err = bm.GetBlocksForTenant(context.Background(), "missing") 461 require.ErrorContains(t, err, "file does not exist") 462 }) 463 464 t.Run("DeleteBlock", func(t *testing.T) { 465 e = &mockBlockEvictor{} 466 e.On("evictBlock", "anonymous", mock.Anything, mock.Anything). 467 Return(nil) 468 469 bm := newFSBlockManager(root, e, fs) 470 for _, block := range blocksByTenant["anonymous"] { 471 err := bm.DeleteBlock(context.Background(), block) 472 require.NoError(t, err) 473 } 474 }) 475 } 476 477 func TestFSBlockManager_isTenantDir(t *testing.T) { 478 const root = "/data" 479 dirPaths := []string{ 480 // Skip, not tenant ids 481 "lost+found", 482 ".DS_Store", 483 484 // Skip, no local dir 485 "1234/head/01HKWWF79V1STKXBNYW7WCMDGM", 486 "1234/head/01HKWWF8939QM6E7BS69X0RASG", 487 488 // Tenant dirs 489 "anonymous/local/01HKWWF3CTFC5EJN6JJ96TY4W9", 490 "anonymous/local/01HKWWF4C298KVTEEQ3RW6TVHZ", 491 "1218/local/01HKWWF5BB2DJVDP0DTMT9MDMN", 492 "1218/local/01HKWWF6AKVZDCWQB12MHWG7FN", 493 "9876/local", 494 } 495 filePaths := []string{ 496 // Skip all files 497 "somefile.txt", 498 } 499 500 fs := &mockFS{ 501 Fs: afero.NewMemMapFs(), 502 Root: root, 503 } 504 fs.createDirectories(t, dirPaths...) 505 fs.createFiles(t, filePaths...) 506 507 gotTenantIDs := []string{} 508 entries, err := fs.ReadDir(fs.Root) 509 require.NoError(t, err) 510 511 bm := &realFSBlockManager{ 512 Root: fs.Root, 513 FS: fs, 514 } 515 for _, entry := range entries { 516 if bm.isTenantDir(fs.Root, entry) { 517 gotTenantIDs = append(gotTenantIDs, entry.Name()) 518 } 519 } 520 slices.Sort(gotTenantIDs) 521 522 wantTenantIDs := []string{"1218", "9876", "anonymous"} 523 require.Equal(t, wantTenantIDs, gotTenantIDs) 524 } 525 526 func TestSortBlocks(t *testing.T) { 527 createAnonymousBlock := func(t *testing.T, blockID string, uploaded bool) *tenantBlock { 528 t.Helper() 529 530 return &tenantBlock{ 531 ID: ulid.MustParse(blockID), 532 TenantID: "anonymous", 533 Path: fmt.Sprintf("/data/anonymous/local/%s", blockID), 534 Uploaded: uploaded, 535 } 536 } 537 538 tests := []struct { 539 Name string 540 Blocks []*tenantBlock 541 Want []*tenantBlock 542 }{ 543 { 544 Name: "uploaded_and_non_uploaded", 545 Blocks: []*tenantBlock{ 546 createAnonymousBlock(t, "01HH5BVHA006AFVGQT5ZYC0GEK", true), // unix ms: 1702061000000 547 createAnonymousBlock(t, "01HH5CT1W0ZW908PVKS1Q4ZYAZ", false), // unix ms: 1702062000000 548 createAnonymousBlock(t, "01HH5DRJE0YSHABVQ85AYZ8JHD", true), // unix ms: 1702063000000 549 createAnonymousBlock(t, "01HH5EQ3001DTZP60DNX4AF7Q0", false), // unix ms: 1702064000000 550 createAnonymousBlock(t, "01HH5FNKJ0P46KJHJHGM7X98BR", true), // unix ms: 1702065000000 551 }, 552 Want: []*tenantBlock{ 553 createAnonymousBlock(t, "01HH5BVHA006AFVGQT5ZYC0GEK", true), // unix ms: 1702061000000 554 createAnonymousBlock(t, "01HH5DRJE0YSHABVQ85AYZ8JHD", true), // unix ms: 1702063000000 555 createAnonymousBlock(t, "01HH5FNKJ0P46KJHJHGM7X98BR", true), // unix ms: 1702065000000 556 createAnonymousBlock(t, "01HH5CT1W0ZW908PVKS1Q4ZYAZ", false), // unix ms: 1702062000000 557 createAnonymousBlock(t, "01HH5EQ3001DTZP60DNX4AF7Q0", false), // unix ms: 1702064000000 558 }, 559 }, 560 { 561 Name: "uploaded_and_non_uploaded_at_same_timestamp", 562 Blocks: []*tenantBlock{ 563 createAnonymousBlock(t, "01HH5BVHA006AFVGQT5ZYC0GEK", false), // unix ms: 1702061000000 564 createAnonymousBlock(t, "01HH5BVHA0ZW908PVKS1Q4ZYAZ", true), // unix ms: 1702061000000 565 }, 566 Want: []*tenantBlock{ 567 createAnonymousBlock(t, "01HH5BVHA0ZW908PVKS1Q4ZYAZ", true), // unix ms: 1702061000000 568 createAnonymousBlock(t, "01HH5BVHA006AFVGQT5ZYC0GEK", false), // unix ms: 1702061000000 569 }, 570 }, 571 } 572 573 for _, tt := range tests { 574 t.Run(tt.Name, func(t *testing.T) { 575 sort.Sort(blocksByUploadAndAge(tt.Blocks)) 576 require.Equal(t, tt.Want, tt.Blocks) 577 }) 578 } 579 } 580 581 type mockFS struct { 582 afero.Fs 583 584 Root string 585 } 586 587 func (mfs *mockFS) Open(name string) (fs.File, error) { 588 return mfs.Fs.Open(name) 589 } 590 591 func (mfs *mockFS) ReadDir(name string) ([]fs.DirEntry, error) { 592 dirs, err := afero.ReadDir(mfs.Fs, name) 593 if err != nil { 594 return nil, err 595 } 596 597 entries := make([]fs.DirEntry, 0, len(dirs)) 598 for _, dir := range dirs { 599 entries = append(entries, fs.FileInfoToDirEntry(dir)) 600 } 601 return entries, nil 602 } 603 604 func (mfs *mockFS) createBlocksForTenant(t *testing.T, tenantID string, blockIDs ...string) { 605 t.Helper() 606 localDirPath := filepath.Join(mfs.Root, tenantID, phlareDBLocalPath) 607 for _, blockID := range blockIDs { 608 path := filepath.Join(localDirPath, blockID) 609 err := mfs.MkdirAll(path, 0755) 610 if err != nil { 611 t.Fatalf("failed to create block: %s: %v", localDirPath, err) 612 return 613 } 614 } 615 } 616 617 func (mfs *mockFS) markBlocksShippedForTenant(t *testing.T, tenantID string, blockIDs ...string) { 618 t.Helper() 619 localDirPath := filepath.Join(mfs.Root, tenantID, phlareDBLocalPath) 620 shipperPath := filepath.Join(localDirPath, shipper.MetaFilename) 621 bytes, err := fs.ReadFile(mfs, shipperPath) 622 if err != nil && !os.IsNotExist(err) { 623 t.Fatalf("failed to read shipper.json: %v", err) 624 return 625 } 626 627 meta := shipper.Meta{} 628 if len(bytes) != 0 { 629 err = json.Unmarshal(bytes, &meta) 630 if err != nil { 631 t.Fatalf("failed to unmarshal shipper.json: %v", err) 632 return 633 } 634 } 635 636 for _, blockID := range blockIDs { 637 id, err := ulid.Parse(blockID) 638 if err != nil { 639 t.Fatalf("failed to create ULID from %s: %v", blockID, err) 640 return 641 } 642 meta.Uploaded = append(meta.Uploaded, id) 643 } 644 645 bytes, err = json.Marshal(meta) 646 if err != nil { 647 t.Fatalf("failed to marshal shipper.json: %v", err) 648 return 649 } 650 err = afero.WriteFile(mfs.Fs, shipperPath, bytes, 0755) 651 if err != nil { 652 t.Fatalf("failed to update shipper.json: %v", err) 653 } 654 } 655 656 func (mfs *mockFS) createDirectories(t *testing.T, paths ...string) { 657 t.Helper() 658 for _, path := range paths { 659 path = filepath.Join(mfs.Root, path) 660 err := mfs.MkdirAll(path, 0755) 661 if err != nil { 662 t.Fatalf("failed to create directory: %s: %v", path, err) 663 return 664 } 665 } 666 } 667 668 func (mfs *mockFS) createFiles(t *testing.T, paths ...string) { 669 t.Helper() 670 for _, path := range paths { 671 path = filepath.Join(mfs.Root, path) 672 _, err := mfs.Create(path) 673 if err != nil { 674 t.Fatalf("failed to create file: %s: %v", path, err) 675 return 676 } 677 } 678 } 679 680 type mockBlockManager struct { 681 mock.Mock 682 } 683 684 func (bm *mockBlockManager) DeleteBlock(ctx context.Context, block *tenantBlock) error { 685 args := bm.Called(ctx, block) 686 return args.Error(0) 687 } 688 689 func (bm *mockBlockManager) GetBlocksForTenant(ctx context.Context, tenantID string) ([]*tenantBlock, error) { 690 args := bm.Called(ctx, tenantID) 691 return args[0].([]*tenantBlock), args.Error(1) 692 } 693 694 func (bm *mockBlockManager) GetTenantIDs(ctx context.Context) ([]string, error) { 695 args := bm.Called(ctx) 696 return args[0].([]string), args.Error(1) 697 } 698 699 type mockBlockEvictor struct { 700 mock.Mock 701 } 702 703 func (e *mockBlockEvictor) evictBlock(tenant string, b ulid.ULID, fn func() error) error { 704 args := e.Called(tenant, b, fn) 705 706 err := fn() 707 if err != nil { 708 return err 709 } 710 711 return args.Error(0) 712 } 713 714 type mockVolumeChecker struct { 715 mock.Mock 716 } 717 718 func (vc *mockVolumeChecker) HasHighDiskUtilization(path string) (*diskutil.VolumeStats, error) { 719 args := vc.Called(path) 720 return args[0].(*diskutil.VolumeStats), args.Error(1) 721 } 722 723 func generateBlockID(t *testing.T, prefix string) string { 724 t.Helper() 725 726 const maxLen = 26 727 const padding = "0" 728 return fmt.Sprintf("%s%s", prefix, strings.Repeat(padding, maxLen-len(prefix))) 729 } 730 731 func generateBlockIDFromTS(t *testing.T, ts time.Time) ulid.ULID { 732 t.Helper() 733 734 entropy := rand.New(rand.NewSource(time.Now().UnixNano())) 735 return ulid.MustNew(ulid.Timestamp(ts), entropy) 736 }