Transaction against concurrency
Tags: concurrency, transactions, postgres
Published: 2024-06-14 19:13:11


Keling kichik app yasab ko'ramiz. Aytaylik user registratsiya qilmoqchimiz, stack uchun Express.js + PostgreSQL:
 
```js
const express = require("express");
const pg = require("pg").Pool;

const app = express();
app.use(express.json());
const dbClient = new pg({ host: "localhost", user: "root", password: "1234", database: "test" });

async function checkEmailExist(email) {
  const users = await dbClient.query(
    `SELECT id
    FROM users
    WHERE email = $1
    LIMIT 1`,
    [email]
  );
  return users.rowCount > 0;
}

async function registerUser(email) {
  return await dbClient.query(
    `INSERT INTO users (email)
     VALUES ($1)
     RETURNING id`,
    [email]
  );
}

app.post("/register", async (req, res) => {
  // get email from request body
  const { email } = req.body;

  // check if email is not taken
  const emailExist = await checkEmailExist(email);
  if (emailExist) return res.status(400).json({ error: "Email already exist" });

  // register new user
  await registerUser(email);

  return res.status(201).json({ data: "User registered successfully!" });
});

async function main() {
  try {
    await dbClient.connect();
    app.listen(3000, () => console.log("OLD server is running on port 3000"));
  } catch (error) {
    console.error(error);
  }
}

main();
```

Boshlanishiga hammasi yaxshi. App lauch bo'ldi va minglab foydalanuvchilar ro'yhatdan o'tdi. Keyin database'ga bir qarab ko'rsangiz ba'zi email'lar duplicate bo'lgan. WTH!

Shu joyida _concurrency_ esga tushadi va bugni reproduce qilishga urinib ko'rasiz(pastdagi script). Ya'ni faraz qilaylik, `checkEmailExist` funksiyasi ishlashi uchun biror N vaqt oladi va shu vaqt oralig'idagi  boshqa /register endpointga kelgan requestlar ham execute bo'ladi. Concurrency! Va qarabsizki sizda duplikatlar bor:
```js
const axios = require("axios");

async function register() {
  return await axios.post("http://localhost:3000/register", {
    email: "johndoe@gmail.com",
  });
}

(async () => {
  for (let i = 0; i < 5; i++) {
    register()
      .then((res) => console.log(res.data))
      .catch((err) => console.error(err.response?.data));
  }
})(); // Surprise! We have 5 johndoe@gmail.com's in db
```

Qanday yechim qilamiz? **Transactions**! Bizga _wrapper_ funksiya kerak bo'ladi:
```js
async function $transaction(f) {
  try {
    // start db transaction
    await dbClient.query("BEGIN");
    await dbClient.query("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE");

    // execute main function
    const result = await f();

    // commit db transaction
    await dbClient.query("COMMIT");

    return result;
  } catch (error) {
    await dbClient.query("ROLLBACK");
    throw error;
  }
}
```

`checkEmailExist` va `registerUser` funksiyalari o'zgarmaydi, rout handlerni azgina o'zgartiramiz:
```js
app.post("/register", async (req, res) => {
  // get email from request body
  const { email } = req.body;

  try {
    await $transaction(async () => {
      // check if email is not taken
      const emailExist = await checkEmailExist(email);
      if (emailExist) {
        throw "Email already exist";
      }

      // register new user
      await registerUser(email);
    });

    return res.json({ data: "User registered successfully!" });
  } catch (error) {
    console.error(error);
    return res.json({ error });
  }
});
```

Va qarabsizki muammo hal!

Qanday test qilaman? `checkEmailExist` funksiyani sekin ishlashini _simulate_ qilamiz, masalan quyidagini qo'shib qo'yamiz:
```js
// simulate delay
await new Promise((resolve) => setTimeout(resolve, 1000));
```

[Loyiha kodlari](https://github.com/zayniddindev/transaction-against-concurrency) | [Telegram kanal](https://t.me/scriptjs)