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  }