2

I'm trying to utilize django's row-level-locking by using the select_for_update utility. As per the documentation, this can only be used when inside of a transaction.atomic block. The side-effect of using a transaction.atomic block is that if my code throws an exception, all the database changes get rolled-back. My use case is such that I'd actually like to keep the database changes, and allow the exception to propagate. This leaves me with code looking like this:

with transaction.atomic():
    user = User.objects.select_for_update.get(id=1234)
    try:
        user.do_something()
    except Exception as e:
        exception = e
    else:
        exception = None

if exception is not None:
    raise exception

This feels like a total anti-pattern and I'm sure I must be missing something. I'm aware I could probably roll-my-own solution by manually using transaction.set_autocommit to manage the transaction, but I'd have thought that there would be a simpler way to get this functionality. Is there a built in way to achieve what I want?

DBrowne
  • 683
  • 4
  • 12

1 Answers1

1

I ended up going with something that looks like this:

from django.db import transaction

class ErrorTolerantTransaction(transaction.Atomic):

    def __exit__(self, exc_type, exc_value, traceback):
        return super().__exit__(None, None, None)


def error_tolerant_transaction(using=None, savepoint=True):
    """
    Wraps a code block in an 'error tolerant' transaction block to allow the use of
    select_for_update but without the effect of automatic rollback on exception.

    Can be invoked as either a decorator or context manager.
    """
    if callable(using):
        return ErrorTolerantTransaction('default', savepoint)(using)

    return ErrorTolerantTransaction(using, savepoint)

I can now put an error_tolerant_transaction in place of transaction.atomic and exceptions can be raised without a forced rollback. Of course database-related exceptions (i.e. IntegrityError) will still cause a rollback, but that's expected behavior given that we're using a transaction. As a bonus, this solution is compatible with transaction.atomic, meaning it can be nested inside an atomic block and vice-versa.

DBrowne
  • 683
  • 4
  • 12
  • 1
    Had the same problem as you and after a good few hours of searching...this is the only solution I have found. Even though `select_for_update` needs to have a well-defined transaction period, some don't want to suffer from rollbacks if that transaction exits through exception. `transaction.atomic` should define a transaction period, and let the user choose whether those changes should be committed on exception or not. – AntiElephant Sep 22 '17 at 09:04