diff --git a/test/api/v3/unit/models/group.test.js b/test/api/v3/unit/models/group.test.js index b11f8dc9a4..d198c4690f 100644 --- a/test/api/v3/unit/models/group.test.js +++ b/test/api/v3/unit/models/group.test.js @@ -1061,8 +1061,45 @@ describe('Group Model', () => { [nonParticipatingMember._id]: false, [undecidedMember._id]: null, }; + }); - sandbox.spy(User, 'update'); + describe('user update retry failures', () => { + let successfulMock = { + exec: () => { + return Promise.resolve({raw: 'sucess'}); + }, + }; + let failedMock = { + exec: () => { + return Promise.reject(new Error('error')); + }, + }; + + it('doesn\'t retry successful operations', async () => { + sandbox.stub(User, 'update').returns(successfulMock); + + await party.finishQuest(quest); + + expect(User.update).to.be.calledTwice; + }); + + it('stops retrying when a successful update has occurred', async () => { + let updateStub = sandbox.stub(User, 'update'); + updateStub.onCall(0).returns(failedMock); + updateStub.returns(successfulMock); + + await party.finishQuest(quest); + + expect(User.update).to.be.calledThrice; + }); + + it('retries failed updates at most five times per user', async () => { + sandbox.stub(User, 'update').returns(failedMock); + + await expect(party.finishQuest(quest)).to.eventually.be.rejected; + + expect(User.update.callCount).to.eql(10); + }); }); it('gives out achievements', async () => { @@ -1171,13 +1208,15 @@ describe('Group Model', () => { context('Party quests', () => { it('updates participating members with rewards', async () => { + sandbox.spy(User, 'update'); await party.finishQuest(quest); - expect(User.update).to.be.calledOnce; + expect(User.update).to.be.calledTwice; expect(User.update).to.be.calledWithMatch({ - _id: { - $in: [questLeader._id, participatingMember._id], - }, + _id: questLeader._id, + }); + expect(User.update).to.be.calledWithMatch({ + _id: participatingMember._id, }); }); @@ -1204,6 +1243,7 @@ describe('Group Model', () => { }); it('updates all users with rewards', async () => { + sandbox.spy(User, 'update'); await party.finishQuest(tavernQuest); expect(User.update).to.be.calledOnce; diff --git a/website/server/models/group.js b/website/server/models/group.js index d04d3c9506..e310d1a21d 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -38,6 +38,7 @@ const LARGE_GROUP_COUNT_MESSAGE_CUTOFF = shared.constants.LARGE_GROUP_COUNT_MESS const CRON_SAFE_MODE = nconf.get('CRON_SAFE_MODE') === 'true'; const CRON_SEMI_SAFE_MODE = nconf.get('CRON_SEMI_SAFE_MODE') === 'true'; +const MAX_UPDATE_RETRIES = 5; export let schema = new Schema({ name: {type: String, required: true}, @@ -577,6 +578,19 @@ schema.statics.cleanGroupQuest = function cleanGroupQuest () { }; }; +async function _updateUserWithRetries (userId, updates, numTry = 1) { + return await User.update({_id: userId}, updates).exec() + .then((raw) => { + return raw; + }).catch((err) => { + if (numTry < MAX_UPDATE_RETRIES) { + return _updateUserWithRetries(userId, updates, ++numTry); + } else { + throw err; + } + }); +} + // Participants: Grant rewards & achievements, finish quest. // Changes the group object update members schema.methods.finishQuest = async function finishQuest (quest) { @@ -623,11 +637,19 @@ schema.methods.finishQuest = async function finishQuest (quest) { } }); - let q = this._id === TAVERN_ID ? {} : {_id: {$in: this.getParticipatingQuestMembers()}}; + let participants = this._id === TAVERN_ID ? {} : this.getParticipatingQuestMembers(); this.quest = {}; this.markModified('quest'); - return await User.update(q, updates, {multi: true}).exec(); + if (this._id === TAVERN_ID) { + return await User.update({}, updates, {multi: true}).exec(); + } + + let promises = participants.map(userId => { + return _updateUserWithRetries(userId, updates); + }); + + return Bluebird.all(promises); }; function _isOnQuest (user, progress, group) {