Django Concurrency

Django Concurrency

Explore django concurrency code snippets and tutorials

Django Concurrency

Concurrency fix 4: database constraints (UniqueConstraint and CheckConstraint)

How can database constraints protect against concurrency problems?

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# ======================================================================
# Q: How can database constraints protect against concurrency problems?
# ======================================================================

class Meta:
    constraints = [
        models.UniqueConstraint(fields=['coupon', 'user'], name='one_use_per_user'),
        models.CheckConstraint(condition=Q(stock__gte=0), name='stock_non_negative'),  # Django 5.1+
    ]

try:
    with transaction.atomic():
        CouponUse.objects.create(coupon=c, user=u)
except IntegrityError:
    ...  # already used
Django Concurrency

Concurrency fix 3: optimistic locking with a version field

How do you implement optimistic locking with a version field in Django?

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# ======================================================================
# Q: How do you implement optimistic locking with a version field in Django?
# ======================================================================

class Document(models.Model):
    body = models.TextField()
    version = models.PositiveIntegerField(default=0)

def save_document(doc_id, new_body, expected_version):
    rows = (Document.objects
            .filter(pk=doc_id, version=expected_version)
            .update(body=new_body, version=F('version') + 1))
    if rows == 0:
        raise ConflictError('Someone else changed the document')  # HTTP 409 -> reload/merge
Django Concurrency

Concurrency fix 2: pessimistic locking with select_for_update()

How do you use pessimistic locking with select_for_update() to prevent race conditions?

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# ======================================================================
# Q: How do you use pessimistic locking with select_for_update() to prevent race
# conditions?
# ======================================================================

from django.db import transaction

with transaction.atomic():
    account = Account.objects.select_for_update().get(pk=account_id)  # SELECT ... FOR UPDATE
    if account.balance < amount:
        raise InsufficientFunds()
    account.balance -= amount
    account.save(update_fields=['balance'])
    Ledger.objects.create(account=account, amount=-amount)
# the lock is released at COMMIT
Django Concurrency

Concurrency fix 1: atomic update with F() expressions

How do you fix a lost update with an atomic update using F() expressions?

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# ======================================================================
# Q: How do you fix a lost update with an atomic update using F() expressions?
# ======================================================================

from django.db.models import F

updated = (Product.objects
           .filter(pk=pk, stock__gte=qty)          # condition evaluated inside the database
           .update(stock=F('stock') - qty))        # UPDATE ... SET stock = stock - qty
if updated == 0:
    raise OutOfStock()
Django Concurrency

Find the bug: lost update (race condition) when decrementing stock

What is the classic lost update bug (race condition) when updating stock?

python
1
2
3
4
5
6
7
8
9
# ======================================================================
# Q: What is the classic lost update bug (race condition) when updating stock?
# ======================================================================

# WRONG: read-modify-write in Python
product = Product.objects.get(pk=pk)      # Worker A: stock=1   Worker B: stock=1
if product.stock > 0:
    product.stock -= 1                    # A: 0                B: 0
    product.save()                        # Sold 2 items, we had 1 (overselling)