'The proper way to achieve database locks (Pessimistic Read and Write) with TypeORM and NodeJS

I have an application that needs to deal with concurrency/race conditions. I noticed user account balances were being updated wrongly. Multiple requests can reach in at the same time and then the the balance data table is messed up.

For example, Request A and B comes in, Request B executes first and then Request A follows. However Request A has already read the the row and might be working with an old value that Request B has already updated. So balance is 5, request B gets the balance ( 5 ) and adds ( 3 ) to make 8 and updates the table. Since both requests came at the same time, request A has already read the balance 5 and will work from 5 instead of waiting and letting Request B finish first and then working with the new balance of 8.

I've tried several examples both blogs and stack overflow for almost a week now and I am totally drained. If anyone has worked or can provide assistance, kindly help me out here please.

I have been trying to replicate a situation where multiple code calls to the database want to modify the same value but they are all executing concurrently although I have specified a "pessimistic_write" lock on the row and other calls are supposed to wait. It seems nothing is working.

Code sample

require("dotenv").config();
const environment = process.env;

module.exports = async function UserAccount(connection, Entities) {
    console.log("==== STARTING =====");

    const queryRunner = connection.createQueryRunner();
    await queryRunner.connect();

    await queryRunner.startTransaction();

    /* const data = await queryRunner.query("SELECT * FROM balances");

    await queryRunner.release(); */

    queryRunner.manager
        .getRepository(Entities.Balances)
        .createQueryBuilder("balances")
        .useTransaction(true)
        .select()
        .setLock("pessimistic_write")
        .where("balances.id = :status", { status: 1 })
        .getOne()
        .then((result) => {
            console.log("= 2 ===================================");
            console.log(result);
            console.log("====================================");

            queryRunner.manager
                .getRepository(Entities.Balances)
                .createQueryBuilder("balanceX")
                .update()
                .set({ balance: `${Number(Math.random()) + Number(result.balance)}` })
                .where("balances.id = :status", { status: 1 })
                .execute()
                .then((result) => {
                    console.log("= 2A ===================================");
                    console.log(result);
                    console.log(Number(result.balance));
                    console.log("====================================");
                });
        });

    queryRunner.manager
        .getRepository(Entities.Balances)
        .createQueryBuilder("balances")
        .select()
        .useTransaction(true)
        .setLock("pessimistic_read")
        .where("balances.id = :status", { status: 1 })
        .getOne()
        .then((result) => {
            console.log("= 1 ==================================");
            console.log(result);
            console.log(Number(result.balance));
            console.log("====================================");
        });

    // queryRunner.commitTransaction();

    // queryRunner.startTransaction();

    // queryRunner.commitTransaction();

    // queryRunner.startTransaction();

    queryRunner.manager
        .getRepository(Entities.Balances)
        .createQueryBuilder("balances")
        .useTransaction(true)
        .select()
        .setLock("pessimistic_write")
        .where("balances.id = :status", { status: 1 })
        .getOne()
        .then((result) => {
            console.log("= 3 ===================================");
            console.log(result);
            console.log("====================================");

            queryRunner.manager
                .getRepository(Entities.Balances)
                .createQueryBuilder("balanceX")
                .update()
                .set({ balance: `${Number(Math.random()) + Number(result.balance)}` })
                .where("balances.id = :status", { status: 1 })
                .execute()
                .then((result) => {
                    console.log("= 3A ===================================");
                    console.log(result);
                    console.log(Number(result.balance));
                    console.log("====================================");
                });
        });

    queryRunner.commitTransaction();

    // await queryRunner.release();

    // await queryRunner.commitTransaction();
    // await queryRunner.release();

    // await queryRunner.startTransaction();

    /* try {
            // await queryRunner.manager.save({});
            await queryRunner.commitTransaction();
        } catch (err) {
            await queryRunner.rollbackTransaction();
        } finally {
            await queryRunner.release();
        } */
};

At this point, I am willing to accept any help. Reading and updating works fine. I am trying to implement lock and updating. Thanks in advance.



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source