]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Further continuing on the common MySQL exception case of
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 23 Mar 2016 20:54:03 +0000 (16:54 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 23 Mar 2016 20:54:03 +0000 (16:54 -0400)
a savepoint being cancelled first covered in :ticket:`2696`,
the failure mode in which the :class:`.Session` is placed when a
SAVEPOINT vanishes before rollback has been improved to allow the
:class:`.Session` to still function outside of that savepoint.
It is assumed that the savepoint operation failed and was cancelled.
fixes #3680

doc/build/changelog/changelog_11.rst
doc/build/changelog/migration_11.rst
lib/sqlalchemy/orm/session.py
test/orm/test_transaction.py

index 178383b85872f6a3524d26f210687797815bdd4c..0a5dc3ea07aba2aa72fc77266bea610865a596bf 100644 (file)
 .. changelog::
     :version: 1.1.0b1
 
+    .. change::
+        :tags: bug, orm, mysql
+        :tickets: 3680
+
+        Further continuing on the common MySQL exception case of
+        a savepoint being cancelled first covered in :ticket:`2696`,
+        the failure mode in which the :class:`.Session` is placed when a
+        SAVEPOINT vanishes before rollback has been improved to allow the
+        :class:`.Session` to still function outside of that savepoint.
+        It is assumed that the savepoint operation failed and was cancelled.
+
+        .. seealso::
+
+            :ref:`change_3680`
+
     .. change::
         :tags: feature, mssql
         :tickets: 3534
index 3e839af63aab69bc30b9b20935d7a6e14da2483d..cca2b1ae855bbe7c9c6992bf315d69abe3b4b803 100644 (file)
@@ -16,7 +16,7 @@ What's New in SQLAlchemy 1.1?
     some issues may be moved to later milestones in order to allow
     for a timely release.
 
-    Document last updated: Feburary 25, 2016
+    Document last updated: March 23, 2016
 
 Introduction
 ============
@@ -290,6 +290,44 @@ time on the outside of the subquery.
 
 :ticket:`3582`
 
+.. _change_3680:
+
+Improved Session state when a SAVEPOINT is cancelled by the database
+--------------------------------------------------------------------
+
+A common case with MySQL is that a SAVEPOINT is cancelled when a deadlock
+occurs within the transaction.  The :class:`.Session` has been modfied
+to deal with this failure mode slightly more gracefully, such that the
+outer, non-savepoint transaction still remains usable::
+
+    s = Session()
+    s.begin_nested()
+
+    s.add(SomeObject())
+
+    try:
+        # assume the flush fails, flush goes to rollback to the
+        # savepoint and that also fails
+        s.flush()
+    except Exception as err:
+        print("Something broke, and our SAVEPOINT vanished too")
+
+    # this is the SAVEPOINT transaction, marked as
+    # DEACTIVE so the rollback() call succeeds
+    s.rollback()
+
+    # this is the outermost transaction, remains ACTIVE
+    # so rollback() or commit() can succeed
+    s.rollback()
+
+This issue is a continuation of :ticket:`2696` where we emit a warning
+so that the original error can be seen when running on Python 2, even though
+the SAVEPOINT exception takes precedence.  On Python 3, exceptions are chained
+so both failures are reported individually.
+
+
+:ticket:`3680`
+
 .. _change_3677:
 
 Erroneous "new instance X conflicts with persistent instance Y" flush errors fixed
index 48ff09b87a566b96e65a8bc65450c2e133e753f4..dc5de7ac6f56e64c50ee565f7a6d83ba5d986986 100644 (file)
@@ -235,7 +235,7 @@ class SessionTransaction(object):
         return SessionTransaction(
             self.session, self, nested=nested)
 
-    def _iterate_parents(self, upto=None):
+    def _iterate_self_and_parents(self, upto=None):
 
         current = self
         result = ()
@@ -269,6 +269,11 @@ class SessionTransaction(object):
         self._key_switches = weakref.WeakKeyDictionary()
 
     def _restore_snapshot(self, dirty_only=False):
+        """Restore the restoration state taken before a transaction began.
+
+        Corresponds to a rollback.
+
+        """
         assert self._is_transaction_boundary
 
         self.session._expunge_states(
@@ -290,6 +295,11 @@ class SessionTransaction(object):
                 s._expire(s.dict, self.session.identity_map._modified)
 
     def _remove_snapshot(self):
+        """Remove the restoration state taken before a transaction began.
+
+        Corresponds to a commit.
+
+        """
         assert self._is_transaction_boundary
 
         if not self.nested and self.session.expire_on_commit:
@@ -358,7 +368,7 @@ class SessionTransaction(object):
 
         stx = self.session.transaction
         if stx is not self:
-            for subtransaction in stx._iterate_parents(upto=self):
+            for subtransaction in stx._iterate_self_and_parents(upto=self):
                 subtransaction.commit()
 
         if not self.session._flushing:
@@ -405,14 +415,18 @@ class SessionTransaction(object):
 
         stx = self.session.transaction
         if stx is not self:
-            for subtransaction in stx._iterate_parents(upto=self):
+            for subtransaction in stx._iterate_self_and_parents(upto=self):
                 subtransaction.close()
 
         boundary = self
+        rollback_err = None
         if self._state in (ACTIVE, PREPARED):
-            for transaction in self._iterate_parents():
+            for transaction in self._iterate_self_and_parents():
                 if transaction._parent is None or transaction.nested:
-                    transaction._rollback_impl()
+                    try:
+                        transaction._rollback_impl()
+                    except:
+                        rollback_err = sys.exc_info()
                     transaction._state = DEACTIVE
                     boundary = transaction
                     break
@@ -421,7 +435,7 @@ class SessionTransaction(object):
 
         sess = self.session
 
-        if sess._enable_transaction_accounting and \
+        if not rollback_err and sess._enable_transaction_accounting and \
                 not sess._is_clean():
 
             # if items were added, deleted, or mutated
@@ -433,19 +447,24 @@ class SessionTransaction(object):
             boundary._restore_snapshot(dirty_only=boundary.nested)
 
         self.close()
+
         if self._parent and _capture_exception:
             self._parent._rollback_exception = sys.exc_info()[1]
 
+        if rollback_err:
+            util.reraise(*rollback_err)
+
         sess.dispatch.after_soft_rollback(sess, self)
 
         return self._parent
 
     def _rollback_impl(self):
-        for t in set(self._connections.values()):
-            t[1].rollback()
-
-        if self.session._enable_transaction_accounting:
-            self._restore_snapshot(dirty_only=self.nested)
+        try:
+            for t in set(self._connections.values()):
+                t[1].rollback()
+        finally:
+            if self.session._enable_transaction_accounting:
+                self._restore_snapshot(dirty_only=self.nested)
 
         self.session.dispatch.after_rollback(self.session)
 
@@ -1078,7 +1097,7 @@ class Session(_SessionClassMethods):
     def _close_impl(self, invalidate):
         self.expunge_all()
         if self.transaction is not None:
-            for transaction in self.transaction._iterate_parents():
+            for transaction in self.transaction._iterate_self_and_parents():
                 transaction.close(invalidate)
 
     def expunge_all(self):
index c1662c9d14e92e39f773be622d989aa4d74ac379..d912c669f13071520beb57b368a0bd17dddac789 100644 (file)
@@ -3,10 +3,10 @@ from sqlalchemy import (
     testing, exc as sa_exc, event, String, Column, Table, select, func)
 from sqlalchemy.testing import (
     fixtures, engines, eq_, assert_raises, assert_raises_message,
-    assert_warnings, mock, expect_warnings)
+    assert_warnings, mock, expect_warnings, is_, is_not_)
 from sqlalchemy.orm import (
     exc as orm_exc, Session, mapper, sessionmaker, create_session,
-    relationship, attributes)
+    relationship, attributes, session as _session)
 from sqlalchemy.testing.util import gc_collect
 from test.orm._fixtures import FixtureTest
 from sqlalchemy import inspect
@@ -1290,6 +1290,35 @@ class SavepointTest(_LocalFixture):
         assert u1 in s
         assert u1 not in s.deleted
 
+    @testing.requires.savepoints
+    def test_savepoint_lost_still_runs(self):
+        User = self.classes.User
+        s = self.session(bind=self.bind)
+        trans = s.begin_nested()
+        s.connection()
+        u1 = User(name='ed')
+        s.add(u1)
+
+        # kill off the transaction
+        nested_trans = trans._connections[self.bind][1]
+        nested_trans._do_commit()
+
+        is_(s.transaction, trans)
+        assert_raises(
+            sa_exc.DBAPIError,
+            s.rollback
+        )
+
+        assert u1 not in s.new
+
+        is_(trans._state, _session.CLOSED)
+        is_not_(s.transaction, trans)
+        is_(s.transaction._state, _session.ACTIVE)
+
+        is_(s.transaction.nested, False)
+
+        is_(s.transaction._parent, None)
+
 
 class AccountingFlagsTest(_LocalFixture):
     __backend__ = True