Here are the culprits of the race condition we encountered in Part 1.
What happened?
We saw that when the API was hit one-by-one manually, the update after the query was correctly executed. This is because when we hit that API again, the room.save()
function had done its job already. Therefore, when the players count reached 4, the query code ran perfectly and set isFull
to true before we manually hit the next API call.
But when we used ab to hit 20 API calls concurrently, how the code worked began to matter. After 4 API calls, the player count reached 4. However, before the query was able to check for it and update the isFull
to false, other API calls were already occurring. This allowed the remaining queries to run too, and 19 players could come on board before the isFull
value was finally set to true. How can we solve this?
The Fix
Step 1: Using "count"
Remember that we created a field called count
in the mongoose schema. Now, add the following API to the index.ts
file.
We could not filter player count in mongoose using player: {$size: {$lt: maxPlayers}}
in our first attempt. It would have been much easier if it worked that way, but it doesn't. So, as an alternative, we introduced the new parameter count
.
When we update players in a room, we increase the count
value by 1 as we insert userId into the player array. Doing so, we can filter using count
instead of IsFull
.
Step 2: Testing
Next, we need to compile, run, and execute the same ab test on the new API.
What results did you get? Did it solve the issue for you? For me, the fixed code created 5 rooms with 4 players each, as expected. You can even test it for a large number of concurrent API calls.
So, let's clear the database and execute a test with 10000 calls with 100 concurrencies.
In this test, the code created 2499 rooms with 4 players each, 1 room with 3 players, and 1 with 1 player. It created 2501 rooms, but we expected 10000/4=2500. That was probably due to a large number of simultaneous API calls and their exaggerated concurrencies. However, it seems like we were able to avoid the race condition.
You can execute a count query in mongo using the code below.
How does it work?
Instead of updating the value of our filtered parameter after the query as we did with isFull
value, we updated the filter count
while we processed queries one by one. Therefore, no matter the number of API calls and their concurrencies, our filter could update the count for each one.
Thus concludes this two-part series about Race Conditions in MongoDB. We'll meet again in the next one, cheers!