Reputation: 3454
I have a Django model with a field defined:
contacts = models.PositiveIntegerField(default=0)
...
Further down I am trying to do a decrement on the field using F()
an expression:
self.contacts = models.F("contacts") - quantity
How do I check contacts - quantity
does not go negative without introducing a race condition?
Upvotes: 2
Views: 1036
Reputation: 53699
You'd want to avoid race conditions. F()
objects are only a first step in this.
The first thing you'd want to do in your transaction is to acquire a lock on the row you're changing. This prevents that you read a value that is outdated by the time you write to the database. This can be done with an update of the row, that will lock the row from further updates until the transaction is committed or rolled back:
with transaction.atomic():
obj.contacts = F('contacts') - quantity
When you have the lock, and done the update, check if data integrity is still intact (i.e. the number of contacts isn't lower than 0). If it is, continue, otherwise, rollback the transaction by raising an exception:
obj.refresh_from_db()
if obj.contacts < 0:
raise ValueError("Capacity exceeded")
If there are enough contacts left, you'll exit the atomic()
block at this point, the transaction is committed, and other requests can acquire the lock and try to update the value. If there aren't enough contacts, the transaction is rolled back, and other requests will never have known the value has changed, as they've been waiting to get the lock.
Now, to put it all together:
from django.db import transaction
from django.db.models import F
def update_contacts(obj, quantity):
with transaction.atomic():
obj.contacts = F('contacts') - quantity
obj.save()
obj.refresh_from_db()
if obj.contacts < 0:
raise ValueError("Not enough contacts.")
(Note: obj.refresh_from_db()
requires 1.8, otherwise just use MyModel.objects.get(pk=obj.pk)
.)
Upvotes: 1