Skip to content

Commit e214df0

Browse files
test following code review
1 parent 29d498b commit e214df0

2 files changed

Lines changed: 80 additions & 75 deletions

File tree

extensions/replication/tasks/ReplicateObject.js

Lines changed: 75 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ const { S3Client, GetBucketReplicationCommand, GetObjectCommand } = require('@aw
44
const { errors, jsutil, versioning } = require('arsenal');
55
const { ObjectMDLocation } = require('arsenal').models;
66
const { 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

914
const ClientManager = require('../../../lib/clients/ClientManager');
1015
const 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
}

tests/unit/replication/ReplicateObject.spec.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ describe('ReplicateObject', () => {
154154
task._getAndPutPartOnce(makeSourceEntry(older), makeDestEntry(), part, fakeLogger, (err, result) => {
155155
assert.ifError(err);
156156
assert.ok(result && result.isCollision, 'should return collision info object');
157-
assert.ok('destMvIdRaw' in result, 'collision info should include destMvIdRaw');
157+
assert.ok('destinationMicroVersionId' in result, 'collision info should include destinationMicroVersionId');
158158
sinon.assert.notCalled(task._publishDataWriteMetrics);
159159
done();
160160
});
@@ -428,9 +428,10 @@ describe('ReplicateObject', () => {
428428
getObjectKey: () => 'key',
429429
getEncodedVersionId: () => encode(generateVersionId('test', 'RG001')),
430430
getMicroVersionId: () => null,
431-
getReplicationContent: () => ['METADATA'],
431+
getReplicationContent: () => ['DATA', 'METADATA'],
432432
getContentLength: () => 0,
433433
getLocation: () => [],
434+
getReducedLocations: () => [],
434435
getLastModified: () => new Date().toISOString(),
435436
getIsDeleteMarker: () => false,
436437
getReplicationBackend: () => 'site',
@@ -439,6 +440,7 @@ describe('ReplicateObject', () => {
439440
};
440441

441442
task.metricsHandler = { rpo: () => {} };
443+
task.mProducer = { publishMetrics: () => {} };
442444

443445
sinon.stub(task, '_setupRoles').callsFake((e, l, cb) => cb(null, 'srcRole', 'dstRole'));
444446
sinon.stub(task, '_setTargetAccountMd').callsFake((e, r, l, cb) => cb(null));
@@ -447,7 +449,7 @@ describe('ReplicateObject', () => {
447449
const putMetadataStub = sinon.stub(task, '_putMetadata')
448450
.callsFake((e, mdOnly, l, cb) => {
449451
assert.strictEqual(mdOnly, false,
450-
'zero-byte objects must not use METADATA mode even when content is METADATA-only');
452+
'zero-byte objects must use DATA,METADATA (create) mode, not METADATA-only (update) mode');
451453
cb(null);
452454
});
453455

0 commit comments

Comments
 (0)