Django.fun

Can Django select_for_update be used to acquire a read lock?

I'm working on a project similar to ecommerce in which I have models that represent a product with a certain amount in the storage, and users can buy some quantity of the product as long as it doesn't exceed the stored amount. I want to avoid a race condition taking place when the server receives multiple requests to buy the same product.

class Product(models.Model):
   amount_in_storage = models.PositiveIntegerField() # for design reasons, this amount is unchangeable, it must be only evaluated once during initialization like constants in C++

   @property
   def amount_available_for_purchase(self):
       return self.amount_in_storage - Purchase.objects.filter(product=self.id).aggregate(models.Sum('amount'))["sum__amount"]
class Purchase(models.Model):
   product = models.ForeignKey(Product, ...)
   amount = models.PositiveIntegerField()
   payment_method = ...

Let's assume that this is the block of code that's responsible for creating a purchase.

@atomic_transaction
def func(product_id, amount_to_purchase):
   product = Product.objects.get(...)
   if product.amount_available_for_purchase > amount_to_purchase:
     # do payment related stuff
     Purchase.objects.create(product=product, amount=amount_to_purchase)
     # do more stuff

I would like to restrict concurrent access to this block of code, Ideally I would like to obtain a read lock access at the if condition, so that if multiple threads try to see if the amount available is greater than the amount to purchase, one of the threads will have to wait until the transaction is done, and then the read request will be evaluated, so I thought leveraging Django's select_for_update and a version field like so:

class Product(models.Model):
   amount_in_storage = models.PositiveIntegerField()
   version = models.PositiveIntegerField() # we use this field just to write to it, no reading will take place

   @property
   def amount_available_for_purchase(self):
       return self.amount_in_storage - Purchase.objects.filter(product=self.id).aggregate(models.Sum('amount'))["sum__amount"]

And I use this field to obtain a lock like so:


@atomic_transaction
def func(product_id, amount_to_purchase):
   product = Product.objects.select_for_update.get(...)

   # acquiring a lock
   product.version += 1 
   product.save()

   if product.amount_available_for_purchase > amount_to_purchase:
     # do payment related stuff
     Purchase.objects.create(product=product, amount=amount_to_purchase)
     # do more stuff

Using select_for_update if multiple threads reach the version modification line, only the first will evaluate, and the rest will have to wait until the whole transaction is completed, hence acquiring a read lock for the line at the if condition. In other words, only 1 thread at a time will have access to this block of code.

My questions are:

  1. Is my approach doing what I think it's doing?
  2. Is this a clean approach? if not, how can this be achieved without complicating the codebase and going through a major refactor?

Answers: 0