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

     1  // Copyright (c) 2015-2023 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  	"bytes"
    22  	"context"
    23  	"encoding/binary"
    24  	"encoding/json"
    25  	"errors"
    26  	"fmt"
    27  	"io"
    28  	"math/rand"
    29  	"net/http"
    30  	"net/url"
    31  	"runtime"
    32  	"strconv"
    33  	"strings"
    34  	"sync"
    35  	"time"
    36  
    37  	"github.com/dustin/go-humanize"
    38  	"github.com/lithammer/shortuuid/v4"
    39  	"github.com/minio/madmin-go/v3"
    40  	"github.com/minio/minio-go/v7"
    41  	miniogo "github.com/minio/minio-go/v7"
    42  	"github.com/minio/minio-go/v7/pkg/credentials"
    43  	"github.com/minio/minio-go/v7/pkg/encrypt"
    44  	"github.com/minio/minio-go/v7/pkg/tags"
    45  	"github.com/minio/minio/internal/config/batch"
    46  	"github.com/minio/minio/internal/crypto"
    47  	"github.com/minio/minio/internal/hash"
    48  	xhttp "github.com/minio/minio/internal/http"
    49  	"github.com/minio/minio/internal/ioutil"
    50  	xioutil "github.com/minio/minio/internal/ioutil"
    51  	"github.com/minio/minio/internal/logger"
    52  	"github.com/minio/pkg/v2/console"
    53  	"github.com/minio/pkg/v2/env"
    54  	"github.com/minio/pkg/v2/policy"
    55  	"github.com/minio/pkg/v2/workers"
    56  	"gopkg.in/yaml.v3"
    57  )
    58  
    59  var globalBatchConfig batch.Config
    60  
    61  // BatchJobRequest this is an internal data structure not for external consumption.
    62  type BatchJobRequest struct {
    63  	ID        string               `yaml:"-" json:"name"`
    64  	User      string               `yaml:"-" json:"user"`
    65  	Started   time.Time            `yaml:"-" json:"started"`
    66  	Replicate *BatchJobReplicateV1 `yaml:"replicate" json:"replicate"`
    67  	KeyRotate *BatchJobKeyRotateV1 `yaml:"keyrotate" json:"keyrotate"`
    68  	Expire    *BatchJobExpire      `yaml:"expire" json:"expire"`
    69  	ctx       context.Context      `msg:"-"`
    70  }
    71  
    72  func notifyEndpoint(ctx context.Context, ri *batchJobInfo, endpoint, token string) error {
    73  	if endpoint == "" {
    74  		return nil
    75  	}
    76  
    77  	buf, err := json.Marshal(ri)
    78  	if err != nil {
    79  		return err
    80  	}
    81  
    82  	ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
    83  	defer cancel()
    84  
    85  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(buf))
    86  	if err != nil {
    87  		return err
    88  	}
    89  
    90  	if token != "" {
    91  		req.Header.Set("Authorization", token)
    92  	}
    93  	req.Header.Set("Content-Type", "application/json")
    94  
    95  	clnt := http.Client{Transport: getRemoteInstanceTransport}
    96  	resp, err := clnt.Do(req)
    97  	if err != nil {
    98  		return err
    99  	}
   100  
   101  	xhttp.DrainBody(resp.Body)
   102  	if resp.StatusCode != http.StatusOK {
   103  		return errors.New(resp.Status)
   104  	}
   105  
   106  	return nil
   107  }
   108  
   109  // Notify notifies notification endpoint if configured regarding job failure or success.
   110  func (r BatchJobReplicateV1) Notify(ctx context.Context, ri *batchJobInfo) error {
   111  	return notifyEndpoint(ctx, ri, r.Flags.Notify.Endpoint, r.Flags.Notify.Token)
   112  }
   113  
   114  // ReplicateFromSource - this is not implemented yet where source is 'remote' and target is local.
   115  func (r *BatchJobReplicateV1) ReplicateFromSource(ctx context.Context, api ObjectLayer, core *miniogo.Core, srcObjInfo ObjectInfo, retry bool) error {
   116  	srcBucket := r.Source.Bucket
   117  	tgtBucket := r.Target.Bucket
   118  	srcObject := srcObjInfo.Name
   119  	tgtObject := srcObjInfo.Name
   120  	if r.Target.Prefix != "" {
   121  		tgtObject = pathJoin(r.Target.Prefix, srcObjInfo.Name)
   122  	}
   123  
   124  	versionID := srcObjInfo.VersionID
   125  	if r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3 {
   126  		versionID = ""
   127  	}
   128  	if srcObjInfo.DeleteMarker {
   129  		_, err := api.DeleteObject(ctx, tgtBucket, tgtObject, ObjectOptions{
   130  			VersionID: versionID,
   131  			// Since we are preserving a delete marker, we have to make sure this is always true.
   132  			// regardless of the current configuration of the bucket we must preserve all versions
   133  			// on the pool being batch replicated from source.
   134  			Versioned:          true,
   135  			MTime:              srcObjInfo.ModTime,
   136  			DeleteMarker:       srcObjInfo.DeleteMarker,
   137  			ReplicationRequest: true,
   138  		})
   139  		return err
   140  	}
   141  
   142  	opts := ObjectOptions{
   143  		VersionID:    srcObjInfo.VersionID,
   144  		MTime:        srcObjInfo.ModTime,
   145  		PreserveETag: srcObjInfo.ETag,
   146  		UserDefined:  srcObjInfo.UserDefined,
   147  	}
   148  	if r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3 {
   149  		opts.VersionID = ""
   150  	}
   151  	if crypto.S3.IsEncrypted(srcObjInfo.UserDefined) {
   152  		opts.ServerSideEncryption = encrypt.NewSSE()
   153  	}
   154  	slc := strings.Split(srcObjInfo.ETag, "-")
   155  	if len(slc) == 2 {
   156  		partsCount, err := strconv.Atoi(slc[1])
   157  		if err != nil {
   158  			return err
   159  		}
   160  		return r.copyWithMultipartfromSource(ctx, api, core, srcObjInfo, opts, partsCount)
   161  	}
   162  	gopts := miniogo.GetObjectOptions{
   163  		VersionID: srcObjInfo.VersionID,
   164  	}
   165  	if err := gopts.SetMatchETag(srcObjInfo.ETag); err != nil {
   166  		return err
   167  	}
   168  	rd, objInfo, _, err := core.GetObject(ctx, srcBucket, srcObject, gopts)
   169  	if err != nil {
   170  		return ErrorRespToObjectError(err, srcBucket, srcObject, srcObjInfo.VersionID)
   171  	}
   172  	defer rd.Close()
   173  
   174  	hr, err := hash.NewReader(ctx, rd, objInfo.Size, "", "", objInfo.Size)
   175  	if err != nil {
   176  		return err
   177  	}
   178  	pReader := NewPutObjReader(hr)
   179  	_, err = api.PutObject(ctx, tgtBucket, tgtObject, pReader, opts)
   180  	return err
   181  }
   182  
   183  func (r *BatchJobReplicateV1) copyWithMultipartfromSource(ctx context.Context, api ObjectLayer, c *miniogo.Core, srcObjInfo ObjectInfo, opts ObjectOptions, partsCount int) (err error) {
   184  	srcBucket := r.Source.Bucket
   185  	tgtBucket := r.Target.Bucket
   186  	srcObject := srcObjInfo.Name
   187  	tgtObject := srcObjInfo.Name
   188  	if r.Target.Prefix != "" {
   189  		tgtObject = pathJoin(r.Target.Prefix, srcObjInfo.Name)
   190  	}
   191  	if r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3 {
   192  		opts.VersionID = ""
   193  	}
   194  	var uploadedParts []CompletePart
   195  	res, err := api.NewMultipartUpload(context.Background(), tgtBucket, tgtObject, opts)
   196  	if err != nil {
   197  		return err
   198  	}
   199  
   200  	defer func() {
   201  		if err != nil {
   202  			// block and abort remote upload upon failure.
   203  			attempts := 1
   204  			for attempts <= 3 {
   205  				aerr := api.AbortMultipartUpload(ctx, tgtBucket, tgtObject, res.UploadID, ObjectOptions{})
   206  				if aerr == nil {
   207  					return
   208  				}
   209  				logger.LogIf(ctx,
   210  					fmt.Errorf("trying %s: Unable to cleanup failed multipart replication %s on remote %s/%s: %w - this may consume space on remote cluster",
   211  						humanize.Ordinal(attempts), res.UploadID, tgtBucket, tgtObject, aerr))
   212  				attempts++
   213  				time.Sleep(time.Second)
   214  			}
   215  		}
   216  	}()
   217  
   218  	var (
   219  		hr    *hash.Reader
   220  		pInfo PartInfo
   221  	)
   222  
   223  	for i := 0; i < partsCount; i++ {
   224  		gopts := miniogo.GetObjectOptions{
   225  			VersionID:  srcObjInfo.VersionID,
   226  			PartNumber: i + 1,
   227  		}
   228  		if err := gopts.SetMatchETag(srcObjInfo.ETag); err != nil {
   229  			return err
   230  		}
   231  		rd, objInfo, _, err := c.GetObject(ctx, srcBucket, srcObject, gopts)
   232  		if err != nil {
   233  			return ErrorRespToObjectError(err, srcBucket, srcObject, srcObjInfo.VersionID)
   234  		}
   235  		defer rd.Close()
   236  
   237  		hr, err = hash.NewReader(ctx, io.LimitReader(rd, objInfo.Size), objInfo.Size, "", "", objInfo.Size)
   238  		if err != nil {
   239  			return err
   240  		}
   241  		pReader := NewPutObjReader(hr)
   242  		opts.PreserveETag = ""
   243  		pInfo, err = api.PutObjectPart(ctx, tgtBucket, tgtObject, res.UploadID, i+1, pReader, opts)
   244  		if err != nil {
   245  			return err
   246  		}
   247  		if pInfo.Size != objInfo.Size {
   248  			return fmt.Errorf("Part size mismatch: got %d, want %d", pInfo.Size, objInfo.Size)
   249  		}
   250  		uploadedParts = append(uploadedParts, CompletePart{
   251  			PartNumber: pInfo.PartNumber,
   252  			ETag:       pInfo.ETag,
   253  		})
   254  	}
   255  	_, err = api.CompleteMultipartUpload(ctx, tgtBucket, tgtObject, res.UploadID, uploadedParts, opts)
   256  	return err
   257  }
   258  
   259  // StartFromSource starts the batch replication job from remote source, resumes if there was a pending job via "job.ID"
   260  func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLayer, job BatchJobRequest) error {
   261  	ri := &batchJobInfo{
   262  		JobID:     job.ID,
   263  		JobType:   string(job.Type()),
   264  		StartTime: job.Started,
   265  	}
   266  	if err := ri.load(ctx, api, job); err != nil {
   267  		return err
   268  	}
   269  	if ri.Complete {
   270  		return nil
   271  	}
   272  	globalBatchJobsMetrics.save(job.ID, ri)
   273  
   274  	delay := job.Replicate.Flags.Retry.Delay
   275  	if delay == 0 {
   276  		delay = batchReplJobDefaultRetryDelay
   277  	}
   278  	rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
   279  
   280  	hasTags := len(r.Flags.Filter.Tags) != 0
   281  	isMetadata := len(r.Flags.Filter.Metadata) != 0
   282  	isStorageClassOnly := len(r.Flags.Filter.Metadata) == 1 && strings.EqualFold(r.Flags.Filter.Metadata[0].Key, xhttp.AmzStorageClass)
   283  
   284  	skip := func(oi ObjectInfo) (ok bool) {
   285  		if r.Flags.Filter.OlderThan > 0 && time.Since(oi.ModTime) < r.Flags.Filter.OlderThan {
   286  			// skip all objects that are newer than specified older duration
   287  			return true
   288  		}
   289  
   290  		if r.Flags.Filter.NewerThan > 0 && time.Since(oi.ModTime) >= r.Flags.Filter.NewerThan {
   291  			// skip all objects that are older than specified newer duration
   292  			return true
   293  		}
   294  
   295  		if !r.Flags.Filter.CreatedAfter.IsZero() && r.Flags.Filter.CreatedAfter.Before(oi.ModTime) {
   296  			// skip all objects that are created before the specified time.
   297  			return true
   298  		}
   299  
   300  		if !r.Flags.Filter.CreatedBefore.IsZero() && r.Flags.Filter.CreatedBefore.After(oi.ModTime) {
   301  			// skip all objects that are created after the specified time.
   302  			return true
   303  		}
   304  
   305  		if hasTags {
   306  			// Only parse object tags if tags filter is specified.
   307  			tagMap := map[string]string{}
   308  			tagStr := oi.UserTags
   309  			if len(tagStr) != 0 {
   310  				t, err := tags.ParseObjectTags(tagStr)
   311  				if err != nil {
   312  					return false
   313  				}
   314  				tagMap = t.ToMap()
   315  			}
   316  			for _, kv := range r.Flags.Filter.Tags {
   317  				for t, v := range tagMap {
   318  					if kv.Match(BatchJobKV{Key: t, Value: v}) {
   319  						return true
   320  					}
   321  				}
   322  			}
   323  
   324  			// None of the provided tags filter match skip the object
   325  			return false
   326  		}
   327  
   328  		for _, kv := range r.Flags.Filter.Metadata {
   329  			for k, v := range oi.UserDefined {
   330  				if !stringsHasPrefixFold(k, "x-amz-meta-") && !isStandardHeader(k) {
   331  					continue
   332  				}
   333  				// We only need to match x-amz-meta or standardHeaders
   334  				if kv.Match(BatchJobKV{Key: k, Value: v}) {
   335  					return true
   336  				}
   337  			}
   338  		}
   339  
   340  		// None of the provided filters match
   341  		return false
   342  	}
   343  
   344  	u, err := url.Parse(r.Source.Endpoint)
   345  	if err != nil {
   346  		return err
   347  	}
   348  
   349  	cred := r.Source.Creds
   350  
   351  	c, err := miniogo.New(u.Host, &miniogo.Options{
   352  		Creds:        credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken),
   353  		Secure:       u.Scheme == "https",
   354  		Transport:    getRemoteInstanceTransport,
   355  		BucketLookup: lookupStyle(r.Source.Path),
   356  	})
   357  	if err != nil {
   358  		return err
   359  	}
   360  
   361  	c.SetAppInfo("minio-"+batchJobPrefix, r.APIVersion+" "+job.ID)
   362  	core := &miniogo.Core{Client: c}
   363  
   364  	workerSize, err := strconv.Atoi(env.Get("_MINIO_BATCH_REPLICATION_WORKERS", strconv.Itoa(runtime.GOMAXPROCS(0)/2)))
   365  	if err != nil {
   366  		return err
   367  	}
   368  
   369  	wk, err := workers.New(workerSize)
   370  	if err != nil {
   371  		// invalid worker size.
   372  		return err
   373  	}
   374  
   375  	retryAttempts := ri.RetryAttempts
   376  	retry := false
   377  	for attempts := 1; attempts <= retryAttempts; attempts++ {
   378  		attempts := attempts
   379  		// one of source/target is s3, skip delete marker and all versions under the same object name.
   380  		s3Type := r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3
   381  		minioSrc := r.Source.Type == BatchJobReplicateResourceMinIO
   382  		ctx, cancel := context.WithCancel(ctx)
   383  		objInfoCh := c.ListObjects(ctx, r.Source.Bucket, miniogo.ListObjectsOptions{
   384  			Prefix:       r.Source.Prefix,
   385  			WithVersions: minioSrc,
   386  			Recursive:    true,
   387  			WithMetadata: true,
   388  		})
   389  		prevObj := ""
   390  		skipReplicate := false
   391  
   392  		for obj := range objInfoCh {
   393  			oi := toObjectInfo(r.Source.Bucket, obj.Key, obj)
   394  			if !minioSrc {
   395  				// Check if metadata filter was requested and it is expected to have
   396  				// all user metadata or just storageClass. If its only storageClass
   397  				// List() already returns relevant information for filter to be applied.
   398  				if isMetadata && !isStorageClassOnly {
   399  					oi2, err := c.StatObject(ctx, r.Source.Bucket, obj.Key, miniogo.StatObjectOptions{})
   400  					if err == nil {
   401  						oi = toObjectInfo(r.Source.Bucket, obj.Key, oi2)
   402  					} else {
   403  						if !isErrMethodNotAllowed(ErrorRespToObjectError(err, r.Source.Bucket, obj.Key)) &&
   404  							!isErrObjectNotFound(ErrorRespToObjectError(err, r.Source.Bucket, obj.Key)) {
   405  							logger.LogIf(ctx, err)
   406  						}
   407  						continue
   408  					}
   409  				}
   410  				if hasTags {
   411  					tags, err := c.GetObjectTagging(ctx, r.Source.Bucket, obj.Key, minio.GetObjectTaggingOptions{})
   412  					if err == nil {
   413  						oi.UserTags = tags.String()
   414  					} else {
   415  						if !isErrMethodNotAllowed(ErrorRespToObjectError(err, r.Source.Bucket, obj.Key)) &&
   416  							!isErrObjectNotFound(ErrorRespToObjectError(err, r.Source.Bucket, obj.Key)) {
   417  							logger.LogIf(ctx, err)
   418  						}
   419  						continue
   420  					}
   421  				}
   422  			}
   423  			if skip(oi) {
   424  				continue
   425  			}
   426  			if obj.Key != prevObj {
   427  				prevObj = obj.Key
   428  				// skip replication of delete marker and all versions under the same object name if one of source or target is s3.
   429  				skipReplicate = obj.IsDeleteMarker && s3Type
   430  			}
   431  			if skipReplicate {
   432  				continue
   433  			}
   434  
   435  			wk.Take()
   436  			go func() {
   437  				defer wk.Give()
   438  				stopFn := globalBatchJobsMetrics.trace(batchJobMetricReplication, job.ID, attempts)
   439  				success := true
   440  				if err := r.ReplicateFromSource(ctx, api, core, oi, retry); err != nil {
   441  					// object must be deleted concurrently, allow these failures but do not count them
   442  					if isErrVersionNotFound(err) || isErrObjectNotFound(err) {
   443  						return
   444  					}
   445  					stopFn(oi, err)
   446  					logger.LogIf(ctx, err)
   447  					success = false
   448  				} else {
   449  					stopFn(oi, nil)
   450  				}
   451  				ri.trackCurrentBucketObject(r.Target.Bucket, oi, success)
   452  				globalBatchJobsMetrics.save(job.ID, ri)
   453  				// persist in-memory state to disk after every 10secs.
   454  				logger.LogIf(ctx, ri.updateAfter(ctx, api, 10*time.Second, job))
   455  
   456  				if wait := globalBatchConfig.ReplicationWait(); wait > 0 {
   457  					time.Sleep(wait)
   458  				}
   459  			}()
   460  		}
   461  		wk.Wait()
   462  
   463  		ri.RetryAttempts = attempts
   464  		ri.Complete = ri.ObjectsFailed == 0
   465  		ri.Failed = ri.ObjectsFailed > 0
   466  
   467  		globalBatchJobsMetrics.save(job.ID, ri)
   468  		// persist in-memory state to disk.
   469  		logger.LogIf(ctx, ri.updateAfter(ctx, api, 0, job))
   470  
   471  		if err := r.Notify(ctx, ri); err != nil {
   472  			logger.LogIf(ctx, fmt.Errorf("unable to notify %v", err))
   473  		}
   474  
   475  		cancel()
   476  		if ri.Failed {
   477  			ri.ObjectsFailed = 0
   478  			ri.Bucket = ""
   479  			ri.Object = ""
   480  			ri.Objects = 0
   481  			ri.BytesFailed = 0
   482  			ri.BytesTransferred = 0
   483  			retry = true // indicate we are retrying..
   484  			time.Sleep(delay + time.Duration(rnd.Float64()*float64(delay)))
   485  			continue
   486  		}
   487  
   488  		break
   489  	}
   490  
   491  	return nil
   492  }
   493  
   494  // toObjectInfo converts minio.ObjectInfo to ObjectInfo
   495  func toObjectInfo(bucket, object string, objInfo miniogo.ObjectInfo) ObjectInfo {
   496  	tags, _ := tags.MapToObjectTags(objInfo.UserTags)
   497  	oi := ObjectInfo{
   498  		Bucket:                    bucket,
   499  		Name:                      object,
   500  		ModTime:                   objInfo.LastModified,
   501  		Size:                      objInfo.Size,
   502  		ETag:                      objInfo.ETag,
   503  		VersionID:                 objInfo.VersionID,
   504  		IsLatest:                  objInfo.IsLatest,
   505  		DeleteMarker:              objInfo.IsDeleteMarker,
   506  		ContentType:               objInfo.ContentType,
   507  		Expires:                   objInfo.Expires,
   508  		StorageClass:              objInfo.StorageClass,
   509  		ReplicationStatusInternal: objInfo.ReplicationStatus,
   510  		UserTags:                  tags.String(),
   511  	}
   512  
   513  	oi.UserDefined = make(map[string]string, len(objInfo.Metadata))
   514  	for k, v := range objInfo.Metadata {
   515  		oi.UserDefined[k] = v[0]
   516  	}
   517  
   518  	ce, ok := oi.UserDefined[xhttp.ContentEncoding]
   519  	if !ok {
   520  		ce, ok = oi.UserDefined[strings.ToLower(xhttp.ContentEncoding)]
   521  	}
   522  	if ok {
   523  		oi.ContentEncoding = ce
   524  	}
   525  
   526  	_, ok = oi.UserDefined[xhttp.AmzStorageClass]
   527  	if !ok {
   528  		oi.UserDefined[xhttp.AmzStorageClass] = objInfo.StorageClass
   529  	}
   530  
   531  	for k, v := range objInfo.UserMetadata {
   532  		oi.UserDefined[k] = v
   533  	}
   534  
   535  	return oi
   536  }
   537  
   538  func (r BatchJobReplicateV1) writeAsArchive(ctx context.Context, objAPI ObjectLayer, remoteClnt *minio.Client, entries []ObjectInfo) error {
   539  	input := make(chan minio.SnowballObject, 1)
   540  	opts := minio.SnowballOptions{
   541  		Opts:     minio.PutObjectOptions{},
   542  		InMemory: *r.Source.Snowball.InMemory,
   543  		Compress: *r.Source.Snowball.Compress,
   544  		SkipErrs: *r.Source.Snowball.SkipErrs,
   545  	}
   546  
   547  	go func() {
   548  		defer xioutil.SafeClose(input)
   549  
   550  		for _, entry := range entries {
   551  			gr, err := objAPI.GetObjectNInfo(ctx, r.Source.Bucket,
   552  				entry.Name, nil, nil, ObjectOptions{
   553  					VersionID: entry.VersionID,
   554  				})
   555  			if err != nil {
   556  				logger.LogIf(ctx, err)
   557  				continue
   558  			}
   559  
   560  			snowballObj := minio.SnowballObject{
   561  				// Create path to store objects within the bucket.
   562  				Key:       entry.Name,
   563  				Size:      entry.Size,
   564  				ModTime:   entry.ModTime,
   565  				VersionID: entry.VersionID,
   566  				Content:   gr,
   567  				Headers:   make(http.Header),
   568  				Close: func() {
   569  					gr.Close()
   570  				},
   571  			}
   572  
   573  			opts, err := batchReplicationOpts(ctx, "", gr.ObjInfo)
   574  			if err != nil {
   575  				logger.LogIf(ctx, err)
   576  				continue
   577  			}
   578  
   579  			for k, vals := range opts.Header() {
   580  				for _, v := range vals {
   581  					snowballObj.Headers.Add(k, v)
   582  				}
   583  			}
   584  
   585  			input <- snowballObj
   586  		}
   587  	}()
   588  
   589  	// Collect and upload all entries.
   590  	return remoteClnt.PutObjectsSnowball(ctx, r.Target.Bucket, opts, input)
   591  }
   592  
   593  // ReplicateToTarget read from source and replicate to configured target
   594  func (r *BatchJobReplicateV1) ReplicateToTarget(ctx context.Context, api ObjectLayer, c *miniogo.Core, srcObjInfo ObjectInfo, retry bool) error {
   595  	srcBucket := r.Source.Bucket
   596  	tgtBucket := r.Target.Bucket
   597  	tgtPrefix := r.Target.Prefix
   598  	srcObject := srcObjInfo.Name
   599  	s3Type := r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3
   600  
   601  	if srcObjInfo.DeleteMarker || !srcObjInfo.VersionPurgeStatus.Empty() {
   602  		if retry && !s3Type {
   603  			if _, err := c.StatObject(ctx, tgtBucket, pathJoin(tgtPrefix, srcObject), miniogo.StatObjectOptions{
   604  				VersionID: srcObjInfo.VersionID,
   605  				Internal: miniogo.AdvancedGetOptions{
   606  					ReplicationProxyRequest: "false",
   607  				},
   608  			}); isErrMethodNotAllowed(ErrorRespToObjectError(err, tgtBucket, pathJoin(tgtPrefix, srcObject))) {
   609  				return nil
   610  			}
   611  		}
   612  
   613  		versionID := srcObjInfo.VersionID
   614  		dmVersionID := ""
   615  		if srcObjInfo.VersionPurgeStatus.Empty() {
   616  			dmVersionID = srcObjInfo.VersionID
   617  		}
   618  		if r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3 {
   619  			dmVersionID = ""
   620  			versionID = ""
   621  		}
   622  		return c.RemoveObject(ctx, tgtBucket, pathJoin(tgtPrefix, srcObject), miniogo.RemoveObjectOptions{
   623  			VersionID: versionID,
   624  			Internal: miniogo.AdvancedRemoveOptions{
   625  				ReplicationDeleteMarker: dmVersionID != "",
   626  				ReplicationMTime:        srcObjInfo.ModTime,
   627  				ReplicationStatus:       miniogo.ReplicationStatusReplica,
   628  				ReplicationRequest:      true, // always set this to distinguish between `mc mirror` replication and serverside
   629  			},
   630  		})
   631  	}
   632  
   633  	if retry && !s3Type { // when we are retrying avoid copying if necessary.
   634  		gopts := miniogo.GetObjectOptions{}
   635  		if err := gopts.SetMatchETag(srcObjInfo.ETag); err != nil {
   636  			return err
   637  		}
   638  		if _, err := c.StatObject(ctx, tgtBucket, pathJoin(tgtPrefix, srcObject), gopts); err == nil {
   639  			return nil
   640  		}
   641  	}
   642  
   643  	versioned := globalBucketVersioningSys.PrefixEnabled(srcBucket, srcObject)
   644  	versionSuspended := globalBucketVersioningSys.PrefixSuspended(srcBucket, srcObject)
   645  
   646  	opts := ObjectOptions{
   647  		VersionID:        srcObjInfo.VersionID,
   648  		Versioned:        versioned,
   649  		VersionSuspended: versionSuspended,
   650  	}
   651  	rd, err := api.GetObjectNInfo(ctx, srcBucket, srcObject, nil, http.Header{}, opts)
   652  	if err != nil {
   653  		return err
   654  	}
   655  	defer rd.Close()
   656  	objInfo := rd.ObjInfo
   657  
   658  	size, err := objInfo.GetActualSize()
   659  	if err != nil {
   660  		return err
   661  	}
   662  
   663  	putOpts, err := batchReplicationOpts(ctx, "", objInfo)
   664  	if err != nil {
   665  		return err
   666  	}
   667  	if r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3 {
   668  		putOpts.Internal = miniogo.AdvancedPutOptions{}
   669  	}
   670  	if objInfo.isMultipart() {
   671  		if err := replicateObjectWithMultipart(ctx, c, tgtBucket, pathJoin(tgtPrefix, objInfo.Name), rd, objInfo, putOpts); err != nil {
   672  			return err
   673  		}
   674  	} else {
   675  		if _, err = c.PutObject(ctx, tgtBucket, pathJoin(tgtPrefix, objInfo.Name), rd, size, "", "", putOpts); err != nil {
   676  			return err
   677  		}
   678  	}
   679  	return nil
   680  }
   681  
   682  //go:generate msgp -file $GOFILE -unexported
   683  
   684  // batchJobInfo current batch replication information
   685  type batchJobInfo struct {
   686  	mu sync.RWMutex `json:"-" msg:"-"`
   687  
   688  	Version       int       `json:"-" msg:"v"`
   689  	JobID         string    `json:"jobID" msg:"jid"`
   690  	JobType       string    `json:"jobType" msg:"jt"`
   691  	StartTime     time.Time `json:"startTime" msg:"st"`
   692  	LastUpdate    time.Time `json:"lastUpdate" msg:"lu"`
   693  	RetryAttempts int       `json:"retryAttempts" msg:"ra"`
   694  
   695  	Complete bool `json:"complete" msg:"cmp"`
   696  	Failed   bool `json:"failed" msg:"fld"`
   697  
   698  	// Last bucket/object batch replicated
   699  	Bucket string `json:"-" msg:"lbkt"`
   700  	Object string `json:"-" msg:"lobj"`
   701  
   702  	// Verbose information
   703  	Objects             int64 `json:"objects" msg:"ob"`
   704  	DeleteMarkers       int64 `json:"deleteMarkers" msg:"dm"`
   705  	ObjectsFailed       int64 `json:"objectsFailed" msg:"obf"`
   706  	DeleteMarkersFailed int64 `json:"deleteMarkersFailed" msg:"dmf"`
   707  	BytesTransferred    int64 `json:"bytesTransferred" msg:"bt"`
   708  	BytesFailed         int64 `json:"bytesFailed" msg:"bf"`
   709  }
   710  
   711  const (
   712  	batchReplName         = "batch-replicate.bin"
   713  	batchReplFormat       = 1
   714  	batchReplVersionV1    = 1
   715  	batchReplVersion      = batchReplVersionV1
   716  	batchJobName          = "job.bin"
   717  	batchJobPrefix        = "batch-jobs"
   718  	batchJobReportsPrefix = batchJobPrefix + "/reports"
   719  
   720  	batchReplJobAPIVersion        = "v1"
   721  	batchReplJobDefaultRetries    = 3
   722  	batchReplJobDefaultRetryDelay = 250 * time.Millisecond
   723  )
   724  
   725  func getJobReportPath(job BatchJobRequest) string {
   726  	var fileName string
   727  	switch {
   728  	case job.Replicate != nil:
   729  		fileName = batchReplName
   730  	case job.KeyRotate != nil:
   731  		fileName = batchKeyRotationName
   732  	case job.Expire != nil:
   733  		fileName = batchExpireName
   734  	}
   735  	return pathJoin(batchJobReportsPrefix, job.ID, fileName)
   736  }
   737  
   738  func getJobPath(job BatchJobRequest) string {
   739  	return pathJoin(batchJobPrefix, job.ID)
   740  }
   741  
   742  func (ri *batchJobInfo) load(ctx context.Context, api ObjectLayer, job BatchJobRequest) error {
   743  	var format, version uint16
   744  	switch {
   745  	case job.Replicate != nil:
   746  		version = batchReplVersionV1
   747  		format = batchReplFormat
   748  	case job.KeyRotate != nil:
   749  		version = batchKeyRotateVersionV1
   750  		format = batchKeyRotationFormat
   751  	case job.Expire != nil:
   752  		version = batchExpireVersionV1
   753  		format = batchExpireFormat
   754  	default:
   755  		return errors.New("no supported batch job request specified")
   756  	}
   757  	data, err := readConfig(ctx, api, getJobReportPath(job))
   758  	if err != nil {
   759  		if errors.Is(err, errConfigNotFound) || isErrObjectNotFound(err) {
   760  			ri.Version = int(version)
   761  			switch {
   762  			case job.Replicate != nil:
   763  				ri.RetryAttempts = batchReplJobDefaultRetries
   764  				if job.Replicate.Flags.Retry.Attempts > 0 {
   765  					ri.RetryAttempts = job.Replicate.Flags.Retry.Attempts
   766  				}
   767  			case job.KeyRotate != nil:
   768  				ri.RetryAttempts = batchKeyRotateJobDefaultRetries
   769  				if job.KeyRotate.Flags.Retry.Attempts > 0 {
   770  					ri.RetryAttempts = job.KeyRotate.Flags.Retry.Attempts
   771  				}
   772  			case job.Expire != nil:
   773  				ri.RetryAttempts = batchExpireJobDefaultRetries
   774  				if job.Expire.Retry.Attempts > 0 {
   775  					ri.RetryAttempts = job.Expire.Retry.Attempts
   776  				}
   777  			}
   778  			return nil
   779  		}
   780  		return err
   781  	}
   782  	if len(data) == 0 {
   783  		// Seems to be empty create a new batchRepl object.
   784  		return nil
   785  	}
   786  	if len(data) <= 4 {
   787  		return fmt.Errorf("%s: no data", ri.JobType)
   788  	}
   789  	// Read header
   790  	switch binary.LittleEndian.Uint16(data[0:2]) {
   791  	case format:
   792  	default:
   793  		return fmt.Errorf("%s: unknown format: %d", ri.JobType, binary.LittleEndian.Uint16(data[0:2]))
   794  	}
   795  	switch binary.LittleEndian.Uint16(data[2:4]) {
   796  	case version:
   797  	default:
   798  		return fmt.Errorf("%s: unknown version: %d", ri.JobType, binary.LittleEndian.Uint16(data[2:4]))
   799  	}
   800  
   801  	ri.mu.Lock()
   802  	defer ri.mu.Unlock()
   803  
   804  	// OK, parse data.
   805  	if _, err = ri.UnmarshalMsg(data[4:]); err != nil {
   806  		return err
   807  	}
   808  
   809  	switch ri.Version {
   810  	case batchReplVersionV1:
   811  	default:
   812  		return fmt.Errorf("unexpected batch %s meta version: %d", ri.JobType, ri.Version)
   813  	}
   814  
   815  	return nil
   816  }
   817  
   818  func (ri *batchJobInfo) clone() *batchJobInfo {
   819  	ri.mu.RLock()
   820  	defer ri.mu.RUnlock()
   821  
   822  	return &batchJobInfo{
   823  		Version:          ri.Version,
   824  		JobID:            ri.JobID,
   825  		JobType:          ri.JobType,
   826  		RetryAttempts:    ri.RetryAttempts,
   827  		Complete:         ri.Complete,
   828  		Failed:           ri.Failed,
   829  		StartTime:        ri.StartTime,
   830  		LastUpdate:       ri.LastUpdate,
   831  		Bucket:           ri.Bucket,
   832  		Object:           ri.Object,
   833  		Objects:          ri.Objects,
   834  		ObjectsFailed:    ri.ObjectsFailed,
   835  		BytesTransferred: ri.BytesTransferred,
   836  		BytesFailed:      ri.BytesFailed,
   837  	}
   838  }
   839  
   840  func (ri *batchJobInfo) countItem(size int64, dmarker, success bool) {
   841  	if ri == nil {
   842  		return
   843  	}
   844  	if success {
   845  		if dmarker {
   846  			ri.DeleteMarkers++
   847  		} else {
   848  			ri.Objects++
   849  			ri.BytesTransferred += size
   850  		}
   851  	} else {
   852  		if dmarker {
   853  			ri.DeleteMarkersFailed++
   854  		} else {
   855  			ri.ObjectsFailed++
   856  			ri.BytesFailed += size
   857  		}
   858  	}
   859  }
   860  
   861  func (ri *batchJobInfo) updateAfter(ctx context.Context, api ObjectLayer, duration time.Duration, job BatchJobRequest) error {
   862  	if ri == nil {
   863  		return errInvalidArgument
   864  	}
   865  	now := UTCNow()
   866  	ri.mu.Lock()
   867  	var (
   868  		format, version uint16
   869  		jobTyp          string
   870  	)
   871  
   872  	if now.Sub(ri.LastUpdate) >= duration {
   873  		switch job.Type() {
   874  		case madmin.BatchJobReplicate:
   875  			format = batchReplFormat
   876  			version = batchReplVersion
   877  			jobTyp = string(job.Type())
   878  			ri.Version = batchReplVersionV1
   879  		case madmin.BatchJobKeyRotate:
   880  			format = batchKeyRotationFormat
   881  			version = batchKeyRotateVersion
   882  			jobTyp = string(job.Type())
   883  			ri.Version = batchKeyRotateVersionV1
   884  		case madmin.BatchJobExpire:
   885  			format = batchExpireFormat
   886  			version = batchExpireVersion
   887  			jobTyp = string(job.Type())
   888  			ri.Version = batchExpireVersionV1
   889  		default:
   890  			return errInvalidArgument
   891  		}
   892  		if serverDebugLog {
   893  			console.Debugf("%s: persisting info on drive: threshold:%s, %s:%#v\n", jobTyp, now.Sub(ri.LastUpdate), jobTyp, ri)
   894  		}
   895  		ri.LastUpdate = now
   896  
   897  		data := make([]byte, 4, ri.Msgsize()+4)
   898  
   899  		// Initialize the header.
   900  		binary.LittleEndian.PutUint16(data[0:2], format)
   901  		binary.LittleEndian.PutUint16(data[2:4], version)
   902  
   903  		buf, err := ri.MarshalMsg(data)
   904  		ri.mu.Unlock()
   905  		if err != nil {
   906  			return err
   907  		}
   908  		return saveConfig(ctx, api, getJobReportPath(job), buf)
   909  	}
   910  	ri.mu.Unlock()
   911  	return nil
   912  }
   913  
   914  // Note: to be used only with batch jobs that affect multiple versions through
   915  // a single action. e.g batch-expire has an option to expire all versions of an
   916  // object which matches the given filters.
   917  func (ri *batchJobInfo) trackMultipleObjectVersions(bucket string, info ObjectInfo, success bool) {
   918  	if success {
   919  		ri.Objects += int64(info.NumVersions)
   920  	} else {
   921  		ri.ObjectsFailed += int64(info.NumVersions)
   922  	}
   923  }
   924  
   925  func (ri *batchJobInfo) trackCurrentBucketObject(bucket string, info ObjectInfo, success bool) {
   926  	if ri == nil {
   927  		return
   928  	}
   929  
   930  	ri.mu.Lock()
   931  	defer ri.mu.Unlock()
   932  
   933  	ri.Bucket = bucket
   934  	ri.Object = info.Name
   935  	ri.countItem(info.Size, info.DeleteMarker, success)
   936  }
   937  
   938  func (ri *batchJobInfo) trackCurrentBucketBatch(bucket string, batch []ObjectInfo) {
   939  	if ri == nil {
   940  		return
   941  	}
   942  
   943  	ri.mu.Lock()
   944  	defer ri.mu.Unlock()
   945  
   946  	ri.Bucket = bucket
   947  	for i := range batch {
   948  		ri.Object = batch[i].Name
   949  		ri.countItem(batch[i].Size, batch[i].DeleteMarker, true)
   950  	}
   951  }
   952  
   953  // Start start the batch replication job, resumes if there was a pending job via "job.ID"
   954  func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job BatchJobRequest) error {
   955  	ri := &batchJobInfo{
   956  		JobID:     job.ID,
   957  		JobType:   string(job.Type()),
   958  		StartTime: job.Started,
   959  	}
   960  	if err := ri.load(ctx, api, job); err != nil {
   961  		return err
   962  	}
   963  	if ri.Complete {
   964  		return nil
   965  	}
   966  	globalBatchJobsMetrics.save(job.ID, ri)
   967  	lastObject := ri.Object
   968  
   969  	delay := job.Replicate.Flags.Retry.Delay
   970  	if delay == 0 {
   971  		delay = batchReplJobDefaultRetryDelay
   972  	}
   973  	rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
   974  
   975  	selectObj := func(info FileInfo) (ok bool) {
   976  		if r.Flags.Filter.OlderThan > 0 && time.Since(info.ModTime) < r.Flags.Filter.OlderThan {
   977  			// skip all objects that are newer than specified older duration
   978  			return false
   979  		}
   980  
   981  		if r.Flags.Filter.NewerThan > 0 && time.Since(info.ModTime) >= r.Flags.Filter.NewerThan {
   982  			// skip all objects that are older than specified newer duration
   983  			return false
   984  		}
   985  
   986  		if !r.Flags.Filter.CreatedAfter.IsZero() && r.Flags.Filter.CreatedAfter.Before(info.ModTime) {
   987  			// skip all objects that are created before the specified time.
   988  			return false
   989  		}
   990  
   991  		if !r.Flags.Filter.CreatedBefore.IsZero() && r.Flags.Filter.CreatedBefore.After(info.ModTime) {
   992  			// skip all objects that are created after the specified time.
   993  			return false
   994  		}
   995  
   996  		if len(r.Flags.Filter.Tags) > 0 {
   997  			// Only parse object tags if tags filter is specified.
   998  			tagMap := map[string]string{}
   999  			tagStr := info.Metadata[xhttp.AmzObjectTagging]
  1000  			if len(tagStr) != 0 {
  1001  				t, err := tags.ParseObjectTags(tagStr)
  1002  				if err != nil {
  1003  					return false
  1004  				}
  1005  				tagMap = t.ToMap()
  1006  			}
  1007  
  1008  			for _, kv := range r.Flags.Filter.Tags {
  1009  				for t, v := range tagMap {
  1010  					if kv.Match(BatchJobKV{Key: t, Value: v}) {
  1011  						return true
  1012  					}
  1013  				}
  1014  			}
  1015  
  1016  			// None of the provided tags filter match skip the object
  1017  			return false
  1018  		}
  1019  
  1020  		if len(r.Flags.Filter.Metadata) > 0 {
  1021  			for _, kv := range r.Flags.Filter.Metadata {
  1022  				for k, v := range info.Metadata {
  1023  					if !stringsHasPrefixFold(k, "x-amz-meta-") && !isStandardHeader(k) {
  1024  						continue
  1025  					}
  1026  					// We only need to match x-amz-meta or standardHeaders
  1027  					if kv.Match(BatchJobKV{Key: k, Value: v}) {
  1028  						return true
  1029  					}
  1030  				}
  1031  			}
  1032  
  1033  			// None of the provided metadata filters match skip the object.
  1034  			return false
  1035  		}
  1036  
  1037  		// if one of source or target is non MinIO, just replicate the top most version like `mc mirror`
  1038  		return !((r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3) && !info.IsLatest)
  1039  	}
  1040  
  1041  	u, err := url.Parse(r.Target.Endpoint)
  1042  	if err != nil {
  1043  		return err
  1044  	}
  1045  
  1046  	cred := r.Target.Creds
  1047  
  1048  	c, err := miniogo.NewCore(u.Host, &miniogo.Options{
  1049  		Creds:        credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken),
  1050  		Secure:       u.Scheme == "https",
  1051  		Transport:    getRemoteInstanceTransport,
  1052  		BucketLookup: lookupStyle(r.Target.Path),
  1053  	})
  1054  	if err != nil {
  1055  		return err
  1056  	}
  1057  
  1058  	c.SetAppInfo("minio-"+batchJobPrefix, r.APIVersion+" "+job.ID)
  1059  
  1060  	var (
  1061  		walkCh = make(chan ObjectInfo, 100)
  1062  		slowCh = make(chan ObjectInfo, 100)
  1063  	)
  1064  
  1065  	if !*r.Source.Snowball.Disable && r.Source.Type.isMinio() && r.Target.Type.isMinio() {
  1066  		go func() {
  1067  			// Snowball currently needs the high level minio-go Client, not the Core one
  1068  			cl, err := miniogo.New(u.Host, &miniogo.Options{
  1069  				Creds:        credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken),
  1070  				Secure:       u.Scheme == "https",
  1071  				Transport:    getRemoteInstanceTransport,
  1072  				BucketLookup: lookupStyle(r.Target.Path),
  1073  			})
  1074  			if err != nil {
  1075  				logger.LogIf(ctx, err)
  1076  				return
  1077  			}
  1078  
  1079  			// Already validated before arriving here
  1080  			smallerThan, _ := humanize.ParseBytes(*r.Source.Snowball.SmallerThan)
  1081  
  1082  			batch := make([]ObjectInfo, 0, *r.Source.Snowball.Batch)
  1083  			writeFn := func(batch []ObjectInfo) {
  1084  				if len(batch) > 0 {
  1085  					if err := r.writeAsArchive(ctx, api, cl, batch); err != nil {
  1086  						logger.LogIf(ctx, err)
  1087  						for _, b := range batch {
  1088  							slowCh <- b
  1089  						}
  1090  					} else {
  1091  						ri.trackCurrentBucketBatch(r.Source.Bucket, batch)
  1092  						globalBatchJobsMetrics.save(job.ID, ri)
  1093  						// persist in-memory state to disk after every 10secs.
  1094  						logger.LogIf(ctx, ri.updateAfter(ctx, api, 10*time.Second, job))
  1095  					}
  1096  				}
  1097  			}
  1098  			for obj := range walkCh {
  1099  				if obj.DeleteMarker || !obj.VersionPurgeStatus.Empty() || obj.Size >= int64(smallerThan) {
  1100  					slowCh <- obj
  1101  					continue
  1102  				}
  1103  
  1104  				batch = append(batch, obj)
  1105  
  1106  				if len(batch) < *r.Source.Snowball.Batch {
  1107  					continue
  1108  				}
  1109  				writeFn(batch)
  1110  				batch = batch[:0]
  1111  			}
  1112  			writeFn(batch)
  1113  			xioutil.SafeClose(slowCh)
  1114  		}()
  1115  	} else {
  1116  		slowCh = walkCh
  1117  	}
  1118  
  1119  	workerSize, err := strconv.Atoi(env.Get("_MINIO_BATCH_REPLICATION_WORKERS", strconv.Itoa(runtime.GOMAXPROCS(0)/2)))
  1120  	if err != nil {
  1121  		return err
  1122  	}
  1123  
  1124  	wk, err := workers.New(workerSize)
  1125  	if err != nil {
  1126  		// invalid worker size.
  1127  		return err
  1128  	}
  1129  
  1130  	walkQuorum := env.Get("_MINIO_BATCH_REPLICATION_WALK_QUORUM", "strict")
  1131  	if walkQuorum == "" {
  1132  		walkQuorum = "strict"
  1133  	}
  1134  
  1135  	retryAttempts := ri.RetryAttempts
  1136  	retry := false
  1137  	for attempts := 1; attempts <= retryAttempts; attempts++ {
  1138  		attempts := attempts
  1139  
  1140  		ctx, cancel := context.WithCancel(ctx)
  1141  		// one of source/target is s3, skip delete marker and all versions under the same object name.
  1142  		s3Type := r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3
  1143  
  1144  		if err := api.Walk(ctx, r.Source.Bucket, r.Source.Prefix, walkCh, WalkOptions{
  1145  			Marker:   lastObject,
  1146  			Filter:   selectObj,
  1147  			AskDisks: walkQuorum,
  1148  		}); err != nil {
  1149  			cancel()
  1150  			// Do not need to retry if we can't list objects on source.
  1151  			return err
  1152  		}
  1153  
  1154  		prevObj := ""
  1155  
  1156  		skipReplicate := false
  1157  		for result := range slowCh {
  1158  			result := result
  1159  			if result.Name != prevObj {
  1160  				prevObj = result.Name
  1161  				skipReplicate = result.DeleteMarker && s3Type
  1162  			}
  1163  			if skipReplicate {
  1164  				continue
  1165  			}
  1166  			wk.Take()
  1167  			go func() {
  1168  				defer wk.Give()
  1169  
  1170  				stopFn := globalBatchJobsMetrics.trace(batchJobMetricReplication, job.ID, attempts)
  1171  				success := true
  1172  				if err := r.ReplicateToTarget(ctx, api, c, result, retry); err != nil {
  1173  					if miniogo.ToErrorResponse(err).Code == "PreconditionFailed" {
  1174  						// pre-condition failed means we already have the object copied over.
  1175  						return
  1176  					}
  1177  					// object must be deleted concurrently, allow these failures but do not count them
  1178  					if isErrVersionNotFound(err) || isErrObjectNotFound(err) {
  1179  						return
  1180  					}
  1181  					stopFn(result, err)
  1182  					logger.LogIf(ctx, err)
  1183  					success = false
  1184  				} else {
  1185  					stopFn(result, nil)
  1186  				}
  1187  				ri.trackCurrentBucketObject(r.Source.Bucket, result, success)
  1188  				globalBatchJobsMetrics.save(job.ID, ri)
  1189  				// persist in-memory state to disk after every 10secs.
  1190  				logger.LogIf(ctx, ri.updateAfter(ctx, api, 10*time.Second, job))
  1191  
  1192  				if wait := globalBatchConfig.ReplicationWait(); wait > 0 {
  1193  					time.Sleep(wait)
  1194  				}
  1195  			}()
  1196  		}
  1197  		wk.Wait()
  1198  
  1199  		ri.RetryAttempts = attempts
  1200  		ri.Complete = ri.ObjectsFailed == 0
  1201  		ri.Failed = ri.ObjectsFailed > 0
  1202  
  1203  		globalBatchJobsMetrics.save(job.ID, ri)
  1204  		// persist in-memory state to disk.
  1205  		logger.LogIf(ctx, ri.updateAfter(ctx, api, 0, job))
  1206  
  1207  		if err := r.Notify(ctx, ri); err != nil {
  1208  			logger.LogIf(ctx, fmt.Errorf("unable to notify %v", err))
  1209  		}
  1210  
  1211  		cancel()
  1212  		if ri.Failed {
  1213  			ri.ObjectsFailed = 0
  1214  			ri.Bucket = ""
  1215  			ri.Object = ""
  1216  			ri.Objects = 0
  1217  			ri.BytesFailed = 0
  1218  			ri.BytesTransferred = 0
  1219  			retry = true // indicate we are retrying..
  1220  			time.Sleep(delay + time.Duration(rnd.Float64()*float64(delay)))
  1221  			continue
  1222  		}
  1223  
  1224  		break
  1225  	}
  1226  
  1227  	return nil
  1228  }
  1229  
  1230  //msgp:ignore batchReplicationJobError
  1231  type batchReplicationJobError struct {
  1232  	Code           string
  1233  	Description    string
  1234  	HTTPStatusCode int
  1235  }
  1236  
  1237  func (e batchReplicationJobError) Error() string {
  1238  	return e.Description
  1239  }
  1240  
  1241  // Validate validates the job definition input
  1242  func (r *BatchJobReplicateV1) Validate(ctx context.Context, job BatchJobRequest, o ObjectLayer) error {
  1243  	if r == nil {
  1244  		return nil
  1245  	}
  1246  
  1247  	if r.APIVersion != batchReplJobAPIVersion {
  1248  		return errInvalidArgument
  1249  	}
  1250  
  1251  	if r.Source.Bucket == "" {
  1252  		return errInvalidArgument
  1253  	}
  1254  	var isRemoteToLocal bool
  1255  	localBkt := r.Source.Bucket
  1256  	if r.Source.Endpoint != "" {
  1257  		localBkt = r.Target.Bucket
  1258  		isRemoteToLocal = true
  1259  	}
  1260  	info, err := o.GetBucketInfo(ctx, localBkt, BucketOptions{})
  1261  	if err != nil {
  1262  		if isErrBucketNotFound(err) {
  1263  			return batchReplicationJobError{
  1264  				Code:           "NoSuchSourceBucket",
  1265  				Description:    fmt.Sprintf("The specified bucket %s does not exist", localBkt),
  1266  				HTTPStatusCode: http.StatusNotFound,
  1267  			}
  1268  		}
  1269  		return err
  1270  	}
  1271  
  1272  	if err := r.Source.Type.Validate(); err != nil {
  1273  		return err
  1274  	}
  1275  	if err := r.Source.Snowball.Validate(); err != nil {
  1276  		return err
  1277  	}
  1278  	if r.Source.Creds.Empty() && r.Target.Creds.Empty() {
  1279  		return errInvalidArgument
  1280  	}
  1281  
  1282  	if !r.Source.Creds.Empty() {
  1283  		if err := r.Source.Creds.Validate(); err != nil {
  1284  			return err
  1285  		}
  1286  	}
  1287  	if r.Target.Endpoint == "" && !r.Target.Creds.Empty() {
  1288  		return errInvalidArgument
  1289  	}
  1290  
  1291  	if r.Source.Endpoint == "" && !r.Source.Creds.Empty() {
  1292  		return errInvalidArgument
  1293  	}
  1294  
  1295  	if r.Source.Endpoint != "" && !r.Source.Type.isMinio() && !r.Source.ValidPath() {
  1296  		return errInvalidArgument
  1297  	}
  1298  
  1299  	if r.Target.Endpoint != "" && !r.Target.Type.isMinio() && !r.Target.ValidPath() {
  1300  		return errInvalidArgument
  1301  	}
  1302  	if r.Target.Bucket == "" {
  1303  		return errInvalidArgument
  1304  	}
  1305  
  1306  	if !r.Target.Creds.Empty() {
  1307  		if err := r.Target.Creds.Validate(); err != nil {
  1308  			return err
  1309  		}
  1310  	}
  1311  
  1312  	if r.Source.Creds.Empty() && r.Target.Creds.Empty() {
  1313  		return errInvalidArgument
  1314  	}
  1315  
  1316  	if err := r.Target.Type.Validate(); err != nil {
  1317  		return err
  1318  	}
  1319  
  1320  	for _, tag := range r.Flags.Filter.Tags {
  1321  		if err := tag.Validate(); err != nil {
  1322  			return err
  1323  		}
  1324  	}
  1325  
  1326  	for _, meta := range r.Flags.Filter.Metadata {
  1327  		if err := meta.Validate(); err != nil {
  1328  			return err
  1329  		}
  1330  	}
  1331  
  1332  	if err := r.Flags.Retry.Validate(); err != nil {
  1333  		return err
  1334  	}
  1335  
  1336  	remoteEp := r.Target.Endpoint
  1337  	remoteBkt := r.Target.Bucket
  1338  	cred := r.Target.Creds
  1339  	pathStyle := r.Target.Path
  1340  
  1341  	if r.Source.Endpoint != "" {
  1342  		remoteEp = r.Source.Endpoint
  1343  		cred = r.Source.Creds
  1344  		remoteBkt = r.Source.Bucket
  1345  		pathStyle = r.Source.Path
  1346  
  1347  	}
  1348  
  1349  	u, err := url.Parse(remoteEp)
  1350  	if err != nil {
  1351  		return err
  1352  	}
  1353  
  1354  	c, err := miniogo.NewCore(u.Host, &miniogo.Options{
  1355  		Creds:        credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken),
  1356  		Secure:       u.Scheme == "https",
  1357  		Transport:    getRemoteInstanceTransport,
  1358  		BucketLookup: lookupStyle(pathStyle),
  1359  	})
  1360  	if err != nil {
  1361  		return err
  1362  	}
  1363  	c.SetAppInfo("minio-"+batchJobPrefix, r.APIVersion+" "+job.ID)
  1364  
  1365  	vcfg, err := c.GetBucketVersioning(ctx, remoteBkt)
  1366  	if err != nil {
  1367  		if miniogo.ToErrorResponse(err).Code == "NoSuchBucket" {
  1368  			return batchReplicationJobError{
  1369  				Code:           "NoSuchTargetBucket",
  1370  				Description:    "The specified target bucket does not exist",
  1371  				HTTPStatusCode: http.StatusNotFound,
  1372  			}
  1373  		}
  1374  		return err
  1375  	}
  1376  	// if both source and target are minio instances
  1377  	minioType := r.Target.Type == BatchJobReplicateResourceMinIO && r.Source.Type == BatchJobReplicateResourceMinIO
  1378  	// If source has versioning enabled, target must have versioning enabled
  1379  	if minioType && ((info.Versioning && !vcfg.Enabled() && !isRemoteToLocal) || (!info.Versioning && vcfg.Enabled() && isRemoteToLocal)) {
  1380  		return batchReplicationJobError{
  1381  			Code: "InvalidBucketState",
  1382  			Description: fmt.Sprintf("The source '%s' has versioning enabled, target '%s' must have versioning enabled",
  1383  				r.Source.Bucket, r.Target.Bucket),
  1384  			HTTPStatusCode: http.StatusBadRequest,
  1385  		}
  1386  	}
  1387  
  1388  	r.clnt = c
  1389  	return nil
  1390  }
  1391  
  1392  // Type returns type of batch job, currently only supports 'replicate'
  1393  func (j BatchJobRequest) Type() madmin.BatchJobType {
  1394  	switch {
  1395  	case j.Replicate != nil:
  1396  		return madmin.BatchJobReplicate
  1397  	case j.KeyRotate != nil:
  1398  		return madmin.BatchJobKeyRotate
  1399  	case j.Expire != nil:
  1400  		return madmin.BatchJobExpire
  1401  	}
  1402  	return madmin.BatchJobType("unknown")
  1403  }
  1404  
  1405  // Validate validates the current job, used by 'save()' before
  1406  // persisting the job request
  1407  func (j BatchJobRequest) Validate(ctx context.Context, o ObjectLayer) error {
  1408  	switch {
  1409  	case j.Replicate != nil:
  1410  		return j.Replicate.Validate(ctx, j, o)
  1411  	case j.KeyRotate != nil:
  1412  		return j.KeyRotate.Validate(ctx, j, o)
  1413  	case j.Expire != nil:
  1414  		return j.Expire.Validate(ctx, j, o)
  1415  	}
  1416  	return errInvalidArgument
  1417  }
  1418  
  1419  func (j BatchJobRequest) delete(ctx context.Context, api ObjectLayer) {
  1420  	deleteConfig(ctx, api, getJobReportPath(j))
  1421  	deleteConfig(ctx, api, getJobPath(j))
  1422  }
  1423  
  1424  func (j *BatchJobRequest) save(ctx context.Context, api ObjectLayer) error {
  1425  	if j.Replicate == nil && j.KeyRotate == nil && j.Expire == nil {
  1426  		return errInvalidArgument
  1427  	}
  1428  
  1429  	if err := j.Validate(ctx, api); err != nil {
  1430  		return err
  1431  	}
  1432  
  1433  	job, err := j.MarshalMsg(nil)
  1434  	if err != nil {
  1435  		return err
  1436  	}
  1437  
  1438  	return saveConfig(ctx, api, getJobPath(*j), job)
  1439  }
  1440  
  1441  func (j *BatchJobRequest) load(ctx context.Context, api ObjectLayer, name string) error {
  1442  	if j == nil {
  1443  		return nil
  1444  	}
  1445  
  1446  	job, err := readConfig(ctx, api, name)
  1447  	if err != nil {
  1448  		if errors.Is(err, errConfigNotFound) || isErrObjectNotFound(err) {
  1449  			err = errNoSuchJob
  1450  		}
  1451  		return err
  1452  	}
  1453  
  1454  	_, err = j.UnmarshalMsg(job)
  1455  	return err
  1456  }
  1457  
  1458  func batchReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (putOpts miniogo.PutObjectOptions, err error) {
  1459  	// TODO: support custom storage class for remote replication
  1460  	putOpts, err = putReplicationOpts(ctx, "", objInfo)
  1461  	if err != nil {
  1462  		return putOpts, err
  1463  	}
  1464  	putOpts.Internal = miniogo.AdvancedPutOptions{
  1465  		SourceVersionID:    objInfo.VersionID,
  1466  		SourceMTime:        objInfo.ModTime,
  1467  		SourceETag:         objInfo.ETag,
  1468  		ReplicationRequest: true,
  1469  	}
  1470  	return putOpts, nil
  1471  }
  1472  
  1473  // ListBatchJobs - lists all currently active batch jobs, optionally takes {jobType}
  1474  // input to list only active batch jobs of 'jobType'
  1475  func (a adminAPIHandlers) ListBatchJobs(w http.ResponseWriter, r *http.Request) {
  1476  	ctx := r.Context()
  1477  
  1478  	objectAPI, _ := validateAdminReq(ctx, w, r, policy.ListBatchJobsAction)
  1479  	if objectAPI == nil {
  1480  		return
  1481  	}
  1482  
  1483  	jobType := r.Form.Get("jobType")
  1484  	if jobType == "" {
  1485  		jobType = string(madmin.BatchJobReplicate)
  1486  	}
  1487  
  1488  	resultCh := make(chan ObjectInfo)
  1489  
  1490  	ctx, cancel := context.WithCancel(ctx)
  1491  	defer cancel()
  1492  
  1493  	if err := objectAPI.Walk(ctx, minioMetaBucket, batchJobPrefix, resultCh, WalkOptions{}); err != nil {
  1494  		writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
  1495  		return
  1496  	}
  1497  
  1498  	listResult := madmin.ListBatchJobsResult{}
  1499  	for result := range resultCh {
  1500  		req := &BatchJobRequest{}
  1501  		if err := req.load(ctx, objectAPI, result.Name); err != nil {
  1502  			if !errors.Is(err, errNoSuchJob) {
  1503  				logger.LogIf(ctx, err)
  1504  			}
  1505  			continue
  1506  		}
  1507  
  1508  		if jobType == string(req.Type()) {
  1509  			listResult.Jobs = append(listResult.Jobs, madmin.BatchJobResult{
  1510  				ID:      req.ID,
  1511  				Type:    req.Type(),
  1512  				Started: req.Started,
  1513  				User:    req.User,
  1514  				Elapsed: time.Since(req.Started),
  1515  			})
  1516  		}
  1517  	}
  1518  
  1519  	logger.LogIf(ctx, json.NewEncoder(w).Encode(&listResult))
  1520  }
  1521  
  1522  var errNoSuchJob = errors.New("no such job")
  1523  
  1524  // DescribeBatchJob returns the currently active batch job definition
  1525  func (a adminAPIHandlers) DescribeBatchJob(w http.ResponseWriter, r *http.Request) {
  1526  	ctx := r.Context()
  1527  
  1528  	objectAPI, _ := validateAdminReq(ctx, w, r, policy.DescribeBatchJobAction)
  1529  	if objectAPI == nil {
  1530  		return
  1531  	}
  1532  
  1533  	jobID := r.Form.Get("jobId")
  1534  	if jobID == "" {
  1535  		writeErrorResponseJSON(ctx, w, toAPIError(ctx, errInvalidArgument), r.URL)
  1536  		return
  1537  	}
  1538  
  1539  	req := &BatchJobRequest{}
  1540  	if err := req.load(ctx, objectAPI, pathJoin(batchJobPrefix, jobID)); err != nil {
  1541  		if !errors.Is(err, errNoSuchJob) {
  1542  			logger.LogIf(ctx, err)
  1543  		}
  1544  
  1545  		writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
  1546  		return
  1547  	}
  1548  
  1549  	buf, err := yaml.Marshal(req)
  1550  	if err != nil {
  1551  		logger.LogIf(ctx, err)
  1552  		writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
  1553  		return
  1554  	}
  1555  
  1556  	w.Write(buf)
  1557  }
  1558  
  1559  // StarBatchJob queue a new job for execution
  1560  func (a adminAPIHandlers) StartBatchJob(w http.ResponseWriter, r *http.Request) {
  1561  	ctx := r.Context()
  1562  
  1563  	objectAPI, creds := validateAdminReq(ctx, w, r, policy.StartBatchJobAction)
  1564  	if objectAPI == nil {
  1565  		return
  1566  	}
  1567  
  1568  	buf, err := io.ReadAll(ioutil.HardLimitReader(r.Body, humanize.MiByte*4))
  1569  	if err != nil {
  1570  		writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
  1571  		return
  1572  	}
  1573  
  1574  	user := creds.AccessKey
  1575  	if creds.ParentUser != "" {
  1576  		user = creds.ParentUser
  1577  	}
  1578  
  1579  	job := &BatchJobRequest{}
  1580  	if err = yaml.Unmarshal(buf, job); err != nil {
  1581  		writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
  1582  		return
  1583  	}
  1584  
  1585  	// Fill with default values
  1586  	if job.Replicate != nil {
  1587  		if job.Replicate.Source.Snowball.Disable == nil {
  1588  			job.Replicate.Source.Snowball.Disable = ptr(false)
  1589  		}
  1590  		if job.Replicate.Source.Snowball.Batch == nil {
  1591  			job.Replicate.Source.Snowball.Batch = ptr(100)
  1592  		}
  1593  		if job.Replicate.Source.Snowball.InMemory == nil {
  1594  			job.Replicate.Source.Snowball.InMemory = ptr(true)
  1595  		}
  1596  		if job.Replicate.Source.Snowball.Compress == nil {
  1597  			job.Replicate.Source.Snowball.Compress = ptr(false)
  1598  		}
  1599  		if job.Replicate.Source.Snowball.SmallerThan == nil {
  1600  			job.Replicate.Source.Snowball.SmallerThan = ptr("5MiB")
  1601  		}
  1602  		if job.Replicate.Source.Snowball.SkipErrs == nil {
  1603  			job.Replicate.Source.Snowball.SkipErrs = ptr(true)
  1604  		}
  1605  	}
  1606  
  1607  	//  Validate the incoming job request
  1608  	if err := job.Validate(ctx, objectAPI); err != nil {
  1609  		writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
  1610  		return
  1611  	}
  1612  
  1613  	job.ID = fmt.Sprintf("%s:%d", shortuuid.New(), GetProxyEndpointLocalIndex(globalProxyEndpoints))
  1614  	job.User = user
  1615  	job.Started = time.Now()
  1616  
  1617  	if err := job.save(ctx, objectAPI); err != nil {
  1618  		writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
  1619  		return
  1620  	}
  1621  
  1622  	if err = globalBatchJobPool.queueJob(job); err != nil {
  1623  		writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
  1624  		return
  1625  	}
  1626  
  1627  	buf, err = json.Marshal(&madmin.BatchJobResult{
  1628  		ID:      job.ID,
  1629  		Type:    job.Type(),
  1630  		Started: job.Started,
  1631  		User:    job.User,
  1632  	})
  1633  	if err != nil {
  1634  		writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
  1635  		return
  1636  	}
  1637  
  1638  	writeSuccessResponseJSON(w, buf)
  1639  }
  1640  
  1641  // CancelBatchJob cancels a job in progress
  1642  func (a adminAPIHandlers) CancelBatchJob(w http.ResponseWriter, r *http.Request) {
  1643  	ctx := r.Context()
  1644  
  1645  	objectAPI, _ := validateAdminReq(ctx, w, r, policy.CancelBatchJobAction)
  1646  	if objectAPI == nil {
  1647  		return
  1648  	}
  1649  
  1650  	jobID := r.Form.Get("id")
  1651  	if jobID == "" {
  1652  		writeErrorResponseJSON(ctx, w, toAPIError(ctx, errInvalidArgument), r.URL)
  1653  		return
  1654  	}
  1655  
  1656  	if _, success := proxyRequestByToken(ctx, w, r, jobID); success {
  1657  		return
  1658  	}
  1659  
  1660  	if err := globalBatchJobPool.canceler(jobID, true); err != nil {
  1661  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrInvalidRequest, err), r.URL)
  1662  		return
  1663  	}
  1664  
  1665  	j := BatchJobRequest{
  1666  		ID: jobID,
  1667  	}
  1668  
  1669  	j.delete(ctx, objectAPI)
  1670  
  1671  	writeSuccessNoContent(w)
  1672  }
  1673  
  1674  //msgp:ignore BatchJobPool
  1675  
  1676  // BatchJobPool batch job pool
  1677  type BatchJobPool struct {
  1678  	ctx          context.Context
  1679  	objLayer     ObjectLayer
  1680  	once         sync.Once
  1681  	mu           sync.Mutex
  1682  	jobCh        chan *BatchJobRequest
  1683  	jmu          sync.Mutex // protects jobCancelers
  1684  	jobCancelers map[string]context.CancelFunc
  1685  	workerKillCh chan struct{}
  1686  	workerSize   int
  1687  }
  1688  
  1689  var globalBatchJobPool *BatchJobPool
  1690  
  1691  // newBatchJobPool creates a pool of job manifest workers of specified size
  1692  func newBatchJobPool(ctx context.Context, o ObjectLayer, workers int) *BatchJobPool {
  1693  	jpool := &BatchJobPool{
  1694  		ctx:          ctx,
  1695  		objLayer:     o,
  1696  		jobCh:        make(chan *BatchJobRequest, 10000),
  1697  		workerKillCh: make(chan struct{}, workers),
  1698  		jobCancelers: make(map[string]context.CancelFunc),
  1699  	}
  1700  	jpool.ResizeWorkers(workers)
  1701  	jpool.resume()
  1702  	return jpool
  1703  }
  1704  
  1705  func (j *BatchJobPool) resume() {
  1706  	results := make(chan ObjectInfo, 100)
  1707  	ctx, cancel := context.WithCancel(j.ctx)
  1708  	defer cancel()
  1709  	if err := j.objLayer.Walk(ctx, minioMetaBucket, batchJobPrefix, results, WalkOptions{}); err != nil {
  1710  		logger.LogIf(j.ctx, err)
  1711  		return
  1712  	}
  1713  	for result := range results {
  1714  		// ignore batch-replicate.bin and batch-rotate.bin entries
  1715  		if strings.HasSuffix(result.Name, slashSeparator) {
  1716  			continue
  1717  		}
  1718  		req := &BatchJobRequest{}
  1719  		if err := req.load(ctx, j.objLayer, result.Name); err != nil {
  1720  			logger.LogIf(ctx, err)
  1721  			continue
  1722  		}
  1723  		_, nodeIdx := parseRequestToken(req.ID)
  1724  		if nodeIdx > -1 && GetProxyEndpointLocalIndex(globalProxyEndpoints) != nodeIdx {
  1725  			// This job doesn't belong on this node.
  1726  			continue
  1727  		}
  1728  		if err := j.queueJob(req); err != nil {
  1729  			logger.LogIf(ctx, err)
  1730  			continue
  1731  		}
  1732  	}
  1733  }
  1734  
  1735  // AddWorker adds a replication worker to the pool
  1736  func (j *BatchJobPool) AddWorker() {
  1737  	if j == nil {
  1738  		return
  1739  	}
  1740  	for {
  1741  		select {
  1742  		case <-j.ctx.Done():
  1743  			return
  1744  		case job, ok := <-j.jobCh:
  1745  			if !ok {
  1746  				return
  1747  			}
  1748  			switch {
  1749  			case job.Replicate != nil:
  1750  				if job.Replicate.RemoteToLocal() {
  1751  					if err := job.Replicate.StartFromSource(job.ctx, j.objLayer, *job); err != nil {
  1752  						if !isErrBucketNotFound(err) {
  1753  							logger.LogIf(j.ctx, err)
  1754  							j.canceler(job.ID, false)
  1755  							continue
  1756  						}
  1757  						// Bucket not found proceed to delete such a job.
  1758  					}
  1759  				} else {
  1760  					if err := job.Replicate.Start(job.ctx, j.objLayer, *job); err != nil {
  1761  						if !isErrBucketNotFound(err) {
  1762  							logger.LogIf(j.ctx, err)
  1763  							j.canceler(job.ID, false)
  1764  							continue
  1765  						}
  1766  						// Bucket not found proceed to delete such a job.
  1767  					}
  1768  				}
  1769  			case job.KeyRotate != nil:
  1770  				if err := job.KeyRotate.Start(job.ctx, j.objLayer, *job); err != nil {
  1771  					if !isErrBucketNotFound(err) {
  1772  						logger.LogIf(j.ctx, err)
  1773  						continue
  1774  					}
  1775  				}
  1776  			case job.Expire != nil:
  1777  				if err := job.Expire.Start(job.ctx, j.objLayer, *job); err != nil {
  1778  					if !isErrBucketNotFound(err) {
  1779  						logger.LogIf(j.ctx, err)
  1780  						continue
  1781  					}
  1782  				}
  1783  			}
  1784  			job.delete(j.ctx, j.objLayer)
  1785  			j.canceler(job.ID, false)
  1786  		case <-j.workerKillCh:
  1787  			return
  1788  		}
  1789  	}
  1790  }
  1791  
  1792  // ResizeWorkers sets replication workers pool to new size
  1793  func (j *BatchJobPool) ResizeWorkers(n int) {
  1794  	if j == nil {
  1795  		return
  1796  	}
  1797  
  1798  	j.mu.Lock()
  1799  	defer j.mu.Unlock()
  1800  
  1801  	for j.workerSize < n {
  1802  		j.workerSize++
  1803  		go j.AddWorker()
  1804  	}
  1805  	for j.workerSize > n {
  1806  		j.workerSize--
  1807  		go func() { j.workerKillCh <- struct{}{} }()
  1808  	}
  1809  }
  1810  
  1811  func (j *BatchJobPool) queueJob(req *BatchJobRequest) error {
  1812  	if j == nil {
  1813  		return errInvalidArgument
  1814  	}
  1815  	jctx, jcancel := context.WithCancel(j.ctx)
  1816  	j.jmu.Lock()
  1817  	j.jobCancelers[req.ID] = jcancel
  1818  	j.jmu.Unlock()
  1819  	req.ctx = jctx
  1820  
  1821  	select {
  1822  	case <-j.ctx.Done():
  1823  		j.once.Do(func() {
  1824  			xioutil.SafeClose(j.jobCh)
  1825  		})
  1826  	case j.jobCh <- req:
  1827  	default:
  1828  		return fmt.Errorf("batch job queue is currently full please try again later %#v", req)
  1829  	}
  1830  	return nil
  1831  }
  1832  
  1833  // delete canceler from the map, cancel job if requested
  1834  func (j *BatchJobPool) canceler(jobID string, cancel bool) error {
  1835  	if j == nil {
  1836  		return errInvalidArgument
  1837  	}
  1838  	j.jmu.Lock()
  1839  	defer j.jmu.Unlock()
  1840  	if canceler, ok := j.jobCancelers[jobID]; ok {
  1841  		if cancel {
  1842  			canceler()
  1843  		}
  1844  	}
  1845  	delete(j.jobCancelers, jobID)
  1846  	return nil
  1847  }
  1848  
  1849  //msgp:ignore batchJobMetrics
  1850  type batchJobMetrics struct {
  1851  	sync.RWMutex
  1852  	metrics map[string]*batchJobInfo
  1853  }
  1854  
  1855  //msgp:ignore batchJobMetric
  1856  //go:generate stringer -type=batchJobMetric -trimprefix=batchJobMetric $GOFILE
  1857  type batchJobMetric uint8
  1858  
  1859  const (
  1860  	batchJobMetricReplication batchJobMetric = iota
  1861  	batchJobMetricKeyRotation
  1862  	batchJobMetricExpire
  1863  )
  1864  
  1865  func batchJobTrace(d batchJobMetric, job string, startTime time.Time, duration time.Duration, info objTraceInfoer, attempts int, err error) madmin.TraceInfo {
  1866  	var errStr string
  1867  	if err != nil {
  1868  		errStr = err.Error()
  1869  	}
  1870  	traceType := madmin.TraceBatchReplication
  1871  	switch d {
  1872  	case batchJobMetricKeyRotation:
  1873  		traceType = madmin.TraceBatchKeyRotation
  1874  	case batchJobMetricExpire:
  1875  		traceType = madmin.TraceBatchExpire
  1876  	}
  1877  	funcName := fmt.Sprintf("%s() (job-name=%s)", d.String(), job)
  1878  	if attempts > 0 {
  1879  		funcName = fmt.Sprintf("%s() (job-name=%s,attempts=%s)", d.String(), job, humanize.Ordinal(attempts))
  1880  	}
  1881  	return madmin.TraceInfo{
  1882  		TraceType: traceType,
  1883  		Time:      startTime,
  1884  		NodeName:  globalLocalNodeName,
  1885  		FuncName:  funcName,
  1886  		Duration:  duration,
  1887  		Path:      fmt.Sprintf("%s (versionID=%s)", info.TraceObjName(), info.TraceVersionID()),
  1888  		Error:     errStr,
  1889  	}
  1890  }
  1891  
  1892  func (ri *batchJobInfo) metric() madmin.JobMetric {
  1893  	m := madmin.JobMetric{
  1894  		JobID:         ri.JobID,
  1895  		JobType:       ri.JobType,
  1896  		StartTime:     ri.StartTime,
  1897  		LastUpdate:    ri.LastUpdate,
  1898  		RetryAttempts: ri.RetryAttempts,
  1899  		Complete:      ri.Complete,
  1900  		Failed:        ri.Failed,
  1901  	}
  1902  
  1903  	switch ri.JobType {
  1904  	case string(madmin.BatchJobReplicate):
  1905  		m.Replicate = &madmin.ReplicateInfo{
  1906  			Bucket:           ri.Bucket,
  1907  			Object:           ri.Object,
  1908  			Objects:          ri.Objects,
  1909  			ObjectsFailed:    ri.ObjectsFailed,
  1910  			BytesTransferred: ri.BytesTransferred,
  1911  			BytesFailed:      ri.BytesFailed,
  1912  		}
  1913  	case string(madmin.BatchJobKeyRotate):
  1914  		m.KeyRotate = &madmin.KeyRotationInfo{
  1915  			Bucket:        ri.Bucket,
  1916  			Object:        ri.Object,
  1917  			Objects:       ri.Objects,
  1918  			ObjectsFailed: ri.ObjectsFailed,
  1919  		}
  1920  	case string(madmin.BatchJobExpire):
  1921  		m.Expired = &madmin.ExpirationInfo{
  1922  			Bucket:        ri.Bucket,
  1923  			Object:        ri.Object,
  1924  			Objects:       ri.Objects,
  1925  			ObjectsFailed: ri.ObjectsFailed,
  1926  		}
  1927  	}
  1928  
  1929  	return m
  1930  }
  1931  
  1932  func (m *batchJobMetrics) report(jobID string) (metrics *madmin.BatchJobMetrics) {
  1933  	metrics = &madmin.BatchJobMetrics{CollectedAt: time.Now(), Jobs: make(map[string]madmin.JobMetric)}
  1934  	m.RLock()
  1935  	defer m.RUnlock()
  1936  
  1937  	if jobID != "" {
  1938  		if job, ok := m.metrics[jobID]; ok {
  1939  			metrics.Jobs[jobID] = job.metric()
  1940  		}
  1941  		return metrics
  1942  	}
  1943  
  1944  	for id, job := range m.metrics {
  1945  		metrics.Jobs[id] = job.metric()
  1946  	}
  1947  	return metrics
  1948  }
  1949  
  1950  // keep job metrics for some time after the job is completed
  1951  // in-case some one wants to look at the older results.
  1952  func (m *batchJobMetrics) purgeJobMetrics() {
  1953  	t := time.NewTicker(6 * time.Hour)
  1954  	defer t.Stop()
  1955  
  1956  	for {
  1957  		select {
  1958  		case <-GlobalContext.Done():
  1959  			return
  1960  		case <-t.C:
  1961  			var toDeleteJobMetrics []string
  1962  			m.RLock()
  1963  			for id, metrics := range m.metrics {
  1964  				if time.Since(metrics.LastUpdate) > 24*time.Hour && (metrics.Complete || metrics.Failed) {
  1965  					toDeleteJobMetrics = append(toDeleteJobMetrics, id)
  1966  				}
  1967  			}
  1968  			m.RUnlock()
  1969  			for _, jobID := range toDeleteJobMetrics {
  1970  				m.delete(jobID)
  1971  			}
  1972  		}
  1973  	}
  1974  }
  1975  
  1976  func (m *batchJobMetrics) delete(jobID string) {
  1977  	m.Lock()
  1978  	defer m.Unlock()
  1979  
  1980  	delete(m.metrics, jobID)
  1981  }
  1982  
  1983  func (m *batchJobMetrics) save(jobID string, ri *batchJobInfo) {
  1984  	m.Lock()
  1985  	defer m.Unlock()
  1986  
  1987  	m.metrics[jobID] = ri.clone()
  1988  }
  1989  
  1990  type objTraceInfoer interface {
  1991  	TraceObjName() string
  1992  	TraceVersionID() string
  1993  }
  1994  
  1995  // TraceObjName returns name of object being traced
  1996  func (td ObjectToDelete) TraceObjName() string {
  1997  	return td.ObjectName
  1998  }
  1999  
  2000  // TraceVersionID returns version-id of object being traced
  2001  func (td ObjectToDelete) TraceVersionID() string {
  2002  	return td.VersionID
  2003  }
  2004  
  2005  // TraceObjName returns name of object being traced
  2006  func (oi ObjectInfo) TraceObjName() string {
  2007  	return oi.Name
  2008  }
  2009  
  2010  // TraceVersionID returns version-id of object being traced
  2011  func (oi ObjectInfo) TraceVersionID() string {
  2012  	return oi.VersionID
  2013  }
  2014  
  2015  func (m *batchJobMetrics) trace(d batchJobMetric, job string, attempts int) func(info objTraceInfoer, err error) {
  2016  	startTime := time.Now()
  2017  	return func(info objTraceInfoer, err error) {
  2018  		duration := time.Since(startTime)
  2019  		if globalTrace.NumSubscribers(madmin.TraceBatch) > 0 {
  2020  			globalTrace.Publish(batchJobTrace(d, job, startTime, duration, info, attempts, err))
  2021  			return
  2022  		}
  2023  		switch d {
  2024  		case batchJobMetricReplication:
  2025  			if globalTrace.NumSubscribers(madmin.TraceBatchReplication) > 0 {
  2026  				globalTrace.Publish(batchJobTrace(d, job, startTime, duration, info, attempts, err))
  2027  			}
  2028  		case batchJobMetricKeyRotation:
  2029  			if globalTrace.NumSubscribers(madmin.TraceBatchKeyRotation) > 0 {
  2030  				globalTrace.Publish(batchJobTrace(d, job, startTime, duration, info, attempts, err))
  2031  			}
  2032  		case batchJobMetricExpire:
  2033  			if globalTrace.NumSubscribers(madmin.TraceBatchExpire) > 0 {
  2034  				globalTrace.Publish(batchJobTrace(d, job, startTime, duration, info, attempts, err))
  2035  			}
  2036  		}
  2037  	}
  2038  }
  2039  
  2040  func lookupStyle(s string) miniogo.BucketLookupType {
  2041  	var lookup miniogo.BucketLookupType
  2042  	switch s {
  2043  	case "on":
  2044  		lookup = miniogo.BucketLookupPath
  2045  	case "off":
  2046  		lookup = miniogo.BucketLookupDNS
  2047  	default:
  2048  		lookup = miniogo.BucketLookupAuto
  2049  
  2050  	}
  2051  	return lookup
  2052  }