github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/data-scanner.go (about)

     1  // Copyright (c) 2015-2021 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"context"
    22  	"encoding/binary"
    23  	"encoding/json"
    24  	"errors"
    25  	"fmt"
    26  	"io/fs"
    27  	"math"
    28  	"math/rand"
    29  	"os"
    30  	"path"
    31  	"strconv"
    32  	"strings"
    33  	"sync"
    34  	"time"
    35  
    36  	"github.com/minio/madmin-go/v3"
    37  	"github.com/minio/minio/internal/bucket/lifecycle"
    38  	"github.com/minio/minio/internal/bucket/object/lock"
    39  	"github.com/minio/minio/internal/bucket/replication"
    40  	"github.com/minio/minio/internal/color"
    41  	"github.com/minio/minio/internal/config/heal"
    42  	"github.com/minio/minio/internal/event"
    43  	xioutil "github.com/minio/minio/internal/ioutil"
    44  	"github.com/minio/minio/internal/logger"
    45  	"github.com/minio/pkg/v2/console"
    46  	uatomic "go.uber.org/atomic"
    47  )
    48  
    49  const (
    50  	dataScannerSleepPerFolder        = time.Millisecond                 // Time to wait between folders.
    51  	dataUsageUpdateDirCycles         = 16                               // Visit all folders every n cycles.
    52  	dataScannerCompactLeastObject    = 500                              // Compact when there is less than this many objects in a branch.
    53  	dataScannerCompactAtChildren     = 10000                            // Compact when there are this many children in a branch.
    54  	dataScannerCompactAtFolders      = dataScannerCompactAtChildren / 4 // Compact when this many subfolders in a single folder.
    55  	dataScannerForceCompactAtFolders = 1_000_000                        // Compact when this many subfolders in a single folder (even top level).
    56  	dataScannerStartDelay            = 1 * time.Minute                  // Time to wait on startup and between cycles.
    57  
    58  	healDeleteDangling   = true
    59  	healObjectSelectProb = 1024 // Overall probability of a file being scanned; one in n.
    60  )
    61  
    62  var (
    63  	globalHealConfig heal.Config
    64  
    65  	// Sleeper values are updated when config is loaded.
    66  	scannerSleeper              = newDynamicSleeper(2, time.Second, true) // Keep defaults same as config defaults
    67  	scannerCycle                = uatomic.NewDuration(dataScannerStartDelay)
    68  	scannerIdleMode             = uatomic.NewInt32(0) // default is throttled when idle
    69  	scannerExcessObjectVersions = uatomic.NewInt64(100)
    70  	scannerExcessFolders        = uatomic.NewInt64(50000)
    71  )
    72  
    73  // initDataScanner will start the scanner in the background.
    74  func initDataScanner(ctx context.Context, objAPI ObjectLayer) {
    75  	go func() {
    76  		r := rand.New(rand.NewSource(time.Now().UnixNano()))
    77  		// Run the data scanner in a loop
    78  		for {
    79  			runDataScanner(ctx, objAPI)
    80  			duration := time.Duration(r.Float64() * float64(scannerCycle.Load()))
    81  			if duration < time.Second {
    82  				// Make sure to sleep at least a second to avoid high CPU ticks.
    83  				duration = time.Second
    84  			}
    85  			time.Sleep(duration)
    86  		}
    87  	}()
    88  }
    89  
    90  func getCycleScanMode(currentCycle, bitrotStartCycle uint64, bitrotStartTime time.Time) madmin.HealScanMode {
    91  	bitrotCycle := globalHealConfig.BitrotScanCycle()
    92  	switch bitrotCycle {
    93  	case -1:
    94  		return madmin.HealNormalScan
    95  	case 0:
    96  		return madmin.HealDeepScan
    97  	}
    98  
    99  	if currentCycle-bitrotStartCycle < healObjectSelectProb {
   100  		return madmin.HealDeepScan
   101  	}
   102  
   103  	if time.Since(bitrotStartTime) > bitrotCycle {
   104  		return madmin.HealDeepScan
   105  	}
   106  
   107  	return madmin.HealNormalScan
   108  }
   109  
   110  type backgroundHealInfo struct {
   111  	BitrotStartTime  time.Time           `json:"bitrotStartTime"`
   112  	BitrotStartCycle uint64              `json:"bitrotStartCycle"`
   113  	CurrentScanMode  madmin.HealScanMode `json:"currentScanMode"`
   114  }
   115  
   116  func readBackgroundHealInfo(ctx context.Context, objAPI ObjectLayer) backgroundHealInfo {
   117  	if globalIsErasureSD {
   118  		return backgroundHealInfo{}
   119  	}
   120  
   121  	// Get last healing information
   122  	buf, err := readConfig(ctx, objAPI, backgroundHealInfoPath)
   123  	if err != nil {
   124  		if !errors.Is(err, errConfigNotFound) {
   125  			logger.LogOnceIf(ctx, err, backgroundHealInfoPath)
   126  		}
   127  		return backgroundHealInfo{}
   128  	}
   129  	var info backgroundHealInfo
   130  	if err = json.Unmarshal(buf, &info); err != nil {
   131  		logger.LogOnceIf(ctx, err, backgroundHealInfoPath)
   132  	}
   133  	return info
   134  }
   135  
   136  func saveBackgroundHealInfo(ctx context.Context, objAPI ObjectLayer, info backgroundHealInfo) {
   137  	if globalIsErasureSD {
   138  		return
   139  	}
   140  
   141  	b, err := json.Marshal(info)
   142  	if err != nil {
   143  		logger.LogIf(ctx, err)
   144  		return
   145  	}
   146  	// Get last healing information
   147  	err = saveConfig(ctx, objAPI, backgroundHealInfoPath, b)
   148  	if err != nil {
   149  		logger.LogIf(ctx, err)
   150  	}
   151  }
   152  
   153  // runDataScanner will start a data scanner.
   154  // The function will block until the context is canceled.
   155  // There should only ever be one scanner running per cluster.
   156  func runDataScanner(ctx context.Context, objAPI ObjectLayer) {
   157  	ctx, cancel := globalLeaderLock.GetLock(ctx)
   158  	defer cancel()
   159  
   160  	// Load current bloom cycle
   161  	var cycleInfo currentScannerCycle
   162  
   163  	buf, _ := readConfig(ctx, objAPI, dataUsageBloomNamePath)
   164  	if len(buf) == 8 {
   165  		cycleInfo.next = binary.LittleEndian.Uint64(buf)
   166  	} else if len(buf) > 8 {
   167  		cycleInfo.next = binary.LittleEndian.Uint64(buf[:8])
   168  		buf = buf[8:]
   169  		_, err := cycleInfo.UnmarshalMsg(buf)
   170  		logger.LogIf(ctx, err)
   171  	}
   172  
   173  	scannerTimer := time.NewTimer(scannerCycle.Load())
   174  	defer scannerTimer.Stop()
   175  	defer globalScannerMetrics.setCycle(nil)
   176  
   177  	for {
   178  		select {
   179  		case <-ctx.Done():
   180  			return
   181  		case <-scannerTimer.C:
   182  			// Reset the timer for next cycle.
   183  			// If scanner takes longer we start at once.
   184  			scannerTimer.Reset(scannerCycle.Load())
   185  
   186  			stopFn := globalScannerMetrics.log(scannerMetricScanCycle)
   187  			cycleInfo.current = cycleInfo.next
   188  			cycleInfo.started = time.Now()
   189  			globalScannerMetrics.setCycle(&cycleInfo)
   190  
   191  			bgHealInfo := readBackgroundHealInfo(ctx, objAPI)
   192  			scanMode := getCycleScanMode(cycleInfo.current, bgHealInfo.BitrotStartCycle, bgHealInfo.BitrotStartTime)
   193  			if bgHealInfo.CurrentScanMode != scanMode {
   194  				newHealInfo := bgHealInfo
   195  				newHealInfo.CurrentScanMode = scanMode
   196  				if scanMode == madmin.HealDeepScan {
   197  					newHealInfo.BitrotStartTime = time.Now().UTC()
   198  					newHealInfo.BitrotStartCycle = cycleInfo.current
   199  				}
   200  				saveBackgroundHealInfo(ctx, objAPI, newHealInfo)
   201  			}
   202  
   203  			// Wait before starting next cycle and wait on startup.
   204  			results := make(chan DataUsageInfo, 1)
   205  			go storeDataUsageInBackend(ctx, objAPI, results)
   206  			err := objAPI.NSScanner(ctx, results, uint32(cycleInfo.current), scanMode)
   207  			logger.LogOnceIf(ctx, err, "ns-scanner")
   208  			res := map[string]string{"cycle": strconv.FormatUint(cycleInfo.current, 10)}
   209  			if err != nil {
   210  				res["error"] = err.Error()
   211  			}
   212  			stopFn(res)
   213  			if err == nil {
   214  				// Store new cycle...
   215  				cycleInfo.next++
   216  				cycleInfo.current = 0
   217  				cycleInfo.cycleCompleted = append(cycleInfo.cycleCompleted, time.Now())
   218  				if len(cycleInfo.cycleCompleted) > dataUsageUpdateDirCycles {
   219  					cycleInfo.cycleCompleted = cycleInfo.cycleCompleted[len(cycleInfo.cycleCompleted)-dataUsageUpdateDirCycles:]
   220  				}
   221  				globalScannerMetrics.setCycle(&cycleInfo)
   222  				tmp := make([]byte, 8, 8+cycleInfo.Msgsize())
   223  				// Cycle for backward compat.
   224  				binary.LittleEndian.PutUint64(tmp, cycleInfo.next)
   225  				tmp, _ = cycleInfo.MarshalMsg(tmp)
   226  				err = saveConfig(ctx, objAPI, dataUsageBloomNamePath, tmp)
   227  				logger.LogOnceIf(ctx, err, dataUsageBloomNamePath)
   228  			}
   229  		}
   230  	}
   231  }
   232  
   233  type cachedFolder struct {
   234  	name              string
   235  	parent            *dataUsageHash
   236  	objectHealProbDiv uint32
   237  }
   238  
   239  type folderScanner struct {
   240  	root        string
   241  	getSize     getSizeFn
   242  	oldCache    dataUsageCache
   243  	newCache    dataUsageCache
   244  	updateCache dataUsageCache
   245  
   246  	dataUsageScannerDebug bool
   247  	healObjectSelect      uint32 // Do a heal check on an object once every n cycles. Must divide into healFolderInclude
   248  	scanMode              madmin.HealScanMode
   249  
   250  	weSleep func() bool
   251  
   252  	disks       []StorageAPI
   253  	disksQuorum int
   254  
   255  	// If set updates will be sent regularly to this channel.
   256  	// Will not be closed when returned.
   257  	updates    chan<- dataUsageEntry
   258  	lastUpdate time.Time
   259  
   260  	// updateCurrentPath should be called whenever a new path is scanned.
   261  	updateCurrentPath func(string)
   262  }
   263  
   264  // Cache structure and compaction:
   265  //
   266  // A cache structure will be kept with a tree of usages.
   267  // The cache is a tree structure where each keeps track of its children.
   268  //
   269  // An uncompacted branch contains a count of the files only directly at the
   270  // branch level, and contains link to children branches or leaves.
   271  //
   272  // The leaves are "compacted" based on a number of properties.
   273  // A compacted leaf contains the totals of all files beneath it.
   274  //
   275  // A leaf is only scanned once every dataUsageUpdateDirCycles,
   276  // rarer if the bloom filter for the path is clean and no lifecycles are applied.
   277  // Skipped leaves have their totals transferred from the previous cycle.
   278  //
   279  // When selected there is a one in healObjectSelectProb that any object will be chosen for heal scan.
   280  //
   281  // Compaction happens when either:
   282  //
   283  // 1) The folder (and subfolders) contains less than dataScannerCompactLeastObject objects.
   284  // 2) The folder itself contains more than dataScannerCompactAtFolders folders.
   285  // 3) The folder only contains objects and no subfolders.
   286  //
   287  // A bucket root will never be compacted.
   288  //
   289  // Furthermore if a has more than dataScannerCompactAtChildren recursive children (uncompacted folders)
   290  // the tree will be recursively scanned and the branches with the least number of objects will be
   291  // compacted until the limit is reached.
   292  //
   293  // This ensures that any branch will never contain an unreasonable amount of other branches,
   294  // and also that small branches with few objects don't take up unreasonable amounts of space.
   295  // This keeps the cache size at a reasonable size for all buckets.
   296  //
   297  // Whenever a branch is scanned, it is assumed that it will be un-compacted
   298  // before it hits any of the above limits.
   299  // This will make the branch rebalance itself when scanned if the distribution of objects has changed.
   300  
   301  // scanDataFolder will scanner the basepath+cache.Info.Name and return an updated cache.
   302  // The returned cache will always be valid, but may not be updated from the existing.
   303  // Before each operation sleepDuration is called which can be used to temporarily halt the scanner.
   304  // If the supplied context is canceled the function will return at the first chance.
   305  func scanDataFolder(ctx context.Context, disks []StorageAPI, basePath string, cache dataUsageCache, getSize getSizeFn, scanMode madmin.HealScanMode, weSleep func() bool) (dataUsageCache, error) {
   306  	switch cache.Info.Name {
   307  	case "", dataUsageRoot:
   308  		return cache, errors.New("internal error: root scan attempted")
   309  	}
   310  	updatePath, closeDisk := globalScannerMetrics.currentPathUpdater(basePath, cache.Info.Name)
   311  	defer closeDisk()
   312  
   313  	s := folderScanner{
   314  		root:                  basePath,
   315  		getSize:               getSize,
   316  		oldCache:              cache,
   317  		newCache:              dataUsageCache{Info: cache.Info},
   318  		updateCache:           dataUsageCache{Info: cache.Info},
   319  		dataUsageScannerDebug: false,
   320  		healObjectSelect:      0,
   321  		scanMode:              scanMode,
   322  		weSleep:               weSleep,
   323  		updates:               cache.Info.updates,
   324  		updateCurrentPath:     updatePath,
   325  		disks:                 disks,
   326  		disksQuorum:           len(disks) / 2,
   327  	}
   328  
   329  	// Enable healing in XL mode.
   330  	if globalIsErasure && !cache.Info.SkipHealing {
   331  		// Do a heal check on an object once every n cycles. Must divide into healFolderInclude
   332  		s.healObjectSelect = healObjectSelectProb
   333  	}
   334  
   335  	done := ctx.Done()
   336  
   337  	// Read top level in bucket.
   338  	select {
   339  	case <-done:
   340  		return cache, ctx.Err()
   341  	default:
   342  	}
   343  	root := dataUsageEntry{}
   344  	folder := cachedFolder{name: cache.Info.Name, objectHealProbDiv: 1}
   345  	err := s.scanFolder(ctx, folder, &root)
   346  	if err != nil {
   347  		// No useful information...
   348  		return cache, err
   349  	}
   350  	s.newCache.Info.LastUpdate = UTCNow()
   351  	s.newCache.Info.NextCycle = cache.Info.NextCycle
   352  	return s.newCache, nil
   353  }
   354  
   355  // sendUpdate() should be called on a regular basis when the newCache contains more recent total than previously.
   356  // May or may not send an update upstream.
   357  func (f *folderScanner) sendUpdate() {
   358  	// Send at most an update every minute.
   359  	if f.updates == nil || time.Since(f.lastUpdate) < time.Minute {
   360  		return
   361  	}
   362  	if flat := f.updateCache.sizeRecursive(f.newCache.Info.Name); flat != nil {
   363  		select {
   364  		case f.updates <- flat.clone():
   365  		default:
   366  		}
   367  		f.lastUpdate = time.Now()
   368  	}
   369  }
   370  
   371  // scanFolder will scan the provided folder.
   372  // Files found in the folders will be added to f.newCache.
   373  // If final is provided folders will be put into f.newFolders or f.existingFolders.
   374  // If final is not provided the folders found are returned from the function.
   375  func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, into *dataUsageEntry) error {
   376  	done := ctx.Done()
   377  	scannerLogPrefix := color.Green("folder-scanner:")
   378  
   379  	noWait := func() {}
   380  
   381  	thisHash := hashPath(folder.name)
   382  	// Store initial compaction state.
   383  	wasCompacted := into.Compacted
   384  
   385  	for {
   386  		select {
   387  		case <-done:
   388  			return ctx.Err()
   389  		default:
   390  		}
   391  		var abandonedChildren dataUsageHashMap
   392  		if !into.Compacted {
   393  			abandonedChildren = f.oldCache.findChildrenCopy(thisHash)
   394  		}
   395  
   396  		// If there are lifecycle rules for the prefix.
   397  		_, prefix := path2BucketObjectWithBasePath(f.root, folder.name)
   398  		var activeLifeCycle *lifecycle.Lifecycle
   399  		if f.oldCache.Info.lifeCycle != nil && f.oldCache.Info.lifeCycle.HasActiveRules(prefix) {
   400  			if f.dataUsageScannerDebug {
   401  				console.Debugf(scannerLogPrefix+" Prefix %q has active rules\n", prefix)
   402  			}
   403  			activeLifeCycle = f.oldCache.Info.lifeCycle
   404  		}
   405  		// If there are replication rules for the prefix.
   406  		var replicationCfg replicationConfig
   407  		if !f.oldCache.Info.replication.Empty() && f.oldCache.Info.replication.Config.HasActiveRules(prefix, true) {
   408  			replicationCfg = f.oldCache.Info.replication
   409  		}
   410  
   411  		if f.weSleep() {
   412  			scannerSleeper.Sleep(ctx, dataScannerSleepPerFolder)
   413  		}
   414  
   415  		var existingFolders, newFolders []cachedFolder
   416  		var foundObjects bool
   417  		err := readDirFn(pathJoin(f.root, folder.name), func(entName string, typ os.FileMode) error {
   418  			// Parse
   419  			entName = pathClean(pathJoin(folder.name, entName))
   420  			if entName == "" || entName == folder.name {
   421  				if f.dataUsageScannerDebug {
   422  					console.Debugf(scannerLogPrefix+" no entity (%s,%s)\n", f.root, entName)
   423  				}
   424  				return nil
   425  			}
   426  			bucket, prefix := path2BucketObjectWithBasePath(f.root, entName)
   427  			if bucket == "" {
   428  				if f.dataUsageScannerDebug {
   429  					console.Debugf(scannerLogPrefix+" no bucket (%s,%s)\n", f.root, entName)
   430  				}
   431  				return errDoneForNow
   432  			}
   433  
   434  			if isReservedOrInvalidBucket(bucket, false) {
   435  				if f.dataUsageScannerDebug {
   436  					console.Debugf(scannerLogPrefix+" invalid bucket: %v, entry: %v\n", bucket, entName)
   437  				}
   438  				return errDoneForNow
   439  			}
   440  
   441  			select {
   442  			case <-done:
   443  				return errDoneForNow
   444  			default:
   445  			}
   446  
   447  			if typ&os.ModeDir != 0 {
   448  				h := hashPath(entName)
   449  				_, exists := f.oldCache.Cache[h.Key()]
   450  				if h == thisHash {
   451  					return nil
   452  				}
   453  				this := cachedFolder{name: entName, parent: &thisHash, objectHealProbDiv: folder.objectHealProbDiv}
   454  				delete(abandonedChildren, h.Key()) // h.Key() already accounted for.
   455  				if exists {
   456  					existingFolders = append(existingFolders, this)
   457  					f.updateCache.copyWithChildren(&f.oldCache, h, &thisHash)
   458  				} else {
   459  					newFolders = append(newFolders, this)
   460  				}
   461  				return nil
   462  			}
   463  
   464  			wait := noWait
   465  			if f.weSleep() {
   466  				// Dynamic time delay.
   467  				wait = scannerSleeper.Timer(ctx)
   468  			}
   469  
   470  			// Get file size, ignore errors.
   471  			item := scannerItem{
   472  				Path:        pathJoin(f.root, entName),
   473  				Typ:         typ,
   474  				bucket:      bucket,
   475  				prefix:      path.Dir(prefix),
   476  				objectName:  path.Base(entName),
   477  				debug:       f.dataUsageScannerDebug,
   478  				lifeCycle:   activeLifeCycle,
   479  				replication: replicationCfg,
   480  			}
   481  
   482  			item.heal.enabled = thisHash.modAlt(f.oldCache.Info.NextCycle/folder.objectHealProbDiv, f.healObjectSelect/folder.objectHealProbDiv) && globalIsErasure
   483  			item.heal.bitrot = f.scanMode == madmin.HealDeepScan
   484  
   485  			// if the drive belongs to an erasure set
   486  			// that is already being healed, skip the
   487  			// healing attempt on this drive.
   488  			item.heal.enabled = item.heal.enabled && f.healObjectSelect > 0
   489  
   490  			sz, err := f.getSize(item)
   491  			if err != nil && err != errIgnoreFileContrib {
   492  				wait() // wait to proceed to next entry.
   493  				if err != errSkipFile && f.dataUsageScannerDebug {
   494  					console.Debugf(scannerLogPrefix+" getSize \"%v/%v\" returned err: %v\n", bucket, item.objectPath(), err)
   495  				}
   496  				return nil
   497  			}
   498  
   499  			// successfully read means we have a valid object.
   500  			foundObjects = true
   501  			// Remove filename i.e is the meta file to construct object name
   502  			item.transformMetaDir()
   503  
   504  			// Object already accounted for, remove from heal map,
   505  			// simply because getSize() function already heals the
   506  			// object.
   507  			delete(abandonedChildren, pathJoin(item.bucket, item.objectPath()))
   508  
   509  			if err != errIgnoreFileContrib {
   510  				into.addSizes(sz)
   511  				into.Objects++
   512  			}
   513  
   514  			wait() // wait to proceed to next entry.
   515  
   516  			return nil
   517  		})
   518  		if err != nil {
   519  			return err
   520  		}
   521  
   522  		if foundObjects && globalIsErasure {
   523  			// If we found an object in erasure mode, we skip subdirs (only datadirs)...
   524  			break
   525  		}
   526  
   527  		// If we have many subfolders, compact ourself.
   528  		shouldCompact := f.newCache.Info.Name != folder.name &&
   529  			len(existingFolders)+len(newFolders) >= dataScannerCompactAtFolders ||
   530  			len(existingFolders)+len(newFolders) >= dataScannerForceCompactAtFolders
   531  
   532  		if totalFolders := len(existingFolders) + len(newFolders); totalFolders > int(scannerExcessFolders.Load()) {
   533  			prefixName := strings.TrimSuffix(folder.name, "/") + "/"
   534  			sendEvent(eventArgs{
   535  				EventName:  event.PrefixManyFolders,
   536  				BucketName: f.root,
   537  				Object: ObjectInfo{
   538  					Name: prefixName,
   539  					Size: int64(totalFolders),
   540  				},
   541  				UserAgent: "Scanner",
   542  				Host:      globalMinioHost,
   543  			})
   544  			auditLogInternal(context.Background(), AuditLogOptions{
   545  				Event:   "scanner:manyprefixes",
   546  				APIName: "Scanner",
   547  				Bucket:  f.root,
   548  				Object:  prefixName,
   549  				Tags: map[string]interface{}{
   550  					"x-minio-prefixes-total": strconv.Itoa(totalFolders),
   551  				},
   552  			})
   553  		}
   554  		if !into.Compacted && shouldCompact {
   555  			into.Compacted = true
   556  			newFolders = append(newFolders, existingFolders...)
   557  			existingFolders = nil
   558  			if f.dataUsageScannerDebug {
   559  				console.Debugf(scannerLogPrefix+" Preemptively compacting: %v, entries: %v\n", folder.name, len(existingFolders)+len(newFolders))
   560  			}
   561  		}
   562  
   563  		scanFolder := func(folder cachedFolder) {
   564  			if contextCanceled(ctx) {
   565  				return
   566  			}
   567  			dst := into
   568  			if !into.Compacted {
   569  				dst = &dataUsageEntry{Compacted: false}
   570  			}
   571  			if err := f.scanFolder(ctx, folder, dst); err != nil {
   572  				return
   573  			}
   574  			if !into.Compacted {
   575  				h := dataUsageHash(folder.name)
   576  				into.addChild(h)
   577  				// We scanned a folder, optionally send update.
   578  				f.updateCache.deleteRecursive(h)
   579  				f.updateCache.copyWithChildren(&f.newCache, h, folder.parent)
   580  				f.sendUpdate()
   581  			}
   582  		}
   583  
   584  		// Transfer existing
   585  		if !into.Compacted {
   586  			for _, folder := range existingFolders {
   587  				h := hashPath(folder.name)
   588  				f.updateCache.copyWithChildren(&f.oldCache, h, folder.parent)
   589  			}
   590  		}
   591  		// Scan new...
   592  		for _, folder := range newFolders {
   593  			h := hashPath(folder.name)
   594  			// Add new folders to the update tree so totals update for these.
   595  			if !into.Compacted {
   596  				var foundAny bool
   597  				parent := thisHash
   598  				for parent != hashPath(f.updateCache.Info.Name) {
   599  					e := f.updateCache.find(parent.Key())
   600  					if e == nil || e.Compacted {
   601  						foundAny = true
   602  						break
   603  					}
   604  					next := f.updateCache.searchParent(parent)
   605  					if next == nil {
   606  						foundAny = true
   607  						break
   608  					}
   609  					parent = *next
   610  				}
   611  				if !foundAny {
   612  					// Add non-compacted empty entry.
   613  					f.updateCache.replaceHashed(h, &thisHash, dataUsageEntry{})
   614  				}
   615  			}
   616  			f.updateCurrentPath(folder.name)
   617  			stopFn := globalScannerMetrics.log(scannerMetricScanFolder, f.root, folder.name)
   618  			scanFolder(folder)
   619  			stopFn(map[string]string{"type": "new"})
   620  
   621  			// Add new folders if this is new and we don't have existing.
   622  			if !into.Compacted {
   623  				parent := f.updateCache.find(thisHash.Key())
   624  				if parent != nil && !parent.Compacted {
   625  					f.updateCache.deleteRecursive(h)
   626  					f.updateCache.copyWithChildren(&f.newCache, h, &thisHash)
   627  				}
   628  			}
   629  		}
   630  
   631  		// Scan existing...
   632  		for _, folder := range existingFolders {
   633  			h := hashPath(folder.name)
   634  			// Check if we should skip scanning folder...
   635  			// We can only skip if we are not indexing into a compacted destination
   636  			// and the entry itself is compacted.
   637  			if !into.Compacted && f.oldCache.isCompacted(h) {
   638  				if !h.mod(f.oldCache.Info.NextCycle, dataUsageUpdateDirCycles) {
   639  					// Transfer and add as child...
   640  					f.newCache.copyWithChildren(&f.oldCache, h, folder.parent)
   641  					into.addChild(h)
   642  					continue
   643  				}
   644  			}
   645  			f.updateCurrentPath(folder.name)
   646  			stopFn := globalScannerMetrics.log(scannerMetricScanFolder, f.root, folder.name)
   647  			scanFolder(folder)
   648  			stopFn(map[string]string{"type": "existing"})
   649  		}
   650  
   651  		// Scan for healing
   652  		if f.healObjectSelect == 0 || len(abandonedChildren) == 0 {
   653  			// If we are not heal scanning, return now.
   654  			break
   655  		}
   656  
   657  		if len(f.disks) == 0 || f.disksQuorum == 0 {
   658  			break
   659  		}
   660  
   661  		bgSeq, found := globalBackgroundHealState.getHealSequenceByToken(bgHealingUUID)
   662  		if !found {
   663  			break
   664  		}
   665  
   666  		// Whatever remains in 'abandonedChildren' are folders at this level
   667  		// that existed in the previous run but wasn't found now.
   668  		//
   669  		// This may be because of 2 reasons:
   670  		//
   671  		// 1) The folder/object was deleted.
   672  		// 2) We come from another disk and this disk missed the write.
   673  		//
   674  		// We therefore perform a heal check.
   675  		// If that doesn't bring it back we remove the folder and assume it was deleted.
   676  		// This means that the next run will not look for it.
   677  		// How to resolve results.
   678  		resolver := metadataResolutionParams{
   679  			dirQuorum: f.disksQuorum,
   680  			objQuorum: f.disksQuorum,
   681  			bucket:    "",
   682  			strict:    false,
   683  		}
   684  
   685  		healObjectsPrefix := color.Green("healObjects:")
   686  		for k := range abandonedChildren {
   687  			bucket, prefix := path2BucketObject(k)
   688  			stopFn := globalScannerMetrics.time(scannerMetricCheckMissing)
   689  			f.updateCurrentPath(k)
   690  
   691  			if bucket != resolver.bucket {
   692  				// Bucket might be missing as well with abandoned children.
   693  				// make sure it is created first otherwise healing won't proceed
   694  				// for objects.
   695  				bgSeq.queueHealTask(healSource{
   696  					bucket: bucket,
   697  				}, madmin.HealItemBucket)
   698  			}
   699  
   700  			resolver.bucket = bucket
   701  
   702  			foundObjs := false
   703  			ctx, cancel := context.WithCancel(ctx)
   704  
   705  			err := listPathRaw(ctx, listPathRawOptions{
   706  				disks:          f.disks,
   707  				bucket:         bucket,
   708  				path:           prefix,
   709  				recursive:      true,
   710  				reportNotFound: true,
   711  				minDisks:       f.disksQuorum,
   712  				agreed: func(entry metaCacheEntry) {
   713  					f.updateCurrentPath(entry.name)
   714  					if f.dataUsageScannerDebug {
   715  						console.Debugf(healObjectsPrefix+" got agreement: %v\n", entry.name)
   716  					}
   717  				},
   718  				// Some disks have data for this.
   719  				partial: func(entries metaCacheEntries, errs []error) {
   720  					entry, ok := entries.resolve(&resolver)
   721  					if !ok {
   722  						// check if we can get one entry at least
   723  						// proceed to heal nonetheless, since
   724  						// this object might be dangling.
   725  						entry, _ = entries.firstFound()
   726  					}
   727  					wait := noWait
   728  					if f.weSleep() {
   729  						// wait timer per object.
   730  						wait = scannerSleeper.Timer(ctx)
   731  					}
   732  					defer wait()
   733  					f.updateCurrentPath(entry.name)
   734  					stopFn := globalScannerMetrics.log(scannerMetricHealAbandonedObject, f.root, entry.name)
   735  					custom := make(map[string]string)
   736  					defer stopFn(custom)
   737  
   738  					if f.dataUsageScannerDebug {
   739  						console.Debugf(healObjectsPrefix+" resolved to: %v, dir: %v\n", entry.name, entry.isDir())
   740  					}
   741  
   742  					if entry.isDir() {
   743  						return
   744  					}
   745  
   746  					// We got an entry which we should be able to heal.
   747  					fiv, err := entry.fileInfoVersions(bucket)
   748  					if err != nil {
   749  						err := bgSeq.queueHealTask(healSource{
   750  							bucket:    bucket,
   751  							object:    entry.name,
   752  							versionID: "",
   753  						}, madmin.HealItemObject)
   754  						if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
   755  							logger.LogOnceIf(ctx, err, entry.name)
   756  						}
   757  						foundObjs = foundObjs || err == nil
   758  						return
   759  					}
   760  
   761  					custom["versions"] = fmt.Sprint(len(fiv.Versions))
   762  					var successVersions, failVersions int
   763  					for _, ver := range fiv.Versions {
   764  						stopFn := globalScannerMetrics.timeSize(scannerMetricHealAbandonedVersion)
   765  						err := bgSeq.queueHealTask(healSource{
   766  							bucket:    bucket,
   767  							object:    fiv.Name,
   768  							versionID: ver.VersionID,
   769  						}, madmin.HealItemObject)
   770  						stopFn(int(ver.Size))
   771  						if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
   772  							logger.LogOnceIf(ctx, err, fiv.Name)
   773  						}
   774  						if err == nil {
   775  							successVersions++
   776  						} else {
   777  							failVersions++
   778  						}
   779  						foundObjs = foundObjs || err == nil
   780  					}
   781  					custom["success_versions"] = fmt.Sprint(successVersions)
   782  					custom["failed_versions"] = fmt.Sprint(failVersions)
   783  				},
   784  				// Too many disks failed.
   785  				finished: func(errs []error) {
   786  					if f.dataUsageScannerDebug {
   787  						console.Debugf(healObjectsPrefix+" too many errors: %v\n", errs)
   788  					}
   789  					cancel()
   790  				},
   791  			})
   792  
   793  			stopFn()
   794  			if f.dataUsageScannerDebug && err != nil && err != errFileNotFound {
   795  				console.Debugf(healObjectsPrefix+" checking returned value %v (%T)\n", err, err)
   796  			}
   797  
   798  			// Add unless healing returned an error.
   799  			if foundObjs {
   800  				this := cachedFolder{name: k, parent: &thisHash, objectHealProbDiv: 1}
   801  				stopFn := globalScannerMetrics.log(scannerMetricScanFolder, f.root, this.name, "HEALED")
   802  				scanFolder(this)
   803  				stopFn(map[string]string{"type": "healed"})
   804  			}
   805  		}
   806  		break
   807  	}
   808  	if !wasCompacted {
   809  		f.newCache.replaceHashed(thisHash, folder.parent, *into)
   810  	}
   811  
   812  	if !into.Compacted && f.newCache.Info.Name != folder.name {
   813  		flat := f.newCache.sizeRecursive(thisHash.Key())
   814  		flat.Compacted = true
   815  		var compact bool
   816  		if flat.Objects < dataScannerCompactLeastObject {
   817  			compact = true
   818  		} else {
   819  			// Compact if we only have objects as children...
   820  			compact = true
   821  			for k := range into.Children {
   822  				if v, ok := f.newCache.Cache[k]; ok {
   823  					if len(v.Children) > 0 || v.Objects > 1 {
   824  						compact = false
   825  						break
   826  					}
   827  				}
   828  			}
   829  
   830  		}
   831  		if compact {
   832  			stop := globalScannerMetrics.log(scannerMetricCompactFolder, folder.name)
   833  			f.newCache.deleteRecursive(thisHash)
   834  			f.newCache.replaceHashed(thisHash, folder.parent, *flat)
   835  			total := map[string]string{
   836  				"objects": strconv.FormatUint(flat.Objects, 10),
   837  				"size":    strconv.FormatInt(flat.Size, 10),
   838  			}
   839  			if flat.Versions > 0 {
   840  				total["versions"] = strconv.FormatUint(flat.Versions, 10)
   841  			}
   842  			stop(total)
   843  		}
   844  
   845  	}
   846  	// Compact if too many children...
   847  	if !into.Compacted {
   848  		f.newCache.reduceChildrenOf(thisHash, dataScannerCompactAtChildren, f.newCache.Info.Name != folder.name)
   849  	}
   850  	if _, ok := f.updateCache.Cache[thisHash.Key()]; !wasCompacted && ok {
   851  		// Replace if existed before.
   852  		if flat := f.newCache.sizeRecursive(thisHash.Key()); flat != nil {
   853  			f.updateCache.deleteRecursive(thisHash)
   854  			f.updateCache.replaceHashed(thisHash, folder.parent, *flat)
   855  		}
   856  	}
   857  
   858  	return nil
   859  }
   860  
   861  // scannerItem represents each file while walking.
   862  type scannerItem struct {
   863  	Path        string
   864  	bucket      string // Bucket.
   865  	prefix      string // Only the prefix if any, does not have final object name.
   866  	objectName  string // Only the object name without prefixes.
   867  	replication replicationConfig
   868  	lifeCycle   *lifecycle.Lifecycle
   869  	Typ         fs.FileMode
   870  	heal        struct {
   871  		enabled bool
   872  		bitrot  bool
   873  	} // Has the object been selected for heal check?
   874  	debug bool
   875  }
   876  
   877  type sizeSummary struct {
   878  	totalSize       int64
   879  	versions        uint64
   880  	deleteMarkers   uint64
   881  	replicatedSize  int64
   882  	replicatedCount int64
   883  	pendingSize     int64
   884  	failedSize      int64
   885  	replicaSize     int64
   886  	replicaCount    int64
   887  	pendingCount    uint64
   888  	failedCount     uint64
   889  	replTargetStats map[string]replTargetSizeSummary
   890  	tiers           map[string]tierStats
   891  }
   892  
   893  // replTargetSizeSummary holds summary of replication stats by target
   894  type replTargetSizeSummary struct {
   895  	replicatedSize  int64
   896  	replicatedCount int64
   897  	pendingSize     int64
   898  	failedSize      int64
   899  	pendingCount    uint64
   900  	failedCount     uint64
   901  }
   902  
   903  type getSizeFn func(item scannerItem) (sizeSummary, error)
   904  
   905  // transformMetaDir will transform a directory to prefix/file.ext
   906  func (i *scannerItem) transformMetaDir() {
   907  	split := strings.Split(i.prefix, SlashSeparator)
   908  	if len(split) > 1 {
   909  		i.prefix = pathJoin(split[:len(split)-1]...)
   910  	} else {
   911  		i.prefix = ""
   912  	}
   913  	// Object name is last element
   914  	i.objectName = split[len(split)-1]
   915  }
   916  
   917  var (
   918  	applyActionsLogPrefix        = color.Green("applyActions:")
   919  	applyVersionActionsLogPrefix = color.Green("applyVersionActions:")
   920  )
   921  
   922  func (i *scannerItem) applyHealing(ctx context.Context, o ObjectLayer, oi ObjectInfo) (size int64) {
   923  	if i.debug {
   924  		if oi.VersionID != "" {
   925  			console.Debugf(applyActionsLogPrefix+" heal checking: %v/%v v(%s)\n", i.bucket, i.objectPath(), oi.VersionID)
   926  		} else {
   927  			console.Debugf(applyActionsLogPrefix+" heal checking: %v/%v\n", i.bucket, i.objectPath())
   928  		}
   929  	}
   930  	scanMode := madmin.HealNormalScan
   931  	if i.heal.bitrot {
   932  		scanMode = madmin.HealDeepScan
   933  	}
   934  	healOpts := madmin.HealOpts{
   935  		Remove:   healDeleteDangling,
   936  		ScanMode: scanMode,
   937  	}
   938  	res, _ := o.HealObject(ctx, i.bucket, i.objectPath(), oi.VersionID, healOpts)
   939  	if res.ObjectSize > 0 {
   940  		return res.ObjectSize
   941  	}
   942  	return 0
   943  }
   944  
   945  func (i *scannerItem) applyLifecycle(ctx context.Context, o ObjectLayer, oi ObjectInfo) (action lifecycle.Action, size int64) {
   946  	size, err := oi.GetActualSize()
   947  	if i.debug {
   948  		logger.LogIf(ctx, err)
   949  	}
   950  	if i.lifeCycle == nil {
   951  		return action, size
   952  	}
   953  
   954  	versionID := oi.VersionID
   955  	vcfg, _ := globalBucketVersioningSys.Get(i.bucket)
   956  	rCfg, _ := globalBucketObjectLockSys.Get(i.bucket)
   957  	replcfg, _ := getReplicationConfig(ctx, i.bucket)
   958  	lcEvt := evalActionFromLifecycle(ctx, *i.lifeCycle, rCfg, replcfg, oi)
   959  	if i.debug {
   960  		if versionID != "" {
   961  			console.Debugf(applyActionsLogPrefix+" lifecycle: %q (version-id=%s), Initial scan: %v\n", i.objectPath(), versionID, lcEvt.Action)
   962  		} else {
   963  			console.Debugf(applyActionsLogPrefix+" lifecycle: %q Initial scan: %v\n", i.objectPath(), lcEvt.Action)
   964  		}
   965  	}
   966  
   967  	switch lcEvt.Action {
   968  	// This version doesn't contribute towards sizeS only when it is permanently deleted.
   969  	// This can happen when,
   970  	// - ExpireObjectAllVersions flag is enabled
   971  	// - NoncurrentVersionExpiration is applicable
   972  	case lifecycle.DeleteVersionAction, lifecycle.DeleteAllVersionsAction:
   973  		size = 0
   974  	case lifecycle.DeleteAction:
   975  		// On a non-versioned bucket, DeleteObject removes the only version permanently.
   976  		if !vcfg.PrefixEnabled(oi.Name) {
   977  			size = 0
   978  		}
   979  	}
   980  
   981  	applyLifecycleAction(lcEvt, lcEventSrc_Scanner, oi)
   982  	return lcEvt.Action, size
   983  }
   984  
   985  // applyNewerNoncurrentVersionLimit removes noncurrent versions older than the most recent NewerNoncurrentVersions configured.
   986  // Note: This function doesn't update sizeSummary since it always removes versions that it doesn't return.
   987  func (i *scannerItem) applyNewerNoncurrentVersionLimit(ctx context.Context, _ ObjectLayer, fivs []FileInfo, expState *expiryState) ([]ObjectInfo, error) {
   988  	done := globalScannerMetrics.time(scannerMetricApplyNonCurrent)
   989  	defer done()
   990  
   991  	rcfg, _ := globalBucketObjectLockSys.Get(i.bucket)
   992  	vcfg, _ := globalBucketVersioningSys.Get(i.bucket)
   993  
   994  	versioned := vcfg != nil && vcfg.Versioned(i.objectPath())
   995  
   996  	objectInfos := make([]ObjectInfo, 0, len(fivs))
   997  
   998  	if i.lifeCycle == nil {
   999  		for _, fi := range fivs {
  1000  			objectInfos = append(objectInfos, fi.ToObjectInfo(i.bucket, i.objectPath(), versioned))
  1001  		}
  1002  		return objectInfos, nil
  1003  	}
  1004  
  1005  	event := i.lifeCycle.NoncurrentVersionsExpirationLimit(lifecycle.ObjectOpts{Name: i.objectPath()})
  1006  	lim := event.NewerNoncurrentVersions
  1007  	if lim == 0 || len(fivs) <= lim+1 { // fewer than lim _noncurrent_ versions
  1008  		for _, fi := range fivs {
  1009  			objectInfos = append(objectInfos, fi.ToObjectInfo(i.bucket, i.objectPath(), versioned))
  1010  		}
  1011  		return objectInfos, nil
  1012  	}
  1013  
  1014  	overflowVersions := fivs[lim+1:]
  1015  	// Retain the current version + most recent lim noncurrent versions
  1016  	for _, fi := range fivs[:lim+1] {
  1017  		objectInfos = append(objectInfos, fi.ToObjectInfo(i.bucket, i.objectPath(), versioned))
  1018  	}
  1019  
  1020  	toDel := make([]ObjectToDelete, 0, len(overflowVersions))
  1021  	for _, fi := range overflowVersions {
  1022  		obj := fi.ToObjectInfo(i.bucket, i.objectPath(), versioned)
  1023  		// skip versions with object locking enabled
  1024  		if rcfg.LockEnabled && enforceRetentionForDeletion(ctx, obj) {
  1025  			if i.debug {
  1026  				if obj.VersionID != "" {
  1027  					console.Debugf(applyVersionActionsLogPrefix+" lifecycle: %s v(%s) is locked, not deleting\n", obj.Name, obj.VersionID)
  1028  				} else {
  1029  					console.Debugf(applyVersionActionsLogPrefix+" lifecycle: %s is locked, not deleting\n", obj.Name)
  1030  				}
  1031  			}
  1032  			// add this version back to remaining versions for
  1033  			// subsequent lifecycle policy applications
  1034  			objectInfos = append(objectInfos, obj)
  1035  			continue
  1036  		}
  1037  
  1038  		// NoncurrentDays not passed yet.
  1039  		if time.Now().UTC().Before(lifecycle.ExpectedExpiryTime(obj.SuccessorModTime, event.NoncurrentDays)) {
  1040  			// add this version back to remaining versions for
  1041  			// subsequent lifecycle policy applications
  1042  			objectInfos = append(objectInfos, obj)
  1043  			continue
  1044  		}
  1045  
  1046  		toDel = append(toDel, ObjectToDelete{
  1047  			ObjectV: ObjectV{
  1048  				ObjectName: obj.Name,
  1049  				VersionID:  obj.VersionID,
  1050  			},
  1051  		})
  1052  	}
  1053  
  1054  	if len(toDel) > 0 {
  1055  		expState.enqueueByNewerNoncurrent(i.bucket, toDel, event)
  1056  	}
  1057  	return objectInfos, nil
  1058  }
  1059  
  1060  // applyVersionActions will apply lifecycle checks on all versions of a scanned item. Returns versions that remain
  1061  // after applying lifecycle checks configured.
  1062  func (i *scannerItem) applyVersionActions(ctx context.Context, o ObjectLayer, fivs []FileInfo, expState *expiryState) ([]ObjectInfo, error) {
  1063  	objInfos, err := i.applyNewerNoncurrentVersionLimit(ctx, o, fivs, expState)
  1064  	if err != nil {
  1065  		return nil, err
  1066  	}
  1067  
  1068  	// Check if we have many versions after applyNewerNoncurrentVersionLimit.
  1069  	if len(objInfos) > int(scannerExcessObjectVersions.Load()) {
  1070  		// Notify object accessed via a GET request.
  1071  		sendEvent(eventArgs{
  1072  			EventName:  event.ObjectManyVersions,
  1073  			BucketName: i.bucket,
  1074  			Object: ObjectInfo{
  1075  				Name: i.objectPath(),
  1076  			},
  1077  			UserAgent:    "Scanner",
  1078  			Host:         globalLocalNodeName,
  1079  			RespElements: map[string]string{"x-minio-versions": strconv.Itoa(len(objInfos))},
  1080  		})
  1081  
  1082  		auditLogInternal(context.Background(), AuditLogOptions{
  1083  			Event:   "scanner:manyversions",
  1084  			APIName: "Scanner",
  1085  			Bucket:  i.bucket,
  1086  			Object:  i.objectPath(),
  1087  			Tags: map[string]interface{}{
  1088  				"x-minio-versions": strconv.Itoa(len(objInfos)),
  1089  			},
  1090  		})
  1091  	}
  1092  
  1093  	return objInfos, nil
  1094  }
  1095  
  1096  // applyActions will apply lifecycle checks on to a scanned item.
  1097  // The resulting size on disk will always be returned.
  1098  // The metadata will be compared to consensus on the object layer before any changes are applied.
  1099  // If no metadata is supplied, -1 is returned if no action is taken.
  1100  func (i *scannerItem) applyActions(ctx context.Context, o ObjectLayer, oi ObjectInfo, sizeS *sizeSummary) (objDeleted bool, size int64) {
  1101  	done := globalScannerMetrics.time(scannerMetricILM)
  1102  	var action lifecycle.Action
  1103  	action, size = i.applyLifecycle(ctx, o, oi)
  1104  	done()
  1105  
  1106  	// Note: objDeleted is true if and only if action ==
  1107  	// lifecycle.DeleteAllVersionsAction
  1108  	if action == lifecycle.DeleteAllVersionsAction {
  1109  		return true, 0
  1110  	}
  1111  
  1112  	// For instance, an applied lifecycle means we remove/transitioned an object
  1113  	// from the current deployment, which means we don't have to call healing
  1114  	// routine even if we are asked to do via heal flag.
  1115  	if action == lifecycle.NoneAction {
  1116  		if i.heal.enabled {
  1117  			done := globalScannerMetrics.time(scannerMetricHealCheck)
  1118  			size = i.applyHealing(ctx, o, oi)
  1119  			done()
  1120  
  1121  			if healDeleteDangling {
  1122  				done := globalScannerMetrics.time(scannerMetricCleanAbandoned)
  1123  				err := o.CheckAbandonedParts(ctx, i.bucket, i.objectPath(), madmin.HealOpts{Remove: healDeleteDangling})
  1124  				done()
  1125  				if err != nil {
  1126  					logger.LogOnceIf(ctx, fmt.Errorf("unable to check object %s/%s for abandoned data: %w", i.bucket, i.objectPath(), err), i.objectPath())
  1127  				}
  1128  			}
  1129  		}
  1130  
  1131  		// replicate only if lifecycle rules are not applied.
  1132  		done := globalScannerMetrics.time(scannerMetricCheckReplication)
  1133  		i.healReplication(ctx, o, oi.Clone(), sizeS)
  1134  		done()
  1135  	}
  1136  	return false, size
  1137  }
  1138  
  1139  func evalActionFromLifecycle(ctx context.Context, lc lifecycle.Lifecycle, lr lock.Retention, rcfg *replication.Config, obj ObjectInfo) lifecycle.Event {
  1140  	event := lc.Eval(obj.ToLifecycleOpts())
  1141  	if serverDebugLog {
  1142  		console.Debugf(applyActionsLogPrefix+" lifecycle: Secondary scan: %v\n", event.Action)
  1143  	}
  1144  
  1145  	if event.Action == lifecycle.NoneAction {
  1146  		return event
  1147  	}
  1148  
  1149  	if obj.IsLatest && event.Action == lifecycle.DeleteAllVersionsAction {
  1150  		if lr.LockEnabled && enforceRetentionForDeletion(ctx, obj) {
  1151  			return lifecycle.Event{Action: lifecycle.NoneAction}
  1152  		}
  1153  	}
  1154  
  1155  	switch event.Action {
  1156  	case lifecycle.DeleteVersionAction, lifecycle.DeleteRestoredVersionAction:
  1157  		// Defensive code, should never happen
  1158  		if obj.VersionID == "" {
  1159  			return lifecycle.Event{Action: lifecycle.NoneAction}
  1160  		}
  1161  		if lr.LockEnabled && enforceRetentionForDeletion(ctx, obj) {
  1162  			if serverDebugLog {
  1163  				if obj.VersionID != "" {
  1164  					console.Debugf(applyActionsLogPrefix+" lifecycle: %s v(%s) is locked, not deleting\n", obj.Name, obj.VersionID)
  1165  				} else {
  1166  					console.Debugf(applyActionsLogPrefix+" lifecycle: %s is locked, not deleting\n", obj.Name)
  1167  				}
  1168  			}
  1169  			return lifecycle.Event{Action: lifecycle.NoneAction}
  1170  		}
  1171  		if rcfg != nil && !obj.VersionPurgeStatus.Empty() && rcfg.HasActiveRules(obj.Name, true) {
  1172  			return lifecycle.Event{Action: lifecycle.NoneAction}
  1173  		}
  1174  	}
  1175  
  1176  	return event
  1177  }
  1178  
  1179  func applyTransitionRule(event lifecycle.Event, src lcEventSrc, obj ObjectInfo) bool {
  1180  	if obj.DeleteMarker {
  1181  		return false
  1182  	}
  1183  	globalTransitionState.queueTransitionTask(obj, event, src)
  1184  	return true
  1185  }
  1186  
  1187  func applyExpiryOnTransitionedObject(ctx context.Context, objLayer ObjectLayer, obj ObjectInfo, lcEvent lifecycle.Event, src lcEventSrc) bool {
  1188  	var err error
  1189  	defer func() {
  1190  		if err != nil {
  1191  			return
  1192  		}
  1193  		// Note: DeleteAllVersions action is not supported for
  1194  		// transitioned objects
  1195  		globalScannerMetrics.timeILM(lcEvent.Action)(1)
  1196  	}()
  1197  
  1198  	if err = expireTransitionedObject(ctx, objLayer, &obj, lcEvent, src); err != nil {
  1199  		if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
  1200  			return false
  1201  		}
  1202  		logger.LogOnceIf(ctx, err, obj.Name)
  1203  		return false
  1204  	}
  1205  	// Notification already sent in *expireTransitionedObject*, just return 'true' here.
  1206  	return true
  1207  }
  1208  
  1209  func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLayer, obj ObjectInfo, lcEvent lifecycle.Event, src lcEventSrc) bool {
  1210  	traceFn := globalLifecycleSys.trace(obj)
  1211  	opts := ObjectOptions{
  1212  		Expiration: ExpirationOptions{Expire: true},
  1213  	}
  1214  
  1215  	if lcEvent.Action.DeleteVersioned() {
  1216  		opts.VersionID = obj.VersionID
  1217  	}
  1218  
  1219  	opts.Versioned = globalBucketVersioningSys.PrefixEnabled(obj.Bucket, obj.Name)
  1220  	opts.VersionSuspended = globalBucketVersioningSys.PrefixSuspended(obj.Bucket, obj.Name)
  1221  
  1222  	if lcEvent.Action.DeleteAll() {
  1223  		opts.DeletePrefix = true
  1224  		// use prefix delete on exact object (this is an optimization to avoid fan-out calls)
  1225  		opts.DeletePrefixObject = true
  1226  	}
  1227  	var (
  1228  		dobj ObjectInfo
  1229  		err  error
  1230  	)
  1231  	defer func() {
  1232  		if err != nil {
  1233  			return
  1234  		}
  1235  
  1236  		if lcEvent.Action != lifecycle.NoneAction {
  1237  			numVersions := uint64(1)
  1238  			if lcEvent.Action == lifecycle.DeleteAllVersionsAction {
  1239  				numVersions = uint64(obj.NumVersions)
  1240  			}
  1241  			globalScannerMetrics.timeILM(lcEvent.Action)(numVersions)
  1242  		}
  1243  	}()
  1244  
  1245  	dobj, err = objLayer.DeleteObject(ctx, obj.Bucket, encodeDirObject(obj.Name), opts)
  1246  	if err != nil {
  1247  		if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
  1248  			return false
  1249  		}
  1250  		// Assume it is still there.
  1251  		logger.LogOnceIf(ctx, err, "non-transition-expiry")
  1252  		return false
  1253  	}
  1254  	if dobj.Name == "" {
  1255  		dobj = obj
  1256  	}
  1257  
  1258  	tags := newLifecycleAuditEvent(src, lcEvent).Tags()
  1259  	// Send audit for the lifecycle delete operation
  1260  	auditLogLifecycle(ctx, dobj, ILMExpiry, tags, traceFn)
  1261  
  1262  	eventName := event.ObjectRemovedDelete
  1263  	if obj.DeleteMarker {
  1264  		eventName = event.ObjectRemovedDeleteMarkerCreated
  1265  	}
  1266  	if lcEvent.Action.DeleteAll() {
  1267  		eventName = event.ObjectRemovedDeleteAllVersions
  1268  	}
  1269  	// Notify object deleted event.
  1270  	sendEvent(eventArgs{
  1271  		EventName:  eventName,
  1272  		BucketName: dobj.Bucket,
  1273  		Object:     dobj,
  1274  		UserAgent:  "Internal: [ILM-Expiry]",
  1275  		Host:       globalLocalNodeName,
  1276  	})
  1277  
  1278  	return true
  1279  }
  1280  
  1281  // Apply object, object version, restored object or restored object version action on the given object
  1282  func applyExpiryRule(event lifecycle.Event, src lcEventSrc, obj ObjectInfo) bool {
  1283  	globalExpiryState.enqueueByDays(obj, event, src)
  1284  	return true
  1285  }
  1286  
  1287  // Perform actions (removal or transitioning of objects), return true the action is successfully performed
  1288  func applyLifecycleAction(event lifecycle.Event, src lcEventSrc, obj ObjectInfo) (success bool) {
  1289  	switch action := event.Action; action {
  1290  	case lifecycle.DeleteVersionAction, lifecycle.DeleteAction,
  1291  		lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction,
  1292  		lifecycle.DeleteAllVersionsAction:
  1293  		success = applyExpiryRule(event, src, obj)
  1294  	case lifecycle.TransitionAction, lifecycle.TransitionVersionAction:
  1295  		success = applyTransitionRule(event, src, obj)
  1296  	}
  1297  	return
  1298  }
  1299  
  1300  // objectPath returns the prefix and object name.
  1301  func (i *scannerItem) objectPath() string {
  1302  	return pathJoin(i.prefix, i.objectName)
  1303  }
  1304  
  1305  // healReplication will heal a scanned item that has failed replication.
  1306  func (i *scannerItem) healReplication(ctx context.Context, o ObjectLayer, oi ObjectInfo, sizeS *sizeSummary) {
  1307  	if oi.VersionID == "" {
  1308  		return
  1309  	}
  1310  	if i.replication.Config == nil {
  1311  		return
  1312  	}
  1313  	roi := queueReplicationHeal(ctx, oi.Bucket, oi, i.replication, 0)
  1314  	if oi.DeleteMarker || !oi.VersionPurgeStatus.Empty() {
  1315  		return
  1316  	}
  1317  
  1318  	if sizeS.replTargetStats == nil && len(roi.TargetStatuses) > 0 {
  1319  		sizeS.replTargetStats = make(map[string]replTargetSizeSummary)
  1320  	}
  1321  
  1322  	for arn, tgtStatus := range roi.TargetStatuses {
  1323  		tgtSizeS, ok := sizeS.replTargetStats[arn]
  1324  		if !ok {
  1325  			tgtSizeS = replTargetSizeSummary{}
  1326  		}
  1327  		switch tgtStatus {
  1328  		case replication.Pending:
  1329  			tgtSizeS.pendingCount++
  1330  			tgtSizeS.pendingSize += oi.Size
  1331  			sizeS.pendingCount++
  1332  			sizeS.pendingSize += oi.Size
  1333  		case replication.Failed:
  1334  			tgtSizeS.failedSize += oi.Size
  1335  			tgtSizeS.failedCount++
  1336  			sizeS.failedSize += oi.Size
  1337  			sizeS.failedCount++
  1338  		case replication.Completed, replication.CompletedLegacy:
  1339  			tgtSizeS.replicatedSize += oi.Size
  1340  			tgtSizeS.replicatedCount++
  1341  			sizeS.replicatedSize += oi.Size
  1342  			sizeS.replicatedCount++
  1343  		}
  1344  		sizeS.replTargetStats[arn] = tgtSizeS
  1345  	}
  1346  
  1347  	if oi.ReplicationStatus == replication.Replica {
  1348  		sizeS.replicaSize += oi.Size
  1349  		sizeS.replicaCount++
  1350  	}
  1351  }
  1352  
  1353  type dynamicSleeper struct {
  1354  	mu sync.RWMutex
  1355  
  1356  	// Sleep factor
  1357  	factor float64
  1358  
  1359  	// maximum sleep cap,
  1360  	// set to <= 0 to disable.
  1361  	maxSleep time.Duration
  1362  
  1363  	// Don't sleep at all, if time taken is below this value.
  1364  	// This is to avoid too small costly sleeps.
  1365  	minSleep time.Duration
  1366  
  1367  	// cycle will be closed
  1368  	cycle chan struct{}
  1369  
  1370  	// isScanner should be set when this is used by the scanner
  1371  	// to record metrics.
  1372  	isScanner bool
  1373  }
  1374  
  1375  // newDynamicSleeper
  1376  func newDynamicSleeper(factor float64, maxWait time.Duration, isScanner bool) *dynamicSleeper {
  1377  	return &dynamicSleeper{
  1378  		factor:    factor,
  1379  		cycle:     make(chan struct{}),
  1380  		maxSleep:  maxWait,
  1381  		minSleep:  100 * time.Microsecond,
  1382  		isScanner: isScanner,
  1383  	}
  1384  }
  1385  
  1386  // Timer returns a timer that has started.
  1387  // When the returned function is called it will wait.
  1388  func (d *dynamicSleeper) Timer(ctx context.Context) func() {
  1389  	t := time.Now()
  1390  	return func() {
  1391  		doneAt := time.Now()
  1392  		for {
  1393  			// Grab current values
  1394  			d.mu.RLock()
  1395  			minWait, maxWait := d.minSleep, d.maxSleep
  1396  			factor := d.factor
  1397  			cycle := d.cycle
  1398  			d.mu.RUnlock()
  1399  			elapsed := doneAt.Sub(t)
  1400  			// Don't sleep for really small amount of time
  1401  			wantSleep := time.Duration(float64(elapsed) * factor)
  1402  			if wantSleep <= minWait {
  1403  				return
  1404  			}
  1405  			if maxWait > 0 && wantSleep > maxWait {
  1406  				wantSleep = maxWait
  1407  			}
  1408  			timer := time.NewTimer(wantSleep)
  1409  			select {
  1410  			case <-ctx.Done():
  1411  				if !timer.Stop() {
  1412  					<-timer.C
  1413  				}
  1414  				if d.isScanner {
  1415  					globalScannerMetrics.incTime(scannerMetricYield, wantSleep)
  1416  				}
  1417  				return
  1418  			case <-timer.C:
  1419  				if d.isScanner {
  1420  					globalScannerMetrics.incTime(scannerMetricYield, wantSleep)
  1421  				}
  1422  				return
  1423  			case <-cycle:
  1424  				if !timer.Stop() {
  1425  					// We expired.
  1426  					<-timer.C
  1427  					if d.isScanner {
  1428  						globalScannerMetrics.incTime(scannerMetricYield, wantSleep)
  1429  					}
  1430  					return
  1431  				}
  1432  			}
  1433  		}
  1434  	}
  1435  }
  1436  
  1437  // Sleep sleeps the specified time multiplied by the sleep factor.
  1438  // If the factor is updated the sleep will be done again with the new factor.
  1439  func (d *dynamicSleeper) Sleep(ctx context.Context, base time.Duration) {
  1440  	for {
  1441  		// Grab current values
  1442  		d.mu.RLock()
  1443  		minWait, maxWait := d.minSleep, d.maxSleep
  1444  		factor := d.factor
  1445  		cycle := d.cycle
  1446  		d.mu.RUnlock()
  1447  		// Don't sleep for really small amount of time
  1448  		wantSleep := time.Duration(float64(base) * factor)
  1449  		if wantSleep <= minWait {
  1450  			return
  1451  		}
  1452  		if maxWait > 0 && wantSleep > maxWait {
  1453  			wantSleep = maxWait
  1454  		}
  1455  		timer := time.NewTimer(wantSleep)
  1456  		select {
  1457  		case <-ctx.Done():
  1458  			if !timer.Stop() {
  1459  				<-timer.C
  1460  				if d.isScanner {
  1461  					globalScannerMetrics.incTime(scannerMetricYield, wantSleep)
  1462  				}
  1463  			}
  1464  			return
  1465  		case <-timer.C:
  1466  			if d.isScanner {
  1467  				globalScannerMetrics.incTime(scannerMetricYield, wantSleep)
  1468  			}
  1469  			return
  1470  		case <-cycle:
  1471  			if !timer.Stop() {
  1472  				// We expired.
  1473  				<-timer.C
  1474  				if d.isScanner {
  1475  					globalScannerMetrics.incTime(scannerMetricYield, wantSleep)
  1476  				}
  1477  				return
  1478  			}
  1479  		}
  1480  	}
  1481  }
  1482  
  1483  // Update the current settings and cycle all waiting.
  1484  // Parameters are the same as in the constructor.
  1485  func (d *dynamicSleeper) Update(factor float64, maxWait time.Duration) error {
  1486  	d.mu.Lock()
  1487  	defer d.mu.Unlock()
  1488  	if math.Abs(d.factor-factor) < 1e-10 && d.maxSleep == maxWait {
  1489  		return nil
  1490  	}
  1491  	// Update values and cycle waiting.
  1492  	xioutil.SafeClose(d.cycle)
  1493  	d.factor = factor
  1494  	d.maxSleep = maxWait
  1495  	d.cycle = make(chan struct{})
  1496  	return nil
  1497  }
  1498  
  1499  const (
  1500  	// ILMExpiry - audit trail for ILM expiry
  1501  	ILMExpiry = "ilm:expiry"
  1502  	// ILMFreeVersionDelete - audit trail for ILM free-version delete
  1503  	ILMFreeVersionDelete = "ilm:free-version-delete"
  1504  	// ILMTransition - audit trail for ILM transitioning.
  1505  	ILMTransition = " ilm:transition"
  1506  )
  1507  
  1508  func auditLogLifecycle(ctx context.Context, oi ObjectInfo, event string, tags map[string]interface{}, traceFn func(event string)) {
  1509  	var apiName string
  1510  	switch event {
  1511  	case ILMExpiry:
  1512  		apiName = "ILMExpiry"
  1513  	case ILMFreeVersionDelete:
  1514  		apiName = "ILMFreeVersionDelete"
  1515  	case ILMTransition:
  1516  		apiName = "ILMTransition"
  1517  	}
  1518  	auditLogInternal(ctx, AuditLogOptions{
  1519  		Event:     event,
  1520  		APIName:   apiName,
  1521  		Bucket:    oi.Bucket,
  1522  		Object:    oi.Name,
  1523  		VersionID: oi.VersionID,
  1524  		Tags:      tags,
  1525  	})
  1526  	traceFn(event)
  1527  }