Node js Race Conditions
--
One of the basic concepts for node js developers is understanding race conditions. Let's make a simple API that registers users for a newsletter to understand race conditions. The only requirement is user should be able to register only once.
We will use Node js, Express js, and MongoDB for development.
Step 1: Install Mongo DB
We will use docker to start the local MongoDB instance.
docker run -p 27017:27017 --name newsletter -d mongo:latest
Note: Please map volume if you want to persist data.
Step 2: version 1 API that will return ‘success’ if it's a new email or error for already enrolled
function wait(timeout) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, timeout);
})
}
app.post('/v1/register', async (req, res) => {
try {
// find if user is already registered
let user = await client.db("newsletter").collection("usersV1").findOne({email:req.body.email})
if (user) {
return res.json({ message: `user already registered` })
}
// adding this line makes easy to test parallel requests from same user
await wait(10000)
const result = await client.db("newsletter").collection("usersV1").insertOne({email:req.body.email});
res.json({ message: `user registered successfully` })
} catch (error) {
console.log(error)
res.json({ message: error.message })
}
})
step 3: test version 1 API with curl
curl --location --request POST 'localhost:3000/v1/register' \
--header 'Content-Type: application/json' \
--data-raw '{
"email":"test1@gmail.com"
}'
When we hit this request from multiple terminals at the same time, same user is registered twice.
step 4: version 2API that will return ‘success’ if it's a new email or ‘error: for already enrolled
app.post('/v2/register', async (req, res) => {
try {
const result = await client.db("newsletter").collection("usersV2").insertOne(req.body);
res.json({ message: `user registered successfully` })
} catch (error) {
console.error(error.message)
res.json({ message: error.message })
}
})
step 5: create a unique index for email field when we connect to DB
client.connect().then(async () => {
console.log(`connected to DB`);
await client.db("newsletter").collection("usersV2").createIndex({ email: 1 }, { unique: true })
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
})
step 6: test v2 API
In the case of V2, never duplicate users will be registered, even when we get more than 1 request from the same user at the same time. We can also validate this using artillery or JMeter.
Complete code:
const express = require('express')
var bodyParser = require('body-parser')
const { MongoClient } = require('mongodb');
// const uri = "mongodb+srv://<username>:<password>@<your-cluster-url>/test?retryWrites=true&w=majority";
const uri = "mongodb://localhost:27017/newsletter";
const client = new MongoClient(uri);
const app = express()
const port = 3000
app.use(bodyParser.json())
app.get('/', (req, res) => {
res.send('Hello World to Race Conditions')
})
app.post('/v1/register', async (req, res) => {
try {
// find if user is already registered
let user = await client.db("newsletter").collection("usersV1").findOne({email:req.body.email})
if (user) {
return res.json({ message: `user already registered` })
}
// adding this line makes easy to test parallel requests from same user
await wait(10000)
const result = await client.db("newsletter").collection("usersV1").insertOne({email:req.body.email});
res.json({ message: `user registered successfully` })
} catch (error) {
console.log(error)
res.json({ message: error.message })
}
})
app.post('/v2/register', async (req, res) => {
try {
const result = await client.db("newsletter").collection("usersV2").insertOne({email:req.body.email});
res.json({ message: `user registered successfully` })
} catch (error) {
console.error(error.message)
res.json({ message: error.message })
}
})
function wait(timeout) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, timeout);
})
}
client.connect().then(async () => {
console.log(`connected to DB`);
await client.db("newsletter").collection("usersV2").createIndex({ email: 1 }, { unique: true })
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
})
GitHub repo:
Share this with anybody you think would benefit from this. Have any suggestions or questions? Feel free to message me on LinkedIn