Dealing with concurrency in Node JS

Introduction

Even though the Event Loop is a single thread we have to take care of race condition since 99% of our code will run in a non main thread. Callbacks and Promises are a good example of it. There are many resources along World Wide Web about how Event Loop works like this one, so the idea of this post is assume that we could have a resource in our code that could be accessed (read and write) by multiple threads.

Here we have a small snippet that shows how to deal with race condition. A common scenario is when we cache some data that was expensive to get in terms of CPU, network, file system or DB.

Implementation

We might implement a cache in multiple ways. A simple way is a in-memory collection; in this case, a Map. The structure of our collection can also be a List, that will depend on our requirements.

Our Map holds users and we use the User ID as the Key and the User itself (through a Promise) the Value. That way, a method getUserById will be very fast: O(1).

I’ll explain step by step but at the end of this post you have the full source code

So let start by our map

const cache = new Map();

Our Map won’t be so smart in this example, it won’t expire elements after a while and it will add as many elements as available memory we have. An advanced solution is to add this kind of logic to avoid problems. Also it will be empty after our server restarts, so is not persistent.

Let’s create a collection of users that simulate our DB

const users = [];
function createSomeUsers() {
    for (let i = 0; i < 10; i++) {
        const user = {
            id: i,
            name: 'user' + 1
        };
        users.push(user);
    }
}

The main method that we want to take care of race condition

function getUserFromDB(userId) {
    let userPromise = cache.get(userId);
    if (typeof userPromise === 'undefined') {
        console.info('Loading ' + userId + ' user from DB...');//SHOULD BE executed only once for each user
        userPromise = new Promise(function (resolve, reject) {
            //setTimeout will be our executeDBQuery
            const threeSeconds = 1000 * 3;
            setTimeout(() => {
                const user = users[userId];
                resolve(user);
            }, threeSeconds);
        });
        //add the user from DB to our cache
        cache.set(userId, userPromise);
    }
    return userPromise;
}

To test our race condition we’ll need create multiple callbacks that simulate a heavy operation. That simulation will be made with the classic setTimeout that will appear later.

function getRandomTime() {
    return Math.round(Math.random() * 1000);
}

Finally the method that simulate the race condition

function executeRace() {
    const userId = 3;
    //get the user #3 10 times to test race condition
    for (let i = 0; i < 10; i++) {
        setTimeout(() => {
            getUserFromDB(userId).then((user) => {
                console.log('[Thread ' + i + ']User result. ID: ' + user.id + ' NAME: ' + user.name);
            }).catch((err) => {
                console.log(err);
            });
        }, getRandomTime());
        console.info('Thread ' + i + ' created');
    }
}

Our last step: call our methods to create some users and to execute the race condition

createSomeUsers();
executeRace();

Let create a file called race_condition.js and execute it like this:

node race_condition.js

The output will be:

Dummy users created
Thread 0 created
Thread 1 created
Thread 2 created
Thread 3 created
Thread 4 created
Thread 5 created
Thread 6 created
Thread 7 created
Thread 8 created
Thread 9 created
Loading 3 user from DB...
[Thread 8]User result. ID: 3 NAME: user1
[Thread 3]User result. ID: 3 NAME: user1
[Thread 1]User result. ID: 3 NAME: user1
[Thread 9]User result. ID: 3 NAME: user1
[Thread 5]User result. ID: 3 NAME: user1
[Thread 2]User result. ID: 3 NAME: user1
[Thread 7]User result. ID: 3 NAME: user1
[Thread 0]User result. ID: 3 NAME: user1
[Thread 6]User result. ID: 3 NAME: user1
[Thread 4]User result. ID: 3 NAME: user1

Notice that [Thread X] output do not appear in order. That’s because of our random time tat simulate a thread that takes time to be resolved.

Full source code

/**
 * A cache implemented with a map collection
 * key: userId. 
 * value: a Promise that can be pending, resolved or rejected. The result of that promise is a user
 * IMPORTANT: 
 *  - This cache has not a max size and a TTL so will grow up indefinitely
 *  - This cache will be reset every time script restart. We could use Redis to avoid this
 */
const cache = new Map();
/**
 * Our collection that will simulate our DB
 */
const users = [];
/**
 * 
 */
function createSomeUsers() {
    for (let i = 0; i < 10; i++) {
        const user = {
            id: i,
            name: 'user' + 1
        };
        users.push(user);
    }
    console.info('Dummy users created');
}


/**
 * 
 * @param {int} userId 
 * @returns Promise<User>
 */
function getUserFromDB(userId) {
    let userPromise = cache.get(userId);
    if (typeof userPromise === 'undefined') {
        console.info('Loading ' + userId + ' user from DB...');//SHOULD BE executed only once for each user
        userPromise = new Promise(function (resolve, reject) {
            //setTimeout will be our executeDBQuery
            const threeSeconds = 1000 * 3;
            setTimeout(() => {
                const user = users[userId];
                resolve(user);
            }, threeSeconds);
        });
        //add the user from DB to our cache
        cache.set(userId, userPromise);
    }
    return userPromise;
}

/**
 * @returns a number between 0 and 1000 milliseconds
 */
function getRandomTime() {
    return Math.round(Math.random() * 1000);
}

/**
 * 
 */
function executeRace() {
    const userId = 3;
    //get the user #3 10 times to test race condition
    for (let i = 0; i < 10; i++) {
        setTimeout(() => {
            getUserFromDB(userId).then((user) => {
                console.log('[Thread ' + i + ']User result. ID: ' + user.id + ' NAME: ' + user.name);
            }).catch((err) => {
                console.log(err);
            });
        }, getRandomTime());
        console.info('Thread ' + i + ' created');
    }
}

createSomeUsers();
executeRace();

Photo by Charles 🇵🇭 on Unsplash

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s