How to handle concurrency and avoid race conditions in an app
To handle concurrency and avoid race conditions when multiple instances of app are running, you need to implement proper locking mechanisms. There are several ways to achieve this, depending on your specific requirements and architecture. Below are some common approaches:
1. Distributed Locking
Using Redis
Redis is a popular in-memory data store that supports distributed locking through its SET
command with the NX
(only set if not exists) and PX
(expiry time in milliseconds) options. One common implementation is the Redlock algorithm.
First, install the redis
and redlock
packages:
npm install redis redlock
Then, implement distributed locking using Redis:
// redisClient.js
const redis = require('redis');
const { promisify } = require('util');
const client = redis.createClient();
client.on('error', (err) => {
console.error('Redis error:', err);
});
const getAsync = promisify(client.get).bind(client);
const setAsync = promisify(client.set).bind(client);
module.exports = { client, getAsync, setAsync };
// lock.js
const Redlock = require('redlock');
const { client } = require('./redisClient');
const redlock = new Redlock(
[client],
{
driftFactor: 0.01, // time in ms
retryCount: 10,
retryDelay: 200, // time in ms
retryJitter: 200, // time in ms
}
);
module.exports = redlock;
// models.js
const redlock = require('./lock');
// Suppose you have ParkingLot, ParkingSpot, Vehicle, and Ticket classes
class ParkingLot {
// ... other methods
async parkVehicle(vehicle) {
const lock = await redlock.lock('locks:parkingLot', 1000); // 1 second lock
try {
const spot = this.findAvailableSpot(vehicle.vehicleType);
if (spot) {
spot.assignVehicle(vehicle);
const ticket = new Ticket(vehicle, new Date());
this.activeTickets.set(vehicle.licensePlate, ticket);
return ticket;
}
return null;
} finally {
await lock.unlock().catch(() => {}); // Ensure the lock is released
}
}
async removeVehicle(licensePlate) {
const lock = await redlock.lock('locks:parkingLot', 1000); // 1 second lock
try {
const ticket = this.activeTickets.get(licensePlate);
if (ticket) {
const spot = this.spots.find(spot => spot.vehicle && spot.vehicle.licensePlate === licensePlate);
spot.removeVehicle();
ticket.setExitTime(new Date());
this.activeTickets.delete(licensePlate);
return ticket;
}
return null;
} finally {
await lock.unlock().catch(() => {}); // Ensure the lock is released
}
}
}
2. Database Transactions
If you are using a relational database, you can use transactions to ensure atomicity. Most relational databases support transactions, which can be used to lock rows or tables during an operation.
Example using PostgreSQL with pg
:
npm install pg
// db.js
const { Pool } = require('pg');
const pool = new Pool({
connectionString: 'postgresql://username:password@localhost:5432/parkinglot'
});
module.exports = pool;
// models.js
const pool = require('./db');
// Same ParkingLot, ParkingSpot, Vehicle, and Ticket classes as before
class ParkingLot {
// ... other methods
async parkVehicle(vehicle) {
const client = await pool.connect();
try {
await client.query('BEGIN');
const spot = await this.findAvailableSpot(client, vehicle.vehicleType);
if (spot) {
await this.assignVehicleToSpot(client, spot, vehicle);
const ticket = new Ticket(vehicle, new Date());
await this.createTicket(client, ticket);
await client.query('COMMIT');
return ticket;
}
await client.query('ROLLBACK');
return null;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
async removeVehicle(licensePlate) {
const client = await pool.connect();
try {
await client.query('BEGIN');
const ticket = await this.findTicketByLicensePlate(client, licensePlate);
if (ticket) {
await this.removeVehicleFromSpot(client, ticket.vehicle);
await this.updateTicketExitTime(client, ticket);
await client.query('COMMIT');
return ticket;
}
await client.query('ROLLBACK');
return null;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
// Helper methods for database operations
async findAvailableSpot(client, vehicleType) {
// Query database to find an available spot
}
async assignVehicleToSpot(client, spot, vehicle) {
// Query database to assign a vehicle to a spot
}
async createTicket(client, ticket) {
// Query database to create a new ticket
}
async findTicketByLicensePlate(client, licensePlate) {
// Query database to find a ticket by license plate
}
async removeVehicleFromSpot(client, vehicle) {
// Query database to remove a vehicle from a spot
}
async updateTicketExitTime(client, ticket) {
// Query database to update the exit time of a ticket
}
}
3. Message Queues
For high-concurrency environments, you can use message queues to serialize access to shared resources. This approach involves sending tasks to a queue and having a worker process them one at a time.
Example using RabbitMQ with amqplib
:
npm install amqplib
// queue.js
const amqp = require('amqplib');
async function sendToQueue(queue, msg) {
const conn = await amqp.connect('amqp://localhost');
const channel = await conn.createChannel();
await channel.assertQueue(queue);
channel.sendToQueue(queue, Buffer.from(msg));
setTimeout(() => {
conn.close();
}, 500);
}
async function receiveFromQueue(queue, callback) {
const conn = await amqp.connect('amqp://localhost');
const channel = await conn.createChannel();
await channel.assertQueue(queue);
channel.consume(queue, (msg) => {
if (msg !== null) {
callback(msg.content.toString());
channel.ack(msg);
}
});
}
module.exports = { sendToQueue, receiveFromQueue };
// worker.js
const { receiveFromQueue } = require('./queue');
const { ParkingLot, Vehicle } = require('./models');
const parkingLot = new ParkingLot();
parkingLot.createParkingSpots(100);
receiveFromQueue('parking', async (msg) => {
const { action, vehicleData } = JSON.parse(msg);
if (action === 'park') {
const vehicle = new Vehicle(vehicleData.licensePlate, vehicleData.vehicleType);
const ticket = await parkingLot.parkVehicle(vehicle);
console.log(ticket ? `Parked: ${ticket.id}` : 'No available spot');
} else if (action === 'remove') {
const ticket = await parkingLot.removeVehicle(vehicleData.licensePlate);
console.log(ticket ? `Removed: ${ticket.id}` : 'Ticket not found');
}
});
// routes.js
const express = require('express');
const router = express.Router();
const { sendToQueue } = require('./queue');
router.post('/park', (req, res) => {
const { licensePlate, vehicleType } = req.body;
sendToQueue('parking', JSON.stringify({ action: 'park', vehicleData: { licensePlate, vehicleType } }));
res.json({ message: 'Parking request sent' });
});
router.post('/remove', (req, res) => {
const { licensePlate } = req.body;
sendToQueue('parking', JSON.stringify({ action: 'remove', vehicleData: { licensePlate } }));
res.json({ message: 'Remove request sent' });
});
module.exports = router;