Skip to content

Commit f40fc3c

Browse files
committed
Merge branch 'w/9.0/bugfix/CLDSRV-669' into tmp/octopus/w/9.1/bugfix/CLDSRV-669
2 parents 3c94fa4 + ef5932e commit f40fc3c

File tree

5 files changed

+811
-48
lines changed

5 files changed

+811
-48
lines changed

lib/api/apiUtils/object/abortMultipartUpload.js

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ const async = require('async');
33
const constants = require('../../../../constants');
44
const { data } = require('../../../data/wrapper');
55
const locationConstraintCheck = require('../object/locationConstraintCheck');
6-
const { standardMetadataValidateBucketAndObj } =
7-
require('../../../metadata/metadataUtils');
6+
const metadataUtils = require('../../../metadata/metadataUtils');
87
const { validateQuotas } = require('../quotas/quotaUtils');
98
const services = require('../../../services');
109
const metadata = require('../../../metadata/wrapper');
@@ -30,7 +29,7 @@ function abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log,
3029

3130
async.waterfall([
3231
function checkDestBucketVal(next) {
33-
standardMetadataValidateBucketAndObj(metadataValParams, authzIdentityResult, log,
32+
metadataUtils.standardMetadataValidateBucketAndObj(metadataValParams, authzIdentityResult, log,
3433
(err, destinationBucket, objectMD) => {
3534
if (err) {
3635
log.error('error validating request', { error: err });
@@ -61,7 +60,7 @@ function abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log,
6160
});
6261
},
6362
function abortExternalMpu(mpuBucket, mpuOverviewObj, destBucket, objectMD,
64-
next) {
63+
next) {
6564
const location = mpuOverviewObj.controllingLocationConstraint;
6665
const originalIdentityAuthzResults = request.actionImplicitDenies;
6766
// eslint-disable-next-line no-param-reassign
@@ -93,29 +92,82 @@ function abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log,
9392
skipDataDelete);
9493
});
9594
},
96-
function deleteObjectMetadata(mpuBucket, storedParts, destBucket, objectMD, skipDataDelete, next) {
97-
if (!objectMD || metadataValMPUparams.uploadId !== objectMD.uploadId) {
95+
// During Abort, we dynamically detect if the previous CompleteMPU call
96+
// created potential object metadata wrongly, e.g. by creating
97+
// an object version when some of the parts are missing.
98+
// By passing a null objectMD, we tell the subsequent steps
99+
// to skip the cleanup.
100+
// Another approach is possible, but not supported by all backends:
101+
// to honor the uploadId filter in standardMetadataValidateBucketAndObj
102+
// ensuring the objMD returned has the right uploadId. But this is not
103+
// supported by Metadata.
104+
function findObjectToCleanup(mpuBucket, storedParts, destBucket,
105+
objectMD, skipDataDelete, next) {
106+
if (!objectMD) {
107+
return next(null, mpuBucket, storedParts, destBucket, null, skipDataDelete);
108+
}
109+
110+
// If objectMD exists and has matching uploadId, use it directly
111+
// This handles all non-versioned cases, and some versioned cases.
112+
if (objectMD.uploadId === uploadId) {
98113
return next(null, mpuBucket, storedParts, destBucket, objectMD, skipDataDelete);
99114
}
100-
// In case there has been an error during cleanup after a complete MPU
101-
// (e.g. failure to delete MPU MD in shadow bucket),
102-
// we need to ensure that the MPU metadata is deleted.
115+
116+
// If bucket is not versioned, no need to check versions:
117+
// as the uploadId is not the same, we skip the cleanup.
118+
if (!destBucket.isVersioningEnabled()) {
119+
return next(null, mpuBucket, storedParts, destBucket, null, skipDataDelete);
120+
}
121+
122+
// Otherwise, list all versions to find one with a matching uploadId.
123+
return services.findObjectVersionByUploadId(bucketName, objectKey, uploadId, log, (err, foundVersion) => {
124+
if (err) {
125+
log.warn('error finding object version by uploadId, proceeding without cleanup', {
126+
error: err,
127+
method: 'abortMultipartUpload.findObjectToCleanup',
128+
});
129+
// On error, continue the abort without an objectMD to clean up.
130+
return next(null, mpuBucket, storedParts, destBucket, null, skipDataDelete);
131+
}
132+
return next(null, mpuBucket, storedParts, destBucket, foundVersion, skipDataDelete);
133+
});
134+
},
135+
function deleteObjectMetadata(mpuBucket, storedParts, destBucket, objectMD,
136+
skipDataDelete, next) {
137+
if (!objectMD) {
138+
return next(null, mpuBucket, storedParts, destBucket, objectMD, skipDataDelete);
139+
}
140+
103141
log.debug('Object has existing metadata, deleting them', {
104142
method: 'abortMultipartUpload',
105143
bucketName,
106144
objectKey,
107145
uploadId,
108146
versionId: objectMD.versionId,
109147
});
110-
return metadata.deleteObjectMD(bucketName, objectKey, { versionId: objectMD.versionId }, log, err => {
148+
return metadata.deleteObjectMD(bucketName, objectKey, {
149+
versionId: objectMD.versionId,
150+
}, log, err => {
111151
if (err) {
112-
log.error('error deleting object metadata', { error: err });
152+
// Handle concurrent deletion of this object metadata
153+
if (err.is?.NoSuchKey) {
154+
log.debug('object metadata already deleted or does not exist', {
155+
method: 'abortMultipartUpload',
156+
bucketName,
157+
objectKey,
158+
versionId: objectMD.versionId,
159+
});
160+
} else {
161+
log.error('error deleting object metadata', { error: err });
162+
}
113163
}
114-
return next(err, mpuBucket, storedParts, destBucket, objectMD, skipDataDelete);
164+
// Continue with the operation regardless of deletion success/failure
165+
// The important part is that we tried to clean up
166+
return next(null, mpuBucket, storedParts, destBucket, objectMD, skipDataDelete);
115167
});
116168
},
117169
function deleteData(mpuBucket, storedParts, destBucket, objectMD,
118-
skipDataDelete, next) {
170+
skipDataDelete, next) {
119171
if (skipDataDelete) {
120172
return next(null, mpuBucket, storedParts, destBucket);
121173
}
@@ -126,16 +178,18 @@ function abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log,
126178
return next(null, mpuBucket, storedParts, destBucket);
127179
}
128180

129-
if (objectMD && objectMD.location && objectMD.uploadId === metadataValMPUparams.uploadId) {
181+
// Add object data locations if they exist
182+
if (objectMD?.location) {
130183
const existingLocations = new Set(locations.map(loc => loc.key));
131-
const remainingObjectLocations = objectMD.location.filter(loc => !existingLocations.has(loc.key));
184+
const remainingObjectLocations = objectMD.
185+
location.filter(loc => !existingLocations.has(loc.key));
132186
locations.push(...remainingObjectLocations);
133187
}
134188

135189
return async.eachLimit(locations, 5, (loc, cb) => {
136190
data.delete(loc, log, err => {
137191
if (err) {
138-
log.fatal('delete ObjectPart failed', { err });
192+
log.warn('delete ObjectPart failed', { err });
139193
}
140194
cb();
141195
});

lib/services.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const { errors, s3middleware } = require('arsenal');
66
const ObjectMD = require('arsenal').models.ObjectMD;
77
const BucketInfo = require('arsenal').models.BucketInfo;
88
const ObjectMDArchive = require('arsenal').models.ObjectMDArchive;
9+
const { versioning } = require('arsenal');
910
const acl = require('./metadata/acl');
1011
const constants = require('../constants');
1112
const { config } = require('./Config');
@@ -417,6 +418,71 @@ const services = {
417418
});
418419
},
419420

421+
/**
422+
* Finds a specific object version by its uploadId by listing and filtering all versions.
423+
* This is used during MPU abort to clean up potentially orphaned object metadata.
424+
* @param {string} bucketName - The name of the bucket.
425+
* @param {string} objectKey - The key of the object.
426+
* @param {string} uploadId - The uploadId to search for.
427+
* @param {object} log - The logger instance.
428+
* @param {function} cb - The callback, called with (err, foundVersion).
429+
* @returns {undefined}
430+
*/
431+
findObjectVersionByUploadId(bucketName, objectKey, uploadId, log, cb) {
432+
let keyMarker = null;
433+
let versionIdMarker = null;
434+
let foundVersion = null;
435+
let shouldContinue = true;
436+
437+
return async.whilst(
438+
() => shouldContinue && !foundVersion,
439+
callback => {
440+
const listParams = {
441+
listingType: 'DelimiterVersions',
442+
// To only list the specific key, we need to add the versionId separator
443+
prefix: `${objectKey}${versioning.VersioningConstants.VersionId.Separator}`,
444+
maxKeys: 1000,
445+
};
446+
447+
if (keyMarker) {
448+
listParams.keyMarker = keyMarker;
449+
}
450+
if (versionIdMarker) {
451+
listParams.versionIdMarker = versionIdMarker;
452+
}
453+
454+
return this.getObjectListing(bucketName, listParams, log, (err, listResponse) => {
455+
if (err) {
456+
log.error('error listing object versions', { error: err });
457+
return callback(err);
458+
}
459+
460+
// Check each version in current batch for matching uploadId
461+
const matchedVersion = (listResponse.Versions || []).find(version =>
462+
version.key === objectKey &&
463+
version.value &&
464+
version.value.uploadId === uploadId
465+
);
466+
467+
if (matchedVersion) {
468+
foundVersion = matchedVersion.value;
469+
}
470+
471+
// Set up for next iteration if needed
472+
if (listResponse.IsTruncated && !foundVersion) {
473+
keyMarker = listResponse.NextKeyMarker;
474+
versionIdMarker = listResponse.NextVersionIdMarker;
475+
} else {
476+
shouldContinue = false;
477+
}
478+
479+
return callback();
480+
});
481+
},
482+
err => cb(err, err ? null : foundVersion)
483+
);
484+
},
485+
420486
/**
421487
* Gets list of objects ready to be lifecycled
422488
* @param {object} bucketName - bucket in which objectMetadata is stored

0 commit comments

Comments
 (0)