Skip to content

Use transaction to update data when joining challenge #15253

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
39 changes: 30 additions & 9 deletions website/server/controllers/api-v3/challenges.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
BadRequest,
NotFound,
NotAuthorized,
InternalServerError,
} from '../../libs/errors';
import * as Tasks from '../../models/task';
import csvStringify from '../../libs/csvStringify';
Expand Down Expand Up @@ -337,19 +338,39 @@ api.joinChallenge = {
if (!group || !challenge.canJoin(user, group)) throw new NotFound(res.t('challengeNotFound'));
group.purchased = undefined;

const addedSuccessfully = await challenge.addToUser(user);
if (!addedSuccessfully) {
throw new NotAuthorized(res.t('userAlreadyInChallenge'));
}
// Start a mongo transaction
const session = await Challenge.startSession();
session.startTransaction();

challenge.memberCount += 1;
let saveResult;

addUserJoinChallengeNotification(user);
try {
// Add the challenge to the user
const addedSuccessfully = await challenge.addToUser(user, session);
if (!addedSuccessfully) {
throw new NotAuthorized(res.t('userAlreadyInChallenge'));
}

// Add all challenge's tasks to user's tasks and save the challenge
const results = await Promise.all([challenge.syncTasksToUser(user), challenge.save()]);
challenge.memberCount += 1;

addUserJoinChallengeNotification(user);
// Add all challenge's tasks to user's tasks and save the challenge
saveResult = await Promise.all([
challenge.syncTasksToUser(user, session),
challenge.save({ session }),
]);

await session.commitTransaction();
session.endSession();
} catch (error) {
// Abort the transaction and end the session if an error occurs
await session.abortTransaction();
session.endSession();
if (error instanceof NotAuthorized) throw error;
throw new InternalServerError(error);
}

const response = results[1].toJSON();
const response = saveResult[1].toJSON();
response.group = getChallengeGroupResponse(group);
const chalLeader = await User.findById(response.leader).select(nameFields).exec();
response.leader = chalLeader ? chalLeader.toJSON({ minimize: true }) : null;
Expand Down
11 changes: 6 additions & 5 deletions website/server/models/challenge.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ schema.methods.canJoin = function canJoinChallenge (user, group) {

// Returns true if the challenge was successfully added to the user
// or false if the user already in the challenge
schema.methods.addToUser = async function addChallengeToUser (user) {
schema.methods.addToUser = async function addChallengeToUser (user, session = null) {
// Add challenge to users challenges atomically (with a condition that checks that it
// is not there already) to prevent multiple concurrent requests from passing through
// see https://github.com/HabitRPG/habitica/issues/11295
Expand All @@ -117,6 +117,7 @@ schema.methods.addToUser = async function addChallengeToUser (user) {
challenges: { $nin: [this._id] },
},
{ $push: { challenges: this._id } },
{ session },
).exec();

return !!result.modifiedCount;
Expand All @@ -132,7 +133,7 @@ schema.methods.canView = function canViewChallenge (user, group) {

// Sync challenge tasks to user, including tags.
// Used when user joins the challenge or to force sync.
schema.methods.syncTasksToUser = async function syncChallengeTasksToUser (user) {
schema.methods.syncTasksToUser = async function syncChallengeTasksToUser (user, session = null) {
const challenge = this;
challenge.shortName = challenge.shortName || challenge.name;

Expand Down Expand Up @@ -194,18 +195,18 @@ schema.methods.syncTasksToUser = async function syncChallengeTasksToUser (user)
if (!matchingTask.notes) matchingTask.notes = chalTask.notes;
// add tag if missing
if (matchingTask.tags.indexOf(challenge._id) === -1) matchingTask.tags.push(challenge._id);
toSave.push(matchingTask.save());
toSave.push(matchingTask.save({ session }));
});

// Flag deleted tasks as "broken"
userTasks.forEach(userTask => {
if (!_.find(challengeTasks, chalTask => chalTask._id === userTask.challenge.taskId)) {
userTask.challenge.broken = 'TASK_DELETED';
toSave.push(userTask.save());
toSave.push(userTask.save({ session }));
}
});

toSave.push(user.save());
toSave.push(user.save({ session }));
return Promise.all(toSave);
};

Expand Down
Loading