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 }