11const async = require ( 'async' ) ;
22const { S3Client, GetBucketReplicationCommand, GetObjectCommand } = require ( '@aws-sdk/client-s3' ) ;
33
4- const errors = require ( 'arsenal' ) . errors ;
5- const jsutil = require ( 'arsenal' ) . jsutil ;
6- const ObjectMDLocation = require ( 'arsenal' ) . models . ObjectMDLocation ;
7- const ReplicationConfiguration = require ( 'arsenal' ) . models . ReplicationConfiguration ;
4+ const { errors, jsutil , versioning } = require ( 'arsenal' ) ;
5+ const { ObjectMDLocation } = require ( 'arsenal' ) . models ;
6+ const { ReplicationConfiguration } = require ( 'arsenal' ) . models ;
7+ const { encode : encodeMicroVersionId , decode } = versioning . VersionID ;
88
99const ClientManager = require ( '../../../lib/clients/ClientManager' ) ;
1010const BackbeatMetadataProxy = require ( '../../../lib/BackbeatMetadataProxy' ) ;
11- const {
11+ const {
1212 BackbeatRoutesClient,
1313 PutDataCommand,
1414 BatchDeleteCommand,
1515 PutMetadataCommand,
1616 GetMetadataCommand,
1717 addContentLengthMiddleware,
1818 attachReqUids,
19+ attachExpectContinueMiddleware,
20+ VersionIdCollisionException,
21+ StaleMicroVersionIdException,
22+ MicroVersionIdAlreadyStoredException,
1923} = require ( '@scality/cloudserverclient' ) ;
2024
2125const mapLimitWaitPendingIfError = require ( '../../../lib/util/mapLimitWaitPendingIfError' ) ;
@@ -31,6 +35,7 @@ const ObjectQueueEntry = require('../../../lib/models/ObjectQueueEntry');
3135const { authTypeAssumeRole } = require ( '../../../lib/constants' ) ;
3236
3337const errorAlreadyCompleted = { } ;
38+ const partAlreadyAtDest = { } ;
3439
3540function _extractAccountIdFromRole ( role ) {
3641 return role . split ( ':' ) [ 4 ] ;
@@ -452,11 +457,18 @@ class ReplicateObject extends BackbeatTask {
452457 const mpuConcLimit = this . repConfig . queueProcessor . mpuPartsConcurrency ;
453458 return mapLimitWaitPendingIfError ( locations , mpuConcLimit , ( part , done ) => {
454459 this . _getAndPutPart ( sourceEntry , destEntry , part , log , done ) ;
455- } , ( err , destLocations ) => {
460+ } , ( err , partResults ) => {
461+ // partAlreadyAtDest signals data already at dest (cascade putData 409);
462+ // exclude it so it is not stored as a new location or orphan-deleted.
463+ const destLocations = ( partResults || [ ] )
464+ . filter ( result => result && result !== partAlreadyAtDest ) ;
456465 if ( err ) {
457466 return this . _deleteOrphans ( destEntry , destLocations , log , ( ) => cb ( err ) ) ;
458467 }
459- return cb ( null , destLocations ) ;
468+ // When no new data locations were written, force metadata-only mode (noNewDataLocations)
469+ // so cloudserver preserves the destination's existing location field instead of overwriting it
470+ const noNewDataLocations = destLocations . length === 0 ;
471+ return cb ( null , destLocations , noNewDataLocations ) ;
460472 } ) ;
461473 }
462474
@@ -570,7 +582,9 @@ class ReplicateObject extends BackbeatTask {
570582 // destination bucket has to be versioning enabled.
571583 VersioningRequired : true ,
572584 RequestUids : log . getSerializedUids ( ) ,
585+ VersionId : sourceEntry . getEncodedVersionId ( ) ,
573586 } ) ;
587+ attachExpectContinueMiddleware ( putCommand , this . backbeatDest . config ?. requestHandler ) ;
574588 addContentLengthMiddleware (
575589 putCommand ,
576590 response . ContentLength ,
@@ -600,6 +614,12 @@ class ReplicateObject extends BackbeatTask {
600614 incomingMsg . destroy ( ) ;
601615 }
602616 }
617+
618+ if ( err instanceof VersionIdCollisionException ) {
619+ const { err : collisionErr , result } = this . _resolveVersionIdCollision (
620+ err , sourceEntry , destEntry , log ) ;
621+ return doneOnce ( collisionErr , result ) ;
622+ }
603623 // eslint-disable-next-line no-param-reassign
604624 err . origin = 'target' ;
605625 log . error ( 'an error occurred on putData to S3' ,
@@ -642,6 +662,43 @@ class ReplicateObject extends BackbeatTask {
642662 } ) ;
643663 }
644664
665+ _resolveVersionIdCollision ( collisionErr , sourceEntry , destEntry , log ) {
666+ const logMeta = {
667+ method : 'ReplicateObject._resolveVersionIdCollision' ,
668+ entry : destEntry . getLogInfo ( ) ,
669+ } ;
670+ const decodedDestinationMicroVersionId = collisionErr . microVersionId
671+ ? decode ( collisionErr . microVersionId ) : null ;
672+ if ( decodedDestinationMicroVersionId instanceof Error ) {
673+ log . error ( 'failed to decode microVersionId from putData 409' ,
674+ { ...logMeta , error : decodedDestinationMicroVersionId . message } ) ;
675+ return { err : decodedDestinationMicroVersionId } ;
676+ }
677+ // null = oldest revision (original putObject, before any metadata update).
678+ // VersionID strings are in reverse chronological order: a lexicographically
679+ // larger string is an older revision.
680+ const isSourceOlderThanDestination = ( sourceMicroVersionId , destinationMicroVersionId ) =>
681+ destinationMicroVersionId !== null &&
682+ ( sourceMicroVersionId === null || sourceMicroVersionId > destinationMicroVersionId ) ;
683+
684+ // Treat '' and undefined as null (no microVersionId set).
685+ const sourceMicroVersionId = sourceEntry . getMicroVersionId ( ) || null ;
686+ const destinationMicroVersionId = decodedDestinationMicroVersionId || null ;
687+ if ( sourceMicroVersionId === destinationMicroVersionId ) {
688+ log . info ( 'cascade loop detected on putData: ' +
689+ 'destination already has this exact revision, skipping' , logMeta ) ;
690+ return { err : new MicroVersionIdAlreadyStoredException ( { } ) } ;
691+ }
692+ if ( isSourceOlderThanDestination ( sourceMicroVersionId , destinationMicroVersionId ) ) {
693+ log . info ( 'cascade stale on putData: ' +
694+ 'destination already has a newer revision' , logMeta ) ;
695+ return { err : new StaleMicroVersionIdException ( { } ) } ;
696+ }
697+ log . info ( 'cascade putData: data already at destination, ' +
698+ 'proceeding with metadata update' , logMeta ) ;
699+ return { err : null , result : partAlreadyAtDest } ;
700+ }
701+
645702 _putMetadataOnce ( entry , mdOnly , log , cb ) {
646703 log . debug ( 'putting metadata' , {
647704 where : 'target' , entry : entry . getLogInfo ( ) ,
@@ -671,6 +728,8 @@ class ReplicateObject extends BackbeatTask {
671728 // destination bucket has to be versioning enabled.
672729 VersioningRequired : true ,
673730 RequestUids : log . getSerializedUids ( ) ,
731+ MicroVersionId : entry . getMicroVersionId ( )
732+ ? encodeMicroVersionId ( entry . getMicroVersionId ( ) ) : '' ,
674733 } ) ;
675734 const writeStartTime = Date . now ( ) ;
676735 return this . backbeatDest . send ( command )
@@ -685,14 +744,14 @@ class ReplicateObject extends BackbeatTask {
685744 return cbOnce ( err ) ;
686745 }
687746 log . error ( 'an error occurred when putting metadata to S3' ,
688- {
689- method : 'ReplicateObject._putMetadataOnce' ,
690- entry : entry . getLogInfo ( ) ,
691- origin : 'target' ,
692- peer : this . destBackbeatHost ,
693- error : err . message ,
694- err,
695- } ) ;
747+ {
748+ method : 'ReplicateObject._putMetadataOnce' ,
749+ entry : entry . getLogInfo ( ) ,
750+ origin : 'target' ,
751+ peer : this . destBackbeatHost ,
752+ error : err . message ,
753+ err,
754+ } ) ;
696755 return cbOnce ( err ) ;
697756 } ) ;
698757 }
@@ -920,13 +979,14 @@ class ReplicateObject extends BackbeatTask {
920979 return this . _getAndPutData ( sourceEntry , destEntry , log ,
921980 next ) ;
922981 }
923- return next ( null , [ ] ) ;
982+ return next ( null , [ ] , false ) ;
924983 } ,
925984 // update location, replication status and put metadata in
926985 // target bucket
927- ( destLocations , next ) => {
986+ ( destLocations , noNewDataLocations , next ) => {
987+ const localMdOnly = mdOnly || noNewDataLocations ;
928988 destEntry . setLocation ( destLocations ) ;
929- this . _putMetadata ( destEntry , mdOnly , log , err => {
989+ this . _putMetadata ( destEntry , localMdOnly , log , err => {
930990 if ( err ) {
931991 return this . _deleteOrphans (
932992 destEntry , destLocations , log , ( ) => next ( err ) ) ;
@@ -946,22 +1006,35 @@ class ReplicateObject extends BackbeatTask {
9461006 next => this . _getAndPutData ( sourceEntry , destEntry , log , next ) ,
9471007 // update location, replication status and put metadata in
9481008 // target bucket
949- ( location , next ) => {
950- destEntry . setLocation ( location ) ;
951- this . _putMetadata ( destEntry , false , log , next ) ;
1009+ ( destLocations , noNewDataLocations , next ) => {
1010+ destEntry . setLocation ( destLocations ) ;
1011+ this . _putMetadata ( destEntry , noNewDataLocations , log , next ) ;
9521012 } ,
9531013 ] , err => this . _handleReplicationOutcome (
9541014 err , sourceEntry , destEntry , kafkaEntry , log , done ) ) ;
9551015 }
9561016
9571017 _handleReplicationOutcome ( err , sourceEntry , destEntry , kafkaEntry ,
9581018 log , done ) {
1019+ if ( err instanceof MicroVersionIdAlreadyStoredException ) {
1020+ log . info ( 'replication completed via cascade loop: ' +
1021+ 'object already at destination with the same revision' ,
1022+ { entry : sourceEntry . getLogInfo ( ) } ) ;
1023+ this . _publishReplicationStatus ( sourceEntry , 'COMPLETED' , { kafkaEntry, log } ) ;
1024+ return done ( null , { committable : false } ) ;
1025+ }
1026+ if ( err instanceof StaleMicroVersionIdException ) {
1027+ log . info ( 'replication completed: destination already holds ' +
1028+ 'this version with a newer revision' ,
1029+ { entry : sourceEntry . getLogInfo ( ) } ) ;
1030+ this . _publishReplicationStatus ( sourceEntry , 'COMPLETED' , { kafkaEntry, log } ) ;
1031+ return done ( null , { committable : false } ) ;
1032+ }
9591033 if ( ! err ) {
9601034 log . debug ( 'replication succeeded for object, publishing ' +
9611035 'replication status as COMPLETED' ,
9621036 { entry : sourceEntry . getLogInfo ( ) } ) ;
963- this . _publishReplicationStatus (
964- sourceEntry , 'COMPLETED' , { kafkaEntry, log } ) ;
1037+ this . _publishReplicationStatus ( sourceEntry , 'COMPLETED' , { kafkaEntry, log } ) ;
9651038 return done ( null , { committable : false } ) ;
9661039 }
9671040 if ( err . BadRole || err . name === 'BadRole' ||
@@ -1020,3 +1093,4 @@ class ReplicateObject extends BackbeatTask {
10201093}
10211094
10221095module . exports = ReplicateObject ;
1096+ module . exports . _cascadeSignals = { partAlreadyAtDest } ;
0 commit comments