mirror of https://github.com/minio/minio.git
heal: Dangling check to evaluate object parts separately (#19797)
This commit is contained in:
parent
0662c90b5c
commit
789cbc6fb2
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue