github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/erasure-healing_test.go (about) 1 // Copyright (c) 2015-2021 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package cmd 19 20 import ( 21 "bytes" 22 "context" 23 "crypto/rand" 24 "crypto/sha256" 25 "errors" 26 "io" 27 "os" 28 "path" 29 "reflect" 30 "testing" 31 "time" 32 33 "github.com/dustin/go-humanize" 34 uuid2 "github.com/google/uuid" 35 "github.com/minio/madmin-go/v3" 36 "github.com/minio/minio/internal/config/storageclass" 37 ) 38 39 // Tests isObjectDangling function 40 func TestIsObjectDangling(t *testing.T) { 41 fi := newFileInfo("test-object", 2, 2) 42 fi.Erasure.Index = 1 43 44 ifi := newFileInfo("test-object", 2, 2) 45 ifi.SetInlineData() 46 ifi.Erasure.Index = 1 47 48 testCases := []struct { 49 name string 50 metaArr []FileInfo 51 errs []error 52 dataErrs []error 53 expectedMeta FileInfo 54 expectedDangling bool 55 }{ 56 { 57 name: "FileInfoExists-case1", 58 metaArr: []FileInfo{ 59 {}, 60 {}, 61 fi, 62 fi, 63 }, 64 errs: []error{ 65 errFileNotFound, 66 errDiskNotFound, 67 nil, 68 nil, 69 }, 70 dataErrs: nil, 71 expectedMeta: fi, 72 expectedDangling: false, 73 }, 74 { 75 name: "FileInfoExists-case2", 76 metaArr: []FileInfo{ 77 {}, 78 {}, 79 fi, 80 fi, 81 }, 82 errs: []error{ 83 errFileNotFound, 84 errFileNotFound, 85 nil, 86 nil, 87 }, 88 dataErrs: nil, 89 expectedMeta: fi, 90 expectedDangling: false, 91 }, 92 { 93 name: "FileInfoUndecided-case1", 94 metaArr: []FileInfo{ 95 {}, 96 {}, 97 {}, 98 fi, 99 }, 100 errs: []error{ 101 errFileNotFound, 102 errDiskNotFound, 103 errDiskNotFound, 104 nil, 105 }, 106 dataErrs: nil, 107 expectedMeta: fi, 108 expectedDangling: false, 109 }, 110 { 111 name: "FileInfoUndecided-case2", 112 metaArr: []FileInfo{}, 113 errs: []error{ 114 errFileNotFound, 115 errDiskNotFound, 116 errDiskNotFound, 117 errFileNotFound, 118 }, 119 dataErrs: nil, 120 expectedMeta: FileInfo{}, 121 expectedDangling: false, 122 }, 123 { 124 name: "FileInfoUndecided-case3(file deleted)", 125 metaArr: []FileInfo{}, 126 errs: []error{ 127 errFileNotFound, 128 errFileNotFound, 129 errFileNotFound, 130 errFileNotFound, 131 }, 132 dataErrs: nil, 133 expectedMeta: FileInfo{}, 134 expectedDangling: false, 135 }, 136 { 137 name: "FileInfoUnDecided-case4", 138 metaArr: []FileInfo{ 139 {}, 140 {}, 141 {}, 142 ifi, 143 }, 144 errs: []error{ 145 errFileNotFound, 146 errFileCorrupt, 147 errFileCorrupt, 148 nil, 149 }, 150 dataErrs: nil, 151 expectedMeta: ifi, 152 expectedDangling: false, 153 }, 154 { 155 name: "FileInfoUnDecided-case5-(ignore errFileCorrupt error)", 156 metaArr: []FileInfo{ 157 {}, 158 {}, 159 {}, 160 fi, 161 }, 162 errs: []error{ 163 errFileNotFound, 164 errFileCorrupt, 165 nil, 166 nil, 167 }, 168 dataErrs: []error{ 169 errFileCorrupt, 170 errFileNotFound, 171 nil, 172 errFileCorrupt, 173 }, 174 expectedMeta: fi, 175 expectedDangling: false, 176 }, 177 { 178 name: "FileInfoUnDecided-case6-(data-dir intact)", 179 metaArr: []FileInfo{ 180 {}, 181 {}, 182 {}, 183 fi, 184 }, 185 errs: []error{ 186 errFileNotFound, 187 errFileNotFound, 188 errFileNotFound, 189 nil, 190 }, 191 dataErrs: []error{ 192 errFileNotFound, 193 errFileCorrupt, 194 nil, 195 nil, 196 }, 197 expectedMeta: fi, 198 expectedDangling: false, 199 }, 200 { 201 name: "FileInfoDecided-case1", 202 metaArr: []FileInfo{ 203 {}, 204 {}, 205 {}, 206 ifi, 207 }, 208 errs: []error{ 209 errFileNotFound, 210 errFileNotFound, 211 errFileNotFound, 212 nil, 213 }, 214 dataErrs: nil, 215 expectedMeta: ifi, 216 expectedDangling: true, 217 }, 218 { 219 name: "FileInfoDecided-case2-delete-marker", 220 metaArr: []FileInfo{ 221 {}, 222 {}, 223 {}, 224 {Deleted: true}, 225 }, 226 errs: []error{ 227 errFileNotFound, 228 errFileNotFound, 229 errFileNotFound, 230 nil, 231 }, 232 dataErrs: nil, 233 expectedMeta: FileInfo{Deleted: true}, 234 expectedDangling: true, 235 }, 236 { 237 name: "FileInfoDecided-case3-(enough data-dir missing)", 238 metaArr: []FileInfo{ 239 {}, 240 {}, 241 {}, 242 fi, 243 }, 244 errs: []error{ 245 errFileNotFound, 246 errFileNotFound, 247 nil, 248 nil, 249 }, 250 dataErrs: []error{ 251 errFileNotFound, 252 errFileNotFound, 253 nil, 254 errFileNotFound, 255 }, 256 expectedMeta: fi, 257 expectedDangling: true, 258 }, 259 // Add new cases as seen 260 } 261 for _, testCase := range testCases { 262 testCase := testCase 263 t.Run(testCase.name, func(t *testing.T) { 264 gotMeta, dangling := isObjectDangling(testCase.metaArr, testCase.errs, testCase.dataErrs) 265 if !gotMeta.Equals(testCase.expectedMeta) { 266 t.Errorf("Expected %#v, got %#v", testCase.expectedMeta, gotMeta) 267 } 268 if dangling != testCase.expectedDangling { 269 t.Errorf("Expected dangling %t, got %t", testCase.expectedDangling, dangling) 270 } 271 }) 272 } 273 } 274 275 // Tests both object and bucket healing. 276 func TestHealing(t *testing.T) { 277 ctx, cancel := context.WithCancel(context.Background()) 278 defer cancel() 279 280 obj, fsDirs, err := prepareErasure16(ctx) 281 if err != nil { 282 t.Fatal(err) 283 } 284 defer obj.Shutdown(context.Background()) 285 286 // initialize the server and obtain the credentials and root. 287 // credentials are necessary to sign the HTTP request. 288 if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil { 289 t.Fatalf("Unable to initialize server config. %s", err) 290 } 291 292 defer removeRoots(fsDirs) 293 294 z := obj.(*erasureServerPools) 295 er := z.serverPools[0].sets[0] 296 297 // Create "bucket" 298 err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{}) 299 if err != nil { 300 t.Fatal(err) 301 } 302 303 bucket := "bucket" 304 object := "object" 305 306 data := make([]byte, 1*humanize.MiByte) 307 length := int64(len(data)) 308 _, err = rand.Read(data) 309 if err != nil { 310 t.Fatal(err) 311 } 312 313 _, err = obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader(data), length, "", ""), ObjectOptions{}) 314 if err != nil { 315 t.Fatal(err) 316 } 317 318 disk := er.getDisks()[0] 319 fileInfoPreHeal, err := disk.ReadVersion(context.Background(), "", bucket, object, "", ReadOptions{ReadData: false, Healing: true}) 320 if err != nil { 321 t.Fatal(err) 322 } 323 324 // Remove the object - to simulate the case where the disk was down when the object 325 // was created. 326 err = removeAll(pathJoin(disk.String(), bucket, object)) 327 if err != nil { 328 t.Fatal(err) 329 } 330 331 // Checking abandoned parts should do nothing 332 err = er.checkAbandonedParts(ctx, bucket, object, madmin.HealOpts{ScanMode: madmin.HealNormalScan, Remove: true}) 333 if err != nil { 334 t.Fatal(err) 335 } 336 337 _, err = er.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan}) 338 if err != nil { 339 t.Fatal(err) 340 } 341 342 fileInfoPostHeal, err := disk.ReadVersion(context.Background(), "", bucket, object, "", ReadOptions{ReadData: false, Healing: true}) 343 if err != nil { 344 t.Fatal(err) 345 } 346 347 // After heal the meta file should be as expected. 348 if !fileInfoPreHeal.Equals(fileInfoPostHeal) { 349 t.Fatal("HealObject failed") 350 } 351 352 err = os.RemoveAll(path.Join(fsDirs[0], bucket, object, "xl.meta")) 353 if err != nil { 354 t.Fatal(err) 355 } 356 357 // Write xl.meta with different modtime to simulate the case where a disk had 358 // gone down when an object was replaced by a new object. 359 fileInfoOutDated := fileInfoPreHeal 360 fileInfoOutDated.ModTime = time.Now() 361 err = disk.WriteMetadata(context.Background(), "", bucket, object, fileInfoOutDated) 362 if err != nil { 363 t.Fatal(err) 364 } 365 366 _, err = er.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealDeepScan}) 367 if err != nil { 368 t.Fatal(err) 369 } 370 371 fileInfoPostHeal, err = disk.ReadVersion(context.Background(), "", bucket, object, "", ReadOptions{ReadData: false, Healing: true}) 372 if err != nil { 373 t.Fatal(err) 374 } 375 376 // After heal the meta file should be as expected. 377 if !fileInfoPreHeal.Equals(fileInfoPostHeal) { 378 t.Fatal("HealObject failed") 379 } 380 381 uuid, _ := uuid2.NewRandom() 382 for _, drive := range fsDirs { 383 dir := path.Join(drive, bucket, object, uuid.String()) 384 err = os.MkdirAll(dir, os.ModePerm) 385 if err != nil { 386 t.Fatal(err) 387 } 388 err = os.WriteFile(pathJoin(dir, "part.1"), []byte("some data"), os.ModePerm) 389 if err != nil { 390 t.Fatal(err) 391 } 392 } 393 394 // This should remove all the unreferenced parts. 395 err = er.checkAbandonedParts(ctx, bucket, object, madmin.HealOpts{ScanMode: madmin.HealNormalScan, Remove: true}) 396 if err != nil { 397 t.Fatal(err) 398 } 399 400 for _, drive := range fsDirs { 401 dir := path.Join(drive, bucket, object, uuid.String()) 402 _, err := os.ReadFile(pathJoin(dir, "part.1")) 403 if err == nil { 404 t.Fatal("expected data dit to be cleaned up") 405 } 406 } 407 408 // Remove the bucket - to simulate the case where bucket was 409 // created when the disk was down. 410 err = os.RemoveAll(path.Join(fsDirs[0], bucket)) 411 if err != nil { 412 t.Fatal(err) 413 } 414 // This would create the bucket. 415 _, err = obj.HealBucket(ctx, bucket, madmin.HealOpts{ 416 DryRun: false, 417 Remove: false, 418 }) 419 if err != nil { 420 t.Fatal(err) 421 } 422 // Stat the bucket to make sure that it was created. 423 _, err = er.getDisks()[0].StatVol(context.Background(), bucket) 424 if err != nil { 425 t.Fatal(err) 426 } 427 } 428 429 // Tests both object and bucket healing. 430 func TestHealingVersioned(t *testing.T) { 431 ctx, cancel := context.WithCancel(context.Background()) 432 defer cancel() 433 434 obj, fsDirs, err := prepareErasure16(ctx) 435 if err != nil { 436 t.Fatal(err) 437 } 438 defer obj.Shutdown(context.Background()) 439 440 // initialize the server and obtain the credentials and root. 441 // credentials are necessary to sign the HTTP request. 442 if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil { 443 t.Fatalf("Unable to initialize server config. %s", err) 444 } 445 446 defer removeRoots(fsDirs) 447 448 z := obj.(*erasureServerPools) 449 er := z.serverPools[0].sets[0] 450 451 // Create "bucket" 452 err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{VersioningEnabled: true}) 453 if err != nil { 454 t.Fatal(err) 455 } 456 457 bucket := "bucket" 458 object := "object" 459 460 data := make([]byte, 1*humanize.MiByte) 461 length := int64(len(data)) 462 _, err = rand.Read(data) 463 if err != nil { 464 t.Fatal(err) 465 } 466 467 oi1, err := obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader(data), length, "", ""), ObjectOptions{}) 468 if err != nil { 469 t.Fatal(err) 470 } 471 // 2nd version. 472 _, _ = rand.Read(data) 473 oi2, err := obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader(data), length, "", ""), ObjectOptions{}) 474 if err != nil { 475 t.Fatal(err) 476 } 477 478 disk := er.getDisks()[0] 479 fileInfoPreHeal1, err := disk.ReadVersion(context.Background(), "", bucket, object, oi1.VersionID, ReadOptions{ReadData: false, Healing: true}) 480 if err != nil { 481 t.Fatal(err) 482 } 483 fileInfoPreHeal2, err := disk.ReadVersion(context.Background(), "", bucket, object, oi2.VersionID, ReadOptions{ReadData: false, Healing: true}) 484 if err != nil { 485 t.Fatal(err) 486 } 487 488 // Remove the object - to simulate the case where the disk was down when the object 489 // was created. 490 err = removeAll(pathJoin(disk.String(), bucket, object)) 491 if err != nil { 492 t.Fatal(err) 493 } 494 495 // Checking abandoned parts should do nothing 496 err = er.checkAbandonedParts(ctx, bucket, object, madmin.HealOpts{ScanMode: madmin.HealNormalScan, Remove: true}) 497 if err != nil { 498 t.Fatal(err) 499 } 500 501 _, err = er.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan}) 502 if err != nil { 503 t.Fatal(err) 504 } 505 506 fileInfoPostHeal1, err := disk.ReadVersion(context.Background(), "", bucket, object, oi1.VersionID, ReadOptions{ReadData: false, Healing: true}) 507 if err != nil { 508 t.Fatal(err) 509 } 510 fileInfoPostHeal2, err := disk.ReadVersion(context.Background(), "", bucket, object, oi2.VersionID, ReadOptions{ReadData: false, Healing: true}) 511 if err != nil { 512 t.Fatal(err) 513 } 514 515 // After heal the meta file should be as expected. 516 if !fileInfoPreHeal1.Equals(fileInfoPostHeal1) { 517 t.Fatal("HealObject failed") 518 } 519 if !fileInfoPreHeal1.Equals(fileInfoPostHeal2) { 520 t.Fatal("HealObject failed") 521 } 522 523 err = os.RemoveAll(path.Join(fsDirs[0], bucket, object, "xl.meta")) 524 if err != nil { 525 t.Fatal(err) 526 } 527 528 // Write xl.meta with different modtime to simulate the case where a disk had 529 // gone down when an object was replaced by a new object. 530 fileInfoOutDated := fileInfoPreHeal1 531 fileInfoOutDated.ModTime = time.Now() 532 err = disk.WriteMetadata(context.Background(), "", bucket, object, fileInfoOutDated) 533 if err != nil { 534 t.Fatal(err) 535 } 536 537 _, err = er.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealDeepScan}) 538 if err != nil { 539 t.Fatal(err) 540 } 541 542 fileInfoPostHeal1, err = disk.ReadVersion(context.Background(), "", bucket, object, "", ReadOptions{ReadData: false, Healing: true}) 543 if err != nil { 544 t.Fatal(err) 545 } 546 547 // After heal the meta file should be as expected. 548 if !fileInfoPreHeal1.Equals(fileInfoPostHeal1) { 549 t.Fatal("HealObject failed") 550 } 551 552 fileInfoPostHeal2, err = disk.ReadVersion(context.Background(), "", bucket, object, "", ReadOptions{ReadData: false, Healing: true}) 553 if err != nil { 554 t.Fatal(err) 555 } 556 557 // After heal the meta file should be as expected. 558 if !fileInfoPreHeal2.Equals(fileInfoPostHeal2) { 559 t.Fatal("HealObject failed") 560 } 561 562 uuid, _ := uuid2.NewRandom() 563 for _, drive := range fsDirs { 564 dir := path.Join(drive, bucket, object, uuid.String()) 565 err = os.MkdirAll(dir, os.ModePerm) 566 if err != nil { 567 t.Fatal(err) 568 } 569 err = os.WriteFile(pathJoin(dir, "part.1"), []byte("some data"), os.ModePerm) 570 if err != nil { 571 t.Fatal(err) 572 } 573 } 574 575 // This should remove all the unreferenced parts. 576 err = er.checkAbandonedParts(ctx, bucket, object, madmin.HealOpts{ScanMode: madmin.HealNormalScan, Remove: true}) 577 if err != nil { 578 t.Fatal(err) 579 } 580 581 for _, drive := range fsDirs { 582 dir := path.Join(drive, bucket, object, uuid.String()) 583 _, err := os.ReadFile(pathJoin(dir, "part.1")) 584 if err == nil { 585 t.Fatal("expected data dit to be cleaned up") 586 } 587 } 588 589 // Remove the bucket - to simulate the case where bucket was 590 // created when the disk was down. 591 err = os.RemoveAll(path.Join(fsDirs[0], bucket)) 592 if err != nil { 593 t.Fatal(err) 594 } 595 // This would create the bucket. 596 _, err = obj.HealBucket(ctx, bucket, madmin.HealOpts{ 597 DryRun: false, 598 Remove: false, 599 }) 600 if err != nil { 601 t.Fatal(err) 602 } 603 // Stat the bucket to make sure that it was created. 604 _, err = er.getDisks()[0].StatVol(context.Background(), bucket) 605 if err != nil { 606 t.Fatal(err) 607 } 608 } 609 610 func TestHealingDanglingObject(t *testing.T) { 611 ctx, cancel := context.WithCancel(context.Background()) 612 defer cancel() 613 614 resetGlobalHealState() 615 defer resetGlobalHealState() 616 617 // Set globalStorageClass.STANDARD to EC:4 for this test 618 saveSC := globalStorageClass 619 defer func() { 620 globalStorageClass.Update(saveSC) 621 }() 622 globalStorageClass.Update(storageclass.Config{ 623 Standard: storageclass.StorageClass{ 624 Parity: 4, 625 }, 626 }) 627 628 nDisks := 16 629 fsDirs, err := getRandomDisks(nDisks) 630 if err != nil { 631 t.Fatal(err) 632 } 633 634 defer removeRoots(fsDirs) 635 636 // Everything is fine, should return nil 637 objLayer, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, fsDirs...)) 638 if err != nil { 639 t.Fatal(err) 640 } 641 642 setObjectLayer(objLayer) 643 644 bucket := getRandomBucketName() 645 object := getRandomObjectName() 646 data := bytes.Repeat([]byte("a"), 128*1024) 647 648 err = objLayer.MakeBucket(ctx, bucket, MakeBucketOptions{}) 649 if err != nil { 650 t.Fatalf("Failed to make a bucket - %v", err) 651 } 652 653 disks := objLayer.(*erasureServerPools).serverPools[0].erasureDisks[0] 654 orgDisks := append([]StorageAPI{}, disks...) 655 656 // Enable versioning. 657 globalBucketMetadataSys.Update(ctx, bucket, bucketVersioningConfig, []byte(`<VersioningConfiguration><Status>Enabled</Status></VersioningConfiguration>`)) 658 659 _, err = objLayer.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), ObjectOptions{ 660 Versioned: true, 661 }) 662 if err != nil { 663 t.Fatal(err) 664 } 665 666 setDisks := func(newDisks ...StorageAPI) { 667 objLayer.(*erasureServerPools).serverPools[0].erasureDisksMu.Lock() 668 copy(disks, newDisks) 669 objLayer.(*erasureServerPools).serverPools[0].erasureDisksMu.Unlock() 670 } 671 getDisk := func(n int) StorageAPI { 672 objLayer.(*erasureServerPools).serverPools[0].erasureDisksMu.Lock() 673 disk := disks[n] 674 objLayer.(*erasureServerPools).serverPools[0].erasureDisksMu.Unlock() 675 return disk 676 } 677 678 // Remove 4 disks. 679 setDisks(nil, nil, nil, nil) 680 681 // Create delete marker under quorum. 682 objInfo, err := objLayer.DeleteObject(ctx, bucket, object, ObjectOptions{Versioned: true}) 683 if err != nil { 684 t.Fatal(err) 685 } 686 687 // Restore... 688 setDisks(orgDisks[:4]...) 689 690 fileInfoPreHeal, err := disks[0].ReadVersion(context.Background(), "", bucket, object, "", ReadOptions{ReadData: false, Healing: true}) 691 if err != nil { 692 t.Fatal(err) 693 } 694 695 if fileInfoPreHeal.NumVersions != 1 { 696 t.Fatalf("Expected versions 1, got %d", fileInfoPreHeal.NumVersions) 697 } 698 699 if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Remove: true}, 700 func(bucket, object, vid string, scanMode madmin.HealScanMode) error { 701 _, err := objLayer.HealObject(ctx, bucket, object, vid, madmin.HealOpts{ScanMode: scanMode, Remove: true}) 702 return err 703 }); err != nil { 704 t.Fatal(err) 705 } 706 707 fileInfoPostHeal, err := disks[0].ReadVersion(context.Background(), "", bucket, object, "", ReadOptions{ReadData: false, Healing: true}) 708 if err != nil { 709 t.Fatal(err) 710 } 711 712 if fileInfoPostHeal.NumVersions != 2 { 713 t.Fatalf("Expected versions 2, got %d", fileInfoPreHeal.NumVersions) 714 } 715 716 if objInfo.DeleteMarker { 717 if _, err = objLayer.DeleteObject(ctx, bucket, object, ObjectOptions{ 718 Versioned: true, 719 VersionID: objInfo.VersionID, 720 }); err != nil { 721 t.Fatal(err) 722 } 723 } 724 725 setDisks(nil, nil, nil, nil) 726 727 rd := mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", "") 728 _, err = objLayer.PutObject(ctx, bucket, object, rd, ObjectOptions{ 729 Versioned: true, 730 }) 731 if err != nil { 732 t.Fatal(err) 733 } 734 735 setDisks(orgDisks[:4]...) 736 disk := getDisk(0) 737 fileInfoPreHeal, err = disk.ReadVersion(context.Background(), "", bucket, object, "", ReadOptions{ReadData: false, Healing: true}) 738 if err != nil { 739 t.Fatal(err) 740 } 741 742 if fileInfoPreHeal.NumVersions != 1 { 743 t.Fatalf("Expected versions 1, got %d", fileInfoPreHeal.NumVersions) 744 } 745 746 if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Remove: true}, 747 func(bucket, object, vid string, scanMode madmin.HealScanMode) error { 748 _, err := objLayer.HealObject(ctx, bucket, object, vid, madmin.HealOpts{ScanMode: scanMode, Remove: true}) 749 return err 750 }); err != nil { 751 t.Fatal(err) 752 } 753 754 disk = getDisk(0) 755 fileInfoPostHeal, err = disk.ReadVersion(context.Background(), "", bucket, object, "", ReadOptions{ReadData: false, Healing: true}) 756 if err != nil { 757 t.Fatal(err) 758 } 759 760 if fileInfoPostHeal.NumVersions != 2 { 761 t.Fatalf("Expected versions 2, got %d", fileInfoPreHeal.NumVersions) 762 } 763 764 rd = mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", "") 765 objInfo, err = objLayer.PutObject(ctx, bucket, object, rd, ObjectOptions{ 766 Versioned: true, 767 }) 768 if err != nil { 769 t.Fatal(err) 770 } 771 772 setDisks(nil, nil, nil, nil) 773 774 // Create delete marker under quorum. 775 _, err = objLayer.DeleteObject(ctx, bucket, object, ObjectOptions{ 776 Versioned: true, 777 VersionID: objInfo.VersionID, 778 }) 779 if err != nil { 780 t.Fatal(err) 781 } 782 783 setDisks(orgDisks[:4]...) 784 785 disk = getDisk(0) 786 fileInfoPreHeal, err = disk.ReadVersion(context.Background(), "", bucket, object, "", ReadOptions{ReadData: false, Healing: true}) 787 if err != nil { 788 t.Fatal(err) 789 } 790 791 if fileInfoPreHeal.NumVersions != 3 { 792 t.Fatalf("Expected versions 3, got %d", fileInfoPreHeal.NumVersions) 793 } 794 795 if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Remove: true}, 796 func(bucket, object, vid string, scanMode madmin.HealScanMode) error { 797 _, err := objLayer.HealObject(ctx, bucket, object, vid, madmin.HealOpts{ScanMode: scanMode, Remove: true}) 798 return err 799 }); err != nil { 800 t.Fatal(err) 801 } 802 803 disk = getDisk(0) 804 fileInfoPostHeal, err = disk.ReadVersion(context.Background(), "", bucket, object, "", ReadOptions{ReadData: false, Healing: true}) 805 if err != nil { 806 t.Fatal(err) 807 } 808 809 if fileInfoPostHeal.NumVersions != 2 { 810 t.Fatalf("Expected versions 2, got %d", fileInfoPreHeal.NumVersions) 811 } 812 } 813 814 func TestHealCorrectQuorum(t *testing.T) { 815 ctx, cancel := context.WithCancel(context.Background()) 816 defer cancel() 817 818 resetGlobalHealState() 819 defer resetGlobalHealState() 820 821 nDisks := 32 822 fsDirs, err := getRandomDisks(nDisks) 823 if err != nil { 824 t.Fatal(err) 825 } 826 827 defer removeRoots(fsDirs) 828 829 pools := mustGetPoolEndpoints(0, fsDirs[:16]...) 830 pools = append(pools, mustGetPoolEndpoints(1, fsDirs[16:]...)...) 831 832 // Everything is fine, should return nil 833 objLayer, _, err := initObjectLayer(ctx, pools) 834 if err != nil { 835 t.Fatal(err) 836 } 837 838 bucket := getRandomBucketName() 839 object := getRandomObjectName() 840 data := bytes.Repeat([]byte("a"), 5*1024*1024) 841 var opts ObjectOptions 842 843 err = objLayer.MakeBucket(ctx, bucket, MakeBucketOptions{}) 844 if err != nil { 845 t.Fatalf("Failed to make a bucket - %v", err) 846 } 847 848 // Create an object with multiple parts uploaded in decreasing 849 // part number. 850 res, err := objLayer.NewMultipartUpload(ctx, bucket, object, opts) 851 if err != nil { 852 t.Fatalf("Failed to create a multipart upload - %v", err) 853 } 854 855 var uploadedParts []CompletePart 856 for _, partID := range []int{2, 1} { 857 pInfo, err1 := objLayer.PutObjectPart(ctx, bucket, object, res.UploadID, partID, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), opts) 858 if err1 != nil { 859 t.Fatalf("Failed to upload a part - %v", err1) 860 } 861 uploadedParts = append(uploadedParts, CompletePart{ 862 PartNumber: pInfo.PartNumber, 863 ETag: pInfo.ETag, 864 }) 865 } 866 867 _, err = objLayer.CompleteMultipartUpload(ctx, bucket, object, res.UploadID, uploadedParts, ObjectOptions{}) 868 if err != nil { 869 t.Fatalf("Failed to complete multipart upload - got: %v", err) 870 } 871 872 cfgFile := pathJoin(bucketMetaPrefix, bucket, ".test.bin") 873 if err = saveConfig(ctx, objLayer, cfgFile, data); err != nil { 874 t.Fatal(err) 875 } 876 877 hopts := madmin.HealOpts{ 878 DryRun: false, 879 Remove: true, 880 ScanMode: madmin.HealNormalScan, 881 } 882 883 // Test 1: Remove the object backend files from the first disk. 884 z := objLayer.(*erasureServerPools) 885 for _, set := range z.serverPools { 886 er := set.sets[0] 887 erasureDisks := er.getDisks() 888 889 fileInfos, errs := readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true) 890 nfi, err := getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs) 891 if errors.Is(err, errFileNotFound) { 892 continue 893 } 894 if err != nil { 895 t.Fatalf("Failed to getLatestFileInfo - %v", err) 896 } 897 898 for i := 0; i < nfi.Erasure.ParityBlocks; i++ { 899 erasureDisks[i].Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), DeleteOptions{ 900 Recursive: false, 901 Immediate: false, 902 }) 903 } 904 905 // Try healing now, it should heal the content properly. 906 _, err = objLayer.HealObject(ctx, bucket, object, "", hopts) 907 if err != nil { 908 t.Fatal(err) 909 } 910 911 fileInfos, errs = readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true) 912 if countErrs(errs, nil) != len(fileInfos) { 913 t.Fatal("Expected all xl.meta healed, but partial heal detected") 914 } 915 916 fileInfos, errs = readAllFileInfo(ctx, erasureDisks, "", minioMetaBucket, cfgFile, "", false, true) 917 nfi, err = getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs) 918 if errors.Is(err, errFileNotFound) { 919 continue 920 } 921 if err != nil { 922 t.Fatalf("Failed to getLatestFileInfo - %v", err) 923 } 924 925 for i := 0; i < nfi.Erasure.ParityBlocks; i++ { 926 erasureDisks[i].Delete(context.Background(), minioMetaBucket, pathJoin(cfgFile, xlStorageFormatFile), DeleteOptions{ 927 Recursive: false, 928 Immediate: false, 929 }) 930 } 931 932 // Try healing now, it should heal the content properly. 933 _, err = objLayer.HealObject(ctx, minioMetaBucket, cfgFile, "", hopts) 934 if err != nil { 935 t.Fatal(err) 936 } 937 938 fileInfos, errs = readAllFileInfo(ctx, erasureDisks, "", minioMetaBucket, cfgFile, "", false, true) 939 if countErrs(errs, nil) != len(fileInfos) { 940 t.Fatal("Expected all xl.meta healed, but partial heal detected") 941 } 942 } 943 } 944 945 func TestHealObjectCorruptedPools(t *testing.T) { 946 ctx, cancel := context.WithCancel(context.Background()) 947 defer cancel() 948 949 resetGlobalHealState() 950 defer resetGlobalHealState() 951 952 nDisks := 32 953 fsDirs, err := getRandomDisks(nDisks) 954 if err != nil { 955 t.Fatal(err) 956 } 957 958 defer removeRoots(fsDirs) 959 960 pools := mustGetPoolEndpoints(0, fsDirs[:16]...) 961 pools = append(pools, mustGetPoolEndpoints(1, fsDirs[16:]...)...) 962 963 // Everything is fine, should return nil 964 objLayer, _, err := initObjectLayer(ctx, pools) 965 if err != nil { 966 t.Fatal(err) 967 } 968 969 bucket := getRandomBucketName() 970 object := getRandomObjectName() 971 data := bytes.Repeat([]byte("a"), 5*1024*1024) 972 var opts ObjectOptions 973 974 err = objLayer.MakeBucket(ctx, bucket, MakeBucketOptions{}) 975 if err != nil { 976 t.Fatalf("Failed to make a bucket - %v", err) 977 } 978 979 // Upload a multipart object in the second pool 980 z := objLayer.(*erasureServerPools) 981 set := z.serverPools[1] 982 983 res, err := set.NewMultipartUpload(ctx, bucket, object, opts) 984 if err != nil { 985 t.Fatalf("Failed to create a multipart upload - %v", err) 986 } 987 uploadID := res.UploadID 988 989 var uploadedParts []CompletePart 990 for _, partID := range []int{2, 1} { 991 pInfo, err1 := set.PutObjectPart(ctx, bucket, object, uploadID, partID, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), opts) 992 if err1 != nil { 993 t.Fatalf("Failed to upload a part - %v", err1) 994 } 995 uploadedParts = append(uploadedParts, CompletePart{ 996 PartNumber: pInfo.PartNumber, 997 ETag: pInfo.ETag, 998 }) 999 } 1000 1001 _, err = set.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, ObjectOptions{}) 1002 if err != nil { 1003 t.Fatalf("Failed to complete multipart upload - %v", err) 1004 } 1005 1006 // Test 1: Remove the object backend files from the first disk. 1007 er := set.sets[0] 1008 erasureDisks := er.getDisks() 1009 firstDisk := erasureDisks[0] 1010 err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), DeleteOptions{ 1011 Recursive: false, 1012 Immediate: false, 1013 }) 1014 if err != nil { 1015 t.Fatalf("Failed to delete a file - %v", err) 1016 } 1017 1018 _, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan}) 1019 if err != nil { 1020 t.Fatalf("Failed to heal object - %v", err) 1021 } 1022 1023 fileInfos, errs := readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true) 1024 fi, err := getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs) 1025 if err != nil { 1026 t.Fatalf("Failed to getLatestFileInfo - %v", err) 1027 } 1028 1029 if _, err = firstDisk.StatInfoFile(context.Background(), bucket, object+"/"+xlStorageFormatFile, false); err != nil { 1030 t.Errorf("Expected xl.meta file to be present but stat failed - %v", err) 1031 } 1032 1033 err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), DeleteOptions{ 1034 Recursive: false, 1035 Immediate: false, 1036 }) 1037 if err != nil { 1038 t.Errorf("Failure during deleting part.1 - %v", err) 1039 } 1040 1041 err = firstDisk.WriteAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), []byte{}) 1042 if err != nil { 1043 t.Errorf("Failure during creating part.1 - %v", err) 1044 } 1045 1046 _, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{DryRun: false, Remove: true, ScanMode: madmin.HealDeepScan}) 1047 if err != nil { 1048 t.Errorf("Expected nil but received %v", err) 1049 } 1050 1051 fileInfos, errs = readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true) 1052 nfi, err := getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs) 1053 if err != nil { 1054 t.Fatalf("Failed to getLatestFileInfo - %v", err) 1055 } 1056 1057 if !reflect.DeepEqual(fi, nfi) { 1058 t.Fatalf("FileInfo not equal after healing: %v != %v", fi, nfi) 1059 } 1060 1061 err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), DeleteOptions{ 1062 Recursive: false, 1063 Immediate: false, 1064 }) 1065 if err != nil { 1066 t.Errorf("Failure during deleting part.1 - %v", err) 1067 } 1068 1069 bdata := bytes.Repeat([]byte("b"), int(nfi.Size)) 1070 err = firstDisk.WriteAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), bdata) 1071 if err != nil { 1072 t.Errorf("Failure during creating part.1 - %v", err) 1073 } 1074 1075 _, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{DryRun: false, Remove: true, ScanMode: madmin.HealDeepScan}) 1076 if err != nil { 1077 t.Errorf("Expected nil but received %v", err) 1078 } 1079 1080 fileInfos, errs = readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true) 1081 nfi, err = getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs) 1082 if err != nil { 1083 t.Fatalf("Failed to getLatestFileInfo - %v", err) 1084 } 1085 1086 if !reflect.DeepEqual(fi, nfi) { 1087 t.Fatalf("FileInfo not equal after healing: %v != %v", fi, nfi) 1088 } 1089 1090 // Test 4: checks if HealObject returns an error when xl.meta is not found 1091 // in more than read quorum number of disks, to create a corrupted situation. 1092 for i := 0; i <= nfi.Erasure.DataBlocks; i++ { 1093 erasureDisks[i].Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), DeleteOptions{ 1094 Recursive: false, 1095 Immediate: false, 1096 }) 1097 } 1098 1099 // Try healing now, expect to receive errFileNotFound. 1100 _, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{DryRun: false, Remove: true, ScanMode: madmin.HealDeepScan}) 1101 if err != nil { 1102 if _, ok := err.(ObjectNotFound); !ok { 1103 t.Errorf("Expect %v but received %v", ObjectNotFound{Bucket: bucket, Object: object}, err) 1104 } 1105 } 1106 1107 // since majority of xl.meta's are not available, object should be successfully deleted. 1108 _, err = objLayer.GetObjectInfo(ctx, bucket, object, ObjectOptions{}) 1109 if _, ok := err.(ObjectNotFound); !ok { 1110 t.Errorf("Expect %v but received %v", ObjectNotFound{Bucket: bucket, Object: object}, err) 1111 } 1112 1113 for i := 0; i < (nfi.Erasure.DataBlocks + nfi.Erasure.ParityBlocks); i++ { 1114 stats, _ := erasureDisks[i].StatInfoFile(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), false) 1115 if len(stats) != 0 { 1116 t.Errorf("Expected xl.meta file to be not present, but succeeded") 1117 } 1118 } 1119 } 1120 1121 func TestHealObjectCorruptedXLMeta(t *testing.T) { 1122 ctx, cancel := context.WithCancel(context.Background()) 1123 defer cancel() 1124 1125 resetGlobalHealState() 1126 defer resetGlobalHealState() 1127 1128 nDisks := 16 1129 fsDirs, err := getRandomDisks(nDisks) 1130 if err != nil { 1131 t.Fatal(err) 1132 } 1133 1134 defer removeRoots(fsDirs) 1135 1136 // Everything is fine, should return nil 1137 objLayer, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, fsDirs...)) 1138 if err != nil { 1139 t.Fatal(err) 1140 } 1141 1142 bucket := getRandomBucketName() 1143 object := getRandomObjectName() 1144 data := bytes.Repeat([]byte("a"), 5*1024*1024) 1145 var opts ObjectOptions 1146 1147 err = objLayer.MakeBucket(ctx, bucket, MakeBucketOptions{}) 1148 if err != nil { 1149 t.Fatalf("Failed to make a bucket - %v", err) 1150 } 1151 1152 // Create an object with multiple parts uploaded in decreasing 1153 // part number. 1154 res, err := objLayer.NewMultipartUpload(ctx, bucket, object, opts) 1155 if err != nil { 1156 t.Fatalf("Failed to create a multipart upload - %v", err) 1157 } 1158 1159 var uploadedParts []CompletePart 1160 for _, partID := range []int{2, 1} { 1161 pInfo, err1 := objLayer.PutObjectPart(ctx, bucket, object, res.UploadID, partID, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), opts) 1162 if err1 != nil { 1163 t.Fatalf("Failed to upload a part - %v", err1) 1164 } 1165 uploadedParts = append(uploadedParts, CompletePart{ 1166 PartNumber: pInfo.PartNumber, 1167 ETag: pInfo.ETag, 1168 }) 1169 } 1170 1171 _, err = objLayer.CompleteMultipartUpload(ctx, bucket, object, res.UploadID, uploadedParts, ObjectOptions{}) 1172 if err != nil { 1173 t.Fatalf("Failed to complete multipart upload - %v", err) 1174 } 1175 1176 z := objLayer.(*erasureServerPools) 1177 er := z.serverPools[0].sets[0] 1178 erasureDisks := er.getDisks() 1179 firstDisk := erasureDisks[0] 1180 1181 // Test 1: Remove the object backend files from the first disk. 1182 fileInfos, errs := readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true) 1183 fi, err := getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs) 1184 if err != nil { 1185 t.Fatalf("Failed to getLatestFileInfo - %v", err) 1186 } 1187 1188 err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), DeleteOptions{ 1189 Recursive: false, 1190 Immediate: false, 1191 }) 1192 if err != nil { 1193 t.Fatalf("Failed to delete a file - %v", err) 1194 } 1195 1196 _, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan}) 1197 if err != nil { 1198 t.Fatalf("Failed to heal object - %v", err) 1199 } 1200 1201 if _, err = firstDisk.StatInfoFile(context.Background(), bucket, object+"/"+xlStorageFormatFile, false); err != nil { 1202 t.Errorf("Expected xl.meta file to be present but stat failed - %v", err) 1203 } 1204 1205 fileInfos, errs = readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true) 1206 nfi1, err := getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs) 1207 if err != nil { 1208 t.Fatalf("Failed to getLatestFileInfo - %v", err) 1209 } 1210 1211 if !reflect.DeepEqual(fi, nfi1) { 1212 t.Fatalf("FileInfo not equal after healing") 1213 } 1214 1215 // Test 2: Test with a corrupted xl.meta 1216 err = firstDisk.WriteAll(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), []byte("abcd")) 1217 if err != nil { 1218 t.Errorf("Failure during creating part.1 - %v", err) 1219 } 1220 1221 _, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan}) 1222 if err != nil { 1223 t.Errorf("Expected nil but received %v", err) 1224 } 1225 1226 fileInfos, errs = readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true) 1227 nfi2, err := getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs) 1228 if err != nil { 1229 t.Fatalf("Failed to getLatestFileInfo - %v", err) 1230 } 1231 1232 if !reflect.DeepEqual(fi, nfi2) { 1233 t.Fatalf("FileInfo not equal after healing") 1234 } 1235 1236 // Test 3: checks if HealObject returns an error when xl.meta is not found 1237 // in more than read quorum number of disks, to create a corrupted situation. 1238 for i := 0; i <= nfi2.Erasure.DataBlocks; i++ { 1239 erasureDisks[i].Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), DeleteOptions{ 1240 Recursive: false, 1241 Immediate: false, 1242 }) 1243 } 1244 1245 // Try healing now, expect to receive errFileNotFound. 1246 _, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{DryRun: false, Remove: true, ScanMode: madmin.HealDeepScan}) 1247 if err != nil { 1248 if _, ok := err.(ObjectNotFound); !ok { 1249 t.Errorf("Expect %v but received %v", ObjectNotFound{Bucket: bucket, Object: object}, err) 1250 } 1251 } 1252 1253 // since majority of xl.meta's are not available, object should be successfully deleted. 1254 _, err = objLayer.GetObjectInfo(ctx, bucket, object, ObjectOptions{}) 1255 if _, ok := err.(ObjectNotFound); !ok { 1256 t.Errorf("Expect %v but received %v", ObjectNotFound{Bucket: bucket, Object: object}, err) 1257 } 1258 } 1259 1260 func TestHealObjectCorruptedParts(t *testing.T) { 1261 ctx, cancel := context.WithCancel(context.Background()) 1262 defer cancel() 1263 1264 resetGlobalHealState() 1265 defer resetGlobalHealState() 1266 1267 nDisks := 16 1268 fsDirs, err := getRandomDisks(nDisks) 1269 if err != nil { 1270 t.Fatal(err) 1271 } 1272 1273 defer removeRoots(fsDirs) 1274 1275 // Everything is fine, should return nil 1276 objLayer, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, fsDirs...)) 1277 if err != nil { 1278 t.Fatal(err) 1279 } 1280 1281 bucket := getRandomBucketName() 1282 object := getRandomObjectName() 1283 data := bytes.Repeat([]byte("a"), 5*1024*1024) 1284 var opts ObjectOptions 1285 1286 err = objLayer.MakeBucket(ctx, bucket, MakeBucketOptions{}) 1287 if err != nil { 1288 t.Fatalf("Failed to make a bucket - %v", err) 1289 } 1290 1291 // Create an object with multiple parts uploaded in decreasing 1292 // part number. 1293 res, err := objLayer.NewMultipartUpload(ctx, bucket, object, opts) 1294 if err != nil { 1295 t.Fatalf("Failed to create a multipart upload - %v", err) 1296 } 1297 1298 var uploadedParts []CompletePart 1299 for _, partID := range []int{2, 1} { 1300 pInfo, err1 := objLayer.PutObjectPart(ctx, bucket, object, res.UploadID, partID, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), opts) 1301 if err1 != nil { 1302 t.Fatalf("Failed to upload a part - %v", err1) 1303 } 1304 uploadedParts = append(uploadedParts, CompletePart{ 1305 PartNumber: pInfo.PartNumber, 1306 ETag: pInfo.ETag, 1307 }) 1308 } 1309 1310 _, err = objLayer.CompleteMultipartUpload(ctx, bucket, object, res.UploadID, uploadedParts, ObjectOptions{}) 1311 if err != nil { 1312 t.Fatalf("Failed to complete multipart upload - %v", err) 1313 } 1314 1315 // Test 1: Remove the object backend files from the first disk. 1316 z := objLayer.(*erasureServerPools) 1317 er := z.serverPools[0].sets[0] 1318 erasureDisks := er.getDisks() 1319 firstDisk := erasureDisks[0] 1320 secondDisk := erasureDisks[1] 1321 1322 fileInfos, errs := readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true) 1323 fi, err := getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs) 1324 if err != nil { 1325 t.Fatalf("Failed to getLatestFileInfo - %v", err) 1326 } 1327 1328 part1Disk1Origin, err := firstDisk.ReadAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1")) 1329 if err != nil { 1330 t.Fatalf("Failed to read a file - %v", err) 1331 } 1332 1333 part1Disk2Origin, err := secondDisk.ReadAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1")) 1334 if err != nil { 1335 t.Fatalf("Failed to read a file - %v", err) 1336 } 1337 1338 // Test 1, remove part.1 1339 err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), DeleteOptions{ 1340 Recursive: false, 1341 Immediate: false, 1342 }) 1343 if err != nil { 1344 t.Fatalf("Failed to delete a file - %v", err) 1345 } 1346 1347 _, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan}) 1348 if err != nil { 1349 t.Fatalf("Failed to heal object - %v", err) 1350 } 1351 1352 part1Replaced, err := firstDisk.ReadAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1")) 1353 if err != nil { 1354 t.Fatalf("Failed to read a file - %v", err) 1355 } 1356 1357 if !reflect.DeepEqual(part1Disk1Origin, part1Replaced) { 1358 t.Fatalf("part.1 not healed correctly") 1359 } 1360 1361 // Test 2, Corrupt part.1 1362 err = firstDisk.WriteAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), []byte("foobytes")) 1363 if err != nil { 1364 t.Fatalf("Failed to write a file - %v", err) 1365 } 1366 1367 _, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan}) 1368 if err != nil { 1369 t.Fatalf("Failed to heal object - %v", err) 1370 } 1371 1372 part1Replaced, err = firstDisk.ReadAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1")) 1373 if err != nil { 1374 t.Fatalf("Failed to read a file - %v", err) 1375 } 1376 1377 if !reflect.DeepEqual(part1Disk1Origin, part1Replaced) { 1378 t.Fatalf("part.1 not healed correctly") 1379 } 1380 1381 // Test 3, Corrupt one part and remove data in another disk 1382 err = firstDisk.WriteAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), []byte("foobytes")) 1383 if err != nil { 1384 t.Fatalf("Failed to write a file - %v", err) 1385 } 1386 1387 err = secondDisk.Delete(context.Background(), bucket, object, DeleteOptions{ 1388 Recursive: true, 1389 Immediate: false, 1390 }) 1391 if err != nil { 1392 t.Fatalf("Failed to delete a file - %v", err) 1393 } 1394 1395 _, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan}) 1396 if err != nil { 1397 t.Fatalf("Failed to heal object - %v", err) 1398 } 1399 1400 partReconstructed, err := firstDisk.ReadAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1")) 1401 if err != nil { 1402 t.Fatalf("Failed to read a file - %v", err) 1403 } 1404 1405 if !reflect.DeepEqual(part1Disk1Origin, partReconstructed) { 1406 t.Fatalf("part.1 not healed correctly") 1407 } 1408 1409 partReconstructed, err = secondDisk.ReadAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1")) 1410 if err != nil { 1411 t.Fatalf("Failed to read a file - %v", err) 1412 } 1413 1414 if !reflect.DeepEqual(part1Disk2Origin, partReconstructed) { 1415 t.Fatalf("part.1 not healed correctly") 1416 } 1417 } 1418 1419 // Tests healing of object. 1420 func TestHealObjectErasure(t *testing.T) { 1421 ctx, cancel := context.WithCancel(context.Background()) 1422 defer cancel() 1423 1424 nDisks := 16 1425 fsDirs, err := getRandomDisks(nDisks) 1426 if err != nil { 1427 t.Fatal(err) 1428 } 1429 1430 defer removeRoots(fsDirs) 1431 1432 // Everything is fine, should return nil 1433 obj, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, fsDirs...)) 1434 if err != nil { 1435 t.Fatal(err) 1436 } 1437 1438 bucket := "bucket" 1439 object := "object" 1440 data := bytes.Repeat([]byte("a"), 5*1024*1024) 1441 var opts ObjectOptions 1442 1443 err = obj.MakeBucket(ctx, bucket, MakeBucketOptions{}) 1444 if err != nil { 1445 t.Fatalf("Failed to make a bucket - %v", err) 1446 } 1447 1448 // Create an object with multiple parts uploaded in decreasing 1449 // part number. 1450 res, err := obj.NewMultipartUpload(ctx, bucket, object, opts) 1451 if err != nil { 1452 t.Fatalf("Failed to create a multipart upload - %v", err) 1453 } 1454 1455 var uploadedParts []CompletePart 1456 for _, partID := range []int{2, 1} { 1457 pInfo, err1 := obj.PutObjectPart(ctx, bucket, object, res.UploadID, partID, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), opts) 1458 if err1 != nil { 1459 t.Fatalf("Failed to upload a part - %v", err1) 1460 } 1461 uploadedParts = append(uploadedParts, CompletePart{ 1462 PartNumber: pInfo.PartNumber, 1463 ETag: pInfo.ETag, 1464 }) 1465 } 1466 1467 // Remove the object backend files from the first disk. 1468 z := obj.(*erasureServerPools) 1469 er := z.serverPools[0].sets[0] 1470 firstDisk := er.getDisks()[0] 1471 1472 _, err = obj.CompleteMultipartUpload(ctx, bucket, object, res.UploadID, uploadedParts, ObjectOptions{}) 1473 if err != nil { 1474 t.Fatalf("Failed to complete multipart upload - %v", err) 1475 } 1476 1477 // Delete the whole object folder 1478 err = firstDisk.Delete(context.Background(), bucket, object, DeleteOptions{ 1479 Recursive: true, 1480 Immediate: false, 1481 }) 1482 if err != nil { 1483 t.Fatalf("Failed to delete a file - %v", err) 1484 } 1485 1486 _, err = obj.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan}) 1487 if err != nil { 1488 t.Fatalf("Failed to heal object - %v", err) 1489 } 1490 1491 if _, err = firstDisk.StatInfoFile(context.Background(), bucket, object+"/"+xlStorageFormatFile, false); err != nil { 1492 t.Errorf("Expected xl.meta file to be present but stat failed - %v", err) 1493 } 1494 1495 erasureDisks := er.getDisks() 1496 z.serverPools[0].erasureDisksMu.Lock() 1497 er.getDisks = func() []StorageAPI { 1498 // Nil more than half the disks, to remove write quorum. 1499 for i := 0; i <= len(erasureDisks)/2; i++ { 1500 erasureDisks[i] = nil 1501 } 1502 return erasureDisks 1503 } 1504 z.serverPools[0].erasureDisksMu.Unlock() 1505 1506 // Try healing now, expect to receive errDiskNotFound. 1507 _, err = obj.HealObject(ctx, bucket, object, "", madmin.HealOpts{ 1508 ScanMode: madmin.HealDeepScan, 1509 }) 1510 // since majority of xl.meta's are not available, object quorum 1511 // can't be read properly will be deleted automatically and 1512 // err is nil 1513 if !isErrObjectNotFound(err) { 1514 t.Fatal(err) 1515 } 1516 } 1517 1518 // Tests healing of empty directories 1519 func TestHealEmptyDirectoryErasure(t *testing.T) { 1520 ctx, cancel := context.WithCancel(context.Background()) 1521 defer cancel() 1522 1523 nDisks := 16 1524 fsDirs, err := getRandomDisks(nDisks) 1525 if err != nil { 1526 t.Fatal(err) 1527 } 1528 defer removeRoots(fsDirs) 1529 1530 // Everything is fine, should return nil 1531 obj, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, fsDirs...)) 1532 if err != nil { 1533 t.Fatal(err) 1534 } 1535 1536 bucket := "bucket" 1537 object := "empty-dir/" 1538 var opts ObjectOptions 1539 1540 err = obj.MakeBucket(ctx, bucket, MakeBucketOptions{}) 1541 if err != nil { 1542 t.Fatalf("Failed to make a bucket - %v", err) 1543 } 1544 1545 // Upload an empty directory 1546 _, err = obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, 1547 bytes.NewReader([]byte{}), 0, "", ""), opts) 1548 if err != nil { 1549 t.Fatal(err) 1550 } 1551 1552 // Remove the object backend files from the first disk. 1553 z := obj.(*erasureServerPools) 1554 er := z.serverPools[0].sets[0] 1555 firstDisk := er.getDisks()[0] 1556 err = firstDisk.DeleteVol(context.Background(), pathJoin(bucket, encodeDirObject(object)), true) 1557 if err != nil { 1558 t.Fatalf("Failed to delete a file - %v", err) 1559 } 1560 1561 // Heal the object 1562 hr, err := obj.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan}) 1563 if err != nil { 1564 t.Fatalf("Failed to heal object - %v", err) 1565 } 1566 1567 // Check if the empty directory is restored in the first disk 1568 _, err = firstDisk.StatVol(context.Background(), pathJoin(bucket, encodeDirObject(object))) 1569 if err != nil { 1570 t.Fatalf("Expected object to be present but stat failed - %v", err) 1571 } 1572 1573 // Check the state of the object in the first disk (should be missing) 1574 if hr.Before.Drives[0].State != madmin.DriveStateMissing { 1575 t.Fatalf("Unexpected drive state: %v", hr.Before.Drives[0].State) 1576 } 1577 1578 // Check the state of all other disks (should be ok) 1579 for i, h := range append(hr.Before.Drives[1:], hr.After.Drives...) { 1580 if h.State != madmin.DriveStateOk { 1581 t.Fatalf("Unexpected drive state (%d): %v", i+1, h.State) 1582 } 1583 } 1584 1585 // Heal the same object again 1586 hr, err = obj.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan}) 1587 if err != nil { 1588 t.Fatalf("Failed to heal object - %v", err) 1589 } 1590 1591 // Check that Before & After states are all okay 1592 for i, h := range append(hr.Before.Drives, hr.After.Drives...) { 1593 if h.State != madmin.DriveStateOk { 1594 t.Fatalf("Unexpected drive state (%d): %v", i+1, h.State) 1595 } 1596 } 1597 } 1598 1599 func TestHealLastDataShard(t *testing.T) { 1600 tests := []struct { 1601 name string 1602 dataSize int64 1603 }{ 1604 {"4KiB", 4 * humanize.KiByte}, 1605 {"64KiB", 64 * humanize.KiByte}, 1606 {"128KiB", 128 * humanize.KiByte}, 1607 {"1MiB", 1 * humanize.MiByte}, 1608 {"5MiB", 5 * humanize.MiByte}, 1609 {"10MiB", 10 * humanize.MiByte}, 1610 {"5MiB-1KiB", 5*humanize.MiByte - 1*humanize.KiByte}, 1611 {"10MiB-1Kib", 10*humanize.MiByte - 1*humanize.KiByte}, 1612 } 1613 1614 for _, test := range tests { 1615 t.Run(test.name, func(t *testing.T) { 1616 ctx, cancel := context.WithCancel(context.Background()) 1617 defer cancel() 1618 1619 nDisks := 16 1620 fsDirs, err := getRandomDisks(nDisks) 1621 if err != nil { 1622 t.Fatal(err) 1623 } 1624 1625 defer removeRoots(fsDirs) 1626 1627 obj, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, fsDirs...)) 1628 if err != nil { 1629 t.Fatal(err) 1630 } 1631 bucket := "bucket" 1632 object := "object" 1633 1634 data := make([]byte, test.dataSize) 1635 _, err = rand.Read(data) 1636 if err != nil { 1637 t.Fatal(err) 1638 } 1639 var opts ObjectOptions 1640 1641 err = obj.MakeBucket(ctx, bucket, MakeBucketOptions{}) 1642 if err != nil { 1643 t.Fatalf("Failed to make a bucket - %v", err) 1644 } 1645 1646 _, err = obj.PutObject(ctx, bucket, object, 1647 mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), opts) 1648 if err != nil { 1649 t.Fatal(err) 1650 } 1651 1652 actualH := sha256.New() 1653 _, err = io.Copy(actualH, bytes.NewReader(data)) 1654 if err != nil { 1655 return 1656 } 1657 actualSha256 := actualH.Sum(nil) 1658 1659 z := obj.(*erasureServerPools) 1660 er := z.serverPools[0].getHashedSet(object) 1661 1662 disks := er.getDisks() 1663 distribution := hashOrder(pathJoin(bucket, object), nDisks) 1664 shuffledDisks := shuffleDisks(disks, distribution) 1665 1666 // remove last data shard 1667 err = removeAll(pathJoin(shuffledDisks[11].String(), bucket, object)) 1668 if err != nil { 1669 t.Fatalf("Failed to delete a file - %v", err) 1670 } 1671 _, err = obj.HealObject(ctx, bucket, object, "", madmin.HealOpts{ 1672 ScanMode: madmin.HealNormalScan, 1673 }) 1674 if err != nil { 1675 t.Fatal(err) 1676 } 1677 1678 firstGr, err := obj.GetObjectNInfo(ctx, bucket, object, nil, nil, ObjectOptions{NoLock: true}) 1679 if err != nil { 1680 t.Fatal(err) 1681 } 1682 defer firstGr.Close() 1683 1684 firstHealedH := sha256.New() 1685 _, err = io.Copy(firstHealedH, firstGr) 1686 if err != nil { 1687 t.Fatal(err) 1688 } 1689 firstHealedDataSha256 := firstHealedH.Sum(nil) 1690 1691 if !bytes.Equal(actualSha256, firstHealedDataSha256) { 1692 t.Fatalf("object healed wrong, expected %x, got %x", 1693 actualSha256, firstHealedDataSha256) 1694 } 1695 1696 // remove another data shard 1697 if err = removeAll(pathJoin(shuffledDisks[1].String(), bucket, object)); err != nil { 1698 t.Fatalf("Failed to delete a file - %v", err) 1699 } 1700 1701 _, err = obj.HealObject(ctx, bucket, object, "", madmin.HealOpts{ 1702 ScanMode: madmin.HealNormalScan, 1703 }) 1704 if err != nil { 1705 t.Fatal(err) 1706 } 1707 1708 secondGr, err := obj.GetObjectNInfo(ctx, bucket, object, nil, nil, ObjectOptions{NoLock: true}) 1709 if err != nil { 1710 t.Fatal(err) 1711 } 1712 defer secondGr.Close() 1713 1714 secondHealedH := sha256.New() 1715 _, err = io.Copy(secondHealedH, secondGr) 1716 if err != nil { 1717 t.Fatal(err) 1718 } 1719 secondHealedDataSha256 := secondHealedH.Sum(nil) 1720 1721 if !bytes.Equal(actualSha256, secondHealedDataSha256) { 1722 t.Fatalf("object healed wrong, expected %x, got %x", 1723 actualSha256, secondHealedDataSha256) 1724 } 1725 }) 1726 } 1727 }