#20571: Using savepoints within transaction.atomic() can result in the entire
transaction being incorrectly and silently rolled back
----------------------------------------------+--------------------
     Reporter:  lamby                         |      Owner:  nobody
         Type:  Uncategorized                 |     Status:  new
    Component:  Database layer (models, ORM)  |    Version:  1.5
     Severity:  Normal                        |   Keywords:
 Triage Stage:  Unreviewed                    |  Has patch:  0
Easy pickings:  0                             |      UI/UX:  0
----------------------------------------------+--------------------
 Using savepoints within transaction.atomic() can result in the entire
 transaction being incorrectly and silently rolled back. Here is a slightly
 contrived example:

 {{{#!python
 from django.db import transaction, DatabaseError

 with transaction.atomic():
     user = User.objects.all()[0]
     user.last_login = datetime.datetime.utcnow() # any change to
 demonstrate problem
     user.save()

     sid = transaction.savepoint()
     try:
         # Will always fail
         User.objects.create(pk=user.pk)
         transaction.savepoint_commit(sid)
     except DatabaseError:
         transaction.savepoint_rollback(sid)

 # outside of atomic() context - user.last_login change has not been
 committed!
 }}}

 What happens is the .create() call fails and marks the outermost atomic
 block as requiring a rollback, so even though we call savepoint_rollback
 (which does exactly right thing), once we leave the outer
 transaction.atomic(), the entire transaction is rolled back. In the
 example above, this means that the change to last_login is not persisted
 in the database. Rather insiduously, it is done entirely silently. :)

 (The outer transaction.atomic() seems a little contrived but note that it
 is equivalent to the wrapper added by ATOMIC_REQUESTS.)

 I'm not entirely sure what the solution is; the
 transaction.atomic(savepoint=False) call within ModelBase.save_base simply
 does not (and cannot) know that some other code will manually handle the
 savepoint rollback and thus no choice but to mark the entire transaction
 as borked. The only way it could know is if .create and .save took yet
 another kwarg, but this seems bizarre and would not be at all obvious.

 Maybe manual savepoints should be become a private API after all
 (contradicting the current note in the documentation). Alternatively, it
 could be clarified that "manual" savepoint management they should not be
 used in conjunction with atomic() blocks (and thus ATOMIC_REQUESTS). The
 behaviour above is certainly not obvious at all.

 (Hm, update_or_create uses savepoints manually, I hope that isn't breaking
 installations?)

-- 
Ticket URL: <https://code.djangoproject.com/ticket/20571>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/048.fa2ae1777066b77aa1cb489ad965c1b1%40djangoproject.com?hl=en.
For more options, visit https://groups.google.com/groups/opt_out.


Reply via email to