Note that there are some explanatory texts on larger screens.

plurals
  1. POConcurrent requests in Django
    text
    copied!<p>I have 2 models: <code>Product</code> and <code>Order</code>.</p> <p><code>Product</code> has an integer field for stock, whereas <code>Order</code> has a status and a foreign key to <code>Product</code>:</p> <pre><code>class Product(models.Model): name = models.CharField(max_length=30) stock = models.PositiveSmallIntegerField(default=1) class Order(models.Model): product = models.ForeignKey('Product') DRAFT = 'DR'; INPROGRESS = 'PR'; ABORTED = 'AB' STATUS = ((INPROGRESS, 'In progress'),(ABORTED, 'Aborted'),) status = models.CharField(max_length = 2, choices = STATUS, default = DRAFT) </code></pre> <p><strong>My goal is to have the product's stock decrease by one for each new order, and increase by one for each order cancellation.</strong> In that purpose, I've overloaded the <code>save</code> method of the <code>Order</code> model as such (inspired by <a href="https://stackoverflow.com/questions/1355150/django-when-saving-how-can-you-check-if-a-field-has-changed">Django: When saving, how can you check if a field has changed?</a>):</p> <pre><code>from django.db.models import F class Order(models.Model): product = models.ForeignKey('Product') status = models.CharField(max_length = 2, choices = STATUS, default = DRAFT) EXISTING_STATUS = set([INPROGRESS]) __original_status = None def __init__(self, *args, **kwargs): super(Order, self).__init__(*args, **kwargs) self.__original_status = self.status def save(self, *args, **kwargs): old_status = self.__original_status new_status = self.status has_changed_status = old_status != new_status if has_changed_status: product = self.product if not old_status in Order.EXISTING_STATUS and new_status in Order.EXISTING_STATUS: product.stock = F('stock') - 1 product.save(update_fields=['stock']) elif old_status in Order.EXISTING_STATUS and not new_status in Order.EXISTING_STATUS: product.stock = F('stock') + 1 product.save(update_fields=['stock']) super(Order, self).save(*args, **kwargs) self.__original_status = self.status </code></pre> <p>Using the RestFramework, I've created 2 views, one for creating new orders, one for cancelling existing orders. Both use a straightforward serializer:</p> <pre><code>class OrderSimpleSerializer(serializers.ModelSerializer): class Meta: model = Order fields = ( 'id', 'product', 'status', ) read_only_fields = ( 'status', ) class OrderList(generics.ListCreateAPIView): model = Order serializer_class = OrderSimpleSerializer def pre_save(self, obj): super(OrderList,self).pre_save(obj) product = obj.product if not product.stock &gt; 0: raise ConflictWithAnotherRequest("Product is not available anymore.") obj.status = Order.INPROGRESS class OrderAbort(generics.RetrieveUpdateAPIView): model = Order serializer_class = OrderSimpleSerializer def pre_save(self, obj): obj.status = Order.ABORTED </code></pre> <p>Here is how to access these two views:</p> <pre><code>from myapp.views import * urlpatterns = patterns('', url(r'^order/$', OrderList.as_view(), name='order-list'), url(r'^order/(?P&lt;pk&gt;[0-9]+)/abort/$', OrderAbort.as_view(), name='order-abort'), ) </code></pre> <p>I am using Django 1.6b4, Python 3.3, Rest Framework 2.7.3 and PostgreSQL 9.2.</p> <h2>My problem is that concurrent requests can increase the stock of a product higher than original stock!</h2> <p>Here is the script I use to demonstrate that:</p> <pre><code>import sys import urllib.request import urllib.parse import json opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor) def create_order(): url = 'http://127.0.0.1:8000/order/' values = {'product':1} data = urllib.parse.urlencode(values).encode('utf-8') request = urllib.request.Request(url, data) response = opener.open(request) return response def cancel_order(order_id): abort_url = 'http://127.0.0.1:8000/order/{}/abort/'.format(order_id) values = {'product':1,'_method':'PUT'} data = urllib.parse.urlencode(values).encode('utf-8') request = urllib.request.Request(abort_url, data) try: response = opener.open(request) except Exception as e: if (e.code != 403): print(e) else: print(response.getcode()) def main(): response = create_order() print(response.getcode()) data = response.read().decode('utf-8') order_id = json.loads(data)['id'] time.sleep(1) for i in range(2): p = Process(target=cancel_order, args=[order_id]) p.start() if __name__ == '__main__': main() </code></pre> <p>This script gives the following output, for a product with a stock of 1:</p> <pre><code>201 # means it creates an order for Product, thus decreasing stock from 1 to 0 200 # means it cancels the order for Product, thus increasing stock from 0 to 1 200 # means it cancels the order for Product, thus increasing stock from 1 to 2 (shouldn't happen) </code></pre> <h1>EDIT</h1> <p>I've added a sample project to reproduce the bug: <a href="https://github.com/ThinkerR/django-concurrency-demo" rel="nofollow noreferrer">https://github.com/ThinkerR/django-concurrency-demo</a></p>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload