Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Not nesting version of @atomic() in Django?

From the docs of atomic()

atomic blocks can be nested

This sound like a great feature, but in my use case I want the opposite: I want the transaction to be durable as soon as the block decorated with @atomic() gets left successfully.

Is there a way to ensure durability in django's transaction handling?

Background

Transaction are ACID. The "D" stands for durability. That's why I think transactions can't be nested without loosing feature "D".

Example: If the inner transaction is successful, but the outer transaction is not, then the outer and the inner transaction get rolled back. The result: The inner transaction was not durable.

I use PostgreSQL, but AFAIK this should not matter much.

like image 320
guettli Avatar asked Sep 27 '16 08:09

guettli


People also ask

Why does atomic() not handle PostgreSQL errors in Django?

This problem cannot occur in Django’s default mode and atomic () handles it automatically. Inside a transaction, when a call to a PostgreSQL cursor raises an exception (typically IntegrityError ), all subsequent SQL in the same transaction will fail with the error “current transaction is aborted, queries ignored until end of transaction block”.

What happens when you exit an atomic block in Django?

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 catch and handle exceptions inside an atomic block, you may hide from Django the fact that a problem has happened. This can result in unexpected behavior.

Is Django autocommit?

If your MySQL setup does not support transactions, then Django will always function in autocommit mode: statements will be executed and committed as soon as they’re called. If your MySQL setup does support transactions, Django will handle transactions as explained in this document.

How do I manage database transactions in a Django project?

Django gives you a few ways to control how database transactions are managed. Django’s default behavior is to run in autocommit mode. Each query is immediately committed to the database, unless a transaction is active.


2 Answers

You can't do that through any API.

Transactions can't be nested while retaining all ACID properties, and not all databases support nested transactions.

Only the outermost atomic block creates a transaction. Inner atomic blocks create a savepoint inside the transaction, and release or roll back the savepoint when exiting the inner block. As such, inner atomic blocks provide atomicity, but as you noted, not e.g. durability.

Since the outermost atomic block creates a transaction, it must provide atomicity, and you can't commit a nested atomic block to the database if the containing transaction is not committed.

The only way to ensure that the inner block is committed, is to make sure that the code in the transaction finishes executing without any errors.

like image 115
knbk Avatar answered Oct 21 '22 03:10

knbk


I agree with knbk's answer that it is not possible: durability is only present at the level of a transaction, and atomic provides that. It does not provide it at the level of save points. Depending on the use case, there may be workarounds.

I'm guessing your use case is something like:

@atomic  # possibly implicit if ATOMIC_REQUESTS is enabled
def my_view():
    run_some_code()  # It's fine if this gets rolled back.
    charge_a_credit_card()  # It's not OK if this gets rolled back.
    run_some_more_code()  # This shouldn't roll back the credit card.

I think you'd want something like:

@transaction.non_atomic_requests
def my_view():
    with atomic():
        run_some_code()
    with atomic():
        charge_a_credit_card()
    with atomic():
        run_some_more_code()

If your use case is for credit cards specifically (as mine was when I had this issue a few years ago), my coworker discovered that credit card processors actually provide mechanisms for handling this. A similar mechanism might work for your use case, depending on the problem structure:

@atomic
def my_view():
    run_some_code()
    result = charge_a_credit_card(capture=False)
    if result.successful:
        transaction.on_commit(lambda: result.capture())
    run_some_more_code()

Another option would be to use a non-transactional persistence mechanism for recording what you're interested in, like a log database, or a redis queue of things to record.

like image 24
RecursivelyIronic Avatar answered Oct 21 '22 05:10

RecursivelyIronic