mirror of
https://github.com/minio/minio.git
synced 2025-04-10 06:30:07 -04:00
Improve listing consistency with version merging (#13723)
This commit is contained in:
parent
8309ddd486
commit
3db931dc0e
2
Makefile
2
Makefile
@ -40,7 +40,7 @@ lint: ## runs golangci-lint suite of linters
|
|||||||
check: test
|
check: test
|
||||||
test: verifiers build ## builds minio, runs linters, tests
|
test: verifiers build ## builds minio, runs linters, tests
|
||||||
@echo "Running unit tests"
|
@echo "Running unit tests"
|
||||||
@GO111MODULE=on CGO_ENABLED=0 go test -tags kqueue ./... 1>/dev/null
|
@GO111MODULE=on CGO_ENABLED=0 go test -tags kqueue ./...
|
||||||
|
|
||||||
test-upgrade: build
|
test-upgrade: build
|
||||||
@echo "Running minio upgrade tests"
|
@echo "Running minio upgrade tests"
|
||||||
|
@ -41,11 +41,13 @@ type adminErasureTestBed struct {
|
|||||||
erasureDirs []string
|
erasureDirs []string
|
||||||
objLayer ObjectLayer
|
objLayer ObjectLayer
|
||||||
router *mux.Router
|
router *mux.Router
|
||||||
|
done context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepareAdminErasureTestBed - helper function that setups a single-node
|
// prepareAdminErasureTestBed - helper function that setups a single-node
|
||||||
// Erasure backend for admin-handler tests.
|
// Erasure backend for admin-handler tests.
|
||||||
func prepareAdminErasureTestBed(ctx context.Context) (*adminErasureTestBed, error) {
|
func prepareAdminErasureTestBed(ctx context.Context) (*adminErasureTestBed, error) {
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
// reset global variables to start afresh.
|
// reset global variables to start afresh.
|
||||||
resetTestGlobals()
|
resetTestGlobals()
|
||||||
@ -57,11 +59,13 @@ func prepareAdminErasureTestBed(ctx context.Context) (*adminErasureTestBed, erro
|
|||||||
// Initializing objectLayer for HealFormatHandler.
|
// Initializing objectLayer for HealFormatHandler.
|
||||||
objLayer, erasureDirs, xlErr := initTestErasureObjLayer(ctx)
|
objLayer, erasureDirs, xlErr := initTestErasureObjLayer(ctx)
|
||||||
if xlErr != nil {
|
if xlErr != nil {
|
||||||
|
cancel()
|
||||||
return nil, xlErr
|
return nil, xlErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize minio server config.
|
// Initialize minio server config.
|
||||||
if err := newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
|
if err := newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
|
||||||
|
cancel()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,12 +88,14 @@ func prepareAdminErasureTestBed(ctx context.Context) (*adminErasureTestBed, erro
|
|||||||
erasureDirs: erasureDirs,
|
erasureDirs: erasureDirs,
|
||||||
objLayer: objLayer,
|
objLayer: objLayer,
|
||||||
router: adminRouter,
|
router: adminRouter,
|
||||||
|
done: cancel,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TearDown - method that resets the test bed for subsequent unit
|
// TearDown - method that resets the test bed for subsequent unit
|
||||||
// tests to start afresh.
|
// tests to start afresh.
|
||||||
func (atb *adminErasureTestBed) TearDown() {
|
func (atb *adminErasureTestBed) TearDown() {
|
||||||
|
atb.done()
|
||||||
removeRoots(atb.erasureDirs)
|
removeRoots(atb.erasureDirs)
|
||||||
resetTestGlobals()
|
resetTestGlobals()
|
||||||
}
|
}
|
||||||
|
@ -656,6 +656,7 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
|||||||
dirQuorum: getReadQuorum(len(f.disks)),
|
dirQuorum: getReadQuorum(len(f.disks)),
|
||||||
objQuorum: getReadQuorum(len(f.disks)),
|
objQuorum: getReadQuorum(len(f.disks)),
|
||||||
bucket: "",
|
bucket: "",
|
||||||
|
strict: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
healObjectsPrefix := color.Green("healObjects:")
|
healObjectsPrefix := color.Green("healObjects:")
|
||||||
|
@ -473,7 +473,11 @@ func (d *dataUpdateTracker) deserialize(src io.Reader, newerThan time.Time) erro
|
|||||||
// start a collector that picks up entries from objectUpdatedCh
|
// start a collector that picks up entries from objectUpdatedCh
|
||||||
// and adds them to the current bloom filter.
|
// and adds them to the current bloom filter.
|
||||||
func (d *dataUpdateTracker) startCollector(ctx context.Context) {
|
func (d *dataUpdateTracker) startCollector(ctx context.Context) {
|
||||||
for in := range d.input {
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case in := <-d.input:
|
||||||
bucket, _ := path2BucketObjectWithBasePath("", in)
|
bucket, _ := path2BucketObjectWithBasePath("", in)
|
||||||
if bucket == "" {
|
if bucket == "" {
|
||||||
if d.debug && len(in) > 0 {
|
if d.debug && len(in) > 0 {
|
||||||
@ -496,6 +500,7 @@ func (d *dataUpdateTracker) startCollector(ctx context.Context) {
|
|||||||
d.mu.Unlock()
|
d.mu.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// markDirty adds the supplied path to the current bloom filter.
|
// markDirty adds the supplied path to the current bloom filter.
|
||||||
func (d *dataUpdateTracker) markDirty(bucket, prefix string) {
|
func (d *dataUpdateTracker) markDirty(bucket, prefix string) {
|
||||||
|
@ -24,7 +24,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -147,10 +146,6 @@ func TestHealing(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHealingDanglingObject(t *testing.T) {
|
func TestHealingDanglingObject(t *testing.T) {
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
t.Skip()
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@ -180,6 +175,9 @@ func TestHealingDanglingObject(t *testing.T) {
|
|||||||
t.Fatalf("Failed to make a bucket - %v", err)
|
t.Fatalf("Failed to make a bucket - %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disks = objLayer.(*erasureServerPools).serverPools[0].erasureDisks[0]
|
||||||
|
orgDisks := append([]StorageAPI{}, disks...)
|
||||||
|
|
||||||
// Enable versioning.
|
// Enable versioning.
|
||||||
globalBucketMetadataSys.Update(bucket, bucketVersioningConfig, []byte(`<VersioningConfiguration><Status>Enabled</Status></VersioningConfiguration>`))
|
globalBucketMetadataSys.Update(bucket, bucketVersioningConfig, []byte(`<VersioningConfiguration><Status>Enabled</Status></VersioningConfiguration>`))
|
||||||
|
|
||||||
@ -190,11 +188,13 @@ func TestHealingDanglingObject(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fsDir := range fsDirs[:4] {
|
setDisks := func(newDisks ...StorageAPI) {
|
||||||
if err = os.Chmod(fsDir, 0400); err != nil {
|
objLayer.(*erasureServerPools).serverPools[0].erasureDisksMu.Lock()
|
||||||
t.Fatal(err)
|
copy(disks, newDisks)
|
||||||
}
|
objLayer.(*erasureServerPools).serverPools[0].erasureDisksMu.Unlock()
|
||||||
}
|
}
|
||||||
|
// Remove 4 disks.
|
||||||
|
setDisks(nil, nil, nil, nil)
|
||||||
|
|
||||||
// Create delete marker under quorum.
|
// Create delete marker under quorum.
|
||||||
objInfo, err := objLayer.DeleteObject(ctx, bucket, object, ObjectOptions{Versioned: true})
|
objInfo, err := objLayer.DeleteObject(ctx, bucket, object, ObjectOptions{Versioned: true})
|
||||||
@ -202,11 +202,8 @@ func TestHealingDanglingObject(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fsDir := range fsDirs[:4] {
|
// Restore...
|
||||||
if err = os.Chmod(fsDir, 0755); err != nil {
|
setDisks(orgDisks[:4]...)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileInfoPreHeal, err := disks[0].ReadVersion(context.Background(), bucket, object, "", false)
|
fileInfoPreHeal, err := disks[0].ReadVersion(context.Background(), bucket, object, "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -243,11 +240,7 @@ func TestHealingDanglingObject(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fsDir := range fsDirs[:4] {
|
setDisks(nil, nil, nil, nil)
|
||||||
if err = os.Chmod(fsDir, 0400); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rd := mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", "")
|
rd := mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", "")
|
||||||
_, err = objLayer.PutObject(ctx, bucket, object, rd, ObjectOptions{
|
_, err = objLayer.PutObject(ctx, bucket, object, rd, ObjectOptions{
|
||||||
@ -257,11 +250,7 @@ func TestHealingDanglingObject(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fsDir := range fsDirs[:4] {
|
setDisks(orgDisks[:4]...)
|
||||||
if err = os.Chmod(fsDir, 0755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileInfoPreHeal, err = disks[0].ReadVersion(context.Background(), bucket, object, "", false)
|
fileInfoPreHeal, err = disks[0].ReadVersion(context.Background(), bucket, object, "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -297,11 +286,7 @@ func TestHealingDanglingObject(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fsDir := range fsDirs[:4] {
|
setDisks(nil, nil, nil, nil)
|
||||||
if err = os.Chmod(fsDir, 0400); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create delete marker under quorum.
|
// Create delete marker under quorum.
|
||||||
_, err = objLayer.DeleteObject(ctx, bucket, object, ObjectOptions{
|
_, err = objLayer.DeleteObject(ctx, bucket, object, ObjectOptions{
|
||||||
@ -312,11 +297,7 @@ func TestHealingDanglingObject(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fsDir := range fsDirs[:4] {
|
setDisks(orgDisks[:4]...)
|
||||||
if err = os.Chmod(fsDir, 0755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileInfoPreHeal, err = disks[0].ReadVersion(context.Background(), bucket, object, "", false)
|
fileInfoPreHeal, err = disks[0].ReadVersion(context.Background(), bucket, object, "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1751,6 +1751,7 @@ func (z *erasureServerPools) HealObjects(ctx context.Context, bucket, prefix str
|
|||||||
dirQuorum: 1,
|
dirQuorum: 1,
|
||||||
objQuorum: 1,
|
objQuorum: 1,
|
||||||
bucket: bucket,
|
bucket: bucket,
|
||||||
|
strict: false, // Allow less strict matching.
|
||||||
}
|
}
|
||||||
|
|
||||||
path := baseDirFromPrefix(prefix)
|
path := baseDirFromPrefix(prefix)
|
||||||
|
@ -37,7 +37,7 @@ type metaCacheEntry struct {
|
|||||||
metadata []byte
|
metadata []byte
|
||||||
|
|
||||||
// cached contains the metadata if decoded.
|
// cached contains the metadata if decoded.
|
||||||
cached *FileInfo
|
cached *xlMetaV2
|
||||||
|
|
||||||
// Indicates the entry can be reused and only one reference to metadata is expected.
|
// Indicates the entry can be reused and only one reference to metadata is expected.
|
||||||
reusable bool
|
reusable bool
|
||||||
@ -58,68 +58,80 @@ func (e metaCacheEntry) hasPrefix(s string) bool {
|
|||||||
return strings.HasPrefix(e.name, s)
|
return strings.HasPrefix(e.name, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// matches returns if the entries match by comparing their latest version fileinfo.
|
// matches returns if the entries have the same versions.
|
||||||
func (e *metaCacheEntry) matches(other *metaCacheEntry, bucket string) bool {
|
// If strict is false we allow signatures to mismatch.
|
||||||
|
func (e *metaCacheEntry) matches(other *metaCacheEntry, strict bool) (prefer *metaCacheEntry, matches bool) {
|
||||||
if e == nil && other == nil {
|
if e == nil && other == nil {
|
||||||
return true
|
return nil, true
|
||||||
}
|
}
|
||||||
if e == nil || other == nil {
|
if e == nil {
|
||||||
return false
|
return other, false
|
||||||
|
}
|
||||||
|
if other == nil {
|
||||||
|
return e, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// This should reject 99%
|
// Name should match...
|
||||||
if len(e.metadata) != len(other.metadata) || e.name != other.name {
|
if e.name != other.name {
|
||||||
return false
|
if e.name < other.name {
|
||||||
|
return e, false
|
||||||
|
}
|
||||||
|
return other, false
|
||||||
}
|
}
|
||||||
|
|
||||||
eFi, eErr := e.fileInfo(bucket)
|
eVers, eErr := e.xlmeta()
|
||||||
oFi, oErr := other.fileInfo(bucket)
|
oVers, oErr := other.xlmeta()
|
||||||
if eErr != nil || oErr != nil {
|
if eErr != nil || oErr != nil {
|
||||||
return eErr == oErr
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// check both fileInfo's have same number of versions, if not skip
|
// check both fileInfo's have same number of versions, if not skip
|
||||||
// the `other` entry.
|
// the `other` entry.
|
||||||
if eFi.NumVersions != oFi.NumVersions {
|
if len(eVers.versions) != len(oVers.versions) {
|
||||||
return false
|
eTime := eVers.latestModtime()
|
||||||
|
oTime := oVers.latestModtime()
|
||||||
|
if !eTime.Equal(oTime) {
|
||||||
|
if eTime.After(oTime) {
|
||||||
|
return e, false
|
||||||
|
}
|
||||||
|
return other, false
|
||||||
|
}
|
||||||
|
// Tiebreak on version count.
|
||||||
|
if len(eVers.versions) > len(oVers.versions) {
|
||||||
|
return e, false
|
||||||
|
}
|
||||||
|
return other, false
|
||||||
}
|
}
|
||||||
|
|
||||||
return eFi.ModTime.Equal(oFi.ModTime) && eFi.Size == oFi.Size && eFi.VersionID == oFi.VersionID
|
// Check if each version matches...
|
||||||
|
for i, eVer := range eVers.versions {
|
||||||
|
oVer := oVers.versions[i]
|
||||||
|
if eVer.header != oVer.header {
|
||||||
|
if !strict && eVer.header.matchesNotStrict(oVer.header) {
|
||||||
|
if prefer == nil {
|
||||||
|
if eVer.header.sortsBefore(oVer.header) {
|
||||||
|
prefer = e
|
||||||
|
} else {
|
||||||
|
prefer = other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if prefer != nil {
|
||||||
|
return prefer, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveEntries returns if the entries match by comparing their latest version fileinfo.
|
if eVer.header.sortsBefore(oVer.header) {
|
||||||
func resolveEntries(a, b *metaCacheEntry, bucket string) *metaCacheEntry {
|
return e, false
|
||||||
if b == nil {
|
|
||||||
return a
|
|
||||||
}
|
}
|
||||||
if a == nil {
|
return other, false
|
||||||
return b
|
|
||||||
}
|
}
|
||||||
|
|
||||||
aFi, err := a.fileInfo(bucket)
|
|
||||||
if err != nil {
|
|
||||||
return b
|
|
||||||
}
|
}
|
||||||
bFi, err := b.fileInfo(bucket)
|
// If we match, return e
|
||||||
if err != nil {
|
if prefer == nil {
|
||||||
return a
|
prefer = e
|
||||||
}
|
}
|
||||||
|
return prefer, true
|
||||||
if aFi.NumVersions == bFi.NumVersions {
|
|
||||||
if aFi.ModTime.Equal(bFi.ModTime) {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
if aFi.ModTime.After(bFi.ModTime) {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
if bFi.NumVersions > aFi.NumVersions {
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
return a
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// isInDir returns whether the entry is in the dir when considering the separator.
|
// isInDir returns whether the entry is in the dir when considering the separator.
|
||||||
@ -143,7 +155,10 @@ func (e metaCacheEntry) isInDir(dir, separator string) bool {
|
|||||||
// If v2 and UNABLE to load metadata true will be returned.
|
// If v2 and UNABLE to load metadata true will be returned.
|
||||||
func (e *metaCacheEntry) isLatestDeletemarker() bool {
|
func (e *metaCacheEntry) isLatestDeletemarker() bool {
|
||||||
if e.cached != nil {
|
if e.cached != nil {
|
||||||
return e.cached.Deleted
|
if len(e.cached.versions) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return e.cached.versions[0].header.Type == DeleteType
|
||||||
}
|
}
|
||||||
if !isXL2V1Format(e.metadata) {
|
if !isXL2V1Format(e.metadata) {
|
||||||
return false
|
return false
|
||||||
@ -152,8 +167,8 @@ func (e *metaCacheEntry) isLatestDeletemarker() bool {
|
|||||||
return meta.IsLatestDeleteMarker()
|
return meta.IsLatestDeleteMarker()
|
||||||
}
|
}
|
||||||
// Fall back...
|
// Fall back...
|
||||||
var xlMeta xlMetaV2
|
xlMeta, err := e.xlmeta()
|
||||||
if err := xlMeta.Load(e.metadata); err != nil || len(xlMeta.versions) == 0 {
|
if err != nil || len(xlMeta.versions) == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return xlMeta.versions[0].header.Type == DeleteType
|
return xlMeta.versions[0].header.Type == DeleteType
|
||||||
@ -162,24 +177,37 @@ func (e *metaCacheEntry) isLatestDeletemarker() bool {
|
|||||||
// fileInfo returns the decoded metadata.
|
// fileInfo returns the decoded metadata.
|
||||||
// If entry is a directory it is returned as that.
|
// If entry is a directory it is returned as that.
|
||||||
// If versioned the latest version will be returned.
|
// If versioned the latest version will be returned.
|
||||||
func (e *metaCacheEntry) fileInfo(bucket string) (*FileInfo, error) {
|
func (e *metaCacheEntry) fileInfo(bucket string) (FileInfo, error) {
|
||||||
if e.isDir() {
|
if e.isDir() {
|
||||||
return &FileInfo{
|
return FileInfo{
|
||||||
Volume: bucket,
|
Volume: bucket,
|
||||||
Name: e.name,
|
Name: e.name,
|
||||||
Mode: uint32(os.ModeDir),
|
Mode: uint32(os.ModeDir),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
if e.cached != nil {
|
||||||
|
return e.cached.ToFileInfo(bucket, e.name, "")
|
||||||
|
}
|
||||||
|
return getFileInfo(e.metadata, bucket, e.name, "", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// xlmeta returns the decoded metadata.
|
||||||
|
// This should not be called on directories.
|
||||||
|
func (e *metaCacheEntry) xlmeta() (*xlMetaV2, error) {
|
||||||
|
if e.isDir() {
|
||||||
|
return nil, errFileNotFound
|
||||||
|
}
|
||||||
if e.cached == nil {
|
if e.cached == nil {
|
||||||
if len(e.metadata) == 0 {
|
if len(e.metadata) == 0 {
|
||||||
// only happens if the entry is not found.
|
// only happens if the entry is not found.
|
||||||
return nil, errFileNotFound
|
return nil, errFileNotFound
|
||||||
}
|
}
|
||||||
fi, err := getFileInfo(e.metadata, bucket, e.name, "", false)
|
var xl xlMetaV2
|
||||||
|
err := xl.LoadOrConvert(e.metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
e.cached = &fi
|
e.cached = &xl
|
||||||
}
|
}
|
||||||
return e.cached, nil
|
return e.cached, nil
|
||||||
}
|
}
|
||||||
@ -200,6 +228,7 @@ func (e *metaCacheEntry) fileInfoVersions(bucket string) (FileInfoVersions, erro
|
|||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
// Too small gains to reuse cache here.
|
||||||
return getFileInfoVersions(e.metadata, bucket, e.name)
|
return getFileInfoVersions(e.metadata, bucket, e.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,16 +269,15 @@ type metadataResolutionParams struct {
|
|||||||
dirQuorum int // Number if disks needed for a directory to 'exist'.
|
dirQuorum int // Number if disks needed for a directory to 'exist'.
|
||||||
objQuorum int // Number of disks needed for an object to 'exist'.
|
objQuorum int // Number of disks needed for an object to 'exist'.
|
||||||
bucket string // Name of the bucket. Used for generating cached fileinfo.
|
bucket string // Name of the bucket. Used for generating cached fileinfo.
|
||||||
|
strict bool // Versions must match exactly, including all metadata.
|
||||||
|
|
||||||
// Reusable slice for resolution
|
// Reusable slice for resolution
|
||||||
candidates []struct {
|
candidates [][]xlMetaV2ShallowVersion
|
||||||
n int
|
|
||||||
e *metaCacheEntry
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolve multiple entries.
|
// resolve multiple entries.
|
||||||
// entries are resolved by majority, then if tied by mod-time and versions.
|
// entries are resolved by majority, then if tied by mod-time and versions.
|
||||||
|
// Names must match on all entries in m.
|
||||||
func (m metaCacheEntries) resolve(r *metadataResolutionParams) (selected *metaCacheEntry, ok bool) {
|
func (m metaCacheEntries) resolve(r *metadataResolutionParams) (selected *metaCacheEntry, ok bool) {
|
||||||
if len(m) == 0 {
|
if len(m) == 0 {
|
||||||
return nil, false
|
return nil, false
|
||||||
@ -257,14 +285,14 @@ func (m metaCacheEntries) resolve(r *metadataResolutionParams) (selected *metaCa
|
|||||||
|
|
||||||
dirExists := 0
|
dirExists := 0
|
||||||
if cap(r.candidates) < len(m) {
|
if cap(r.candidates) < len(m) {
|
||||||
r.candidates = make([]struct {
|
r.candidates = make([][]xlMetaV2ShallowVersion, 0, len(m))
|
||||||
n int
|
|
||||||
e *metaCacheEntry
|
|
||||||
}, 0, len(m))
|
|
||||||
}
|
}
|
||||||
r.candidates = r.candidates[:0]
|
r.candidates = r.candidates[:0]
|
||||||
|
objsAgree := 0
|
||||||
|
objsValid := 0
|
||||||
for i := range m {
|
for i := range m {
|
||||||
entry := &m[i]
|
entry := &m[i]
|
||||||
|
// Empty entry
|
||||||
if entry.name == "" {
|
if entry.name == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -275,72 +303,67 @@ func (m metaCacheEntries) resolve(r *metadataResolutionParams) (selected *metaCa
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get new entry metadata
|
// Get new entry metadata,
|
||||||
if _, err := entry.fileInfo(r.bucket); err != nil {
|
// shallow decode.
|
||||||
|
xl, err := entry.xlmeta()
|
||||||
|
if err != nil {
|
||||||
logger.LogIf(context.Background(), err)
|
logger.LogIf(context.Background(), err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
objsValid++
|
||||||
|
|
||||||
found := false
|
// Add all valid to candidates.
|
||||||
for i, c := range r.candidates {
|
r.candidates = append(r.candidates, xl.versions)
|
||||||
if c.e.matches(entry, r.bucket) {
|
|
||||||
c.n++
|
|
||||||
r.candidates[i] = c
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
r.candidates = append(r.candidates, struct {
|
|
||||||
n int
|
|
||||||
e *metaCacheEntry
|
|
||||||
}{n: 1, e: entry})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if selected != nil && selected.isDir() && dirExists > r.dirQuorum {
|
|
||||||
return selected, true
|
|
||||||
}
|
|
||||||
|
|
||||||
switch len(r.candidates) {
|
// We select the first object we find as a candidate and see if all match that.
|
||||||
case 0:
|
// This is to quickly identify if all agree.
|
||||||
if selected == nil {
|
if selected == nil {
|
||||||
|
selected = entry
|
||||||
|
objsAgree = 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Names match, check meta...
|
||||||
|
if prefer, ok := entry.matches(selected, r.strict); ok {
|
||||||
|
selected = prefer
|
||||||
|
objsAgree++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return dir entries, if enough...
|
||||||
|
if selected != nil && selected.isDir() && dirExists >= r.dirQuorum {
|
||||||
|
return selected, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we would never be able to reach read quorum.
|
||||||
|
if objsValid < r.objQuorum {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
if !selected.isDir() || dirExists < r.dirQuorum {
|
// If all objects agree.
|
||||||
|
if selected != nil && objsAgree == objsValid {
|
||||||
|
return selected, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge if we have disagreement.
|
||||||
|
// Create a new merged result.
|
||||||
|
selected = &metaCacheEntry{
|
||||||
|
name: selected.name,
|
||||||
|
reusable: true,
|
||||||
|
cached: &xlMetaV2{metaV: selected.cached.metaV},
|
||||||
|
}
|
||||||
|
selected.cached.versions = mergeXLV2Versions(r.objQuorum, r.strict, r.candidates...)
|
||||||
|
if len(selected.cached.versions) == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reserialize
|
||||||
|
var err error
|
||||||
|
selected.metadata, err = selected.cached.AppendTo(metaDataPoolGet())
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(context.Background(), err)
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
return selected, true
|
return selected, true
|
||||||
case 1:
|
|
||||||
cand := r.candidates[0]
|
|
||||||
if cand.n < r.objQuorum {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return cand.e, true
|
|
||||||
default:
|
|
||||||
// Sort by matches....
|
|
||||||
sort.Slice(r.candidates, func(i, j int) bool {
|
|
||||||
return r.candidates[i].n > r.candidates[j].n
|
|
||||||
})
|
|
||||||
|
|
||||||
// Check if we have enough.
|
|
||||||
if r.candidates[0].n < r.objQuorum {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// if r.objQuorum == 1 then it is guaranteed that
|
|
||||||
// this resolver is for HealObjects(), so use resolveEntries()
|
|
||||||
// instead to resolve candidates, this check is only useful
|
|
||||||
// for regular cases of ListObjects()
|
|
||||||
if r.candidates[0].n > r.candidates[1].n && r.objQuorum > 1 {
|
|
||||||
ok := r.candidates[0].e != nil && r.candidates[0].e.name != ""
|
|
||||||
return r.candidates[0].e, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
e := resolveEntries(r.candidates[0].e, r.candidates[1].e, r.bucket)
|
|
||||||
// Tie between two, resolve using modtime+versions.
|
|
||||||
ok := e != nil && e.name != ""
|
|
||||||
return e, ok
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// firstFound returns the first found and the number of set entries.
|
// firstFound returns the first found and the number of set entries.
|
||||||
|
@ -18,9 +18,12 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_metaCacheEntries_sort(t *testing.T) {
|
func Test_metaCacheEntries_sort(t *testing.T) {
|
||||||
@ -219,3 +222,438 @@ func Test_metaCacheEntry_isInDir(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_metaCacheEntries_resolve(t *testing.T) {
|
||||||
|
baseTime, err := time.Parse("2006/01/02", "2015/02/25")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var inputs = []xlMetaV2{
|
||||||
|
0: {
|
||||||
|
versions: []xlMetaV2ShallowVersion{
|
||||||
|
{header: xlMetaV2VersionHeader{
|
||||||
|
VersionID: [16]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
|
||||||
|
ModTime: baseTime.Add(30 * time.Minute).UnixNano(),
|
||||||
|
Signature: [4]byte{1, 1, 1, 1},
|
||||||
|
Type: ObjectType,
|
||||||
|
Flags: 0,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Mismatches Modtime+Signature and older...
|
||||||
|
1: {
|
||||||
|
versions: []xlMetaV2ShallowVersion{
|
||||||
|
{header: xlMetaV2VersionHeader{
|
||||||
|
VersionID: [16]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
|
||||||
|
ModTime: baseTime.Add(15 * time.Minute).UnixNano(),
|
||||||
|
Signature: [4]byte{2, 1, 1, 1},
|
||||||
|
Type: ObjectType,
|
||||||
|
Flags: 0,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Has another version prior to the one we want.
|
||||||
|
2: {
|
||||||
|
versions: []xlMetaV2ShallowVersion{
|
||||||
|
{header: xlMetaV2VersionHeader{
|
||||||
|
VersionID: [16]byte{2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
|
||||||
|
ModTime: baseTime.Add(60 * time.Minute).UnixNano(),
|
||||||
|
Signature: [4]byte{2, 1, 1, 1},
|
||||||
|
Type: ObjectType,
|
||||||
|
Flags: 0,
|
||||||
|
}},
|
||||||
|
{header: xlMetaV2VersionHeader{
|
||||||
|
VersionID: [16]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
|
||||||
|
ModTime: baseTime.Add(30 * time.Minute).UnixNano(),
|
||||||
|
Signature: [4]byte{1, 1, 1, 1},
|
||||||
|
Type: ObjectType,
|
||||||
|
Flags: 0,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Has a completely different version id
|
||||||
|
3: {
|
||||||
|
versions: []xlMetaV2ShallowVersion{
|
||||||
|
{header: xlMetaV2VersionHeader{
|
||||||
|
VersionID: [16]byte{3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
|
||||||
|
ModTime: baseTime.Add(60 * time.Minute).UnixNano(),
|
||||||
|
Signature: [4]byte{1, 1, 1, 1},
|
||||||
|
Type: ObjectType,
|
||||||
|
Flags: 0,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
versions: []xlMetaV2ShallowVersion{},
|
||||||
|
},
|
||||||
|
// Has a zero version id
|
||||||
|
5: {
|
||||||
|
versions: []xlMetaV2ShallowVersion{
|
||||||
|
{header: xlMetaV2VersionHeader{
|
||||||
|
VersionID: [16]byte{},
|
||||||
|
ModTime: baseTime.Add(60 * time.Minute).UnixNano(),
|
||||||
|
Signature: [4]byte{5, 1, 1, 1},
|
||||||
|
Type: ObjectType,
|
||||||
|
Flags: 0,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Zero version, modtime newer..
|
||||||
|
6: {
|
||||||
|
versions: []xlMetaV2ShallowVersion{
|
||||||
|
{header: xlMetaV2VersionHeader{
|
||||||
|
VersionID: [16]byte{},
|
||||||
|
ModTime: baseTime.Add(90 * time.Minute).UnixNano(),
|
||||||
|
Signature: [4]byte{6, 1, 1, 1},
|
||||||
|
Type: ObjectType,
|
||||||
|
Flags: 0,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
7: {
|
||||||
|
versions: []xlMetaV2ShallowVersion{
|
||||||
|
{header: xlMetaV2VersionHeader{
|
||||||
|
VersionID: [16]byte{},
|
||||||
|
ModTime: baseTime.Add(90 * time.Minute).UnixNano(),
|
||||||
|
Signature: [4]byte{6, 1, 1, 1},
|
||||||
|
Type: ObjectType,
|
||||||
|
Flags: 0,
|
||||||
|
}},
|
||||||
|
{header: xlMetaV2VersionHeader{
|
||||||
|
VersionID: [16]byte{2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
|
||||||
|
ModTime: baseTime.Add(60 * time.Minute).UnixNano(),
|
||||||
|
Signature: [4]byte{2, 1, 1, 1},
|
||||||
|
Type: ObjectType,
|
||||||
|
Flags: 0,
|
||||||
|
}},
|
||||||
|
{header: xlMetaV2VersionHeader{
|
||||||
|
VersionID: [16]byte{3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
|
||||||
|
ModTime: baseTime.Add(60 * time.Minute).UnixNano(),
|
||||||
|
Signature: [4]byte{1, 1, 1, 1},
|
||||||
|
Type: ObjectType,
|
||||||
|
Flags: 0,
|
||||||
|
}},
|
||||||
|
|
||||||
|
{header: xlMetaV2VersionHeader{
|
||||||
|
VersionID: [16]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
|
||||||
|
ModTime: baseTime.Add(30 * time.Minute).UnixNano(),
|
||||||
|
Signature: [4]byte{1, 1, 1, 1},
|
||||||
|
Type: ObjectType,
|
||||||
|
Flags: 0,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Delete marker.
|
||||||
|
8: {
|
||||||
|
versions: []xlMetaV2ShallowVersion{
|
||||||
|
{header: xlMetaV2VersionHeader{
|
||||||
|
VersionID: [16]byte{7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7},
|
||||||
|
ModTime: baseTime.Add(90 * time.Minute).UnixNano(),
|
||||||
|
Signature: [4]byte{6, 1, 1, 1},
|
||||||
|
Type: DeleteType,
|
||||||
|
Flags: 0,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Delete marker and version from 1
|
||||||
|
9: {
|
||||||
|
versions: []xlMetaV2ShallowVersion{
|
||||||
|
{header: xlMetaV2VersionHeader{
|
||||||
|
VersionID: [16]byte{7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7},
|
||||||
|
ModTime: baseTime.Add(90 * time.Minute).UnixNano(),
|
||||||
|
Signature: [4]byte{6, 1, 1, 1},
|
||||||
|
Type: DeleteType,
|
||||||
|
Flags: 0,
|
||||||
|
}},
|
||||||
|
{header: xlMetaV2VersionHeader{
|
||||||
|
VersionID: [16]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
|
||||||
|
ModTime: baseTime.Add(15 * time.Minute).UnixNano(),
|
||||||
|
Signature: [4]byte{2, 1, 1, 1},
|
||||||
|
Type: ObjectType,
|
||||||
|
Flags: 0,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
inputSerialized := make([]metaCacheEntry, len(inputs))
|
||||||
|
for i, xl := range inputs {
|
||||||
|
xl.sortByModTime()
|
||||||
|
var err error
|
||||||
|
var entry = metaCacheEntry{
|
||||||
|
name: "testobject",
|
||||||
|
}
|
||||||
|
entry.metadata, err = xl.AppendTo(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
inputSerialized[i] = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
m metaCacheEntries
|
||||||
|
r metadataResolutionParams
|
||||||
|
wantSelected *metaCacheEntry
|
||||||
|
wantOk bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "consistent",
|
||||||
|
m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], inputSerialized[0]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 4, objQuorum: 4, strict: false},
|
||||||
|
wantSelected: &inputSerialized[0],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "consistent-strict",
|
||||||
|
m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], inputSerialized[0]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 4, objQuorum: 4, strict: false},
|
||||||
|
wantSelected: &inputSerialized[0],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one zero, below quorum",
|
||||||
|
m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], metaCacheEntry{}},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 4, objQuorum: 4, strict: false},
|
||||||
|
wantSelected: nil,
|
||||||
|
wantOk: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one zero, below quorum, strict",
|
||||||
|
m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], metaCacheEntry{}},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 4, objQuorum: 4, strict: true},
|
||||||
|
wantSelected: nil,
|
||||||
|
wantOk: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one zero, at quorum",
|
||||||
|
m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], metaCacheEntry{}},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 3, objQuorum: 3, strict: false},
|
||||||
|
wantSelected: &inputSerialized[0],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one zero, at quorum, strict",
|
||||||
|
m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], metaCacheEntry{}},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 3, objQuorum: 3, strict: true},
|
||||||
|
wantSelected: &inputSerialized[0],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "modtime, signature mismatch",
|
||||||
|
m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], inputSerialized[1]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 4, objQuorum: 4, strict: false},
|
||||||
|
wantSelected: &inputSerialized[0],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "modtime,signature mismatch, strict",
|
||||||
|
m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], inputSerialized[1]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 4, objQuorum: 4, strict: true},
|
||||||
|
wantSelected: nil,
|
||||||
|
wantOk: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "modtime, signature mismatch, at quorum",
|
||||||
|
m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], inputSerialized[1]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 3, objQuorum: 3, strict: false},
|
||||||
|
wantSelected: &inputSerialized[0],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "modtime,signature mismatch, at quorum, strict",
|
||||||
|
m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], inputSerialized[1]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 3, objQuorum: 3, strict: true},
|
||||||
|
wantSelected: &inputSerialized[0],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "additional version",
|
||||||
|
m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], inputSerialized[2]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 4, objQuorum: 4, strict: false},
|
||||||
|
wantSelected: &inputSerialized[0],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Since we have the same version in all inputs, that is strictly ok.
|
||||||
|
name: "additional version, strict",
|
||||||
|
m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], inputSerialized[2]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 4, objQuorum: 4, strict: true},
|
||||||
|
wantSelected: &inputSerialized[0],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Since we have the same version in all inputs, that is strictly ok.
|
||||||
|
name: "additional version, quorum one",
|
||||||
|
m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], inputSerialized[2]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 1, objQuorum: 1, strict: true},
|
||||||
|
// We get the both versions, since we only request quorum 1
|
||||||
|
wantSelected: &inputSerialized[2],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "additional version, quorum two",
|
||||||
|
m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], inputSerialized[2]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 2, objQuorum: 2, strict: true},
|
||||||
|
wantSelected: &inputSerialized[0],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "2 additional versions, quorum two",
|
||||||
|
m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[2], inputSerialized[2]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 2, objQuorum: 2, strict: true},
|
||||||
|
wantSelected: &inputSerialized[2],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// inputSerialized[1] have older versions of the second in inputSerialized[2]
|
||||||
|
name: "modtimemismatch",
|
||||||
|
m: metaCacheEntries{inputSerialized[1], inputSerialized[1], inputSerialized[2], inputSerialized[2]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 2, objQuorum: 2, strict: false},
|
||||||
|
wantSelected: &inputSerialized[2],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// inputSerialized[1] have older versions of the second in inputSerialized[2]
|
||||||
|
name: "modtimemismatch,strict",
|
||||||
|
m: metaCacheEntries{inputSerialized[1], inputSerialized[1], inputSerialized[2], inputSerialized[2]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 2, objQuorum: 2, strict: true},
|
||||||
|
wantSelected: &inputSerialized[2],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// inputSerialized[1] have older versions of the second in inputSerialized[2], but
|
||||||
|
// since it is not strict, we should get it that one (with latest modtime)
|
||||||
|
name: "modtimemismatch,not strict",
|
||||||
|
m: metaCacheEntries{inputSerialized[1], inputSerialized[1], inputSerialized[2], inputSerialized[2]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 4, objQuorum: 4, strict: false},
|
||||||
|
wantSelected: &inputSerialized[0],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one-q1",
|
||||||
|
m: metaCacheEntries{inputSerialized[0], inputSerialized[4], inputSerialized[4], inputSerialized[4]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 1, objQuorum: 1, strict: false},
|
||||||
|
wantSelected: &inputSerialized[0],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one-q1-strict",
|
||||||
|
m: metaCacheEntries{inputSerialized[0], inputSerialized[4], inputSerialized[4], inputSerialized[4]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 1, objQuorum: 1, strict: true},
|
||||||
|
wantSelected: &inputSerialized[0],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one-q2",
|
||||||
|
m: metaCacheEntries{inputSerialized[0], inputSerialized[4], inputSerialized[4], inputSerialized[4]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 2, objQuorum: 2, strict: false},
|
||||||
|
wantSelected: nil,
|
||||||
|
wantOk: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one-q2-strict",
|
||||||
|
m: metaCacheEntries{inputSerialized[0], inputSerialized[4], inputSerialized[4], inputSerialized[4]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 2, objQuorum: 2, strict: true},
|
||||||
|
wantSelected: nil,
|
||||||
|
wantOk: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "two-diff-q2",
|
||||||
|
m: metaCacheEntries{inputSerialized[0], inputSerialized[3], inputSerialized[4], inputSerialized[4]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 2, objQuorum: 2, strict: false},
|
||||||
|
wantSelected: nil,
|
||||||
|
wantOk: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zeroid",
|
||||||
|
m: metaCacheEntries{inputSerialized[5], inputSerialized[5], inputSerialized[6], inputSerialized[6]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 2, objQuorum: 2, strict: false},
|
||||||
|
wantSelected: &inputSerialized[6],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// When ID is zero, do not allow non-strict matches to reach quorum.
|
||||||
|
name: "zeroid-belowq",
|
||||||
|
m: metaCacheEntries{inputSerialized[5], inputSerialized[5], inputSerialized[6], inputSerialized[6]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 3, objQuorum: 3, strict: false},
|
||||||
|
wantSelected: nil,
|
||||||
|
wantOk: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "merge4",
|
||||||
|
m: metaCacheEntries{inputSerialized[2], inputSerialized[3], inputSerialized[5], inputSerialized[6]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 1, objQuorum: 1, strict: false},
|
||||||
|
wantSelected: &inputSerialized[7],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deletemarker",
|
||||||
|
m: metaCacheEntries{inputSerialized[8], inputSerialized[4], inputSerialized[4], inputSerialized[4]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 1, objQuorum: 1, strict: false},
|
||||||
|
wantSelected: &inputSerialized[8],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deletemarker-nonq",
|
||||||
|
m: metaCacheEntries{inputSerialized[8], inputSerialized[8], inputSerialized[4], inputSerialized[4]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 3, objQuorum: 3, strict: false},
|
||||||
|
wantSelected: nil,
|
||||||
|
wantOk: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deletemarker-nonq",
|
||||||
|
m: metaCacheEntries{inputSerialized[8], inputSerialized[8], inputSerialized[8], inputSerialized[1]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 3, objQuorum: 3, strict: false},
|
||||||
|
wantSelected: &inputSerialized[8],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deletemarker-mixed",
|
||||||
|
m: metaCacheEntries{inputSerialized[8], inputSerialized[8], inputSerialized[1], inputSerialized[1]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 2, objQuorum: 2, strict: false},
|
||||||
|
wantSelected: &inputSerialized[9],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deletemarker-q3",
|
||||||
|
m: metaCacheEntries{inputSerialized[8], inputSerialized[9], inputSerialized[9], inputSerialized[1]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 3, objQuorum: 3, strict: false},
|
||||||
|
wantSelected: &inputSerialized[9],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deletemarker-q3-strict",
|
||||||
|
m: metaCacheEntries{inputSerialized[8], inputSerialized[9], inputSerialized[9], inputSerialized[1]},
|
||||||
|
r: metadataResolutionParams{dirQuorum: 3, objQuorum: 3, strict: true},
|
||||||
|
wantSelected: &inputSerialized[9],
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for testID, tt := range tests {
|
||||||
|
rng := rand.New(rand.NewSource(0))
|
||||||
|
// Run for a number of times, shuffling the input to ensure that output is consistent.
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
t.Run(fmt.Sprintf("test-%d-%s-run-%d", testID, tt.name, i), func(t *testing.T) {
|
||||||
|
if i > 0 {
|
||||||
|
rng.Shuffle(len(tt.m), func(i, j int) {
|
||||||
|
tt.m[i], tt.m[j] = tt.m[j], tt.m[i]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
gotSelected, gotOk := tt.m.resolve(&tt.r)
|
||||||
|
if gotOk != tt.wantOk {
|
||||||
|
t.Errorf("resolve() gotOk = %v, want %v", gotOk, tt.wantOk)
|
||||||
|
}
|
||||||
|
if gotSelected != nil {
|
||||||
|
gotSelected.cached = nil
|
||||||
|
gotSelected.reusable = false
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(gotSelected, tt.wantSelected) {
|
||||||
|
wantM, _ := tt.wantSelected.xlmeta()
|
||||||
|
gotM, _ := gotSelected.xlmeta()
|
||||||
|
t.Errorf("resolve() gotSelected = \n%#v, want \n%#v", *gotM, *wantM)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -764,7 +764,7 @@ type listPathRawOptions struct {
|
|||||||
// agreed is called if all disks agreed.
|
// agreed is called if all disks agreed.
|
||||||
agreed func(entry metaCacheEntry)
|
agreed func(entry metaCacheEntry)
|
||||||
|
|
||||||
// partial will be returned when there is disagreement between disks.
|
// partial will be called when there is disagreement between disks.
|
||||||
// if disk did not return any result, but also haven't errored
|
// if disk did not return any result, but also haven't errored
|
||||||
// the entry will be empty and errs will
|
// the entry will be empty and errs will
|
||||||
partial func(entries metaCacheEntries, nAgreed int, errs []error)
|
partial func(entries metaCacheEntries, nAgreed int, errs []error)
|
||||||
@ -905,7 +905,7 @@ func listPathRaw(ctx context.Context, opts listPathRawOptions) (err error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// If exact match, we agree.
|
// If exact match, we agree.
|
||||||
if current.matches(&entry, opts.bucket) {
|
if _, ok := current.matches(&entry, true); ok {
|
||||||
topEntries[i] = entry
|
topEntries[i] = entry
|
||||||
agree++
|
agree++
|
||||||
continue
|
continue
|
||||||
|
@ -31,13 +31,13 @@ func _() {
|
|||||||
_ = x[storageMetricUpdateMetadata-20]
|
_ = x[storageMetricUpdateMetadata-20]
|
||||||
_ = x[storageMetricReadVersion-21]
|
_ = x[storageMetricReadVersion-21]
|
||||||
_ = x[storageMetricReadAll-22]
|
_ = x[storageMetricReadAll-22]
|
||||||
_ = x[storageStatInfoFile-23]
|
_ = x[storageMetricStatInfoFile-23]
|
||||||
_ = x[storageMetricLast-24]
|
_ = x[storageMetricLast-24]
|
||||||
}
|
}
|
||||||
|
|
||||||
const _storageMetric_name = "MakeVolBulkMakeVolListVolsStatVolDeleteVolWalkDirListDirReadFileAppendFileCreateFileReadFileStreamRenameFileRenameDataCheckPartsDeleteDeleteVersionsVerifyFileWriteAllDeleteVersionWriteMetadataUpdateMetadataReadVersionReadAllstorageStatInfoFileLast"
|
const _storageMetric_name = "MakeVolBulkMakeVolListVolsStatVolDeleteVolWalkDirListDirReadFileAppendFileCreateFileReadFileStreamRenameFileRenameDataCheckPartsDeleteDeleteVersionsVerifyFileWriteAllDeleteVersionWriteMetadataUpdateMetadataReadVersionReadAllStatInfoFileLast"
|
||||||
|
|
||||||
var _storageMetric_index = [...]uint8{0, 11, 18, 26, 33, 42, 49, 56, 64, 74, 84, 98, 108, 118, 128, 134, 148, 158, 166, 179, 192, 206, 217, 224, 243, 247}
|
var _storageMetric_index = [...]uint8{0, 11, 18, 26, 33, 42, 49, 56, 64, 74, 84, 98, 108, 118, 128, 134, 148, 158, 166, 179, 192, 206, 217, 224, 236, 240}
|
||||||
|
|
||||||
func (i storageMetric) String() string {
|
func (i storageMetric) String() string {
|
||||||
if i >= storageMetric(len(_storageMetric_index)-1) {
|
if i >= storageMetric(len(_storageMetric_index)-1) {
|
||||||
|
@ -57,7 +57,7 @@ const (
|
|||||||
storageMetricUpdateMetadata
|
storageMetricUpdateMetadata
|
||||||
storageMetricReadVersion
|
storageMetricReadVersion
|
||||||
storageMetricReadAll
|
storageMetricReadAll
|
||||||
storageStatInfoFile
|
storageMetricStatInfoFile
|
||||||
|
|
||||||
// .... add more
|
// .... add more
|
||||||
|
|
||||||
@ -548,7 +548,7 @@ func (p *xlStorageDiskIDCheck) ReadAll(ctx context.Context, volume string, path
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *xlStorageDiskIDCheck) StatInfoFile(ctx context.Context, volume, path string, glob bool) (stat []StatInfo, err error) {
|
func (p *xlStorageDiskIDCheck) StatInfoFile(ctx context.Context, volume, path string, glob bool) (stat []StatInfo, err error) {
|
||||||
defer p.updateStorageMetrics(storageStatInfoFile, volume, path)()
|
defer p.updateStorageMetrics(storageMetricStatInfoFile, volume, path)()
|
||||||
|
|
||||||
if contextCanceled(ctx) {
|
if contextCanceled(ctx) {
|
||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
|
@ -18,22 +18,41 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
|
||||||
"github.com/minio/minio/internal/logger"
|
|
||||||
"github.com/zeebo/xxh3"
|
"github.com/zeebo/xxh3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getFileInfoVersions(xlMetaBuf []byte, volume, path string) (FileInfoVersions, error) {
|
func getFileInfoVersions(xlMetaBuf []byte, volume, path string) (FileInfoVersions, error) {
|
||||||
if isXL2V1Format(xlMetaBuf) {
|
fivs, err := getAllFileInfoVersions(xlMetaBuf, volume, path)
|
||||||
|
if err != nil {
|
||||||
|
return fivs, err
|
||||||
|
}
|
||||||
|
n := 0
|
||||||
|
for _, fi := range fivs.Versions {
|
||||||
|
// Filter our tier object delete marker
|
||||||
|
if !fi.TierFreeVersion() {
|
||||||
|
fivs.Versions[n] = fi
|
||||||
|
n++
|
||||||
|
} else {
|
||||||
|
fivs.FreeVersions = append(fivs.FreeVersions, fi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fivs.Versions = fivs.Versions[:n]
|
||||||
|
// Update numversions
|
||||||
|
for i := range fivs.Versions {
|
||||||
|
fivs.Versions[i].NumVersions = n
|
||||||
|
}
|
||||||
|
return fivs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAllFileInfoVersions(xlMetaBuf []byte, volume, path string) (FileInfoVersions, error) {
|
||||||
var versions []FileInfo
|
var versions []FileInfo
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if buf, _ := isIndexedMetaV2(xlMetaBuf); buf != nil {
|
if buf, _ := isIndexedMetaV2(xlMetaBuf); buf != nil {
|
||||||
versions, err = buf.ListVersions(volume, path)
|
versions, err = buf.ListVersions(volume, path)
|
||||||
} else {
|
} else {
|
||||||
var xlMeta xlMetaV2
|
var xlMeta xlMetaV2
|
||||||
if err := xlMeta.Load(xlMetaBuf); err != nil {
|
if err := xlMeta.LoadOrConvert(xlMetaBuf); err != nil {
|
||||||
return FileInfoVersions{}, err
|
return FileInfoVersions{}, err
|
||||||
}
|
}
|
||||||
versions, err = xlMeta.ListVersions(volume, path)
|
versions, err = xlMeta.ListVersions(volume, path)
|
||||||
@ -42,52 +61,15 @@ func getFileInfoVersions(xlMetaBuf []byte, volume, path string) (FileInfoVersion
|
|||||||
return FileInfoVersions{}, err
|
return FileInfoVersions{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var freeVersions []FileInfo
|
|
||||||
n := 0
|
|
||||||
for _, fi := range versions {
|
|
||||||
if fi.TierFreeVersion() {
|
|
||||||
freeVersions = append(freeVersions, fi)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
versions[n] = fi
|
|
||||||
n++
|
|
||||||
}
|
|
||||||
versions = versions[:n]
|
|
||||||
for _, ver := range versions {
|
|
||||||
ver.NumVersions = n
|
|
||||||
}
|
|
||||||
return FileInfoVersions{
|
return FileInfoVersions{
|
||||||
Volume: volume,
|
Volume: volume,
|
||||||
Name: path,
|
Name: path,
|
||||||
Versions: versions,
|
Versions: versions,
|
||||||
FreeVersions: freeVersions,
|
|
||||||
LatestModTime: versions[0].ModTime,
|
LatestModTime: versions[0].ModTime,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
xlMeta := &xlMetaV1Object{}
|
|
||||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
|
||||||
if err := json.Unmarshal(xlMetaBuf, xlMeta); err != nil {
|
|
||||||
return FileInfoVersions{}, errFileCorrupt
|
|
||||||
}
|
|
||||||
|
|
||||||
fi, err := xlMeta.ToFileInfo(volume, path)
|
|
||||||
if err != nil {
|
|
||||||
return FileInfoVersions{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
fi.IsLatest = true // No versions so current version is latest.
|
|
||||||
fi.NumVersions = 1 // just this version
|
|
||||||
return FileInfoVersions{
|
|
||||||
Volume: volume,
|
|
||||||
Name: path,
|
|
||||||
Versions: []FileInfo{fi},
|
|
||||||
LatestModTime: fi.ModTime,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFileInfo(xlMetaBuf []byte, volume, path, versionID string, data bool) (FileInfo, error) {
|
func getFileInfo(xlMetaBuf []byte, volume, path, versionID string, data bool) (FileInfo, error) {
|
||||||
if isXL2V1Format(xlMetaBuf) {
|
|
||||||
var fi FileInfo
|
var fi FileInfo
|
||||||
var err error
|
var err error
|
||||||
var inData xlMetaInlineData
|
var inData xlMetaInlineData
|
||||||
@ -96,7 +78,7 @@ func getFileInfo(xlMetaBuf []byte, volume, path, versionID string, data bool) (F
|
|||||||
fi, err = buf.ToFileInfo(volume, path, versionID)
|
fi, err = buf.ToFileInfo(volume, path, versionID)
|
||||||
} else {
|
} else {
|
||||||
var xlMeta xlMetaV2
|
var xlMeta xlMetaV2
|
||||||
if err := xlMeta.Load(xlMetaBuf); err != nil {
|
if err := xlMeta.LoadOrConvert(xlMetaBuf); err != nil {
|
||||||
return FileInfo{}, err
|
return FileInfo{}, err
|
||||||
}
|
}
|
||||||
inData = xlMeta.data
|
inData = xlMeta.data
|
||||||
@ -105,7 +87,7 @@ func getFileInfo(xlMetaBuf []byte, volume, path, versionID string, data bool) (F
|
|||||||
if !data || err != nil {
|
if !data || err != nil {
|
||||||
return fi, err
|
return fi, err
|
||||||
}
|
}
|
||||||
versionID := fi.VersionID
|
versionID = fi.VersionID
|
||||||
if versionID == "" {
|
if versionID == "" {
|
||||||
versionID = nullVersionID
|
versionID = nullVersionID
|
||||||
}
|
}
|
||||||
@ -119,23 +101,6 @@ func getFileInfo(xlMetaBuf []byte, volume, path, versionID string, data bool) (F
|
|||||||
return fi, nil
|
return fi, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
xlMeta := &xlMetaV1Object{}
|
|
||||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
|
||||||
if err := json.Unmarshal(xlMetaBuf, xlMeta); err != nil {
|
|
||||||
logger.LogIf(GlobalContext, fmt.Errorf("unable to unmarshal json object: %v", err))
|
|
||||||
return FileInfo{}, errFileCorrupt
|
|
||||||
}
|
|
||||||
|
|
||||||
fi, err := xlMeta.ToFileInfo(volume, path)
|
|
||||||
if err != nil {
|
|
||||||
return FileInfo{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
fi.XLV1 = true // indicates older version
|
|
||||||
fi.IsLatest = true // No versions so current version is latest.
|
|
||||||
return fi, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// getXLDiskLoc will return the pool/set/disk id if it can be located in the object layer.
|
// getXLDiskLoc will return the pool/set/disk id if it can be located in the object layer.
|
||||||
// Will return -1 for unknown values.
|
// Will return -1 for unknown values.
|
||||||
func getXLDiskLoc(diskID string) (poolIdx, setIdx, diskIdx int) {
|
func getXLDiskLoc(diskID string) (poolIdx, setIdx, diskIdx int) {
|
||||||
|
@ -202,6 +202,7 @@ func (m *xlMetaV1Object) ToFileInfo(volume, path string) (FileInfo, error) {
|
|||||||
VersionID: m.VersionID,
|
VersionID: m.VersionID,
|
||||||
DataDir: m.DataDir,
|
DataDir: m.DataDir,
|
||||||
XLV1: true,
|
XLV1: true,
|
||||||
|
NumVersions: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
return fi, nil
|
return fi, nil
|
||||||
|
@ -33,6 +33,7 @@ import (
|
|||||||
|
|
||||||
"github.com/cespare/xxhash/v2"
|
"github.com/cespare/xxhash/v2"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||||
"github.com/minio/minio/internal/bucket/replication"
|
"github.com/minio/minio/internal/bucket/replication"
|
||||||
xhttp "github.com/minio/minio/internal/http"
|
xhttp "github.com/minio/minio/internal/http"
|
||||||
@ -49,7 +50,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
//go:generate msgp -file=$GOFILE -unexported
|
//go:generate msgp -file=$GOFILE -unexported
|
||||||
//go:generate stringer -type VersionType -output=xl-storage-format-v2_string.go $GOFILE
|
//go:generate stringer -type VersionType,ErasureAlgo -output=xl-storage-format-v2_string.go $GOFILE
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Breaking changes.
|
// Breaking changes.
|
||||||
@ -130,14 +131,6 @@ func (e ErasureAlgo) valid() bool {
|
|||||||
return e > invalidErasureAlgo && e < lastErasureAlgo
|
return e > invalidErasureAlgo && e < lastErasureAlgo
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e ErasureAlgo) String() string {
|
|
||||||
switch e {
|
|
||||||
case ReedSolomon:
|
|
||||||
return "reedsolomon"
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChecksumAlgo defines common type of different checksum algorithms
|
// ChecksumAlgo defines common type of different checksum algorithms
|
||||||
type ChecksumAlgo uint8
|
type ChecksumAlgo uint8
|
||||||
|
|
||||||
@ -269,6 +262,45 @@ func (x xlMetaV2VersionHeader) String() string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// matchesNotStrict returns whether x and o have both have non-zero version,
|
||||||
|
// their versions match and their type match.
|
||||||
|
func (x xlMetaV2VersionHeader) matchesNotStrict(o xlMetaV2VersionHeader) bool {
|
||||||
|
return x.VersionID != [16]byte{} &&
|
||||||
|
x.VersionID == o.VersionID &&
|
||||||
|
x.Type == o.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
// sortsBefore can be used as a tiebreaker for stable sorting/selecting.
|
||||||
|
// Returns false on ties.
|
||||||
|
func (x xlMetaV2VersionHeader) sortsBefore(o xlMetaV2VersionHeader) bool {
|
||||||
|
if x == o {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Prefer newest modtime.
|
||||||
|
if x.ModTime != o.ModTime {
|
||||||
|
return x.ModTime > o.ModTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following doesn't make too much sense, but we want sort to be consistent nonetheless.
|
||||||
|
// Prefer lower types
|
||||||
|
if x.Type != o.Type {
|
||||||
|
return x.Type < o.Type
|
||||||
|
}
|
||||||
|
// Consistent sort on signature
|
||||||
|
if v := bytes.Compare(x.Signature[:], o.Signature[:]); v != 0 {
|
||||||
|
return v > 0
|
||||||
|
}
|
||||||
|
// On ID mismatch
|
||||||
|
if v := bytes.Compare(x.VersionID[:], o.VersionID[:]); v != 0 {
|
||||||
|
return v > 0
|
||||||
|
}
|
||||||
|
// Flags
|
||||||
|
if x.Flags != o.Flags {
|
||||||
|
return x.Flags > o.Flags
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Valid xl meta xlMetaV2Version is valid
|
// Valid xl meta xlMetaV2Version is valid
|
||||||
func (j xlMetaV2Version) Valid() bool {
|
func (j xlMetaV2Version) Valid() bool {
|
||||||
if !j.Type.valid() {
|
if !j.Type.valid() {
|
||||||
@ -372,6 +404,7 @@ func (j xlMetaV2Version) getVersionID() [16]byte {
|
|||||||
return [16]byte{}
|
return [16]byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToFileInfo returns FileInfo of the underlying type.
|
||||||
func (j *xlMetaV2Version) ToFileInfo(volume, path string) (FileInfo, error) {
|
func (j *xlMetaV2Version) ToFileInfo(volume, path string) (FileInfo, error) {
|
||||||
switch j.Type {
|
switch j.Type {
|
||||||
case ObjectType:
|
case ObjectType:
|
||||||
@ -788,6 +821,26 @@ type xlMetaV2 struct {
|
|||||||
metaV uint8
|
metaV uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadOrConvert will load the metadata in the buffer.
|
||||||
|
// If this is a legacy format, it will automatically be converted to XLV2.
|
||||||
|
func (x *xlMetaV2) LoadOrConvert(buf []byte) error {
|
||||||
|
if isXL2V1Format(buf) {
|
||||||
|
return x.Load(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
xlMeta := &xlMetaV1Object{}
|
||||||
|
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
|
if err := json.Unmarshal(buf, xlMeta); err != nil {
|
||||||
|
return errFileCorrupt
|
||||||
|
}
|
||||||
|
if len(x.versions) > 0 {
|
||||||
|
x.versions = x.versions[:0]
|
||||||
|
}
|
||||||
|
x.data = nil
|
||||||
|
x.metaV = xlMetaVersion
|
||||||
|
return x.AddLegacy(xlMeta)
|
||||||
|
}
|
||||||
|
|
||||||
// Load all versions of the stored data.
|
// Load all versions of the stored data.
|
||||||
// Note that references to the incoming buffer will be kept.
|
// Note that references to the incoming buffer will be kept.
|
||||||
func (x *xlMetaV2) Load(buf []byte) error {
|
func (x *xlMetaV2) Load(buf []byte) error {
|
||||||
@ -924,6 +977,14 @@ func (x *xlMetaV2) loadLegacy(buf []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// latestModtime returns the modtime of the latest version.
|
||||||
|
func (x *xlMetaV2) latestModtime() time.Time {
|
||||||
|
if x == nil || len(x.versions) == 0 {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
return time.Unix(0, x.versions[0].header.ModTime)
|
||||||
|
}
|
||||||
|
|
||||||
func (x *xlMetaV2) addVersion(ver xlMetaV2Version) error {
|
func (x *xlMetaV2) addVersion(ver xlMetaV2Version) error {
|
||||||
modTime := ver.getModTime().UnixNano()
|
modTime := ver.getModTime().UnixNano()
|
||||||
if !ver.Valid() {
|
if !ver.Valid() {
|
||||||
@ -1059,14 +1120,14 @@ func (x *xlMetaV2) setIdx(idx int, ver xlMetaV2Version) (err error) {
|
|||||||
func (x *xlMetaV2) sortByModTime() {
|
func (x *xlMetaV2) sortByModTime() {
|
||||||
// Quick check
|
// Quick check
|
||||||
if len(x.versions) <= 1 || sort.SliceIsSorted(x.versions, func(i, j int) bool {
|
if len(x.versions) <= 1 || sort.SliceIsSorted(x.versions, func(i, j int) bool {
|
||||||
return x.versions[i].header.ModTime > x.versions[j].header.ModTime
|
return x.versions[i].header.sortsBefore(x.versions[j].header)
|
||||||
}) {
|
}) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// We should sort.
|
// We should sort.
|
||||||
sort.Slice(x.versions, func(i, j int) bool {
|
sort.Slice(x.versions, func(i, j int) bool {
|
||||||
return x.versions[i].header.ModTime > x.versions[j].header.ModTime
|
return x.versions[i].header.sortsBefore(x.versions[j].header)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1506,7 +1567,6 @@ func (x *xlMetaV2) AddLegacy(m *xlMetaV1Object) error {
|
|||||||
return errFileCorrupt
|
return errFileCorrupt
|
||||||
}
|
}
|
||||||
m.VersionID = nullVersionID
|
m.VersionID = nullVersionID
|
||||||
m.DataDir = legacyDataDir
|
|
||||||
|
|
||||||
return x.addVersion(xlMetaV2Version{ObjectV1: m, Type: LegacyType})
|
return x.addVersion(xlMetaV2Version{ObjectV1: m, Type: LegacyType})
|
||||||
}
|
}
|
||||||
@ -1605,6 +1665,137 @@ func (x xlMetaV2) ListVersions(volume, path string) ([]FileInfo, error) {
|
|||||||
return versions, nil
|
return versions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mergeXLV2Versions will merge all versions, typically from different disks
|
||||||
|
// that have at least quorum entries in all metas.
|
||||||
|
// Quorum must be the minimum number of matching metadata files.
|
||||||
|
// Quorum should be > 1 and <= len(versions).
|
||||||
|
// If strict is set to false, entries that match type
|
||||||
|
func mergeXLV2Versions(quorum int, strict bool, versions ...[]xlMetaV2ShallowVersion) (merged []xlMetaV2ShallowVersion) {
|
||||||
|
if len(versions) < quorum || len(versions) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(versions) == 1 {
|
||||||
|
return versions[0]
|
||||||
|
}
|
||||||
|
if quorum == 1 {
|
||||||
|
// No need for non-strict checks if quorum is 1.
|
||||||
|
strict = true
|
||||||
|
}
|
||||||
|
// Our result
|
||||||
|
merged = make([]xlMetaV2ShallowVersion, 0, len(versions[0]))
|
||||||
|
tops := make([]xlMetaV2ShallowVersion, len(versions))
|
||||||
|
for {
|
||||||
|
// Step 1 create slice with all top versions.
|
||||||
|
tops = tops[:0]
|
||||||
|
var topSig [4]byte
|
||||||
|
var topID [16]byte
|
||||||
|
consistent := true // Are all signatures consistent (shortcut)
|
||||||
|
for _, vers := range versions {
|
||||||
|
if len(vers) == 0 {
|
||||||
|
consistent = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ver := vers[0]
|
||||||
|
if len(tops) == 0 {
|
||||||
|
consistent = true
|
||||||
|
topSig = ver.header.Signature
|
||||||
|
topID = ver.header.VersionID
|
||||||
|
} else {
|
||||||
|
consistent = consistent && topSig == ver.header.Signature && topID == ver.header.VersionID
|
||||||
|
}
|
||||||
|
tops = append(tops, vers[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if done...
|
||||||
|
if len(tops) < quorum {
|
||||||
|
// We couldn't gather enough for quorum
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var latest xlMetaV2ShallowVersion
|
||||||
|
var latestCount int
|
||||||
|
if consistent {
|
||||||
|
// All had the same signature, easy.
|
||||||
|
latest = tops[0]
|
||||||
|
latestCount = len(tops)
|
||||||
|
merged = append(merged, latest)
|
||||||
|
} else {
|
||||||
|
// Find latest.
|
||||||
|
for i, ver := range tops {
|
||||||
|
if ver.header == latest.header {
|
||||||
|
latestCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i == 0 || ver.header.sortsBefore(latest.header) {
|
||||||
|
if i == 0 {
|
||||||
|
latestCount = 1
|
||||||
|
} else if !strict && ver.header.matchesNotStrict(latest.header) {
|
||||||
|
latestCount++
|
||||||
|
} else {
|
||||||
|
latestCount = 1
|
||||||
|
}
|
||||||
|
latest = ver
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mismatch, but older.
|
||||||
|
if !strict && ver.header.matchesNotStrict(latest.header) {
|
||||||
|
// If non-nil version ID and it matches, assume match, but keep newest.
|
||||||
|
if ver.header.sortsBefore(latest.header) {
|
||||||
|
latest = ver
|
||||||
|
}
|
||||||
|
latestCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if latestCount >= quorum {
|
||||||
|
merged = append(merged, latest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from all streams up until latest modtime or if selected.
|
||||||
|
for i, vers := range versions {
|
||||||
|
for _, ver := range vers {
|
||||||
|
// Truncate later modtimes, not selected.
|
||||||
|
if ver.header.ModTime > latest.header.ModTime {
|
||||||
|
versions[i] = versions[i][1:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Truncate matches
|
||||||
|
if ver.header == latest.header {
|
||||||
|
versions[i] = versions[i][1:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate non-empty version and type matches
|
||||||
|
if latest.header.VersionID == ver.header.VersionID {
|
||||||
|
versions[i] = versions[i][1:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Skip versions with version id we already emitted.
|
||||||
|
for _, mergedV := range merged {
|
||||||
|
if ver.header.VersionID == mergedV.header.VersionID {
|
||||||
|
versions[i] = versions[i][1:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Keep top entry (and remaining)...
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sanity check. Enable if duplicates show up.
|
||||||
|
if false {
|
||||||
|
var found = make(map[[16]byte]struct{})
|
||||||
|
for _, ver := range merged {
|
||||||
|
if _, ok := found[ver.header.VersionID]; ok {
|
||||||
|
panic("found dupe")
|
||||||
|
}
|
||||||
|
found[ver.header.VersionID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
type xlMetaBuf []byte
|
type xlMetaBuf []byte
|
||||||
|
|
||||||
// ToFileInfo converts xlMetaV2 into a common FileInfo datastructure
|
// ToFileInfo converts xlMetaV2 into a common FileInfo datastructure
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Code generated by "stringer -type VersionType -output=xl-storage-format-v2_string.go xl-storage-format-v2.go"; DO NOT EDIT.
|
// Code generated by "stringer -type VersionType,ErasureAlgo -output=xl-storage-format-v2_string.go xl-storage-format-v2.go"; DO NOT EDIT.
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
@ -25,3 +25,22 @@ func (i VersionType) String() string {
|
|||||||
}
|
}
|
||||||
return _VersionType_name[_VersionType_index[i]:_VersionType_index[i+1]]
|
return _VersionType_name[_VersionType_index[i]:_VersionType_index[i+1]]
|
||||||
}
|
}
|
||||||
|
func _() {
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[invalidErasureAlgo-0]
|
||||||
|
_ = x[ReedSolomon-1]
|
||||||
|
_ = x[lastErasureAlgo-2]
|
||||||
|
}
|
||||||
|
|
||||||
|
const _ErasureAlgo_name = "invalidErasureAlgoReedSolomonlastErasureAlgo"
|
||||||
|
|
||||||
|
var _ErasureAlgo_index = [...]uint8{0, 18, 29, 44}
|
||||||
|
|
||||||
|
func (i ErasureAlgo) String() string {
|
||||||
|
if i >= ErasureAlgo(len(_ErasureAlgo_index)-1) {
|
||||||
|
return "ErasureAlgo(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||||
|
}
|
||||||
|
return _ErasureAlgo_name[_ErasureAlgo_index[i]:_ErasureAlgo_index[i+1]]
|
||||||
|
}
|
||||||
|
@ -2058,6 +2058,7 @@ func (s *xlStorage) RenameData(ctx context.Context, srcVolume, srcPath string, f
|
|||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
// Data appears corrupt. Drop data.
|
// Data appears corrupt. Drop data.
|
||||||
} else {
|
} else {
|
||||||
|
xlMetaLegacy.DataDir = legacyDataDir
|
||||||
if err = xlMeta.AddLegacy(xlMetaLegacy); err != nil {
|
if err = xlMeta.AddLegacy(xlMetaLegacy); err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user