We're using mongo 4.2, node 14.17.3, mongodb package 3.6.10 - with default read (local
) and write concern (w:1
) on a replica set with 3 instances.
Our ecommerce system:
- uses a transaction to create a sale from the stock (to ensure stock decrease and sale increase are atomic)
- once this transaction has ended, launches a payement with stripe
- once this payment is successful, stores the id of this payment with an
updateOne
on the previously created sale.
This works well, but, mostly on heavy loads (but also some other times), the stripe id is missing from the sale, and the updateOne
returns a modifiedCount
of 0.
Here is a simplified version of the code:
const allocate = async(sale, stocksToAllocate) => {
const saleJSON = await this.mapping.encode(sale); // transforms sale class instance to json literal
const session = client.startSession();
try {
await session.withTransaction(async() => {
if (sale._id)
await this.repository.replaceOne({_id: sale._id}, saleJSON, {session});
else
await this.repository.insertOne(saleJSON, {session});
for (const stockToAllocate of stocksToAllocate) {
const stockJSON = await this.stockRepository.findOne({_id: stockToAllocate._id}, {session});
const stock = await this.stockMapping.decode(stockJSON); // transforms json literal to a stock class instance
if (stockToAllocate.count < 0) {
stock.replenish(-stockToAllocate.count, sale.delivery.shippingAt);
} else if (stock.canAffect(stockToAllocate.count, sale.delivery.shippingAt)) {
stock.affect(stockToAllocate.count, sale.delivery.shippingAt);
} else {
throw new ValidationError('Stock unavailable', {id: stock._id});
}
const updatedStockJSON = await this.stockMapping.encode(stock);
await this.stockRepository.replaceOne({_id: stockToAllocate._id}, updatedStockJSON, {session});
}
});
} finally {
await session.endSession();
}
const invoiceItem = {
customer: sale.getStripeCustomerId(),
subscription: sale.getStripeSubscriptionId(),
amount: sale.getPrice().ttc,
currency: 'EUR',
description: sale.getName()
};
const updatedInvoiceItem = await stripe.invoiceItems.create(invoiceItem);
await this.repository.updateOne({_id: sale._id}, {$set: {'payment.stripe.chargeId': updatedInvoiceItem.id}}); // this updateOne returns a modifiedCount of 0 sometimes
};
Thanks a lot.
CodePudding user response:
This issue is not related to Mongo.
Before the shared code sample, this.repository
is wrapped on each request to add the user id to the each db request (something like this.repository = this.repository.shield(userId)
).
On heavy loads, on the same node instance, a first user may be awaiting the answer from Stripe, while a second user is launching the transaction; thus erasing the shield
. So when Stripe answers to the first user, the mongo query is enriched with a userId linked to the second user and not the first one.
Fixing this will fix the issue.