Establish lock to avoid race scenario in subscriptions (#14267)

* fix(subscription): establish lock to avoid race scenario

* fix(lint): import syntax

* fix(lint): whitespace, dependency cycle

* fix(subs): skip locking on gifts and groups

* fix(subs): correctly reset _subSignature

* fix(sub): use findOneAndUpdate for unlock

* fix(test): save newly created user for some reason

Co-authored-by: SabreCat <sabe@habitica.com>
This commit is contained in:
Sabe Jones
2022-10-25 16:44:33 -05:00
committed by GitHub
parent 22a0c72f6e
commit 90250d1a25
3 changed files with 38 additions and 4 deletions

View File

@@ -326,6 +326,7 @@ describe('Apple Payments', () => {
it('errors when a user is already subscribed', async () => {
payments.createSubscription.restore();
user = new User();
await user.save();
await applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing);

View File

@@ -11,6 +11,7 @@ import { // eslint-disable-line import/no-cycle
model as Group,
basicFields as basicGroupFields,
} from '../../models/group';
import { model as User } from '../../models/user'; // eslint-disable-line import/no-cycle
import {
NotAuthorized,
NotFound,
@@ -79,6 +80,22 @@ async function createSubscription (data) {
let emailType = 'subscription-begins';
let recipientIsSubscribed = recipient.isSubscribed();
if (data.user && !data.gift && !data.groupId) {
const unlockedUser = await User.findOneAndUpdate(
{
_id: data.user._id,
$or: [
{ _subSignature: 'NOT_RUNNING' },
{ _subSignature: { $exists: false } },
],
},
{ $set: { _subSignature: 'SUB_IN_PROGRESS' } },
);
if (!unlockedUser) {
throw new NotFound('User not found or subscription already processing.');
}
}
// If we are buying a group subscription
if (data.groupId) {
const groupFields = basicGroupFields.concat(' purchased');
@@ -282,10 +299,6 @@ async function createSubscription (data) {
}
}
if (group) await group.save();
if (data.user && data.user.isModified()) await data.user.save();
if (data.gift) await data.gift.member.save();
slack.sendSubscriptionNotification({
buyer: {
id: data.user._id,
@@ -302,6 +315,24 @@ async function createSubscription (data) {
groupId,
autoRenews,
});
if (group) {
await group.save();
}
if (data.user) {
if (data.user.isModified()) {
await data.user.save();
}
if (!data.gift && !data.groupId) {
await User.findOneAndUpdate(
{ _id: data.user._id },
{ $set: { _subSignature: 'NOT_RUNNING' } },
);
}
}
if (data.gift) {
await data.gift.member.save();
}
}
// Cancels a subscription or group plan, setting termination to happen later

View File

@@ -430,6 +430,8 @@ export default new Schema({
lastCron: { $type: Date, default: Date.now },
_cronSignature: { $type: String, default: 'NOT_RUNNING' }, // Private property used to avoid double cron
// Lock property to avoid double subscription. Not strictly private because we query on it
_subSignature: { $type: String, default: 'NOT_RUNNING' },
// {GROUP_ID: Boolean}, represents whether they have unseen chat messages
newMessages: {