I am quite new to Django, and I am not sure how to manage concurrency.
I am building an application to reserve equipments. I have a Reservation model with a ManytomanyField of Equipments. When I create a new reservation, I have to ensure that all the equipments are available. The first version of my code doesn't care about concurrency:
def validate_reservation(keys, begin, end):
eqpts = Equipment.objects.filter(pk__in=keys)
if all(eqpt.is_available(begin, end) for eqpt in eqpts):
res = Reservation(eqpts, begin, end)
res.save()
return True
else:
return False
The is_available method checks if there already are any reservation for the given dates:
def is_available(self, begin, end):
return not self.reservations.filter(begin__lte=end, end__gte=begin).exists()
The problem is that two users running this code may create Conflicting Reservations, if the first saves his reservation after the second checked the availability of the equipments.
I believe that I can solve the issue with transactions, so this I what I came up with:
def validate_reservation(keys, begin, end):
with transaction.atomic():
eqpts = Equipment.objects.select_for_update().filter(pk__in=keys)
if all(eqpt.is_available(begin, end) for eqpt in eqpts):
res = Reservation(eqpts, begin, end)
res.save()
return True
else:
return False
Does this have the intended behaviour ? How can I know if the transaction failed and notify the user?
Is there a different and better way ?
Yes, this should work as long as you consistently use select_for_update()
before modifying anything that could affect your availability check.
This code will not interfere with itself, since the select_for_update()
will acquire row-level locks on the key rows at the beginning. But let's say you have another view that allows people to enter a reservation pk
and change the beginning and ending times, or add a new piece of equipment. That could cause an inconsistency unless you explicitly do the same select_for_update()
on the Equipment
rows associated with that Reservation
.
Note that your check is not very efficient. Anytime you're executing queries in a loop you should look for an alternative. In your case you should be able to find conflicts with a single query (that JOINs
the tables):
has_conflict = Reservation.objects.filter(begin__lte=end,
end__gte=begin,
equipment__pk__in=keys).exists()
(Of course, you still have the concurrency problem so you'll still need to acquire an appropriate lock or use a different transaction isolation level.)
How can I know if the transaction failed and notify the user?
The transaction will not fail, it will just block until the locks have been released. You can use select_for_update(nowait=True)
(on some databases) if you want the transaction to fail instead.