Transaction against concurrency

RMAG news

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();

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

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;
}
}

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 });
}
});

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));

Loyiha kodlari | Telegram kanal

Please follow and like us:
Pin Share