diff --git a/spec/MongoStorageAdapter.spec.js b/spec/MongoStorageAdapter.spec.js index facf1b2c41..f541eb5503 100644 --- a/spec/MongoStorageAdapter.spec.js +++ b/spec/MongoStorageAdapter.spec.js @@ -526,6 +526,55 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { expect(createdIndex.sparse).toBeFalsy(); }); + it('should ignore IndexKeySpecsConflict for an equivalent existing index', async () => { + const adapter = new MongoStorageAdapter({ uri: databaseURI }); + const error = Object.assign(new Error('IndexKeySpecsConflict'), { code: 86 }); + const mongoCollection = { + createIndex: jasmine.createSpy('createIndex').and.rejectWith(error), + indexes: jasmine.createSpy('indexes').and.resolveTo([ + { + name: 'ttl', + key: { expire: 1 }, + sparse: true, + expireAfterSeconds: 0, + }, + ]), + }; + spyOn(adapter, '_adaptiveCollection').and.resolveTo({ _mongoCollection: mongoCollection }); + + const schema = { fields: { expire: { type: 'Date' } } }; + const result = await adapter.ensureIndex('_Idempotency', schema, ['expire'], 'ttl', false, { + ttl: 0, + }); + + expect(result).toBe('ttl'); + expect(mongoCollection.indexes).toHaveBeenCalled(); + }); + + it('should reject IndexKeySpecsConflict for a different existing index', async () => { + const adapter = new MongoStorageAdapter({ uri: databaseURI }); + const error = Object.assign(new Error('IndexKeySpecsConflict'), { code: 86 }); + const mongoCollection = { + createIndex: jasmine.createSpy('createIndex').and.rejectWith(error), + indexes: jasmine.createSpy('indexes').and.resolveTo([ + { + name: 'ttl', + key: { other: 1 }, + sparse: true, + expireAfterSeconds: 0, + }, + ]), + }; + spyOn(adapter, '_adaptiveCollection').and.resolveTo({ _mongoCollection: mongoCollection }); + + const schema = { fields: { expire: { type: 'Date' } } }; + await adapter.ensureIndex('_Idempotency', schema, ['expire'], 'ttl', false, { ttl: 0 }).then( + () => fail('Expected IndexKeySpecsConflict to be rejected'), + rejectedError => expect(rejectedError).toBe(error) + ); + expect(mongoCollection.indexes).toHaveBeenCalled(); + }); + if (process.env.MONGODB_TOPOLOGY === 'replicaset') { describe('transactions', () => { const headers = { diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 9c17b2a18b..ae3db6c28e 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -58,6 +58,51 @@ function isTransientError(error) { return false; } +const INDEX_KEY_SPECS_CONFLICT = 86; +const INDEX_OPTIONS_IGNORED_IN_COMPARISON = [ + 'v', + 'key', + 'name', + 'ns', + 'background', + 'enableOrderedIndex', +]; +const BOOLEAN_INDEX_OPTIONS = ['hidden', 'sparse', 'unique']; + +function isSameIndexKey(existingKey, requestedKey) { + if (!existingKey || !requestedKey) { + return false; + } + + const existingKeys = Object.keys(existingKey); + const requestedKeys = Object.keys(requestedKey); + return ( + _.isEqual(existingKeys, requestedKeys) && + requestedKeys.every(key => _.isEqual(existingKey[key], requestedKey[key])) + ); +} + +function normalizeIndexOptionsForComparison(indexOptions) { + const normalizedOptions = _.omit(indexOptions, INDEX_OPTIONS_IGNORED_IN_COMPARISON); + BOOLEAN_INDEX_OPTIONS.forEach(option => { + if (!normalizedOptions[option]) { + delete normalizedOptions[option]; + } + }); + return normalizedOptions; +} + +function isEquivalentExistingIndex(existingIndex, indexCreationRequest, indexOptions) { + if (!existingIndex || !isSameIndexKey(existingIndex.key, indexCreationRequest)) { + return false; + } + + return _.isEqual( + normalizeIndexOptionsForComparison(existingIndex), + normalizeIndexOptionsForComparison(indexOptions) + ); +} + const storageAdapterAllCollections = mongoAdapter => { return mongoAdapter .connect() @@ -816,7 +861,19 @@ export class MongoStorageAdapter implements StorageAdapter { return this._adaptiveCollection(className) .then(collection => - collection._mongoCollection.createIndex(indexCreationRequest, indexOptions) + collection._mongoCollection.createIndex(indexCreationRequest, indexOptions).catch(error => { + if (error.code !== INDEX_KEY_SPECS_CONFLICT || !indexName) { + throw error; + } + + return collection._mongoCollection.indexes().then(indexes => { + const existingIndex = indexes.find(index => index.name === indexName); + if (isEquivalentExistingIndex(existingIndex, indexCreationRequest, indexOptions)) { + return indexName; + } + throw error; + }); + }) ) .catch(err => this.handleError(err)); }