heal: Dangling check to evaluate object parts separately (#19797)

This commit is contained in:
Anis Eleuch 2024-06-10 16:51:27 +01:00 committed by GitHub
parent 0662c90b5c
commit 789cbc6fb2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 614 additions and 183 deletions

View File

@ -20,6 +20,7 @@ package cmd
import ( import (
"bytes" "bytes"
"context" "context"
"slices"
"time" "time"
"github.com/minio/madmin-go/v3" "github.com/minio/madmin-go/v3"
@ -253,6 +254,33 @@ func listOnlineDisks(disks []StorageAPI, partsMetadata []FileInfo, errs []error,
return onlineDisks, modTime, "" return onlineDisks, modTime, ""
} }
// Convert verify or check parts returned error to integer representation
func convPartErrToInt(err error) int {
err = unwrapAll(err)
switch err {
case nil:
return checkPartSuccess
case errFileNotFound, errFileVersionNotFound:
return checkPartFileNotFound
case errFileCorrupt:
return checkPartFileCorrupt
case errVolumeNotFound:
return checkPartVolumeNotFound
case errDiskNotFound:
return checkPartDiskNotFound
default:
return checkPartUnknown
}
}
func partNeedsHealing(partErrs []int) bool {
return slices.IndexFunc(partErrs, func(i int) bool { return i != checkPartSuccess && i != checkPartUnknown }) > -1
}
func hasPartErr(partErrs []int) bool {
return slices.IndexFunc(partErrs, func(i int) bool { return i != checkPartSuccess }) > -1
}
// disksWithAllParts - This function needs to be called with // disksWithAllParts - This function needs to be called with
// []StorageAPI returned by listOnlineDisks. Returns, // []StorageAPI returned by listOnlineDisks. Returns,
// //
@ -262,10 +290,19 @@ func listOnlineDisks(disks []StorageAPI, partsMetadata []FileInfo, errs []error,
// a not-found error or a hash-mismatch error. // a not-found error or a hash-mismatch error.
func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetadata []FileInfo, func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetadata []FileInfo,
errs []error, latestMeta FileInfo, bucket, object string, errs []error, latestMeta FileInfo, bucket, object string,
scanMode madmin.HealScanMode) ([]StorageAPI, []error, time.Time, scanMode madmin.HealScanMode,
) { ) (availableDisks []StorageAPI, dataErrsByDisk map[int][]int, dataErrsByPart map[int][]int) {
availableDisks := make([]StorageAPI, len(onlineDisks)) availableDisks = make([]StorageAPI, len(onlineDisks))
dataErrs := make([]error, len(onlineDisks))
dataErrsByDisk = make(map[int][]int, len(onlineDisks))
for i := range onlineDisks {
dataErrsByDisk[i] = make([]int, len(latestMeta.Parts))
}
dataErrsByPart = make(map[int][]int, len(latestMeta.Parts))
for i := range latestMeta.Parts {
dataErrsByPart[i] = make([]int, len(onlineDisks))
}
inconsistent := 0 inconsistent := 0
for i, meta := range partsMetadata { for i, meta := range partsMetadata {
@ -295,19 +332,21 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
erasureDistributionReliable = false erasureDistributionReliable = false
} }
metaErrs := make([]error, len(errs))
for i, onlineDisk := range onlineDisks { for i, onlineDisk := range onlineDisks {
if errs[i] != nil { if errs[i] != nil {
dataErrs[i] = errs[i] metaErrs[i] = errs[i]
continue continue
} }
if onlineDisk == OfflineDisk { if onlineDisk == OfflineDisk {
dataErrs[i] = errDiskNotFound metaErrs[i] = errDiskNotFound
continue continue
} }
meta := partsMetadata[i] meta := partsMetadata[i]
if !meta.ModTime.Equal(latestMeta.ModTime) || meta.DataDir != latestMeta.DataDir { if !meta.ModTime.Equal(latestMeta.ModTime) || meta.DataDir != latestMeta.DataDir {
dataErrs[i] = errFileCorrupt metaErrs[i] = errFileCorrupt
partsMetadata[i] = FileInfo{} partsMetadata[i] = FileInfo{}
continue continue
} }
@ -315,7 +354,7 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
if erasureDistributionReliable { if erasureDistributionReliable {
if !meta.IsValid() { if !meta.IsValid() {
partsMetadata[i] = FileInfo{} partsMetadata[i] = FileInfo{}
dataErrs[i] = errFileCorrupt metaErrs[i] = errFileCorrupt
continue continue
} }
@ -325,46 +364,79 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
// attempt a fix if possible, assuming other entries // attempt a fix if possible, assuming other entries
// might have the right erasure distribution. // might have the right erasure distribution.
partsMetadata[i] = FileInfo{} partsMetadata[i] = FileInfo{}
dataErrs[i] = errFileCorrupt metaErrs[i] = errFileCorrupt
continue continue
} }
} }
} }
}
// Copy meta errors to part errors
for i, err := range metaErrs {
if err != nil {
partErr := convPartErrToInt(err)
for p := range latestMeta.Parts {
dataErrsByPart[p][i] = partErr
}
}
}
for i, onlineDisk := range onlineDisks {
if metaErrs[i] != nil {
continue
}
meta := partsMetadata[i]
if meta.Deleted || meta.IsRemote() {
continue
}
// Always check data, if we got it. // Always check data, if we got it.
if (len(meta.Data) > 0 || meta.Size == 0) && len(meta.Parts) > 0 { if (len(meta.Data) > 0 || meta.Size == 0) && len(meta.Parts) > 0 {
checksumInfo := meta.Erasure.GetChecksumInfo(meta.Parts[0].Number) checksumInfo := meta.Erasure.GetChecksumInfo(meta.Parts[0].Number)
dataErrs[i] = bitrotVerify(bytes.NewReader(meta.Data), verifyErr := bitrotVerify(bytes.NewReader(meta.Data),
int64(len(meta.Data)), int64(len(meta.Data)),
meta.Erasure.ShardFileSize(meta.Size), meta.Erasure.ShardFileSize(meta.Size),
checksumInfo.Algorithm, checksumInfo.Algorithm,
checksumInfo.Hash, meta.Erasure.ShardSize()) checksumInfo.Hash, meta.Erasure.ShardSize())
if dataErrs[i] == nil { dataErrsByPart[0][i] = convPartErrToInt(verifyErr)
// All parts verified, mark it as all data available.
availableDisks[i] = onlineDisk
} else {
// upon errors just make that disk's fileinfo invalid
partsMetadata[i] = FileInfo{}
}
continue continue
} }
var (
verifyErr error
verifyResp *CheckPartsResp
)
meta.DataDir = latestMeta.DataDir meta.DataDir = latestMeta.DataDir
switch scanMode { switch scanMode {
case madmin.HealDeepScan: case madmin.HealDeepScan:
// disk has a valid xl.meta but may not have all the // disk has a valid xl.meta but may not have all the
// parts. This is considered an outdated disk, since // parts. This is considered an outdated disk, since
// it needs healing too. // it needs healing too.
if !meta.Deleted && !meta.IsRemote() { verifyResp, verifyErr = onlineDisk.VerifyFile(ctx, bucket, object, meta)
dataErrs[i] = onlineDisk.VerifyFile(ctx, bucket, object, meta) default:
} verifyResp, verifyErr = onlineDisk.CheckParts(ctx, bucket, object, meta)
case madmin.HealNormalScan:
if !meta.Deleted && !meta.IsRemote() {
dataErrs[i] = onlineDisk.CheckParts(ctx, bucket, object, meta)
}
} }
if dataErrs[i] == nil { for p := range latestMeta.Parts {
if verifyErr != nil {
dataErrsByPart[p][i] = convPartErrToInt(verifyErr)
} else {
dataErrsByPart[p][i] = verifyResp.Results[p]
}
}
}
// Build dataErrs by disk from dataErrs by part
for part, disks := range dataErrsByPart {
for disk := range disks {
dataErrsByDisk[disk][part] = dataErrsByPart[part][disk]
}
}
for i, onlineDisk := range onlineDisks {
if metaErrs[i] == nil && !hasPartErr(dataErrsByDisk[i]) {
// All parts verified, mark it as all data available. // All parts verified, mark it as all data available.
availableDisks[i] = onlineDisk availableDisks[i] = onlineDisk
} else { } else {
@ -373,5 +445,5 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
} }
} }
return availableDisks, dataErrs, timeSentinel return
} }

View File

@ -308,9 +308,8 @@ func TestListOnlineDisks(t *testing.T) {
t.Fatalf("Expected modTime to be equal to %v but was found to be %v", t.Fatalf("Expected modTime to be equal to %v but was found to be %v",
test.expectedTime, modTime) test.expectedTime, modTime)
} }
availableDisks, newErrs, _ := disksWithAllParts(ctx, onlineDisks, partsMetadata, availableDisks, _, _ := disksWithAllParts(ctx, onlineDisks, partsMetadata,
test.errs, fi, bucket, object, madmin.HealDeepScan) test.errs, fi, bucket, object, madmin.HealDeepScan)
test.errs = newErrs
if test._tamperBackend != noTamper { if test._tamperBackend != noTamper {
if tamperedIndex != -1 && availableDisks[tamperedIndex] != nil { if tamperedIndex != -1 && availableDisks[tamperedIndex] != nil {
@ -491,9 +490,8 @@ func TestListOnlineDisksSmallObjects(t *testing.T) {
test.expectedTime, modTime) test.expectedTime, modTime)
} }
availableDisks, newErrs, _ := disksWithAllParts(ctx, onlineDisks, partsMetadata, availableDisks, _, _ := disksWithAllParts(ctx, onlineDisks, partsMetadata,
test.errs, fi, bucket, object, madmin.HealDeepScan) test.errs, fi, bucket, object, madmin.HealDeepScan)
test.errs = newErrs
if test._tamperBackend != noTamper { if test._tamperBackend != noTamper {
if tamperedIndex != -1 && availableDisks[tamperedIndex] != nil { if tamperedIndex != -1 && availableDisks[tamperedIndex] != nil {
@ -554,7 +552,7 @@ func TestDisksWithAllParts(t *testing.T) {
erasureDisks, _, _ = listOnlineDisks(erasureDisks, partsMetadata, errs, readQuorum) erasureDisks, _, _ = listOnlineDisks(erasureDisks, partsMetadata, errs, readQuorum)
filteredDisks, errs, _ := disksWithAllParts(ctx, erasureDisks, partsMetadata, filteredDisks, _, dataErrsPerDisk := disksWithAllParts(ctx, erasureDisks, partsMetadata,
errs, fi, bucket, object, madmin.HealDeepScan) errs, fi, bucket, object, madmin.HealDeepScan)
if len(filteredDisks) != len(erasureDisks) { if len(filteredDisks) != len(erasureDisks) {
@ -562,8 +560,8 @@ func TestDisksWithAllParts(t *testing.T) {
} }
for diskIndex, disk := range filteredDisks { for diskIndex, disk := range filteredDisks {
if errs[diskIndex] != nil { if partNeedsHealing(dataErrsPerDisk[diskIndex]) {
t.Errorf("Unexpected error %s", errs[diskIndex]) t.Errorf("Unexpected error: %v", dataErrsPerDisk[diskIndex])
} }
if disk == nil { if disk == nil {
@ -634,7 +632,7 @@ func TestDisksWithAllParts(t *testing.T) {
} }
errs = make([]error, len(erasureDisks)) errs = make([]error, len(erasureDisks))
filteredDisks, errs, _ = disksWithAllParts(ctx, erasureDisks, partsMetadata, filteredDisks, dataErrsPerDisk, _ = disksWithAllParts(ctx, erasureDisks, partsMetadata,
errs, fi, bucket, object, madmin.HealDeepScan) errs, fi, bucket, object, madmin.HealDeepScan)
if len(filteredDisks) != len(erasureDisks) { if len(filteredDisks) != len(erasureDisks) {
@ -646,15 +644,15 @@ func TestDisksWithAllParts(t *testing.T) {
if disk != nil { if disk != nil {
t.Errorf("Drive not filtered as expected, drive: %d", diskIndex) t.Errorf("Drive not filtered as expected, drive: %d", diskIndex)
} }
if errs[diskIndex] == nil { if !partNeedsHealing(dataErrsPerDisk[diskIndex]) {
t.Errorf("Expected error not received, driveIndex: %d", diskIndex) t.Errorf("Disk expected to be healed, driveIndex: %d", diskIndex)
} }
} else { } else {
if disk == nil { if disk == nil {
t.Errorf("Drive erroneously filtered, driveIndex: %d", diskIndex) t.Errorf("Drive erroneously filtered, driveIndex: %d", diskIndex)
} }
if errs[diskIndex] != nil { if partNeedsHealing(dataErrsPerDisk[diskIndex]) {
t.Errorf("Unexpected error, %s, driveIndex: %d", errs[diskIndex], diskIndex) t.Errorf("Disk not expected to be healed, driveIndex: %d", diskIndex)
} }
} }

View File

@ -32,6 +32,7 @@ import (
"github.com/minio/minio/internal/grid" "github.com/minio/minio/internal/grid"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
"github.com/minio/pkg/v3/sync/errgroup" "github.com/minio/pkg/v3/sync/errgroup"
"golang.org/x/exp/slices"
) )
//go:generate stringer -type=healingMetric -trimprefix=healingMetric $GOFILE //go:generate stringer -type=healingMetric -trimprefix=healingMetric $GOFILE
@ -144,36 +145,41 @@ func listAllBuckets(ctx context.Context, storageDisks []StorageAPI, healBuckets
return reduceReadQuorumErrs(ctx, g.Wait(), bucketMetadataOpIgnoredErrs, readQuorum) return reduceReadQuorumErrs(ctx, g.Wait(), bucketMetadataOpIgnoredErrs, readQuorum)
} }
var errLegacyXLMeta = errors.New("legacy XL meta")
var errOutdatedXLMeta = errors.New("outdated XL meta")
var errPartMissingOrCorrupt = errors.New("part missing or corrupt")
// Only heal on disks where we are sure that healing is needed. We can expand // Only heal on disks where we are sure that healing is needed. We can expand
// this list as and when we figure out more errors can be added to this list safely. // this list as and when we figure out more errors can be added to this list safely.
func shouldHealObjectOnDisk(erErr, dataErr error, meta FileInfo, latestMeta FileInfo) bool { func shouldHealObjectOnDisk(erErr error, partsErrs []int, meta FileInfo, latestMeta FileInfo) (bool, error) {
switch { if errors.Is(erErr, errFileNotFound) || errors.Is(erErr, errFileVersionNotFound) || errors.Is(erErr, errFileCorrupt) {
case errors.Is(erErr, errFileNotFound) || errors.Is(erErr, errFileVersionNotFound): return true, erErr
return true
case errors.Is(erErr, errFileCorrupt):
return true
} }
if erErr == nil { if erErr == nil {
if meta.XLV1 { if meta.XLV1 {
// Legacy means heal always // Legacy means heal always
// always check first. // always check first.
return true return true, errLegacyXLMeta
}
if !latestMeta.Equals(meta) {
return true, errOutdatedXLMeta
} }
if !meta.Deleted && !meta.IsRemote() { if !meta.Deleted && !meta.IsRemote() {
// If xl.meta was read fine but there may be problem with the part.N files. // If xl.meta was read fine but there may be problem with the part.N files.
if IsErr(dataErr, []error{ for _, partErr := range partsErrs {
errFileNotFound, if slices.Contains([]int{
errFileVersionNotFound, checkPartFileNotFound,
errFileCorrupt, checkPartFileCorrupt,
}...) { }, partErr) {
return true return true, errPartMissingOrCorrupt
}
} }
} }
if !latestMeta.Equals(meta) { return false, nil
return true
}
} }
return false return false, erErr
} }
const ( const (
@ -332,7 +338,7 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
// used here for reconstruction. This is done to ensure that // used here for reconstruction. This is done to ensure that
// we do not skip drives that have inconsistent metadata to be // we do not skip drives that have inconsistent metadata to be
// skipped from purging when they are stale. // skipped from purging when they are stale.
availableDisks, dataErrs, _ := disksWithAllParts(ctx, onlineDisks, partsMetadata, availableDisks, dataErrsByDisk, dataErrsByPart := disksWithAllParts(ctx, onlineDisks, partsMetadata,
errs, latestMeta, bucket, object, scanMode) errs, latestMeta, bucket, object, scanMode)
var erasure Erasure var erasure Erasure
@ -355,15 +361,20 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
// to be healed. // to be healed.
outDatedDisks := make([]StorageAPI, len(storageDisks)) outDatedDisks := make([]StorageAPI, len(storageDisks))
disksToHealCount := 0 disksToHealCount := 0
for i, v := range availableDisks { for i := range availableDisks {
yes, reason := shouldHealObjectOnDisk(errs[i], dataErrsByDisk[i], partsMetadata[i], latestMeta)
if yes {
outDatedDisks[i] = storageDisks[i]
disksToHealCount++
}
driveState := "" driveState := ""
switch { switch {
case v != nil: case reason == nil:
driveState = madmin.DriveStateOk driveState = madmin.DriveStateOk
case errors.Is(errs[i], errDiskNotFound), errors.Is(dataErrs[i], errDiskNotFound): case IsErr(reason, errDiskNotFound):
driveState = madmin.DriveStateOffline driveState = madmin.DriveStateOffline
case IsErr(errs[i], errFileNotFound, errFileVersionNotFound, errVolumeNotFound), case IsErr(reason, errFileNotFound, errFileVersionNotFound, errVolumeNotFound, errPartMissingOrCorrupt, errOutdatedXLMeta, errLegacyXLMeta):
IsErr(dataErrs[i], errFileNotFound, errFileVersionNotFound, errVolumeNotFound):
driveState = madmin.DriveStateMissing driveState = madmin.DriveStateMissing
default: default:
// all remaining cases imply corrupt data/metadata // all remaining cases imply corrupt data/metadata
@ -380,12 +391,6 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
Endpoint: storageEndpoints[i].String(), Endpoint: storageEndpoints[i].String(),
State: driveState, State: driveState,
}) })
if shouldHealObjectOnDisk(errs[i], dataErrs[i], partsMetadata[i], latestMeta) {
outDatedDisks[i] = storageDisks[i]
disksToHealCount++
continue
}
} }
if isAllNotFound(errs) { if isAllNotFound(errs) {
@ -412,7 +417,7 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
if !latestMeta.XLV1 && !latestMeta.Deleted && disksToHealCount > latestMeta.Erasure.ParityBlocks { if !latestMeta.XLV1 && !latestMeta.Deleted && disksToHealCount > latestMeta.Erasure.ParityBlocks {
// Allow for dangling deletes, on versions that have DataDir missing etc. // Allow for dangling deletes, on versions that have DataDir missing etc.
// this would end up restoring the correct readable versions. // this would end up restoring the correct readable versions.
m, err := er.deleteIfDangling(ctx, bucket, object, partsMetadata, errs, dataErrs, ObjectOptions{ m, err := er.deleteIfDangling(ctx, bucket, object, partsMetadata, errs, dataErrsByPart, ObjectOptions{
VersionID: versionID, VersionID: versionID,
}) })
errs = make([]error, len(errs)) errs = make([]error, len(errs))
@ -908,35 +913,52 @@ func isObjectDirDangling(errs []error) (ok bool) {
return found < notFound && found > 0 return found < notFound && found > 0
} }
func danglingMetaErrsCount(cerrs []error) (notFoundCount int, nonActionableCount int) {
for _, readErr := range cerrs {
if readErr == nil {
continue
}
switch {
case errors.Is(readErr, errFileNotFound) || errors.Is(readErr, errFileVersionNotFound):
notFoundCount++
default:
// All other errors are non-actionable
nonActionableCount++
}
}
return
}
func danglingPartErrsCount(results []int) (notFoundCount int, nonActionableCount int) {
for _, partResult := range results {
switch partResult {
case checkPartSuccess:
continue
case checkPartFileNotFound:
notFoundCount++
default:
// All other errors are non-actionable
nonActionableCount++
}
}
return
}
// Object is considered dangling/corrupted if and only // Object is considered dangling/corrupted if and only
// if total disks - a combination of corrupted and missing // if total disks - a combination of corrupted and missing
// files is lesser than number of data blocks. // files is lesser than number of data blocks.
func isObjectDangling(metaArr []FileInfo, errs []error, dataErrs []error) (validMeta FileInfo, ok bool) { func isObjectDangling(metaArr []FileInfo, errs []error, dataErrsByPart map[int][]int) (validMeta FileInfo, ok bool) {
// We can consider an object data not reliable // We can consider an object data not reliable
// when xl.meta is not found in read quorum disks. // when xl.meta is not found in read quorum disks.
// or when xl.meta is not readable in read quorum disks. // or when xl.meta is not readable in read quorum disks.
danglingErrsCount := func(cerrs []error) (int, int) { notFoundMetaErrs, nonActionableMetaErrs := danglingMetaErrsCount(errs)
var (
notFoundCount int
nonActionableCount int
)
for _, readErr := range cerrs {
if readErr == nil {
continue
}
switch {
case errors.Is(readErr, errFileNotFound) || errors.Is(readErr, errFileVersionNotFound):
notFoundCount++
default:
// All other errors are non-actionable
nonActionableCount++
}
}
return notFoundCount, nonActionableCount
}
notFoundMetaErrs, nonActionableMetaErrs := danglingErrsCount(errs) notFoundPartsErrs, nonActionablePartsErrs := 0, 0
notFoundPartsErrs, nonActionablePartsErrs := danglingErrsCount(dataErrs) for _, dataErrs := range dataErrsByPart {
if nf, na := danglingPartErrsCount(dataErrs); nf > notFoundPartsErrs {
notFoundPartsErrs, nonActionablePartsErrs = nf, na
}
}
for _, m := range metaArr { for _, m := range metaArr {
if m.IsValid() { if m.IsValid() {
@ -948,7 +970,7 @@ func isObjectDangling(metaArr []FileInfo, errs []error, dataErrs []error) (valid
if !validMeta.IsValid() { if !validMeta.IsValid() {
// validMeta is invalid because all xl.meta is missing apparently // validMeta is invalid because all xl.meta is missing apparently
// we should figure out if dataDirs are also missing > dataBlocks. // we should figure out if dataDirs are also missing > dataBlocks.
dataBlocks := (len(dataErrs) + 1) / 2 dataBlocks := (len(metaArr) + 1) / 2
if notFoundPartsErrs > dataBlocks { if notFoundPartsErrs > dataBlocks {
// Not using parity to ensure that we do not delete // Not using parity to ensure that we do not delete
// any valid content, if any is recoverable. But if // any valid content, if any is recoverable. But if

View File

@ -49,7 +49,7 @@ func TestIsObjectDangling(t *testing.T) {
name string name string
metaArr []FileInfo metaArr []FileInfo
errs []error errs []error
dataErrs []error dataErrs map[int][]int
expectedMeta FileInfo expectedMeta FileInfo
expectedDangling bool expectedDangling bool
}{ }{
@ -165,11 +165,8 @@ func TestIsObjectDangling(t *testing.T) {
nil, nil,
nil, nil,
}, },
dataErrs: []error{ dataErrs: map[int][]int{
errFileCorrupt, 0: {checkPartFileCorrupt, checkPartFileNotFound, checkPartSuccess, checkPartFileCorrupt},
errFileNotFound,
nil,
errFileCorrupt,
}, },
expectedMeta: fi, expectedMeta: fi,
expectedDangling: false, expectedDangling: false,
@ -188,11 +185,8 @@ func TestIsObjectDangling(t *testing.T) {
errFileNotFound, errFileNotFound,
nil, nil,
}, },
dataErrs: []error{ dataErrs: map[int][]int{
errFileNotFound, 0: {checkPartFileNotFound, checkPartFileCorrupt, checkPartSuccess, checkPartSuccess},
errFileCorrupt,
nil,
nil,
}, },
expectedMeta: fi, expectedMeta: fi,
expectedDangling: false, expectedDangling: false,
@ -247,15 +241,58 @@ func TestIsObjectDangling(t *testing.T) {
nil, nil,
nil, nil,
}, },
dataErrs: []error{ dataErrs: map[int][]int{
errFileNotFound, 0: {checkPartFileNotFound, checkPartFileNotFound, checkPartSuccess, checkPartFileNotFound},
errFileNotFound,
nil,
errFileNotFound,
}, },
expectedMeta: fi, expectedMeta: fi,
expectedDangling: true, expectedDangling: true,
}, },
{
name: "FileInfoDecided-case4-(missing data-dir for part 2)",
metaArr: []FileInfo{
{},
{},
{},
fi,
},
errs: []error{
errFileNotFound,
errFileNotFound,
nil,
nil,
},
dataErrs: map[int][]int{
0: {checkPartSuccess, checkPartSuccess, checkPartSuccess, checkPartSuccess},
1: {checkPartSuccess, checkPartFileNotFound, checkPartFileNotFound, checkPartFileNotFound},
},
expectedMeta: fi,
expectedDangling: true,
},
{
name: "FileInfoDecided-case4-(enough data-dir existing for each part)",
metaArr: []FileInfo{
{},
{},
{},
fi,
},
errs: []error{
errFileNotFound,
errFileNotFound,
nil,
nil,
},
dataErrs: map[int][]int{
0: {checkPartFileNotFound, checkPartSuccess, checkPartSuccess, checkPartSuccess},
1: {checkPartSuccess, checkPartFileNotFound, checkPartSuccess, checkPartSuccess},
2: {checkPartSuccess, checkPartSuccess, checkPartFileNotFound, checkPartSuccess},
3: {checkPartSuccess, checkPartSuccess, checkPartSuccess, checkPartFileNotFound},
},
expectedMeta: fi,
expectedDangling: false,
},
// Add new cases as seen // Add new cases as seen
} }
for _, testCase := range testCases { for _, testCase := range testCases {

View File

@ -484,8 +484,8 @@ func joinErrs(errs []error) []string {
return s return s
} }
func (er erasureObjects) deleteIfDangling(ctx context.Context, bucket, object string, metaArr []FileInfo, errs []error, dataErrs []error, opts ObjectOptions) (FileInfo, error) { func (er erasureObjects) deleteIfDangling(ctx context.Context, bucket, object string, metaArr []FileInfo, errs []error, dataErrsByPart map[int][]int, opts ObjectOptions) (FileInfo, error) {
m, ok := isObjectDangling(metaArr, errs, dataErrs) m, ok := isObjectDangling(metaArr, errs, dataErrsByPart)
if !ok { if !ok {
// We only come here if we cannot figure out if the object // We only come here if we cannot figure out if the object
// can be deleted safely, in such a scenario return ReadQuorum error. // can be deleted safely, in such a scenario return ReadQuorum error.
@ -495,7 +495,7 @@ func (er erasureObjects) deleteIfDangling(ctx context.Context, bucket, object st
tags["set"] = er.setIndex tags["set"] = er.setIndex
tags["pool"] = er.poolIndex tags["pool"] = er.poolIndex
tags["merrs"] = joinErrs(errs) tags["merrs"] = joinErrs(errs)
tags["derrs"] = joinErrs(dataErrs) tags["derrs"] = dataErrsByPart
if m.IsValid() { if m.IsValid() {
tags["size"] = m.Size tags["size"] = m.Size
tags["mtime"] = m.ModTime.Format(http.TimeFormat) tags["mtime"] = m.ModTime.Format(http.TimeFormat)
@ -509,8 +509,20 @@ func (er erasureObjects) deleteIfDangling(ctx context.Context, bucket, object st
// count the number of offline disks // count the number of offline disks
offline := 0 offline := 0
for i := 0; i < max(len(errs), len(dataErrs)); i++ { for i := 0; i < len(errs); i++ {
if i < len(errs) && errors.Is(errs[i], errDiskNotFound) || i < len(dataErrs) && errors.Is(dataErrs[i], errDiskNotFound) { var found bool
switch {
case errors.Is(errs[i], errDiskNotFound):
found = true
default:
for p := range dataErrsByPart {
if dataErrsByPart[p][i] == checkPartDiskNotFound {
found = true
break
}
}
}
if found {
offline++ offline++
} }
} }

View File

@ -215,9 +215,9 @@ func (d *naughtyDisk) RenameFile(ctx context.Context, srcVolume, srcPath, dstVol
return d.disk.RenameFile(ctx, srcVolume, srcPath, dstVolume, dstPath) return d.disk.RenameFile(ctx, srcVolume, srcPath, dstVolume, dstPath)
} }
func (d *naughtyDisk) CheckParts(ctx context.Context, volume string, path string, fi FileInfo) (err error) { func (d *naughtyDisk) CheckParts(ctx context.Context, volume string, path string, fi FileInfo) (*CheckPartsResp, error) {
if err := d.calcError(); err != nil { if err := d.calcError(); err != nil {
return err return nil, err
} }
return d.disk.CheckParts(ctx, volume, path, fi) return d.disk.CheckParts(ctx, volume, path, fi)
} }
@ -289,9 +289,9 @@ func (d *naughtyDisk) ReadXL(ctx context.Context, volume string, path string, re
return d.disk.ReadXL(ctx, volume, path, readData) return d.disk.ReadXL(ctx, volume, path, readData)
} }
func (d *naughtyDisk) VerifyFile(ctx context.Context, volume, path string, fi FileInfo) error { func (d *naughtyDisk) VerifyFile(ctx context.Context, volume, path string, fi FileInfo) (*CheckPartsResp, error) {
if err := d.calcError(); err != nil { if err := d.calcError(); err != nil {
return err return nil, err
} }
return d.disk.VerifyFile(ctx, volume, path, fi) return d.disk.VerifyFile(ctx, volume, path, fi)
} }

View File

@ -502,6 +502,23 @@ type RenameDataResp struct {
OldDataDir string // contains '<uuid>', it is designed to be passed as value to Delete(bucket, pathJoin(object, dataDir)) OldDataDir string // contains '<uuid>', it is designed to be passed as value to Delete(bucket, pathJoin(object, dataDir))
} }
const (
checkPartUnknown int = iota
// Changing the order can cause a data loss
// when running two nodes with incompatible versions
checkPartSuccess
checkPartDiskNotFound
checkPartVolumeNotFound
checkPartFileNotFound
checkPartFileCorrupt
)
// CheckPartsResp is a response of the storage CheckParts and VerifyFile APIs
type CheckPartsResp struct {
Results []int
}
// LocalDiskIDs - GetLocalIDs response. // LocalDiskIDs - GetLocalIDs response.
type LocalDiskIDs struct { type LocalDiskIDs struct {
IDs []string IDs []string

View File

@ -273,6 +273,145 @@ func (z *CheckPartsHandlerParams) Msgsize() (s int) {
return return
} }
// DecodeMsg implements msgp.Decodable
func (z *CheckPartsResp) DecodeMsg(dc *msgp.Reader) (err error) {
var field []byte
_ = field
var zb0001 uint32
zb0001, err = dc.ReadMapHeader()
if err != nil {
err = msgp.WrapError(err)
return
}
for zb0001 > 0 {
zb0001--
field, err = dc.ReadMapKeyPtr()
if err != nil {
err = msgp.WrapError(err)
return
}
switch msgp.UnsafeString(field) {
case "Results":
var zb0002 uint32
zb0002, err = dc.ReadArrayHeader()
if err != nil {
err = msgp.WrapError(err, "Results")
return
}
if cap(z.Results) >= int(zb0002) {
z.Results = (z.Results)[:zb0002]
} else {
z.Results = make([]int, zb0002)
}
for za0001 := range z.Results {
z.Results[za0001], err = dc.ReadInt()
if err != nil {
err = msgp.WrapError(err, "Results", za0001)
return
}
}
default:
err = dc.Skip()
if err != nil {
err = msgp.WrapError(err)
return
}
}
}
return
}
// EncodeMsg implements msgp.Encodable
func (z *CheckPartsResp) EncodeMsg(en *msgp.Writer) (err error) {
// map header, size 1
// write "Results"
err = en.Append(0x81, 0xa7, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73)
if err != nil {
return
}
err = en.WriteArrayHeader(uint32(len(z.Results)))
if err != nil {
err = msgp.WrapError(err, "Results")
return
}
for za0001 := range z.Results {
err = en.WriteInt(z.Results[za0001])
if err != nil {
err = msgp.WrapError(err, "Results", za0001)
return
}
}
return
}
// MarshalMsg implements msgp.Marshaler
func (z *CheckPartsResp) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize())
// map header, size 1
// string "Results"
o = append(o, 0x81, 0xa7, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73)
o = msgp.AppendArrayHeader(o, uint32(len(z.Results)))
for za0001 := range z.Results {
o = msgp.AppendInt(o, z.Results[za0001])
}
return
}
// UnmarshalMsg implements msgp.Unmarshaler
func (z *CheckPartsResp) UnmarshalMsg(bts []byte) (o []byte, err error) {
var field []byte
_ = field
var zb0001 uint32
zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
for zb0001 > 0 {
zb0001--
field, bts, err = msgp.ReadMapKeyZC(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
switch msgp.UnsafeString(field) {
case "Results":
var zb0002 uint32
zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Results")
return
}
if cap(z.Results) >= int(zb0002) {
z.Results = (z.Results)[:zb0002]
} else {
z.Results = make([]int, zb0002)
}
for za0001 := range z.Results {
z.Results[za0001], bts, err = msgp.ReadIntBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Results", za0001)
return
}
}
default:
bts, err = msgp.Skip(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
}
}
o = bts
return
}
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z *CheckPartsResp) Msgsize() (s int) {
s = 1 + 8 + msgp.ArrayHeaderSize + (len(z.Results) * (msgp.IntSize))
return
}
// DecodeMsg implements msgp.Decodable // DecodeMsg implements msgp.Decodable
func (z *DeleteFileHandlerParams) DecodeMsg(dc *msgp.Reader) (err error) { func (z *DeleteFileHandlerParams) DecodeMsg(dc *msgp.Reader) (err error) {
var field []byte var field []byte

View File

@ -235,6 +235,119 @@ func BenchmarkDecodeCheckPartsHandlerParams(b *testing.B) {
} }
} }
func TestMarshalUnmarshalCheckPartsResp(t *testing.T) {
v := CheckPartsResp{}
bts, err := v.MarshalMsg(nil)
if err != nil {
t.Fatal(err)
}
left, err := v.UnmarshalMsg(bts)
if err != nil {
t.Fatal(err)
}
if len(left) > 0 {
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
}
left, err = msgp.Skip(bts)
if err != nil {
t.Fatal(err)
}
if len(left) > 0 {
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
}
}
func BenchmarkMarshalMsgCheckPartsResp(b *testing.B) {
v := CheckPartsResp{}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v.MarshalMsg(nil)
}
}
func BenchmarkAppendMsgCheckPartsResp(b *testing.B) {
v := CheckPartsResp{}
bts := make([]byte, 0, v.Msgsize())
bts, _ = v.MarshalMsg(bts[0:0])
b.SetBytes(int64(len(bts)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
bts, _ = v.MarshalMsg(bts[0:0])
}
}
func BenchmarkUnmarshalCheckPartsResp(b *testing.B) {
v := CheckPartsResp{}
bts, _ := v.MarshalMsg(nil)
b.ReportAllocs()
b.SetBytes(int64(len(bts)))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := v.UnmarshalMsg(bts)
if err != nil {
b.Fatal(err)
}
}
}
func TestEncodeDecodeCheckPartsResp(t *testing.T) {
v := CheckPartsResp{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
m := v.Msgsize()
if buf.Len() > m {
t.Log("WARNING: TestEncodeDecodeCheckPartsResp Msgsize() is inaccurate")
}
vn := CheckPartsResp{}
err := msgp.Decode(&buf, &vn)
if err != nil {
t.Error(err)
}
buf.Reset()
msgp.Encode(&buf, &v)
err = msgp.NewReader(&buf).Skip()
if err != nil {
t.Error(err)
}
}
func BenchmarkEncodeCheckPartsResp(b *testing.B) {
v := CheckPartsResp{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
b.SetBytes(int64(buf.Len()))
en := msgp.NewWriter(msgp.Nowhere)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v.EncodeMsg(en)
}
en.Flush()
}
func BenchmarkDecodeCheckPartsResp(b *testing.B) {
v := CheckPartsResp{}
var buf bytes.Buffer
msgp.Encode(&buf, &v)
b.SetBytes(int64(buf.Len()))
rd := msgp.NewEndlessReader(buf.Bytes(), b)
dc := msgp.NewReader(rd)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
err := v.DecodeMsg(dc)
if err != nil {
b.Fatal(err)
}
}
}
func TestMarshalUnmarshalDeleteFileHandlerParams(t *testing.T) { func TestMarshalUnmarshalDeleteFileHandlerParams(t *testing.T) {
v := DeleteFileHandlerParams{} v := DeleteFileHandlerParams{}
bts, err := v.MarshalMsg(nil) bts, err := v.MarshalMsg(nil)

View File

@ -94,9 +94,9 @@ type StorageAPI interface {
CreateFile(ctx context.Context, origvolume, olume, path string, size int64, reader io.Reader) error CreateFile(ctx context.Context, origvolume, olume, path string, size int64, reader io.Reader) error
ReadFileStream(ctx context.Context, volume, path string, offset, length int64) (io.ReadCloser, error) ReadFileStream(ctx context.Context, volume, path string, offset, length int64) (io.ReadCloser, error)
RenameFile(ctx context.Context, srcVolume, srcPath, dstVolume, dstPath string) error RenameFile(ctx context.Context, srcVolume, srcPath, dstVolume, dstPath string) error
CheckParts(ctx context.Context, volume string, path string, fi FileInfo) error CheckParts(ctx context.Context, volume string, path string, fi FileInfo) (*CheckPartsResp, error)
Delete(ctx context.Context, volume string, path string, opts DeleteOptions) (err error) Delete(ctx context.Context, volume string, path string, opts DeleteOptions) (err error)
VerifyFile(ctx context.Context, volume, path string, fi FileInfo) error VerifyFile(ctx context.Context, volume, path string, fi FileInfo) (*CheckPartsResp, error)
StatInfoFile(ctx context.Context, volume, path string, glob bool) (stat []StatInfo, err error) StatInfoFile(ctx context.Context, volume, path string, glob bool) (stat []StatInfo, err error)
ReadMultiple(ctx context.Context, req ReadMultipleReq, resp chan<- ReadMultipleResp) error ReadMultiple(ctx context.Context, req ReadMultipleReq, resp chan<- ReadMultipleResp) error
CleanAbandonedData(ctx context.Context, volume string, path string) error CleanAbandonedData(ctx context.Context, volume string, path string) error

View File

@ -449,14 +449,18 @@ func (client *storageRESTClient) WriteAll(ctx context.Context, volume string, pa
} }
// CheckParts - stat all file parts. // CheckParts - stat all file parts.
func (client *storageRESTClient) CheckParts(ctx context.Context, volume string, path string, fi FileInfo) error { func (client *storageRESTClient) CheckParts(ctx context.Context, volume string, path string, fi FileInfo) (*CheckPartsResp, error) {
_, err := storageCheckPartsRPC.Call(ctx, client.gridConn, &CheckPartsHandlerParams{ var resp *CheckPartsResp
resp, err := storageCheckPartsRPC.Call(ctx, client.gridConn, &CheckPartsHandlerParams{
DiskID: *client.diskID.Load(), DiskID: *client.diskID.Load(),
Volume: volume, Volume: volume,
FilePath: path, FilePath: path,
FI: fi, FI: fi,
}) })
return toStorageErr(err) if err != nil {
return nil, err
}
return resp, nil
} }
// RenameData - rename source path to destination path atomically, metadata and data file. // RenameData - rename source path to destination path atomically, metadata and data file.
@ -748,33 +752,33 @@ func (client *storageRESTClient) RenameFile(ctx context.Context, srcVolume, srcP
return toStorageErr(err) return toStorageErr(err)
} }
func (client *storageRESTClient) VerifyFile(ctx context.Context, volume, path string, fi FileInfo) error { func (client *storageRESTClient) VerifyFile(ctx context.Context, volume, path string, fi FileInfo) (*CheckPartsResp, error) {
values := make(url.Values) values := make(url.Values)
values.Set(storageRESTVolume, volume) values.Set(storageRESTVolume, volume)
values.Set(storageRESTFilePath, path) values.Set(storageRESTFilePath, path)
var reader bytes.Buffer var reader bytes.Buffer
if err := msgp.Encode(&reader, &fi); err != nil { if err := msgp.Encode(&reader, &fi); err != nil {
return err return nil, err
} }
respBody, err := client.call(ctx, storageRESTMethodVerifyFile, values, &reader, -1) respBody, err := client.call(ctx, storageRESTMethodVerifyFile, values, &reader, -1)
defer xhttp.DrainBody(respBody) defer xhttp.DrainBody(respBody)
if err != nil { if err != nil {
return err return nil, err
} }
respReader, err := waitForHTTPResponse(respBody) respReader, err := waitForHTTPResponse(respBody)
if err != nil { if err != nil {
return toStorageErr(err) return nil, toStorageErr(err)
} }
verifyResp := &VerifyFileResp{} verifyResp := &CheckPartsResp{}
if err = gob.NewDecoder(respReader).Decode(verifyResp); err != nil { if err = gob.NewDecoder(respReader).Decode(verifyResp); err != nil {
return toStorageErr(err) return nil, toStorageErr(err)
} }
return toStorageErr(verifyResp.Err) return verifyResp, nil
} }
func (client *storageRESTClient) StatInfoFile(ctx context.Context, volume, path string, glob bool) (stat []StatInfo, err error) { func (client *storageRESTClient) StatInfoFile(ctx context.Context, volume, path string, glob bool) (stat []StatInfo, err error) {

View File

@ -20,7 +20,7 @@ package cmd
//go:generate msgp -file $GOFILE -unexported //go:generate msgp -file $GOFILE -unexported
const ( const (
storageRESTVersion = "v57" // Remove TotalTokens from DiskMetrics storageRESTVersion = "v58" // Change VerifyFile signature
storageRESTVersionPrefix = SlashSeparator + storageRESTVersion storageRESTVersionPrefix = SlashSeparator + storageRESTVersion
storageRESTPrefix = minioReservedBucketPath + "/storage" storageRESTPrefix = minioReservedBucketPath + "/storage"
) )

View File

@ -58,7 +58,7 @@ type storageRESTServer struct {
} }
var ( var (
storageCheckPartsRPC = grid.NewSingleHandler[*CheckPartsHandlerParams, grid.NoPayload](grid.HandlerCheckParts, func() *CheckPartsHandlerParams { return &CheckPartsHandlerParams{} }, grid.NewNoPayload) storageCheckPartsRPC = grid.NewSingleHandler[*CheckPartsHandlerParams, *CheckPartsResp](grid.HandlerCheckParts2, func() *CheckPartsHandlerParams { return &CheckPartsHandlerParams{} }, func() *CheckPartsResp { return &CheckPartsResp{} })
storageDeleteFileRPC = grid.NewSingleHandler[*DeleteFileHandlerParams, grid.NoPayload](grid.HandlerDeleteFile, func() *DeleteFileHandlerParams { return &DeleteFileHandlerParams{} }, grid.NewNoPayload).AllowCallRequestPool(true) storageDeleteFileRPC = grid.NewSingleHandler[*DeleteFileHandlerParams, grid.NoPayload](grid.HandlerDeleteFile, func() *DeleteFileHandlerParams { return &DeleteFileHandlerParams{} }, grid.NewNoPayload).AllowCallRequestPool(true)
storageDeleteVersionRPC = grid.NewSingleHandler[*DeleteVersionHandlerParams, grid.NoPayload](grid.HandlerDeleteVersion, func() *DeleteVersionHandlerParams { return &DeleteVersionHandlerParams{} }, grid.NewNoPayload) storageDeleteVersionRPC = grid.NewSingleHandler[*DeleteVersionHandlerParams, grid.NoPayload](grid.HandlerDeleteVersion, func() *DeleteVersionHandlerParams { return &DeleteVersionHandlerParams{} }, grid.NewNoPayload)
storageDiskInfoRPC = grid.NewSingleHandler[*DiskInfoOptions, *DiskInfo](grid.HandlerDiskInfo, func() *DiskInfoOptions { return &DiskInfoOptions{} }, func() *DiskInfo { return &DiskInfo{} }).WithSharedResponse().AllowCallRequestPool(true) storageDiskInfoRPC = grid.NewSingleHandler[*DiskInfoOptions, *DiskInfo](grid.HandlerDiskInfo, func() *DiskInfoOptions { return &DiskInfoOptions{} }, func() *DiskInfo { return &DiskInfo{} }).WithSharedResponse().AllowCallRequestPool(true)
@ -439,13 +439,15 @@ func (s *storageRESTServer) UpdateMetadataHandler(p *MetadataHandlerParams) (gri
} }
// CheckPartsHandler - check if a file metadata exists. // CheckPartsHandler - check if a file metadata exists.
func (s *storageRESTServer) CheckPartsHandler(p *CheckPartsHandlerParams) (grid.NoPayload, *grid.RemoteErr) { func (s *storageRESTServer) CheckPartsHandler(p *CheckPartsHandlerParams) (*CheckPartsResp, *grid.RemoteErr) {
if !s.checkID(p.DiskID) { if !s.checkID(p.DiskID) {
return grid.NewNPErr(errDiskNotFound) return nil, grid.NewRemoteErr(errDiskNotFound)
} }
volume := p.Volume volume := p.Volume
filePath := p.FilePath filePath := p.FilePath
return grid.NewNPErr(s.getStorage().CheckParts(context.Background(), volume, filePath, p.FI))
resp, err := s.getStorage().CheckParts(context.Background(), volume, filePath, p.FI)
return resp, grid.NewRemoteErr(err)
} }
func (s *storageRESTServer) WriteAllHandler(p *WriteAllHandlerParams) (grid.NoPayload, *grid.RemoteErr) { func (s *storageRESTServer) WriteAllHandler(p *WriteAllHandlerParams) (grid.NoPayload, *grid.RemoteErr) {
@ -1097,11 +1099,6 @@ func waitForHTTPStream(respBody io.ReadCloser, w io.Writer) error {
} }
} }
// VerifyFileResp - VerifyFile()'s response.
type VerifyFileResp struct {
Err error
}
// VerifyFileHandler - Verify all part of file for bitrot errors. // VerifyFileHandler - Verify all part of file for bitrot errors.
func (s *storageRESTServer) VerifyFileHandler(w http.ResponseWriter, r *http.Request) { func (s *storageRESTServer) VerifyFileHandler(w http.ResponseWriter, r *http.Request) {
if !s.IsValid(w, r) { if !s.IsValid(w, r) {
@ -1124,13 +1121,15 @@ func (s *storageRESTServer) VerifyFileHandler(w http.ResponseWriter, r *http.Req
setEventStreamHeaders(w) setEventStreamHeaders(w)
encoder := gob.NewEncoder(w) encoder := gob.NewEncoder(w)
done := keepHTTPResponseAlive(w) done := keepHTTPResponseAlive(w)
err := s.getStorage().VerifyFile(r.Context(), volume, filePath, fi) resp, err := s.getStorage().VerifyFile(r.Context(), volume, filePath, fi)
done(nil) done(nil)
vresp := &VerifyFileResp{}
if err != nil { if err != nil {
vresp.Err = StorageErr(err.Error()) s.writeErrorResponse(w, err)
return
} }
encoder.Encode(vresp)
encoder.Encode(resp)
} }
func checkDiskFatalErrs(errs []error) error { func checkDiskFatalErrs(errs []error) error {

View File

@ -487,15 +487,16 @@ func (p *xlStorageDiskIDCheck) RenameData(ctx context.Context, srcVolume, srcPat
}) })
} }
func (p *xlStorageDiskIDCheck) CheckParts(ctx context.Context, volume string, path string, fi FileInfo) (err error) { func (p *xlStorageDiskIDCheck) CheckParts(ctx context.Context, volume string, path string, fi FileInfo) (*CheckPartsResp, error) {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricCheckParts, volume, path) ctx, done, err := p.TrackDiskHealth(ctx, storageMetricCheckParts, volume, path)
if err != nil { if err != nil {
return err return nil, err
} }
defer done(0, &err) defer done(0, &err)
w := xioutil.NewDeadlineWorker(globalDriveConfig.GetMaxTimeout()) return xioutil.WithDeadline[*CheckPartsResp](ctx, globalDriveConfig.GetMaxTimeout(), func(ctx context.Context) (res *CheckPartsResp, err error) {
return w.Run(func() error { return p.storage.CheckParts(ctx, volume, path, fi) }) return p.storage.CheckParts(ctx, volume, path, fi)
})
} }
func (p *xlStorageDiskIDCheck) Delete(ctx context.Context, volume string, path string, deleteOpts DeleteOptions) (err error) { func (p *xlStorageDiskIDCheck) Delete(ctx context.Context, volume string, path string, deleteOpts DeleteOptions) (err error) {
@ -564,10 +565,10 @@ func (p *xlStorageDiskIDCheck) DeleteVersions(ctx context.Context, volume string
return errs return errs
} }
func (p *xlStorageDiskIDCheck) VerifyFile(ctx context.Context, volume, path string, fi FileInfo) (err error) { func (p *xlStorageDiskIDCheck) VerifyFile(ctx context.Context, volume, path string, fi FileInfo) (*CheckPartsResp, error) {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricVerifyFile, volume, path) ctx, done, err := p.TrackDiskHealth(ctx, storageMetricVerifyFile, volume, path)
if err != nil { if err != nil {
return err return nil, err
} }
defer done(0, &err) defer done(0, &err)

View File

@ -2312,18 +2312,25 @@ func (s *xlStorage) AppendFile(ctx context.Context, volume string, path string,
} }
// CheckParts check if path has necessary parts available. // CheckParts check if path has necessary parts available.
func (s *xlStorage) CheckParts(ctx context.Context, volume string, path string, fi FileInfo) error { func (s *xlStorage) CheckParts(ctx context.Context, volume string, path string, fi FileInfo) (*CheckPartsResp, error) {
volumeDir, err := s.getVolDir(volume) volumeDir, err := s.getVolDir(volume)
if err != nil { if err != nil {
return err return nil, err
} }
for _, part := range fi.Parts { err = checkPathLength(pathJoin(volumeDir, path))
if err != nil {
return nil, err
}
resp := CheckPartsResp{
// By default, all results have an unknown status
Results: make([]int, len(fi.Parts)),
}
for i, part := range fi.Parts {
partPath := pathJoin(path, fi.DataDir, fmt.Sprintf("part.%d", part.Number)) partPath := pathJoin(path, fi.DataDir, fmt.Sprintf("part.%d", part.Number))
filePath := pathJoin(volumeDir, partPath) filePath := pathJoin(volumeDir, partPath)
if err = checkPathLength(filePath); err != nil {
return err
}
st, err := Lstat(filePath) st, err := Lstat(filePath)
if err != nil { if err != nil {
if osIsNotExist(err) { if osIsNotExist(err) {
@ -2331,24 +2338,30 @@ func (s *xlStorage) CheckParts(ctx context.Context, volume string, path string,
// Stat a volume entry. // Stat a volume entry.
if verr := Access(volumeDir); verr != nil { if verr := Access(volumeDir); verr != nil {
if osIsNotExist(verr) { if osIsNotExist(verr) {
return errVolumeNotFound resp.Results[i] = checkPartVolumeNotFound
} }
return verr continue
} }
} }
} }
return osErrToFileErr(err) if osErrToFileErr(err) == errFileNotFound {
resp.Results[i] = checkPartFileNotFound
}
continue
} }
if st.Mode().IsDir() { if st.Mode().IsDir() {
return errFileNotFound resp.Results[i] = checkPartFileNotFound
continue
} }
// Check if shard is truncated. // Check if shard is truncated.
if st.Size() < fi.Erasure.ShardFileSize(part.Size) { if st.Size() < fi.Erasure.ShardFileSize(part.Size) {
return errFileCorrupt resp.Results[i] = checkPartFileCorrupt
continue
} }
resp.Results[i] = checkPartSuccess
} }
return nil return &resp, nil
} }
// deleteFile deletes a file or a directory if its empty unless recursive // deleteFile deletes a file or a directory if its empty unless recursive
@ -2922,42 +2935,43 @@ func (s *xlStorage) bitrotVerify(ctx context.Context, partPath string, partSize
return bitrotVerify(diskHealthReader(ctx, file), fi.Size(), partSize, algo, sum, shardSize) return bitrotVerify(diskHealthReader(ctx, file), fi.Size(), partSize, algo, sum, shardSize)
} }
func (s *xlStorage) VerifyFile(ctx context.Context, volume, path string, fi FileInfo) (err error) { func (s *xlStorage) VerifyFile(ctx context.Context, volume, path string, fi FileInfo) (*CheckPartsResp, error) {
volumeDir, err := s.getVolDir(volume) volumeDir, err := s.getVolDir(volume)
if err != nil { if err != nil {
return err return nil, err
} }
if !skipAccessChecks(volume) { if !skipAccessChecks(volume) {
// Stat a volume entry. // Stat a volume entry.
if err = Access(volumeDir); err != nil { if err = Access(volumeDir); err != nil {
return convertAccessError(err, errVolumeAccessDenied) return nil, convertAccessError(err, errVolumeAccessDenied)
} }
} }
resp := CheckPartsResp{
// By default, the result is unknown per part
Results: make([]int, len(fi.Parts)),
}
erasure := fi.Erasure erasure := fi.Erasure
for _, part := range fi.Parts { for i, part := range fi.Parts {
checksumInfo := erasure.GetChecksumInfo(part.Number) checksumInfo := erasure.GetChecksumInfo(part.Number)
partPath := pathJoin(volumeDir, path, fi.DataDir, fmt.Sprintf("part.%d", part.Number)) partPath := pathJoin(volumeDir, path, fi.DataDir, fmt.Sprintf("part.%d", part.Number))
if err := s.bitrotVerify(ctx, partPath, err := s.bitrotVerify(ctx, partPath,
erasure.ShardFileSize(part.Size), erasure.ShardFileSize(part.Size),
checksumInfo.Algorithm, checksumInfo.Algorithm,
checksumInfo.Hash, erasure.ShardSize()); err != nil { checksumInfo.Hash, erasure.ShardSize())
if !IsErr(err, []error{
errFileNotFound, resp.Results[i] = convPartErrToInt(err)
errVolumeNotFound,
errFileCorrupt, // Only log unknown errors
errFileAccessDenied, if resp.Results[i] == checkPartUnknown && err != errFileAccessDenied {
errFileVersionNotFound, logger.GetReqInfo(ctx).AppendTags("disk", s.String())
}...) { storageLogOnceIf(ctx, err, partPath)
logger.GetReqInfo(ctx).AppendTags("disk", s.String())
storageLogOnceIf(ctx, err, partPath)
}
return err
} }
} }
return nil return &resp, nil
} }
// ReadMultiple will read multiple files and send each back as response. // ReadMultiple will read multiple files and send each back as response.

View File

@ -112,6 +112,7 @@ const (
HandlerListBuckets HandlerListBuckets
HandlerRenameDataInline HandlerRenameDataInline
HandlerRenameData2 HandlerRenameData2
HandlerCheckParts2
// Add more above here ^^^ // Add more above here ^^^
// If all handlers are used, the type of Handler can be changed. // If all handlers are used, the type of Handler can be changed.
@ -192,6 +193,7 @@ var handlerPrefixes = [handlerLast]string{
HandlerListBuckets: peerPrefixS3, HandlerListBuckets: peerPrefixS3,
HandlerRenameDataInline: storagePrefix, HandlerRenameDataInline: storagePrefix,
HandlerRenameData2: storagePrefix, HandlerRenameData2: storagePrefix,
HandlerCheckParts2: storagePrefix,
} }
const ( const (

View File

@ -82,14 +82,15 @@ func _() {
_ = x[HandlerListBuckets-71] _ = x[HandlerListBuckets-71]
_ = x[HandlerRenameDataInline-72] _ = x[HandlerRenameDataInline-72]
_ = x[HandlerRenameData2-73] _ = x[HandlerRenameData2-73]
_ = x[handlerTest-74] _ = x[HandlerCheckParts2-74]
_ = x[handlerTest2-75] _ = x[handlerTest-75]
_ = x[handlerLast-76] _ = x[handlerTest2-76]
_ = x[handlerLast-77]
} }
const _HandlerID_name = "handlerInvalidLockLockLockRLockLockUnlockLockRUnlockLockRefreshLockForceUnlockWalkDirStatVolDiskInfoNSScannerReadXLReadVersionDeleteFileDeleteVersionUpdateMetadataWriteMetadataCheckPartsRenameDataRenameFileReadAllServerVerifyTraceListenDeleteBucketMetadataLoadBucketMetadataReloadSiteReplicationConfigReloadPoolMetaStopRebalanceLoadRebalanceMetaLoadTransitionTierConfigDeletePolicyLoadPolicyLoadPolicyMappingDeleteServiceAccountLoadServiceAccountDeleteUserLoadUserLoadGroupHealBucketMakeBucketHeadBucketDeleteBucketGetMetricsGetResourceMetricsGetMemInfoGetProcInfoGetOSInfoGetPartitionsGetNetInfoGetCPUsServerInfoGetSysConfigGetSysServicesGetSysErrorsGetAllBucketStatsGetBucketStatsGetSRMetricsGetPeerMetricsGetMetacacheListingUpdateMetacacheListingGetPeerBucketMetricsStorageInfoConsoleLogListDirGetLocksBackgroundHealStatusGetLastDayTierStatsSignalServiceGetBandwidthWriteAllListBucketsRenameDataInlineRenameData2handlerTesthandlerTest2handlerLast" const _HandlerID_name = "handlerInvalidLockLockLockRLockLockUnlockLockRUnlockLockRefreshLockForceUnlockWalkDirStatVolDiskInfoNSScannerReadXLReadVersionDeleteFileDeleteVersionUpdateMetadataWriteMetadataCheckPartsRenameDataRenameFileReadAllServerVerifyTraceListenDeleteBucketMetadataLoadBucketMetadataReloadSiteReplicationConfigReloadPoolMetaStopRebalanceLoadRebalanceMetaLoadTransitionTierConfigDeletePolicyLoadPolicyLoadPolicyMappingDeleteServiceAccountLoadServiceAccountDeleteUserLoadUserLoadGroupHealBucketMakeBucketHeadBucketDeleteBucketGetMetricsGetResourceMetricsGetMemInfoGetProcInfoGetOSInfoGetPartitionsGetNetInfoGetCPUsServerInfoGetSysConfigGetSysServicesGetSysErrorsGetAllBucketStatsGetBucketStatsGetSRMetricsGetPeerMetricsGetMetacacheListingUpdateMetacacheListingGetPeerBucketMetricsStorageInfoConsoleLogListDirGetLocksBackgroundHealStatusGetLastDayTierStatsSignalServiceGetBandwidthWriteAllListBucketsRenameDataInlineRenameData2CheckParts2handlerTesthandlerTest2handlerLast"
var _HandlerID_index = [...]uint16{0, 14, 22, 31, 41, 52, 63, 78, 85, 92, 100, 109, 115, 126, 136, 149, 163, 176, 186, 196, 206, 213, 225, 230, 236, 256, 274, 301, 315, 328, 345, 369, 381, 391, 408, 428, 446, 456, 464, 473, 483, 493, 503, 515, 525, 543, 553, 564, 573, 586, 596, 603, 613, 625, 639, 651, 668, 682, 694, 708, 727, 749, 769, 780, 790, 797, 805, 825, 844, 857, 869, 877, 888, 904, 915, 926, 938, 949} var _HandlerID_index = [...]uint16{0, 14, 22, 31, 41, 52, 63, 78, 85, 92, 100, 109, 115, 126, 136, 149, 163, 176, 186, 196, 206, 213, 225, 230, 236, 256, 274, 301, 315, 328, 345, 369, 381, 391, 408, 428, 446, 456, 464, 473, 483, 493, 503, 515, 525, 543, 553, 564, 573, 586, 596, 603, 613, 625, 639, 651, 668, 682, 694, 708, 727, 749, 769, 780, 790, 797, 805, 825, 844, 857, 869, 877, 888, 904, 915, 926, 937, 949, 960}
func (i HandlerID) String() string { func (i HandlerID) String() string {
if i >= HandlerID(len(_HandlerID_index)-1) { if i >= HandlerID(len(_HandlerID_index)-1) {