DEV Community

Zayniddin Atabaev
Zayniddin Atabaev

Posted on

Transaction against concurrency

Keling kichik app yasab ko'ramiz. Aytaylik user registratsiya qilmoqchimiz, stack uchun Express.js + PostgreSQL:

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();
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

Qanday yechim qilamiz? Transactions! Bizga wrapper funksiya kerak bo'ladi:

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

checkEmailExist va registerUser funksiyalari o'zgarmaydi, rout handlerni azgina o'zgartiramiz:

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 });
  }
});
Enter fullscreen mode Exit fullscreen mode

Va qarabsizki muammo hal!

Qanday test qilaman? checkEmailExist funksiyani sekin ishlashini simulate qilamiz, masalan quyidagini qo'shib qo'yamiz:

// simulate delay
await new Promise((resolve) => setTimeout(resolve, 1000));
Enter fullscreen mode Exit fullscreen mode

Loyiha kodlari | Telegram kanal

Top comments (0)