0

I have the following (paraphrased) code that's subject to race conditions:

def calculate_and_cache(template, template_response):
    # run a fairly slow and intensive calculation:
    calculated_object = calculate_slowly(template, template_response)
    cached_calculation = Calculation(calculated=calculated_object,
                                     template=template,
                                     template_response=template_response)
    # try to save the calculation just computed:
    try:
        cached_calculation.save()
        return cached_calculation
    # if another thread beat you to saving this, catch the exception
    # but return the object that was just calculated
    except DatabaseError as error:
        log(error)
        return cached_calculation

And it's raising a DatabaseTransactionError:

TransactionManagementError: An error occurred in the current transaction.
You can't execute queries until the end of the 'atomic' block.

The docs have this to say about DTE's:

When exiting an atomic block, Django looks at whether it’s exited normally or with an exception to determine whether to commit or roll back.... If you attempt to run database queries before the rollback happens, Django will raise a TransactionManagementError.

But they also have this, much more vague thing to say about them as well:

TransactionManagementError is raised for any and all problems related to database transactions.

My questions, in order of ascending generality:

  1. Will catching a DatabaseError actually address the race condition by letting the save() exit gracefully while still returning the object?
  2. Where does the atomic block begin in the above code and where does it end?
  3. What am I doing wrong and how can I fix it?
M.javid
  • 6,387
  • 3
  • 41
  • 56
Brendan W
  • 3,303
  • 3
  • 18
  • 37

1 Answers1

1

The Django docs on controlling transactions explicitly have an example of catching exceptions in atomic blocks.

In your case, you don't appear to be using the atomic decorator at all, so first you need to add the required import.

from django.db import transaction

Then you need to move the code that could raise a database error into an atomic block:

try:
    with transaction.atomic():
        cached_calculation.save()
    return cached_calculation
# if another thread beat you to saving this, catch the exception
# but return the object that was just calculated
except DatabaseError as error:
    log(error)
    return cached_calculation
Alasdair
  • 298,606
  • 55
  • 578
  • 516
  • Given that I'm not using the `atomic` decorator, how is it that I'm running into problems exiting an atomic block? – Brendan W Aug 25 '15 at 15:39
  • As I understand it, you're not running into problems 'exiting an atomic block'. The problem is that you are trying to perform a query following an error that hasn't been rolled back. – Alasdair Aug 25 '15 at 15:47
  • Hmm. It seems like the only query I make is `cached_calculation.save()`, and I also suspect that to be the part raising the error--how can that query both cause the error and follow the error? – Brendan W Aug 25 '15 at 15:50
  • Also, atomic blocks just function to make a collection of queries occur together all-or-nothing. How can that apply to a single query? – Brendan W Aug 25 '15 at 15:57
  • Actually just read this: http://stackoverflow.com/questions/21468742/is-a-single-sql-server-statement-atomic-and-consistent, and apparently a single query can be non-atomic – Brendan W Aug 25 '15 at 16:06
  • I can't see the query that is causing the `DatabaseTransactionError`, but you haven't shown the actual code or traceback. Using the atomic decorator ensures that the transaction is rolled back before the exception is handled, it doesn't matter whether that block has one query or many. – Alasdair Aug 25 '15 at 16:11
  • Everything else is a lookup: I think this is the only thing that could be throwing an error. But it seems like you're supposed to wrap transactions in atomic blocks to catch errors. So I'm gonna say this pretty much takes care of my question, and thanks!! – Brendan W Aug 25 '15 at 16:15
  • I don't think it matters whether the query is a select, an update or anything else. I believe that **any** query will cause the `DatabaseTransactionError`. – Alasdair Aug 25 '15 at 16:31