@@ -587,6 +587,9 @@ function putMetadata(request, response, bucketInfo, objMd, log, callback) {
587587 // Note: '' is falsy so the presence check must be !== undefined, not truthy.
588588 const hasMicroVersionId = encodedMicroVersionId !== undefined ;
589589 let incomingMicroVersionId = null ;
590+ const metadataOptions = {
591+ overheadField : constants . overheadField ,
592+ } ;
590593 if ( hasMicroVersionId && objMd ) {
591594 // '' means source has no microVersionId, treated as older revision
592595 incomingMicroVersionId = encodedMicroVersionId === '' ? null : decode ( encodedMicroVersionId ) ;
@@ -640,6 +643,13 @@ function putMetadata(request, response, bucketInfo, objMd, log, callback) {
640643 conflictErr . mvId ,
641644 ) ;
642645 }
646+
647+ // Atomic counterpart of the JS pre-checks above, performed at database level.
648+ if ( incomingMicroVersionId !== null ) {
649+ metadataOptions . conditions = {
650+ $or : [ { microVersionId : { $exists : false } } , { microVersionId : { $gt : incomingMicroVersionId } } ] ,
651+ } ;
652+ }
643653 }
644654
645655 return _getRequestPayload ( request , ( err , payload ) => {
@@ -791,25 +801,21 @@ function putMetadata(request, response, bucketInfo, objMd, log, callback) {
791801 omVal . replicationInfo . isNFS = ! omVal . replicationInfo . isReplica ;
792802 }
793803
794- const options = {
795- overheadField : constants . overheadField ,
796- } ;
797-
798804 // NOTE: When 'versioning' is set to true and no 'versionId' is specified,
799805 // it results in the creation of a "new" version, which also updates the master.
800806 // NOTE: Since option fields are converted to strings when they're sent to Metadata via the query string,
801807 // Metadata interprets the value "false" as if it were true.
802808 // Therefore, to avoid this confusion, we don't pass the versioning parameter at all if its value is false.
803809 if ( versioning ) {
804- options . versioning = true ;
810+ metadataOptions . versioning = true ;
805811 }
806812
807813 // NOTE: When options fields are sent to Metadata through the query string,
808814 // they are converted to strings. As a result, Metadata interprets the value undefined
809815 // in the versionId field as an empty string ('').
810816 // To prevent this, the versionId field is only included in options when it is defined.
811817 if ( versionId !== undefined ) {
812- options . versionId = versionId ;
818+ metadataOptions . versionId = versionId ;
813819 omVal . versionId = versionId ;
814820
815821 if ( isNull ) {
@@ -829,13 +835,13 @@ function putMetadata(request, response, bucketInfo, objMd, log, callback) {
829835 // The update is only done when putting a new version as updating versions that already exist
830836 // shouldn't affect the master.
831837 if ( ! objMd ) {
832- options . repairMaster = true ;
838+ metadataOptions . repairMaster = true ;
833839 }
834840 }
835841
836842 // If the new null keys logic (S3C-7352) is not supported (compatibility mode), 'isNull' remains undefined.
837843 if ( ! nullVersionCompatMode ) {
838- options . isNull = isNull ;
844+ metadataOptions . isNull = isNull ;
839845 }
840846
841847 const isReplicationWrite = ! ! headers [ 'x-scal-replication-content' ] ;
@@ -937,7 +943,7 @@ function putMetadata(request, response, bucketInfo, objMd, log, callback) {
937943 ) ;
938944
939945 if ( versioningPreprocessingResult ) {
940- options . deleteNullKey = versioningPreprocessingResult . deleteNullKey ;
946+ metadataOptions . deleteNullKey = versioningPreprocessingResult . deleteNullKey ;
941947
942948 // The master references a null version only via extraMD,
943949 // which is set solely in nullVersionCompatMode. In null-key
@@ -953,9 +959,9 @@ function putMetadata(request, response, bucketInfo, objMd, log, callback) {
953959 log . trace ( 'putting object version' , {
954960 objectKey : request . objectKey ,
955961 omVal,
956- options ,
962+ metadataOptions ,
957963 } ) ;
958- return metadata . putObjectMD ( bucketName , objectKey , omVal , options , log , ( err , md ) => {
964+ return metadata . putObjectMD ( bucketName , objectKey , omVal , metadataOptions , log , ( err , md ) => {
959965 if ( err ) {
960966 // Handle duplicate key error during repair operation
961967 // This can happen due to race conditions when multiple operations
@@ -964,7 +970,7 @@ function putMetadata(request, response, bucketInfo, objMd, log, callback) {
964970 // treat this as success.
965971 const errorMessage = err . message || err . toString ( ) || '' ;
966972 const isRepairDuplicateKeyError =
967- options . repairMaster &&
973+ metadataOptions . repairMaster &&
968974 ( errorMessage . includes ( 'E11000' ) ||
969975 errorMessage . includes ( 'duplicate key' ) ||
970976 errorMessage . includes ( 'repair' ) ) ;
@@ -1045,7 +1051,25 @@ function putMetadata(request, response, bucketInfo, objMd, log, callback) {
10451051 } ) ;
10461052 } ,
10471053 ] ,
1048- callback ,
1054+ ( err , results ) => {
1055+ if ( err ?. is ?. PreconditionFailed && metadataOptions . conditions ) {
1056+ log . debug ( 'putMetadata: write rejected, incoming microVersionId is not newer than stored' , {
1057+ method : 'putMetadata' ,
1058+ bucketName,
1059+ objectKey,
1060+ } ) ;
1061+ request . resume ( ) ;
1062+ return _respondWithHeaderCrrConflict (
1063+ response ,
1064+ log ,
1065+ callback ,
1066+ StaleMicroVersionIdException . name ,
1067+ 'incoming revision is not newer than stored' ,
1068+ objMd ?. microVersionId ,
1069+ ) ;
1070+ }
1071+ return callback ( err , results ) ;
1072+ } ,
10491073 ) ;
10501074 } ) ;
10511075}
0 commit comments