storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/global-heal.go (about)

     1  /*
     2   * MinIO Cloud Storage, (C) 2019 MinIO, Inc.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package cmd
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"sort"
    24  	"time"
    25  
    26  	"storj.io/minio/cmd/logger"
    27  	"storj.io/minio/pkg/color"
    28  	"storj.io/minio/pkg/console"
    29  	"storj.io/minio/pkg/madmin"
    30  	"storj.io/minio/pkg/wildcard"
    31  )
    32  
    33  const (
    34  	bgHealingUUID = "0000-0000-0000-0000"
    35  )
    36  
    37  // NewBgHealSequence creates a background healing sequence
    38  // operation which scans all objects and heal them.
    39  func newBgHealSequence() *healSequence {
    40  	reqInfo := &logger.ReqInfo{API: "BackgroundHeal"}
    41  	ctx, cancelCtx := context.WithCancel(logger.SetReqInfo(GlobalContext, reqInfo))
    42  
    43  	hs := madmin.HealOpts{
    44  		// Remove objects that do not have read-quorum
    45  		Remove:   true,
    46  		ScanMode: madmin.HealNormalScan,
    47  	}
    48  
    49  	return &healSequence{
    50  		sourceCh:    make(chan healSource),
    51  		respCh:      make(chan healResult),
    52  		startTime:   UTCNow(),
    53  		clientToken: bgHealingUUID,
    54  		// run-background heal with reserved bucket
    55  		bucket:   minioReservedBucket,
    56  		settings: hs,
    57  		currentStatus: healSequenceStatus{
    58  			Summary:      healNotStartedStatus,
    59  			HealSettings: hs,
    60  		},
    61  		cancelCtx:          cancelCtx,
    62  		ctx:                ctx,
    63  		reportProgress:     false,
    64  		scannedItemsMap:    make(map[madmin.HealItemType]int64),
    65  		healedItemsMap:     make(map[madmin.HealItemType]int64),
    66  		healFailedItemsMap: make(map[string]int64),
    67  	}
    68  }
    69  
    70  // getBackgroundHealStatus will return the
    71  func getBackgroundHealStatus(ctx context.Context, o ObjectLayer) (madmin.BgHealState, bool) {
    72  	if globalBackgroundHealState == nil {
    73  		return madmin.BgHealState{}, false
    74  	}
    75  
    76  	bgSeq, ok := globalBackgroundHealState.getHealSequenceByToken(bgHealingUUID)
    77  	if !ok {
    78  		return madmin.BgHealState{}, false
    79  	}
    80  
    81  	var healDisksMap = map[string]struct{}{}
    82  	for _, ep := range getLocalDisksToHeal() {
    83  		healDisksMap[ep.String()] = struct{}{}
    84  	}
    85  	status := madmin.BgHealState{
    86  		ScannedItemsCount: bgSeq.getScannedItemsCount(),
    87  	}
    88  
    89  	if o == nil {
    90  		healing := globalBackgroundHealState.getLocalHealingDisks()
    91  		for _, disk := range healing {
    92  			status.HealDisks = append(status.HealDisks, disk.Endpoint)
    93  		}
    94  
    95  		return status, true
    96  	}
    97  
    98  	// ignores any errors here.
    99  	si, _ := o.StorageInfo(ctx)
   100  
   101  	indexed := make(map[string][]madmin.Disk)
   102  	for _, disk := range si.Disks {
   103  		setIdx := fmt.Sprintf("%d-%d", disk.PoolIndex, disk.SetIndex)
   104  		indexed[setIdx] = append(indexed[setIdx], disk)
   105  	}
   106  
   107  	for id, disks := range indexed {
   108  		ss := madmin.SetStatus{
   109  			ID:        id,
   110  			SetIndex:  disks[0].SetIndex,
   111  			PoolIndex: disks[0].PoolIndex,
   112  		}
   113  		for _, disk := range disks {
   114  			ss.Disks = append(ss.Disks, disk)
   115  			if disk.Healing {
   116  				ss.HealStatus = "Healing"
   117  				ss.HealPriority = "high"
   118  				status.HealDisks = append(status.HealDisks, disk.Endpoint)
   119  			}
   120  		}
   121  		sortDisks(ss.Disks)
   122  		status.Sets = append(status.Sets, ss)
   123  	}
   124  	sort.Slice(status.Sets, func(i, j int) bool {
   125  		return status.Sets[i].ID < status.Sets[j].ID
   126  	})
   127  
   128  	return status, true
   129  
   130  }
   131  
   132  func mustGetHealSequence(ctx context.Context) *healSequence {
   133  	// Get background heal sequence to send elements to heal
   134  	for {
   135  		globalHealStateLK.RLock()
   136  		hstate := globalBackgroundHealState
   137  		globalHealStateLK.RUnlock()
   138  
   139  		if hstate == nil {
   140  			time.Sleep(time.Second)
   141  			continue
   142  		}
   143  
   144  		bgSeq, ok := hstate.getHealSequenceByToken(bgHealingUUID)
   145  		if !ok {
   146  			time.Sleep(time.Second)
   147  			continue
   148  		}
   149  		return bgSeq
   150  	}
   151  }
   152  
   153  // healErasureSet lists and heals all objects in a specific erasure set
   154  func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []BucketInfo, tracker *healingTracker) error {
   155  	bgSeq := mustGetHealSequence(ctx)
   156  	buckets = append(buckets, BucketInfo{
   157  		Name: pathJoin(minioMetaBucket, minioConfigPrefix),
   158  	})
   159  
   160  	// Try to pro-actively heal backend-encrypted file.
   161  	if _, err := er.HealObject(ctx, minioMetaBucket, backendEncryptedFile, "", madmin.HealOpts{}); err != nil {
   162  		if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
   163  			logger.LogIf(ctx, err)
   164  		}
   165  	}
   166  
   167  	// Heal all buckets with all objects
   168  	for _, bucket := range buckets {
   169  		if tracker.isHealed(bucket.Name) {
   170  			continue
   171  		}
   172  		var forwardTo string
   173  		// If we resume to the same bucket, forward to last known item.
   174  		if tracker.Bucket != "" {
   175  			if tracker.Bucket == bucket.Name {
   176  				forwardTo = tracker.Bucket
   177  			} else {
   178  				// Reset to where last bucket ended if resuming.
   179  				tracker.resume()
   180  			}
   181  		}
   182  		tracker.Object = ""
   183  		tracker.Bucket = bucket.Name
   184  		// Heal current bucket
   185  		if _, err := er.HealBucket(ctx, bucket.Name, madmin.HealOpts{}); err != nil {
   186  			if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
   187  				logger.LogIf(ctx, err)
   188  			}
   189  		}
   190  
   191  		if serverDebugLog {
   192  			console.Debugf(color.Green("healDisk:")+" healing bucket %s content on erasure set %d\n", bucket.Name, tracker.SetIndex+1)
   193  		}
   194  
   195  		disks, _ := er.getOnlineDisksWithHealing()
   196  		if len(disks) == 0 {
   197  			return errors.New("healErasureSet: No non-healing disks found")
   198  		}
   199  
   200  		// Limit listing to 3 drives.
   201  		if len(disks) > 3 {
   202  			disks = disks[:3]
   203  		}
   204  
   205  		healEntry := func(entry metaCacheEntry) {
   206  			if entry.isDir() {
   207  				return
   208  			}
   209  			// We might land at .metacache, .trash, .multipart
   210  			// no need to heal them skip, only when bucket
   211  			// is '.minio.sys'
   212  			if bucket.Name == minioMetaBucket {
   213  				if wildcard.Match("buckets/*/.metacache/*", entry.name) {
   214  					return
   215  				}
   216  				if wildcard.Match("tmp/.trash/*", entry.name) {
   217  					return
   218  				}
   219  				if wildcard.Match("multipart/*", entry.name) {
   220  					return
   221  				}
   222  			}
   223  			fivs, err := entry.fileInfoVersions(bucket.Name)
   224  			if err != nil {
   225  				logger.LogIf(ctx, err)
   226  				return
   227  			}
   228  			waitForLowHTTPReq(globalHealConfig.IOCount, globalHealConfig.Sleep)
   229  			for _, version := range fivs.Versions {
   230  				if _, err := er.HealObject(ctx, bucket.Name, version.Name, version.VersionID, madmin.HealOpts{
   231  					ScanMode: madmin.HealNormalScan, Remove: healDeleteDangling}); err != nil {
   232  					if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
   233  						// If not deleted, assume they failed.
   234  						tracker.ObjectsFailed++
   235  						tracker.BytesFailed += uint64(version.Size)
   236  						logger.LogIf(ctx, err)
   237  					}
   238  				} else {
   239  					tracker.ObjectsHealed++
   240  					tracker.BytesDone += uint64(version.Size)
   241  				}
   242  				bgSeq.logHeal(madmin.HealItemObject)
   243  			}
   244  			tracker.Object = entry.name
   245  			if time.Since(tracker.LastUpdate) > time.Minute {
   246  				logger.LogIf(ctx, tracker.update(ctx))
   247  			}
   248  		}
   249  
   250  		// How to resolve partial results.
   251  		resolver := metadataResolutionParams{
   252  			dirQuorum: 1,
   253  			objQuorum: 1,
   254  			bucket:    bucket.Name,
   255  		}
   256  
   257  		err := listPathRaw(ctx, listPathRawOptions{
   258  			disks:          disks,
   259  			bucket:         bucket.Name,
   260  			recursive:      true,
   261  			forwardTo:      forwardTo,
   262  			minDisks:       1,
   263  			reportNotFound: false,
   264  			agreed:         healEntry,
   265  			partial: func(entries metaCacheEntries, nAgreed int, errs []error) {
   266  				entry, ok := entries.resolve(&resolver)
   267  				if ok {
   268  					healEntry(*entry)
   269  				}
   270  			},
   271  			finished: nil,
   272  		})
   273  
   274  		select {
   275  		// If context is canceled don't mark as done...
   276  		case <-ctx.Done():
   277  			return ctx.Err()
   278  		default:
   279  			logger.LogIf(ctx, err)
   280  			tracker.bucketDone(bucket.Name)
   281  			logger.LogIf(ctx, tracker.update(ctx))
   282  		}
   283  	}
   284  	tracker.Object = ""
   285  	tracker.Bucket = ""
   286  
   287  	return nil
   288  }
   289  
   290  // healObject heals given object path in deep to fix bitrot.
   291  func healObject(bucket, object, versionID string, scan madmin.HealScanMode) {
   292  	// Get background heal sequence to send elements to heal
   293  	bgSeq, ok := globalBackgroundHealState.getHealSequenceByToken(bgHealingUUID)
   294  	if ok {
   295  		bgSeq.sourceCh <- healSource{
   296  			bucket:    bucket,
   297  			object:    object,
   298  			versionID: versionID,
   299  			opts: &madmin.HealOpts{
   300  				Remove:   true, // if found dangling purge it.
   301  				ScanMode: scan,
   302  			},
   303  		}
   304  	}
   305  }