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 }