@@ -4,7 +4,12 @@ const { S3Client, GetBucketReplicationCommand, GetObjectCommand } = require('@aw
44const { errors, jsutil, versioning } = require ( 'arsenal' ) ;
55const { ObjectMDLocation } = require ( 'arsenal' ) . models ;
66const { ReplicationConfiguration } = require ( 'arsenal' ) . models ;
7- const { encode : encodeMicroVersionId , decode : decodeMicroVersionId } = versioning . VersionID ;
7+ const {
8+ encode : encodeMicroVersionId ,
9+ decode : decodeMicroVersionId ,
10+ compare : compareMicroVersionId ,
11+ Ordering,
12+ } = versioning . VersionID ;
813
914const ClientManager = require ( '../../../lib/clients/ClientManager' ) ;
1015const BackbeatMetadataProxy = require ( '../../../lib/BackbeatMetadataProxy' ) ;
@@ -457,28 +462,31 @@ class ReplicateObject extends BackbeatTask {
457462 return mapLimitWaitPendingIfError ( locations , mpuConcLimit , ( part , done ) => {
458463 this . _getAndPutPart ( sourceEntry , destEntry , part , log , done ) ;
459464 } , ( err , partResults ) => {
460- const collisionResult = ( partResults || [ ] ) . find ( r => r && r . isCollision ) ;
465+ let collisionResult ;
466+ const nonCollisionParts = [ ] ;
467+ for ( const result of ( partResults || [ ] ) ) {
468+ if ( ! result ) {
469+ continue ;
470+ }
471+ if ( result . isCollision ) {
472+ collisionResult = collisionResult || result ;
473+ } else {
474+ nonCollisionParts . push ( result ) ;
475+ }
476+ }
461477 const hasPutDataConflict = collisionResult !== undefined ;
462478 if ( err || hasPutDataConflict ) {
463479 // On error or conflict, drop all parts written
464- return this . _deleteOrphans ( destEntry ,
465- ( partResults || [ ] ) . filter ( r => r && ! r . isCollision ) , log , ( ) => {
466- if ( hasPutDataConflict ) {
467- // Another replicant already stored the object data
468- // Compare microVersionIds to decide what to do with metadata:
469- // - source is newer : proceed in metadata-only mode
470- // - same or older : skip putMetadata
471- const destMvId = collisionResult . destMvIdRaw ;
472- const srcMvId = sourceEntry . getMicroVersionId ( ) || null ;
473- const isLoop = srcMvId === destMvId ;
474- const isStale = destMvId !== null &&
475- ( srcMvId === null || srcMvId > destMvId ) ;
476- return cb ( null , [ ] , true , isLoop || isStale ) ;
477- }
478- return cb ( err ) ;
479- } ) ;
480+ return this . _deleteOrphans ( destEntry , nonCollisionParts , log , ( ) => {
481+ if ( hasPutDataConflict ) {
482+ return cb ( null , [ ] , {
483+ microVersionId : collisionResult . destinationMicroVersionId ,
484+ } ) ;
485+ }
486+ return cb ( err ) ;
487+ } ) ;
480488 }
481- return cb ( null , partResults , false , false ) ;
489+ return cb ( null , partResults , undefined ) ;
482490 } ) ;
483491 }
484492
@@ -626,22 +634,14 @@ class ReplicateObject extends BackbeatTask {
626634 }
627635
628636 if ( err instanceof VersionIdCollisionException ) {
629- const destMvIdRaw = err . microVersionId
630- ? decodeMicroVersionId ( err . microVersionId ) : null ;
631- if ( destMvIdRaw instanceof Error ) {
632- log . error ( 'failed to decode microVersionId from putData 409' ,
633- {
634- method : 'ReplicateObject._getAndPutPartOnce' ,
635- entry : destEntry . getLogInfo ( ) ,
636- error : destMvIdRaw . message ,
637- } ) ;
638- return doneOnce ( destMvIdRaw ) ;
639- }
640637 log . info ( 'cascade putData: data already at destination' , {
641638 method : 'ReplicateObject._getAndPutPartOnce' ,
642639 entry : destEntry . getLogInfo ( ) ,
643640 } ) ;
644- return doneOnce ( null , { isCollision : true , destMvIdRaw } ) ;
641+ return doneOnce ( null , {
642+ isCollision : true ,
643+ destinationMicroVersionId : err . microVersionId ,
644+ } ) ;
645645 }
646646 // eslint-disable-next-line no-param-reassign
647647 err . origin = 'target' ;
@@ -726,10 +726,8 @@ class ReplicateObject extends BackbeatTask {
726726 . catch ( err => {
727727 // eslint-disable-next-line no-param-reassign
728728 err . origin = 'target' ;
729- if ( err . ObjNotFound || err . name === 'ObjNotFound' ) {
730- return cbOnce ( err ) ;
731- }
732- if ( err instanceof MicroVersionIdAlreadyStoredException ||
729+ if ( err . ObjNotFound || err . name === 'ObjNotFound' ||
730+ err instanceof MicroVersionIdAlreadyStoredException ||
733731 err instanceof StaleMicroVersionIdException ) {
734732 return cbOnce ( err ) ;
735733 }
@@ -969,30 +967,27 @@ class ReplicateObject extends BackbeatTask {
969967 return this . _getAndPutData ( sourceEntry , destEntry , log ,
970968 next ) ;
971969 }
972- return next ( null , [ ] , false , false ) ;
970+ return next ( null , [ ] , undefined ) ;
973971 } ,
974972 // update location, replication status and put metadata in
975973 // target bucket
976- ( destLocations , noNewDataLocations , skipMetadata , next ) => {
977- if ( skipMetadata ) {
974+ ( destLocations , conflict , next ) => {
975+ if ( this . _shouldSkipMetadata ( sourceEntry . getMicroVersionId ( ) , conflict , log ) ) {
978976 log . info ( 'skipping putMetadata: destination already has same or newer revision' , {
979977 entry : destEntry . getLogInfo ( ) ,
980978 } ) ;
981979 return next ( ) ;
982980 }
983- // METADATA mode tells cloudserver to preserve the destination's existing
984- // data location instead of overwriting it. It applies when either:
981+ // METADATA tells cloudserver to UPDATE the existing metadata document
982+ // (preserving the stored location). DATA,METADATA tells cloudserver to
983+ // CREATE a new document. Use METADATA when:
985984 // - this is a metadata-only replication entry (mdOnly), or
986- // - data was already at the destination from another path (noNewDataLocations,
987- // i.e. cascade putData 409).
988- // Zero-byte objects have no location to preserve, so METADATA mode is
989- // always wrong for them regardless of the above flags.
990- const localMdOnly = ( mdOnly || noNewDataLocations ) && sourceEntry . getContentLength ( ) > 0 ;
985+ // - data was already written to the destination from another path (conflict).
986+ const localMdOnly = mdOnly || conflict !== undefined ;
991987 destEntry . setLocation ( destLocations ) ;
992988 return this . _putMetadata ( destEntry , localMdOnly , log , err => {
993989 if ( err ) {
994- return this . _deleteOrphans (
995- destEntry , destLocations , log , ( ) => next ( err ) ) ;
990+ return this . _deleteOrphans ( destEntry , destLocations , log , ( ) => next ( err ) ) ;
996991 }
997992 return next ( ) ;
998993 } ) ;
@@ -1001,47 +996,55 @@ class ReplicateObject extends BackbeatTask {
1001996 err , sourceEntry , destEntry , kafkaEntry , log , done ) ) ;
1002997 }
1003998
999+ // Returns true if putMetadata can be skipped because the destination already
1000+ // holds this revision or a newer one. Returns false when there is no conflict,
1001+ // when the destination microVersionId is absent or can't be parsed (proceed
1002+ // conservatively), or when the source holds a newer revision.
1003+ _shouldSkipMetadata ( sourceMicroVersionId , conflict , log ) {
1004+ if ( ! conflict ) {
1005+ return false ;
1006+ }
1007+ let destMvId = null ;
1008+ if ( conflict . microVersionId ) {
1009+ const decoded = decodeMicroVersionId ( conflict . microVersionId ) ;
1010+ if ( decoded instanceof Error ) {
1011+ log . warn ( 'could not decode microVersionId from putData 409, ' +
1012+ 'proceeding to putMetadata without skip optimisation' , {
1013+ error : decoded . message ,
1014+ } ) ;
1015+ } else {
1016+ destMvId = decoded ;
1017+ }
1018+ }
1019+ const comparison = compareMicroVersionId ( sourceMicroVersionId , destMvId ) ;
1020+ return destMvId !== null &&
1021+ ( comparison === Ordering . OLDER || comparison === Ordering . EQUAL ) ;
1022+ }
1023+
10041024 _processQueueEntryRetryFull ( sourceEntry , destEntry , kafkaEntry , log , done ) {
10051025 return async . waterfall ( [
10061026 next => this . _getAndPutData ( sourceEntry , destEntry , log , next ) ,
1007- // update location, replication status and put metadata in
1008- // target bucket
1009- ( destLocations , noNewDataLocations , skipMetadata , next ) => {
1010- if ( skipMetadata ) {
1027+ ( destLocations , conflict , next ) => {
1028+ if ( this . _shouldSkipMetadata ( sourceEntry . getMicroVersionId ( ) , conflict , log ) ) {
10111029 log . info ( 'skipping putMetadata: destination already has same or newer revision' , {
10121030 entry : destEntry . getLogInfo ( ) ,
10131031 } ) ;
10141032 return next ( ) ;
10151033 }
1016- // On retry, only keep METADATA mode when data was already at the destination
1017- // from another path (cascade 409) and the object is non-zero-byte.
1018- // For zero-byte there is no location to preserve; always write metadata directly.
1019- const retryMdOnly = noNewDataLocations && sourceEntry . getContentLength ( ) > 0 ;
1034+ const localMdOnly = conflict !== undefined ;
10201035 destEntry . setLocation ( destLocations ) ;
1021- return this . _putMetadata ( destEntry , retryMdOnly , log , err => {
1022- if ( err ) {
1023- return next ( err ) ;
1024- }
1025- return next ( ) ;
1026- } ) ;
1036+ return this . _putMetadata ( destEntry , localMdOnly , log , next ) ;
10271037 } ,
10281038 ] , err => this . _handleReplicationOutcome (
10291039 err , sourceEntry , destEntry , kafkaEntry , log , done ) ) ;
10301040 }
10311041
10321042 _handleReplicationOutcome ( err , sourceEntry , destEntry , kafkaEntry ,
10331043 log , done ) {
1034- if ( err instanceof MicroVersionIdAlreadyStoredException ) {
1035- log . info ( 'replication completed via cascade loop: ' +
1036- 'object already at destination with the same revision' ,
1037- { entry : sourceEntry . getLogInfo ( ) } ) ;
1038- this . _publishReplicationStatus ( sourceEntry , 'COMPLETED' , { kafkaEntry, log } ) ;
1039- return done ( null , { committable : false } ) ;
1040- }
1041- if ( err instanceof StaleMicroVersionIdException ) {
1042- log . info ( 'replication completed: destination already holds ' +
1043- 'this version with a newer revision' ,
1044- { entry : sourceEntry . getLogInfo ( ) } ) ;
1044+ if ( err instanceof MicroVersionIdAlreadyStoredException ||
1045+ err instanceof StaleMicroVersionIdException ) {
1046+ log . info ( 'replication completed: object already at destination' ,
1047+ { entry : sourceEntry . getLogInfo ( ) , reason : err . name } ) ;
10451048 this . _publishReplicationStatus ( sourceEntry , 'COMPLETED' , { kafkaEntry, log } ) ;
10461049 return done ( null , { committable : false } ) ;
10471050 }
0 commit comments