DB Race Condition Errors, Silent Killer of DB Consistency


If you are a web-developer that works on simple and soft web projects, then probably you have never noticed about Database race conditions.
First of all, let me talk a little about race conditions. What are they, how they happen and what should we do about them.
Race conditions
Suppose a DB row that holds data of a random user of your website. you wanna add some budgets to its balance. In normal situation, you may do something like this: (Note that this code snippet is just for example. Using magic tricks and clean coding are not goals of this content.)
user = models.UserProfile.objects.get(username='Adam')
user.budget += 1000
user.save()
In the first look, everything looks normal. Now we wanna add some more complex processes right before saving the new budget of user. For example checking the available budget with user’s bank account balance through an API. Then if there was any problem with verifying user’s balance, the budget amount that was supposed to be added, will change. OK?
For handling such processes we have to do some actions in our web-app that takes a bit of time.
Now what will be happen if during verifying the user’s budget by bank API, another process override the content of the budget field?
In this situation two processes write on a specific field and race condition error occurs.
Race condition error may cause critical problems in such web-app those a great probability of overriding a row data by two parallel processes exists in them. Like bank apps, financials or etc.
In a bank transaction, the transaction must not cost any balance of user account until the transaction is over completely successful. That if it is not so, a user can exchange a single amount of money to other accounts more than one time.
The process of exchanging is as follows: When the bank received the exchange request from user, it checks the balance of account and if there were enough amounts of money, then submits the request.
Now exactly after the balance check process and before submitting the request, user can apply more requests to exchange money, and since no money has been deducted from balance yet (Because the process is not submitted yet and balance will deducted after the process has been submitted), the bank program will verify other request also. (If the bank doesn’t care about race conditions).
Ways to prevent
Now, that we now what is race condition and how it may occur, It’s time to know what can we do against these critical errors.
Using DB transactions
The first step and must important one, is using Database transaction. although it does not prevent RC errors singly, but it’s the foundation of building such apps like bank applications that have parallel processes.
Row blocking
The main task to do is designing a logic that blocks the DB row when we start to Updating or changing that.
You can design this logic yourself. But frameworks like Django, provide enhanced solutions to apply this logic when you need.
Django select_for_update()
method
Like other powerful web frameworks, Django provides lots of functionalities to apply complex database related logics in your app. One of them is select_for_update()
method.
In short, when you use this method inside of an atomic transaction, It blocks the row data until the transaction closes.(Committed or rolled back)
To do this, we can just calling it from objects
manager of a Model:
from django.db import transaction
with transaction.atomic():
row = models.SomeModel.objects.select_for_update().get(pk=some_pk)
In above code, the row of some_model table will be blocked until the transaction context manager ends.
At the end, you can also implement this logic yourself. You can define a new field in your model named is_locked = models.BooleanField(default=False)
or something like that and when you want to start editing the object you’ll make it True. (Don’t forget to change the is_blocked back to False after your changes committed, Otherwise the row will stay blocked forever!)
Conclusion
In summary, race conditions in web applications, especially those involving database transactions, can lead to severe issues, such as data inconsistency or even financial mistakes. Understanding how race conditions occur is key to preventing them, particularly in applications where critical processes, such as financial transactions, are involved.
By employing techniques like database transactions and row locking, such as Django's select_for_update()
, you can prevent database inconsistencies.
Subscribe to my newsletter
Read articles from Mostafa Motahari directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Mostafa Motahari
Mostafa Motahari
Back-End developer and Python-Django lover.