On 02/19/2015 01:04 AM, Mario Gudelj wrote:
Great reply. You're a champion, Carl!


Couldn't say more. Very thorough answer and interesting :)

On 19 February 2015 at 05:31, Carl Meyer <c...@oddbird.net <mailto:c...@oddbird.net>> wrote:

    Hi Abraham,

    On 02/17/2015 10:01 PM, Abraham Varricatt wrote:
    > I'm trying to make an app where folks can order X quantity of an
    item.
    > The condition is that the order should only be made if inventory
    exists.
    > Assume that we have stock of Y items. This means that only if Y >= X
    > should we allow the sale to go through. And once we accept an
    order, the
    > inventory should be updated so that Y = Y - X.

    In general, the construct you need is called a "transaction", which
    ensures that a series of database operations will either all be
    committed together, or rolled back if they can't be successfully
    completed. The Django API for that is "django.db.transaction.atomic".

    > The way I've currently implemented this is with a pair of
    getter/setter
    > methods. I have a model 'ShopItem' which has two fields called
    > 'quantity' and 'name' (this is unique). I've made a utility class
    > (outside model source file) where I've made the following 2
    functions;
    >
    > def get_quantity(name):
    >   stuff_left = 0
    >   try:
    >     item = ShopItem.objects.get(name=name)
    >     stuff_left = item.quantity
    >   except ShopItem.DoesNotExist:
    >     pass # return zero
    >   return stuff_left
    >
    > def set_quantity(name, stuff_left):
    >   item = ShopItem.objects.get(name=name)
    >   item.quantity = stuff_left
    >   item.save()
    >
    >
    > Elsewhere in my project, if I need to display the remaining
    quantity of
    > an item in a view, I'll pass the result from get_quantity(). At sale
    > time here is how I update things;
    >
    > def customer_buy(name, number):
    >   temp = get_quantity(name)
    >   if (temp >= number):
    >     temp = temp - number
    >     set_quantity(name, temp)
    >     // do other things
    >   else:
    >     // sale failed, do something else
    >
    > I tested this on my system and things appear to work. But I'm
    concerned
    > about race conditions. What if two different customers came in
    to buy
    > the same item? As in; if I only have 7 items in stock, but one
    customer
    > came to buy 6nos and another 5nos. There is a chance (as per my
    > understanding) that both orders will be accepted - even worse, my
    > inventory will not accurately reflect the updated situation.
    >
    > Any ideas on how this issue can be resolved? I've come across this
    > -
    
https://docs.djangoproject.com/en/1.6/ref/models/instances/#updating-attributes-based-on-existing-fields
    > , but not sure how to apply it to the current scenario.

    If we're only considering the changes to your ShopItem model, you
    don't
    even need an explicit transaction to avoid race conditions,
    because (as
    the docs you linked show) the operation can be completed in a single
    database query, which is inherently atomic. This is how it would
    look to
    do it in a single query:

        from django.db.models import F

        def customer_buy(name, number):
            ShopItem.objects.filter(
                name=name).update(quantity=F('quantity')-number)

    You want this to fail if someone tries to purchase a larger quantity
    than are available. The best way to do this is via a database-level
    "check constraint" on the column, such that the database itself will
    never permit a negative quantity. If you make 'quantity' a
    PositiveIntegerField (the name is wrong, it actually allows zero
    too) on
    your model, and your database is PostgreSQL (or Oracle), Django
    will add
    this constraint for you automatically. Then if someone tries to
    purchase
    more than are available, you'll get an IntegrityError, which you'd
    want
    to catch and handle in some way:

        from django.db import IntegrityError
        from django.db.models import F

        class InsufficientInventory(Exception):
            pass

        def customer_buy(name, number):
            try:
                ShopItem.objects.filter(
    name=name).update(quantity=F('quantity')-number)
            except IntegrityError:
                # signal to the calling code that the purchase failed
    - the
                # calling code should catch this exception and notify the
                # user that the purchase failed due to lack of inventory,
                # and tell them the updated available quantity
                raise InsufficientInventory()

    You also want this function to handle the case where the given product
    name doesn't exist. To help with this case, the `update` method
    returns
    the number of rows updated:

        from django.db import IntegrityError
        from django.db.models import F

        class InsufficientInventory(Exception):
            pass

        def customer_buy(name, number):
            try:
                updated = ShopItem.objects.filter(
    name=name).update(quantity=F('quantity')-number)
            except IntegrityError:
                raise InsufficientInventory()
            if not updated:
                # Here we reuse Django's built-in DoesNotExist exception;
                # you could define your own exception class instead.
                raise ShopItem.DoesNotExist()

    With this code, you've solved the bad race conditions -- quantity will
    never go negative, and a sale will never appear to succeed when it
    should have failed, because of two users submitting an order
    simultaneously.

    There is still a sort of higher-level race condition that can happen
    when two people both load the order page at the same time, and
    then one
    of them orders first. This type of race condition is basically
    impossible to avoid in a web app -- the best you can usually do is
    simply let a user know that their order failed because someone else
    ordered that item first while they were filling out the order form.

    (In general, the term for this approach is "optimistic locking",
    because
    you are "optimistically" allowing concurrent uses and hoping they
    don't
    conflict, but if they do, you catch it and alert the user so they can
    try again. The alternative is to lock everyone else out of the order
    page while one user is ordering; this is called "pessimistic locking".
    Usually optimistic locking is preferable, presuming the most
    common case
    is "no conflict" -- e.g., enough inventory that both users can
    successfully complete their order -- and you can handle conflicts in
    such a way that the user whose transaction fails doesn't lose all
    their
    work.)

    In a real shopping-cart scenario, it's likely that you need to do more
    than just update the quantity field on your ShopItem model -- you may
    need to create an Order object too, and you want the Order object and
    the ShopItem quantity decrement to both succeed or fail together; you
    never want one of them to succeed and the other one to fail. This
    is the
    scenario where you need to wrap both operations in
    `transaction.atomic`;
    you can look it up in the docs to see how it's used.

    Hope this helps,

    Carl

    --
    You received this message because you are subscribed to the Google
    Groups "Django users" group.
    To unsubscribe from this group and stop receiving emails from it,
    send an email to django-users+unsubscr...@googlegroups.com
    <mailto:django-users%2bunsubscr...@googlegroups.com>.
    To post to this group, send email to django-users@googlegroups.com
    <mailto:django-users@googlegroups.com>.
    Visit this group at http://groups.google.com/group/django-users.
    To view this discussion on the web visit
    
https://groups.google.com/d/msgid/django-users/54E4DA7F.5090902%40oddbird.net.
    For more options, visit https://groups.google.com/d/optout.


--
You received this message because you are subscribed to the Google Groups "Django users" group. To unsubscribe from this group and stop receiving emails from it, send an email to django-users+unsubscr...@googlegroups.com <mailto:django-users+unsubscr...@googlegroups.com>. To post to this group, send email to django-users@googlegroups.com <mailto:django-users@googlegroups.com>.
Visit this group at http://groups.google.com/group/django-users.
To view this discussion on the web visit https://groups.google.com/d/msgid/django-users/CAHqTbjmfzZLSFc0Bmo3irt4X2594BkobVZvn_8brLXTV1YkkYQ%40mail.gmail.com <https://groups.google.com/d/msgid/django-users/CAHqTbjmfzZLSFc0Bmo3irt4X2594BkobVZvn_8brLXTV1YkkYQ%40mail.gmail.com?utm_medium=email&utm_source=footer>.
For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "Django 
users" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to django-users+unsubscr...@googlegroups.com.
To post to this group, send email to django-users@googlegroups.com.
Visit this group at http://groups.google.com/group/django-users.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-users/54E59C30.8060104%40arkade.info.
For more options, visit https://groups.google.com/d/optout.

Reply via email to