// Copyright (c) 2015-2021 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
package cmd
import (
"context"
"errors"
"fmt"
"io"
"os"
pathutil "path"
"strings"
"sync"
"time"
"github.com/minio/minio/internal/grid"
xioutil "github.com/minio/minio/internal/ioutil"
)
func renameAllBucketMetacache(epPath string) error {
// Rename all previous `.minio.sys/buckets//.metacache` to
// to `.minio.sys/tmp/` for deletion.
return readDirFn(pathJoin(epPath, minioMetaBucket, bucketMetaPrefix), func(name string, typ os.FileMode) error {
if typ == os.ModeDir {
tmpMetacacheOld := pathutil.Join(epPath, minioMetaTmpDeletedBucket, mustGetUUID())
if err := renameAll(pathJoin(epPath, minioMetaBucket, metacachePrefixForID(name, slashSeparator)),
tmpMetacacheOld, epPath); err != nil && err != errFileNotFound {
return fmt.Errorf("unable to rename (%s -> %s) %w",
pathJoin(epPath, minioMetaBucket+metacachePrefixForID(minioMetaBucket, slashSeparator)),
tmpMetacacheOld,
osErrToFileErr(err))
}
}
return nil
})
}
// listPath will return the requested entries.
// If no more entries are in the listing io.EOF is returned,
// otherwise nil or an unexpected error is returned.
// The listPathOptions given will be checked and modified internally.
// Required important fields are Bucket, Prefix, Separator.
// Other important fields are Limit, Marker.
// List ID always derived from the Marker.
func (z *erasureServerPools) listPath(ctx context.Context, o *listPathOptions) (entries metaCacheEntriesSorted, err error) {
if err := checkListObjsArgs(ctx, o.Bucket, o.Prefix, o.Marker); err != nil {
return entries, err
}
// Marker points to before the prefix, just ignore it.
if o.Marker < o.Prefix {
o.Marker = ""
}
// Marker is set validate pre-condition.
if o.Marker != "" && o.Prefix != "" {
// Marker not common with prefix is not implemented. Send an empty response
if !HasPrefix(o.Marker, o.Prefix) {
return entries, z.listPathShortcutEOF(ctx, o.Bucket)
}
}
// With max keys of zero we have reached eof, return right here.
if o.Limit == 0 {
return entries, z.listPathShortcutEOF(ctx, o.Bucket)
}
// For delimiter and prefix as '/' we do not list anything at all
// along // with the prefix. On a flat namespace with 'prefix'
// as '/' we don't have any entries, since all the keys are
// of form 'keyName/...'
if strings.HasPrefix(o.Prefix, SlashSeparator) {
return entries, z.listPathShortcutEOF(ctx, o.Bucket)
}
// If delimiter is slashSeparator we must return directories of
// the non-recursive scan unless explicitly requested.
o.IncludeDirectories = o.Separator == slashSeparator
if (o.Separator == slashSeparator || o.Separator == "") && !o.Recursive {
o.Recursive = o.Separator != slashSeparator
o.Separator = slashSeparator
} else {
// Default is recursive, if delimiter is set then list non recursive.
o.Recursive = true
}
// Decode and get the optional list id from the marker.
o.parseMarker()
if o.BaseDir == "" {
o.BaseDir = baseDirFromPrefix(o.Prefix)
}
o.Transient = o.Transient || isReservedOrInvalidBucket(o.Bucket, false)
o.SetFilter()
if o.Transient {
o.Create = false
}
// We have 2 cases:
// 1) Cold listing, just list.
// 2) Returning, but with no id. Start async listing.
// 3) Returning, with ID, stream from list.
//
// If we don't have a list id we must ask the server if it has a cache or create a new.
if o.ID != "" && !o.Transient {
// Create or ping with handout...
rpc := globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix))
var c *metacache
if rpc == nil {
resp := localMetacacheMgr.getBucket(ctx, o.Bucket).findCache(*o)
c = &resp
} else {
rctx, cancel := context.WithTimeout(ctx, 5*time.Second)
c, err = rpc.GetMetacacheListing(rctx, *o)
cancel()
}
if err != nil {
if errors.Is(err, context.Canceled) {
// Context is canceled, return at once.
// request canceled, no entries to return
return entries, io.EOF
}
if !IsErr(err, context.DeadlineExceeded, grid.ErrDisconnected) {
// Report error once per bucket, but continue listing.x
storageLogOnceIf(ctx, err, "GetMetacacheListing:"+o.Bucket)
}
o.Transient = true
o.Create = false
o.ID = mustGetUUID()
} else {
if c.fileNotFound {
// No cache found, no entries found.
return entries, io.EOF
}
if c.status == scanStateError || c.status == scanStateNone {
o.ID = ""
o.Create = false
o.debugln("scan status", c.status, " - waiting a roundtrip to create")
} else {
// Continue listing
o.ID = c.id
go c.keepAlive(ctx, rpc)
}
}
}
if o.ID != "" && !o.Transient {
// We have an existing list ID, continue streaming.
if o.Create {
o.debugln("Creating", o)
entries, err = z.listAndSave(ctx, o)
if err == nil || err == io.EOF {
return entries, err
}
entries.truncate(0)
} else {
if o.pool < len(z.serverPools) && o.set < len(z.serverPools[o.pool].sets) {
o.debugln("Resuming", o)
entries, err = z.serverPools[o.pool].sets[o.set].streamMetadataParts(ctx, *o)
entries.reuse = true // We read from stream and are not sharing results.
if err == nil {
return entries, nil
}
} else {
err = fmt.Errorf("invalid pool/set")
o.pool, o.set = 0, 0
}
}
if IsErr(err, []error{
nil,
context.Canceled,
context.DeadlineExceeded,
// io.EOF is expected and should be returned but no need to log it.
io.EOF,
}...) {
// Expected good errors we don't need to return error.
return entries, err
}
entries.truncate(0)
rpc := globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix))
go func(o listPathOptions) {
if rpc != nil {
ctx, cancel := context.WithTimeout(GlobalContext, 5*time.Second)
defer cancel()
c, err := rpc.GetMetacacheListing(ctx, o)
if err == nil {
c.error = "no longer used"
c.status = scanStateError
rpc.UpdateMetacacheListing(ctx, *c)
}
}
}(*o)
o.ID = ""
}
if contextCanceled(ctx) {
return entries, ctx.Err()
}
// Do listing in-place.
// Create output for our results.
// Create filter for results.
o.debugln("Raw List", o)
filterCh := make(chan metaCacheEntry, o.Limit)
listCtx, cancelList := context.WithCancel(ctx)
filteredResults := o.gatherResults(listCtx, filterCh)
var wg sync.WaitGroup
wg.Add(1)
var listErr error
go func(o listPathOptions) {
defer wg.Done()
if o.Lifecycle == nil {
// No filtering ahead, ask drives to stop
// listing exactly at a specific limit.
o.StopDiskAtLimit = true
}
listErr = z.listMerged(listCtx, o, filterCh)
o.debugln("listMerged returned with", listErr)
}(*o)
entries, err = filteredResults()
cancelList()
wg.Wait()
if listErr != nil && !errors.Is(listErr, context.Canceled) {
return entries, listErr
}
entries.reuse = true
truncated := entries.len() > o.Limit || err == nil
entries.truncate(o.Limit)
if !o.Transient && truncated {
if o.ID == "" {
entries.listID = mustGetUUID()
} else {
entries.listID = o.ID
}
}
if !truncated {
return entries, io.EOF
}
return entries, nil
}
// listPathShortcutEOF verifies bucket existence for shortcuts that return
// without consulting the storage layer. Keep this check out of the normal
// listing path since GetBucketInfo fans out to peers and disks.
func (z *erasureServerPools) listPathShortcutEOF(ctx context.Context, bucket string) error {
if _, err := z.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
return err
}
return io.EOF
}
// listingRLock covers the lock domains used by normal same-key mutations.
// Multi-pool writes use the pools lock. In a single pool, DELETE/COPY use the
// first set while PUT and metadata updates use the destination set, so recovery
// takes both in that order. No writer routing or wire protocol changes here.
func (z *erasureServerPools) listingRLock(ctx context.Context, bucket, object string, source *erasureObjects) (LockContext, func(), error) {
firstSet := z.serverPools[0].sets[0]
first := z.NewNSLock(bucket, object)
firstCtx, err := first.GetRLock(ctx, globalListingOperationTimeout)
if err != nil {
return LockContext{}, nil, err
}
if !z.SinglePool() || source == firstSet || (!source.nsMutex.isDistErasure && source.nsMutex == firstSet.nsMutex) {
return firstCtx, func() { first.RUnlock(firstCtx) }, nil
}
// Shared nsMutex pointers do not prove shared distributed locker sets.
second := source.NewNSLock(bucket, object)
secondCtx, err := second.GetRLock(firstCtx.Context(), globalListingOperationTimeout)
if err != nil {
first.RUnlock(firstCtx)
return LockContext{}, nil, err
}
return secondCtx, func() {
second.RUnlock(secondCtx)
first.RUnlock(firstCtx)
}, nil
}
// resolveListingGroup recovers a complete same-name group, never a private
// per-set contribution. A strictly absent source requires current reads from
// every pool, since another pool's earlier walk may predate a migration copy.
func (z *erasureServerPools) resolveListingGroup(ctx context.Context, bucket string, group []*metaCacheEntry) (*metaCacheEntry, error) {
var source *erasureObjects
for _, entry := range group {
if entry.listingSet != nil {
source = entry.listingSet
break
}
}
if source == nil {
return nil, errInvalidArgument
}
// Recovery consumes this group. Return reusable walk buffers only after
// their contents have been copied into the newly encoded result.
defer func() {
for _, entry := range group {
if entry.reusable {
metaDataPoolPut(entry.metadata)
}
}
}()
name := group[0].name
object := encodeDirObject(name)
if z.SinglePool() {
target := z.serverPools[0].getHashedSet(object)
for _, entry := range group {
if entry.listingSet != nil && entry.listingSet != target {
// Unexpected placement is not evidence of absence at the
// current write target. Do not extend recovery to arbitrary sets.
return nil, listingReadQuorumError(bucket, object)
}
}
}
type readResult struct {
fi FileInfo
absent bool
}
reads := make(map[*erasureObjects]readResult)
currentOnly := false
var current *FileInfo
err := func() error {
lockCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
lkctx, unlock, err := z.listingRLock(lockCtx, bucket, object, source)
if err != nil {
return err
}
defer unlock()
read := func(set *erasureObjects) (readResult, error) {
if result, ok := reads[set]; ok {
return result, nil
}
fi, absent, err := set.readListingFileInfo(lkctx.Context(), bucket, object)
result := readResult{fi: fi, absent: absent}
if err == nil {
reads[set] = result
}
return result, err
}
for _, entry := range group {
if entry.listingSet == nil {
continue
}
result, err := read(entry.listingSet)
if err != nil {
return err
}
currentOnly = currentOnly || result.absent
}
if !currentOnly {
return lkctx.Context().Err()
}
// Migration publishes the replacement through the pooled write lock
// before source cleanup. While this read lock is held, new publication
// cannot pass these reads; any concurrent cleanup already has a copy.
// Unknown pools still fail rather than being counted as absent.
for _, pool := range z.serverPools {
result, err := read(pool.getHashedSet(object))
if err != nil {
return err
}
if result.absent {
continue
}
// Match latest GET: newest ModTime, then lowest pool index.
if current == nil || result.fi.ModTime.After(current.ModTime) {
fi := result.fi
current = &fi
}
}
return lkctx.Context().Err()
}()
if err != nil {
return nil, err
}
// All FileInfo values above are owned decoded results. Encoding and merge
// allocation happen after RUnlock, as do all stream sends and cache RPCs.
encode := func(fi FileInfo) (*metaCacheEntry, error) {
xl := &xlMetaV2{}
if err := xl.AddVersion(fi); err != nil {
return nil, listingReadQuorumError(bucket, object)
}
entry := &metaCacheEntry{name: name, cached: xl, reusable: true}
var err error
entry.metadata, err = xl.AppendTo(metaDataPoolGet())
return entry, err
}
if currentOnly {
if current == nil {
return nil, nil
}
// Replace the whole group: no stale live copy or marker can override
// this current global decision.
return encode(*current)
}
versions := make([][]xlMetaV2ShallowVersion, 0, len(group))
for _, entry := range group {
if entry.listingSet != nil {
fi := reads[entry.listingSet].fi
xl := &xlMetaV2{}
if err := xl.AddVersion(fi); err != nil {
return nil, listingReadQuorumError(bucket, object)
}
versions = append(versions, xl.versions)
continue
}
xl, err := entry.xlmeta()
if err != nil {
return nil, listingReadQuorumError(bucket, object)
}
versions = append(versions, xl.versions)
}
// Preserve the existing cross-pool merge rule for recovered live metadata
// and delete markers when no source was strictly absent.
xl := &xlMetaV2{versions: mergeXLV2Versions(1, true, 0, versions...)}
// Do not retain shallow versions referencing reusable input buffers.
resolved := &metaCacheEntry{name: name, reusable: true}
resolved.metadata, err = xl.AppendTo(metaDataPoolGet())
return resolved, err
}
// listMerged will list across all sets and return a merged results stream.
// The result channel is closed when no more results are expected.
func (z *erasureServerPools) listMerged(ctx context.Context, o listPathOptions, results chan<- metaCacheEntry) error {
var mu sync.Mutex
var wg sync.WaitGroup
var errs []error
allAtEOF := true
var inputs []chan metaCacheEntry
mu.Lock()
// Ask all sets and merge entries.
listCtx, cancelList := context.WithCancel(ctx)
defer cancelList()
for _, pool := range z.serverPools {
for _, set := range pool.sets {
wg.Add(1)
innerResults := make(chan metaCacheEntry, 100)
inputs = append(inputs, innerResults)
go func(i int, set *erasureObjects) {
defer wg.Done()
err := set.listPath(listCtx, o, innerResults)
mu.Lock()
if err == nil {
allAtEOF = false
}
errs[i] = err
mu.Unlock()
// The merger must observe the terminal error before treating
// this stream as exhausted and advancing to another pool.
xioutil.SafeClose(innerResults)
}(len(errs), set)
errs = append(errs, nil)
}
}
mu.Unlock()
// Gather results to a single channel.
// Quorum is one since we are merging across sets.
err := mergeEntryChannelsWithResolver(ctx, inputs, results, 1, func(i int) error {
mu.Lock()
err := errs[i]
mu.Unlock()
// Keep the existing aggregate handling of absent paths and buckets.
if errors.Is(err, io.EOF) || isAllNotFound([]error{err}) {
return nil
}
return err
}, func(group []*metaCacheEntry) (*metaCacheEntry, error) {
return z.resolveListingGroup(ctx, o.Bucket, group)
})
cancelList()
wg.Wait()
// we should return 'errs' from per disk
if isAllNotFound(errs) {
if isAllVolumeNotFound(errs) {
return errVolumeNotFound
}
return nil
}
if err != nil {
return err
}
if contextCanceled(ctx) {
return ctx.Err()
}
for _, err := range errs {
if errors.Is(err, io.EOF) {
continue
}
if err == nil || contextCanceled(ctx) || errors.Is(err, context.Canceled) {
allAtEOF = false
continue
}
storageLogIf(ctx, err)
return err
}
if allAtEOF {
return io.EOF
}
return nil
}
// triggerExpiryAndRepl applies lifecycle and replication actions on the listing
// It returns true if the listing is non-versioned and the given object is expired.
func triggerExpiryAndRepl(ctx context.Context, o listPathOptions, obj metaCacheEntry) (skip bool) {
versioned := o.Versioning != nil && o.Versioning.Versioned(obj.name)
// skip latest object from listing only for regular
// listObjects calls, versioned based listing cannot
// filter out between versions 'obj' cannot be truncated
// in such a manner, so look for skipping an object only
// for regular ListObjects() call only.
if !o.Versioned && !o.V1 {
fi, err := obj.fileInfo(o.Bucket)
if err != nil {
return skip
}
objInfo := fi.ToObjectInfo(o.Bucket, obj.name, versioned)
if o.Lifecycle != nil {
act := evalActionFromLifecycle(ctx, *o.Lifecycle, o.Retention, o.Replication.Config, objInfo).Action
skip = act.Delete() && !act.DeleteRestored()
}
}
fiv, err := obj.fileInfoVersions(o.Bucket)
if err != nil {
return skip
}
// Expire all versions if needed, if not attempt to queue for replication.
for _, version := range fiv.Versions {
objInfo := version.ToObjectInfo(o.Bucket, obj.name, versioned)
if o.Lifecycle != nil {
evt := evalActionFromLifecycle(ctx, *o.Lifecycle, o.Retention, o.Replication.Config, objInfo)
if evt.Action.Delete() {
globalExpiryState.enqueueByDays(objInfo, evt, lcEventSrc_s3ListObjects)
if !evt.Action.DeleteRestored() {
continue
} // queue version for replication upon expired restored copies if needed.
}
}
queueReplicationHeal(ctx, o.Bucket, objInfo, o.Replication, 0)
}
return skip
}
func (z *erasureServerPools) listAndSave(ctx context.Context, o *listPathOptions) (entries metaCacheEntriesSorted, err error) {
// Use ID as the object name...
o.pool = z.getAvailablePoolIdx(ctx, minioMetaBucket, o.ID, 10<<20)
if o.pool < 0 {
// No space or similar, don't persist the listing.
o.pool = 0
o.Create = false
o.ID = ""
o.Transient = true
return entries, errDiskFull
}
o.set = z.serverPools[o.pool].getHashedSetIndex(o.ID)
saver := z.serverPools[o.pool].sets[o.set]
// Disconnect from call above, but cancel on exit.
listCtx, cancel := context.WithCancel(GlobalContext)
saveCh := make(chan metaCacheEntry, metacacheBlockSize)
inCh := make(chan metaCacheEntry, metacacheBlockSize)
outCh := make(chan metaCacheEntry, o.Limit)
filteredResults := o.gatherResults(ctx, outCh)
mc := o.newMetacache()
meta := metaCacheRPC{meta: &mc, cancel: cancel, rpc: globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix)), o: *o}
// Save listing...
go func() {
if err := saver.saveMetaCacheStream(listCtx, &meta, saveCh); err != nil {
meta.setErr(err.Error())
}
cancel()
}()
// Publish the typed terminal result before either consumer observes EOF.
// mergeEntryChannels closes inCh before listMerged has inspected set errors.
listingDone := make(chan struct{})
var cacheErrorDone chan struct{}
var listingErr error
go func(o listPathOptions) {
listingErr = z.listMerged(listCtx, o, inCh)
failed := listingErr != nil && !errors.Is(listingErr, io.EOF)
if failed {
cacheErrorDone = make(chan struct{})
}
close(listingDone)
if failed {
meta.setErr(listingErr.Error())
cancel()
close(cacheErrorDone)
}
o.debugln("listAndSave: listing", o.ID, "finished with ", listingErr)
}(*o)
// Keep track of when we return since we no longer have to send entries to output.
var funcReturned bool
var funcReturnedMu sync.Mutex
defer func() {
funcReturnedMu.Lock()
funcReturned = true
funcReturnedMu.Unlock()
}()
// Write listing to results and saver.
go func() {
var returned bool
for entry := range inCh {
if o.shouldSkip(ctx, entry) {
continue
}
if !returned {
funcReturnedMu.Lock()
returned = funcReturned
funcReturnedMu.Unlock()
outCh <- entry
if returned {
xioutil.SafeClose(outCh)
}
}
entry.reusable = returned
saveCh <- entry
}
// The typed scan result is enough to finish the HTTP response.
<-listingDone
if !returned {
xioutil.SafeClose(outCh)
}
// Closing saveCh writes an end-of-stream cache block. On failure,
// wait until the error is recorded and the writer is canceled.
if cacheErrorDone != nil {
<-cacheErrorDone
}
xioutil.SafeClose(saveCh)
}()
entries, err = filteredResults()
if err == nil || errors.Is(err, io.EOF) {
// Full pages may return before the scan ends. A final page has
// crossed the completion barrier above and must preserve scan errors.
select {
case <-listingDone:
if listingErr != nil && !errors.Is(listingErr, io.EOF) {
return entries, listingErr
}
default:
}
if cacheErr := meta.getErr(); cacheErr != nil {
return entries, cacheErr
}
}
return entries, err
}