mirror of
https://github.com/pgsty/minio.git
synced 2026-08-09 07:43:29 +03:00
Improve listing consistency with version merging (#13723)
This commit is contained in:
+142
-119
@@ -37,7 +37,7 @@ type metaCacheEntry struct {
|
||||
metadata []byte
|
||||
|
||||
// cached contains the metadata if decoded.
|
||||
cached *FileInfo
|
||||
cached *xlMetaV2
|
||||
|
||||
// Indicates the entry can be reused and only one reference to metadata is expected.
|
||||
reusable bool
|
||||
@@ -58,68 +58,80 @@ func (e metaCacheEntry) hasPrefix(s string) bool {
|
||||
return strings.HasPrefix(e.name, s)
|
||||
}
|
||||
|
||||
// matches returns if the entries match by comparing their latest version fileinfo.
|
||||
func (e *metaCacheEntry) matches(other *metaCacheEntry, bucket string) bool {
|
||||
// matches returns if the entries have the same versions.
|
||||
// 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 {
|
||||
return true
|
||||
return nil, true
|
||||
}
|
||||
if e == nil || other == nil {
|
||||
return false
|
||||
if e == nil {
|
||||
return other, false
|
||||
}
|
||||
if other == nil {
|
||||
return e, false
|
||||
}
|
||||
|
||||
// This should reject 99%
|
||||
if len(e.metadata) != len(other.metadata) || e.name != other.name {
|
||||
return false
|
||||
// Name should match...
|
||||
if e.name != other.name {
|
||||
if e.name < other.name {
|
||||
return e, false
|
||||
}
|
||||
return other, false
|
||||
}
|
||||
|
||||
eFi, eErr := e.fileInfo(bucket)
|
||||
oFi, oErr := other.fileInfo(bucket)
|
||||
eVers, eErr := e.xlmeta()
|
||||
oVers, oErr := other.xlmeta()
|
||||
if eErr != nil || oErr != nil {
|
||||
return eErr == oErr
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// check both fileInfo's have same number of versions, if not skip
|
||||
// the `other` entry.
|
||||
if eFi.NumVersions != oFi.NumVersions {
|
||||
return false
|
||||
}
|
||||
|
||||
return eFi.ModTime.Equal(oFi.ModTime) && eFi.Size == oFi.Size && eFi.VersionID == oFi.VersionID
|
||||
}
|
||||
|
||||
// resolveEntries returns if the entries match by comparing their latest version fileinfo.
|
||||
func resolveEntries(a, b *metaCacheEntry, bucket string) *metaCacheEntry {
|
||||
if b == nil {
|
||||
return a
|
||||
}
|
||||
if a == nil {
|
||||
return b
|
||||
}
|
||||
|
||||
aFi, err := a.fileInfo(bucket)
|
||||
if err != nil {
|
||||
return b
|
||||
}
|
||||
bFi, err := b.fileInfo(bucket)
|
||||
if err != nil {
|
||||
return a
|
||||
}
|
||||
|
||||
if aFi.NumVersions == bFi.NumVersions {
|
||||
if aFi.ModTime.Equal(bFi.ModTime) {
|
||||
return a
|
||||
if len(eVers.versions) != len(oVers.versions) {
|
||||
eTime := eVers.latestModtime()
|
||||
oTime := oVers.latestModtime()
|
||||
if !eTime.Equal(oTime) {
|
||||
if eTime.After(oTime) {
|
||||
return e, false
|
||||
}
|
||||
return other, false
|
||||
}
|
||||
if aFi.ModTime.After(bFi.ModTime) {
|
||||
return a
|
||||
// Tiebreak on version count.
|
||||
if len(eVers.versions) > len(oVers.versions) {
|
||||
return e, false
|
||||
}
|
||||
return b
|
||||
return other, false
|
||||
}
|
||||
|
||||
if bFi.NumVersions > aFi.NumVersions {
|
||||
return b
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
return a
|
||||
if eVer.header.sortsBefore(oVer.header) {
|
||||
return e, false
|
||||
}
|
||||
return other, false
|
||||
}
|
||||
}
|
||||
// If we match, return e
|
||||
if prefer == nil {
|
||||
prefer = e
|
||||
}
|
||||
return prefer, true
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (e *metaCacheEntry) isLatestDeletemarker() bool {
|
||||
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) {
|
||||
return false
|
||||
@@ -152,8 +167,8 @@ func (e *metaCacheEntry) isLatestDeletemarker() bool {
|
||||
return meta.IsLatestDeleteMarker()
|
||||
}
|
||||
// Fall back...
|
||||
var xlMeta xlMetaV2
|
||||
if err := xlMeta.Load(e.metadata); err != nil || len(xlMeta.versions) == 0 {
|
||||
xlMeta, err := e.xlmeta()
|
||||
if err != nil || len(xlMeta.versions) == 0 {
|
||||
return true
|
||||
}
|
||||
return xlMeta.versions[0].header.Type == DeleteType
|
||||
@@ -162,24 +177,37 @@ func (e *metaCacheEntry) isLatestDeletemarker() bool {
|
||||
// fileInfo returns the decoded metadata.
|
||||
// If entry is a directory it is returned as that.
|
||||
// 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() {
|
||||
return &FileInfo{
|
||||
return FileInfo{
|
||||
Volume: bucket,
|
||||
Name: e.name,
|
||||
Mode: uint32(os.ModeDir),
|
||||
}, 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 len(e.metadata) == 0 {
|
||||
// only happens if the entry is not found.
|
||||
return nil, errFileNotFound
|
||||
}
|
||||
fi, err := getFileInfo(e.metadata, bucket, e.name, "", false)
|
||||
var xl xlMetaV2
|
||||
err := xl.LoadOrConvert(e.metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.cached = &fi
|
||||
e.cached = &xl
|
||||
}
|
||||
return e.cached, nil
|
||||
}
|
||||
@@ -200,6 +228,7 @@ func (e *metaCacheEntry) fileInfoVersions(bucket string) (FileInfoVersions, erro
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
// Too small gains to reuse cache here.
|
||||
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'.
|
||||
objQuorum int // Number of disks needed for an object to 'exist'.
|
||||
bucket string // Name of the bucket. Used for generating cached fileinfo.
|
||||
strict bool // Versions must match exactly, including all metadata.
|
||||
|
||||
// Reusable slice for resolution
|
||||
candidates []struct {
|
||||
n int
|
||||
e *metaCacheEntry
|
||||
}
|
||||
candidates [][]xlMetaV2ShallowVersion
|
||||
}
|
||||
|
||||
// resolve multiple entries.
|
||||
// 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) {
|
||||
if len(m) == 0 {
|
||||
return nil, false
|
||||
@@ -257,14 +285,14 @@ func (m metaCacheEntries) resolve(r *metadataResolutionParams) (selected *metaCa
|
||||
|
||||
dirExists := 0
|
||||
if cap(r.candidates) < len(m) {
|
||||
r.candidates = make([]struct {
|
||||
n int
|
||||
e *metaCacheEntry
|
||||
}, 0, len(m))
|
||||
r.candidates = make([][]xlMetaV2ShallowVersion, 0, len(m))
|
||||
}
|
||||
r.candidates = r.candidates[:0]
|
||||
objsAgree := 0
|
||||
objsValid := 0
|
||||
for i := range m {
|
||||
entry := &m[i]
|
||||
// Empty entry
|
||||
if entry.name == "" {
|
||||
continue
|
||||
}
|
||||
@@ -275,72 +303,67 @@ func (m metaCacheEntries) resolve(r *metadataResolutionParams) (selected *metaCa
|
||||
continue
|
||||
}
|
||||
|
||||
// Get new entry metadata
|
||||
if _, err := entry.fileInfo(r.bucket); err != nil {
|
||||
// Get new entry metadata,
|
||||
// shallow decode.
|
||||
xl, err := entry.xlmeta()
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
}
|
||||
objsValid++
|
||||
|
||||
found := false
|
||||
for i, c := range r.candidates {
|
||||
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
|
||||
}
|
||||
// Add all valid to candidates.
|
||||
r.candidates = append(r.candidates, xl.versions)
|
||||
|
||||
switch len(r.candidates) {
|
||||
case 0:
|
||||
// We select the first object we find as a candidate and see if all match that.
|
||||
// This is to quickly identify if all agree.
|
||||
if selected == nil {
|
||||
return nil, false
|
||||
selected = entry
|
||||
objsAgree = 1
|
||||
continue
|
||||
}
|
||||
if !selected.isDir() || dirExists < r.dirQuorum {
|
||||
return nil, false
|
||||
// Names match, check meta...
|
||||
if prefer, ok := entry.matches(selected, r.strict); ok {
|
||||
selected = prefer
|
||||
objsAgree++
|
||||
continue
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
// 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 selected, true
|
||||
}
|
||||
|
||||
// firstFound returns the first found and the number of set entries.
|
||||
|
||||
Reference in New Issue
Block a user