Home   dsa  

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;
Published on: Jul 10, 2024, 12:12 AM  
 

Comments

Add your comment