Solve parallel saves to mongodb

Viewed 47

so I've recently tried to create a booking site in which you can select a date you wish to book. Then you'd send a request to /api/v1/reserve which would check if the data is valid. Generate a stripe payment intent, save the reservation to the database and send the payment intent id to the front end (for confirmation). My problem is that if two clients click send at the same time, they both pay for the same date which shouldn't happen.

router.post("/reservation", async (req, res) => {

    //Check if everything is in the reqest
    if (!req.body?.name || !req.body?.phone || !req.body?.date) return res.sendStatus(400);
    if (!req.body.phone.match(/^[+]{0,1}[4]{1,2}[8][\s0-9]*$/)) return res.status(400).json({
        type: ErrorTypes.PHONE
    });


    //Validating date
    const reservationDate = new Date(req.body.date);
    if (reservationDate < new Date()) return res.status(400).json({
        type: ErrorTypes.DATE
    });
    //Checking if date is already reserved
    if (await checkIfDateReserved(reservationDate)) return res.status(400).json({
        type: ErrorTypes.RESERVED
    })
    //if reservation date is weekend raise the price 
    const estimatedPrice = reservationDate.getDay() == 6 || reservationDate.getDay() == 0 ? 3500 : 2000;
    try {
        //Create payment intet
        const intent = await stripe.paymentIntents.create({
            amount: estimatedPrice,
            currency: "PLN",
            payment_method_types: ['card']

        });

        const document = new ReservationModel({
            name: req.body.name,
            reservationDate: reservationDate,
            phone: req.body.phone,
            complete: false,
            intentId: intent.id
        });
        await document.save();
        //If everything went smoothley, send the secret to the client
        //Send the client the intent secret to confirm the payment
        res.json({ clientSecret: intent.client_secret });


    } catch (error) {
        res.status(500);
    }

});

Basically, both processes run in parallel in different threads and they check if the date is reserved at the same time and reserve it at the same time. How can I make it so the database will wait until the first save request is finished before processing another? The reservationDate field is unique but it seems MongoDB still ignores it.

PS: Also, I thought about using transactions but I don't really know how would they apply here.

1 Answers

so I ended up using an answer in this question thanks to 0x1C1B. The mutex ended up working perfectly. Leaving the code if someone needs it.

const mutex = new Mutex();

router.post("/reservation", async (req, res) => {

    //Check if everything is in the reqest
    if (!req.body?.name || !req.body?.phone || !req.body?.date) return res.sendStatus(400);
    if (!req.body.phone.match(/^[\s0-9]{9,11}/)) return res.status(400).json({
        type: ErrorTypes.PHONE
    });


    //Validating date
    const reservationDate = new Date(req.body.date);
    if (reservationDate < new Date()) return res.status(400).json({
        type: ErrorTypes.DATE
    });
    await mutex.lock(); //Lock the mutex to avoid processing two reservations at once
    //Checking if date is already reserved
    if (await checkIfDateReserved(reservationDate)) {
        mutex.release(); //Release the mutex so other requests can continue
        return res.status(400).json({
            type: ErrorTypes.RESERVED
        })
    }
    //if reservation date is weekend raise the price 
    const estimatedPrice = reservationDate.getDay() == 6 || reservationDate.getDay() == 0 ? 3500 : 2000;
    try {
        //Create payment intet
        const intent = await stripe.paymentIntents.create({
            amount: estimatedPrice,
            currency: "PLN",
            payment_method_types: ['card']

        });

        const document = new ReservationModel({
            name: req.body.name,
            reservationDate: reservationDate,
            phone: req.body.phone,
            complete: false,
            intentId: intent.id
        });
        await document.save();
        //If everything went smoothley, send the secret to the client
        //Send the client the intent secret to confirm the payment
        res.json({ clientSecret: intent.client_secret }); //Send the client_secret required for payment confirmation
        //If the confirmation doesn't go through in 5 minutes the reservation is canceled due to "createdAt" field in the schema (see ../../schemas/Reservation.ts)
        //If the payment goes through the automatic deletion is cancelled (see index.ts:59)
        mutex.release(); //Release mutex after reservation has been saved to db
        //So other users can't reserve it

    } catch (error) {
        res.status(500);
        mutex.release();
    }

});
Related