Skip to content

Commit 40075b7

Browse files
author
Kerkesni
committed
Use insertOne for creating new version objects
In `putObjectVerCase1`, switch from using `updateOne` with upsert to `insertOne` for creating new version objects. The `putObjectVerCase1` function is designed exclusively for creating new object versions, each with a freshly generated, unique version ID. There is no intended scenario where a version object with the same ID should already exist. Using `updateOne` with `upsert:true` was therefore logically incorrect. It could silently overwrite an existing version object in the case of a version ID collision, leading to data corruption. By switching to `insertOne`, the operation will now correctly fail with a duplicate key error if a collision occurs. This fail-fast approach properly treats a version ID collision as an error, ensuring system integrity. Issue: ARSN-506
1 parent 8e90d70 commit 40075b7

2 files changed

Lines changed: 72 additions & 7 deletions

File tree

lib/storage/metadata/mongoclient/MongoClientInterface.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -826,14 +826,11 @@ class MongoClientInterface {
826826
const masterKey = formatMasterKey(objName, params.vFormat);
827827
// initiating array of operations with version creation
828828
const ops: AnyBulkWriteOperation<ObjectMetastoreDocument>[] = [{
829-
updateOne: {
830-
filter: {
829+
insertOne: {
830+
document: {
831831
_id: versionKey,
832-
},
833-
update: {
834-
$set: { _id: versionKey, value: objVal },
835-
},
836-
upsert: true,
832+
value: objVal,
833+
} as ObjectMetastoreDocument,
837834
},
838835
}];
839836
// filter to get master

tests/functional/metadata/mongodb/putObject.spec.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
const async = require('async');
22
const assert = require('assert');
3+
const sinon = require('sinon');
34
const werelogs = require('werelogs');
45
const { MongoMemoryReplSet } = require('mongodb-memory-server');
56
const { errors, versioning } = require('../../../../index');
67
const logger = new werelogs.Logger('MongoClientInterface', 'debug', 'debug');
78
const BucketInfo = require('../../../../lib/models/BucketInfo').default;
89
const MetadataWrapper =
910
require('../../../../lib/storage/metadata/MetadataWrapper');
11+
const { VersionID } = require('../../../../lib/versioning');
1012
const { BucketVersioningKeyFormat } = versioning.VersioningConstants;
1113

1214
const IMPL_NAME = 'mongodb';
@@ -128,6 +130,7 @@ describe('MongoClientInterface:metadata.putObjectMD', () => {
128130
});
129131

130132
afterEach(done => {
133+
sinon.restore();
131134
metadata.deleteBucket(BUCKET_NAME, logger, done);
132135
});
133136

@@ -354,6 +357,71 @@ describe('MongoClientInterface:metadata.putObjectMD', () => {
354357
], done);
355358
});
356359

360+
it(`should fail on versionID conflict when putting a new version ${variation.it}`, done => {
361+
const objVal = {
362+
key: OBJECT_NAME,
363+
};
364+
const params = {
365+
versioning: true,
366+
versionId: null,
367+
repairMaster: null,
368+
};
369+
370+
// simulate a versionId collision by always generating the same versionId
371+
const genVID = sinon.stub(VersionID, 'generateUniqueVersionId')
372+
.returns('test-version-id');
373+
374+
async.series([
375+
// We first create a master and a version
376+
next => metadata.putObjectMD(BUCKET_NAME, OBJECT_NAME, objVal, params, logger, next),
377+
// We put another version of the object with the same versionId
378+
next => metadata.putObjectMD(BUCKET_NAME, OBJECT_NAME, objVal, params, logger, next),
379+
], err => {
380+
assert(err.is.InternalError,
381+
`expected InternalError, got ${err.name} with message: ${err.message}`,
382+
);
383+
// make sure the retry triggered on the first collision detection
384+
assert(genVID.calledThrice,
385+
`expected generateUniqueVersionId to be called thrice, got ${genVID.callCount} times`);
386+
done();
387+
});
388+
});
389+
390+
it(`should succeed on version creation after a versionId collision ${variation.it}`, done => {
391+
const objVal = {
392+
key: OBJECT_NAME,
393+
};
394+
const params = {
395+
versioning: true,
396+
versionId: null,
397+
repairMaster: null,
398+
};
399+
400+
// simulate a versionId collision by always generating the same versionId
401+
const genVID = sinon.stub(VersionID, 'generateUniqueVersionId')
402+
.onFirstCall().returns('test-version-id')
403+
.onSecondCall().returns('test-version-id') // trigger collision
404+
.onThirdCall().returns('test-version-id-retry'); // change versionId on retry
405+
406+
async.series([
407+
// We first create a master and a version
408+
next => metadata.putObjectMD(BUCKET_NAME, OBJECT_NAME, objVal, params, logger, next),
409+
// We put another version of the object with the same versionId
410+
next => metadata.putObjectMD(BUCKET_NAME, OBJECT_NAME, objVal, params, logger, next),
411+
], (err, res) => {
412+
assert.ifError(err, `expected no error, got ${err}`);
413+
// make sure the retry triggered on the first collision detection
414+
assert(genVID.calledThrice,
415+
`expected generateUniqueVersionId to be called thrice, got ${genVID.callCount} times`);
416+
// make sure the last call returned a different versionId
417+
const vid1 = JSON.parse(res[0]).versionId;
418+
const vid2 = JSON.parse(res[1]).versionId;
419+
assert.notStrictEqual(vid1, vid2,
420+
`expected different versionIds, got ${vid1} and ${vid2}`);
421+
done();
422+
});
423+
});
424+
357425
itOnlyInV1(`Should delete master when last version is delete marker ${variation.it}`, done => {
358426
const objVal = {
359427
key: OBJECT_NAME,

0 commit comments

Comments
 (0)