Handle Race Conditions In NodeJS Using Mutex

September 17, 2019Mathilde Duboille4 min read

tartan track

Even if JavaScript is single-threaded, there can be concurrency issues like race-conditions due to asynchronism. This article aims to tell you how I handled this using mutual exclusion. But first, let’s recall what is a race condition before getting into the problem and telling you how I solved it.

Race condition

A race condition occurs when multiple processes try to access a same shared resource and at least one of them tries to modify its value. For example, imagine we have a shared value a = 3 and two processes A and B. Imagine process A wants to add 5 to the current value of a whereas process B wants to add 2 to a only if a < 5. Depending on which process executes first, the result won’t be the one expected. If process A executes first, the value of a will be 8 whereas it will be 10 if process B executes first. To avoid race conditions, we use mutual exclusion. I'll talk about it more in detail later in this article.

Now let me explain the issue I had.

Context

One of my project at Theodo consisted in building a NodeJS application that proposed multiple events and contests for its customers. There was also a premium membership feature that allowed users to participate in free events. To participate, users just need to click on the participation button on the event page, and they receive a ticket. Multiple participations to the same event are disallowed by disabling the button after the participation success.

Moreover, there is a security check in the back-end to prevent users with an existing paid order to get another ticket. You can see the simplified code below:

async function participateInFreeEvent(user: User, eventId: number): Promise<void> {
	const existOrder = await findOrder(eventId, user.id);
    if (!existOrder) {
        const order = buildNewOrder(eventId, user.id);
        createOrder(order.id, eventId, user.id);
    }
}

First, I search in the database for an existing order related to a user and an event. If no order exists, then a new order is created and saved in the database. Otherwise, nothing is done.

However, some people managed to get several tickets by clicking many times quickly on this button. This was a race-condition issue.

What was the problem exactly ?

The previous condition wasn’t enough. Indeed, even if JavaScript is mono-threaded, it doesn’t prevent race-conditions. When you deal with asynchronous functions, the thread doesn’t block its execution but it either executes the next line that doesn’t depend on the asynchronous call or continues the execution corresponding to a response event. As a result, two different executions can intertwine.

Take the example below: a user makes 2 consecutive requests to the back-end, and suppose he or she has no order corresponding to this event. As JavaScript is mono-threaded, the execution stack will be:

Execution stack

But the execution order of the different lines of the function will be:

Code execution without lock

findOrder and createOrder are asynchronous calls since they read and write into the database. As a consequence, the two requests will intertwine. As you can see in the figure above, the second findOrder is executed right after the one of the first request. Hence, the second evaluation of !existOrder will be true since the call has been made before creating an order.

Conclusion: our user will receive 2 tickets.

Solution

I had to find a way of locking this part of the code to execute the whole function before allowing another request to execute the same code, and so avoid race-conditions. I did this using mutex, with the async-mutex library (you can install it by running yarn add async-mutex).

A mutex is a mutual exclusion object which creates a resource that can be shared between multiple threads of a program. The resource can be seen as a lock that only one thread can acquire. If another thread wants to acquire the lock, it has to wait until the lock is released. But beware that the lock should always be released eventually, no matter what happens during the execution. Otherwise, it will lead to deadlocks, and your program will be blocked.

In order not to slow down the purchase of tickets, I used one mutex per user, that I stored in a Map. If the user isn’t mapped to a mutex, a new instance is created in the map using the user id as a key, and then I used the mutex like this :

import { Mutex, MutexInterface } from 'async-mutex';

class PaymentService {
	private locks : Map<string, MutexInterface>;

	constructor() {
		this.locks = new Map();
	}

	public async participateInFreeEvent(user: User, eventId: number): Promise<void> {
        if (!this.locks.has(user.id)) {
          this.locks.set(user.id, new Mutex());
        }
        
        this.locks
            .get(user.id)
            .acquire()
            .then(async (release) => {
                try {
                    const existOrder = await findOrder(eventId, user.id);
                    if (!existOrder) {
                        const order = buildNewOrder(eventId, user.id);
                        createOrder(order.id, eventId, user.id);
                    }
                } catch (error) {
                } finally {
                    release();
                }
            },
        );
    }
}

You can see that with the try-catch-finally block, I ensure that the lock will always be released. Now, the execution will look like the figure below:

Code execution with lock

This way, a user will only be able to participate once.

Of course, this is not the only way of solving this kind of problem. Transactions are also really helpful in this case.

Also, keep in mind that it solved the problem on a single server instance. If you have multiple servers, then you should use distributed mutex to let all processes know that the lock is acquired.

Mathilde Duboille

Mathilde Duboille

Web Developer at Theodo