]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- revert the change first made in a6fe4dc, as we are now generalizing
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 28 Jan 2016 20:01:31 +0000 (15:01 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 28 Jan 2016 20:02:49 +0000 (15:02 -0500)
the warning here to all safe_reraise() cases in Python 2.
- Revisiting :ticket:`2696`, first released in 1.0.10, which attempts to
work around Python 2's lack of exception context reporting by emitting
a warning for an exception that was interrupted by a second exception
when attempting to roll back the already-failed transaction; this
issue continues to occur for MySQL backends in conjunction with a
savepoint that gets unexpectedly lost, which then causes a
"no such savepoint" error when the rollback is attempted, obscuring
what the original condition was.

The approach has been generalized to the Core "safe
reraise" function which takes place across the ORM and Core in any
place that a transaction is being rolled back in response to an error
which occurred trying to commit, including the context managers
provided by :class:`.Session` and :class:`.Connection`, and taking
place for operations such as a failure on "RELEASE SAVEPOINT".
Previously, the fix was only in place for a specific path within
the ORM flush/commit process; it now takes place for all transational
context managers as well.
fixes #2696

(cherry picked from commit 8a1e619fb20df1be6ad2e0c563e451e17eb17628)

doc/build/changelog/changelog_10.rst
lib/sqlalchemy/orm/session.py
lib/sqlalchemy/util/langhelpers.py
test/base/test_utils.py
test/engine/test_transaction.py
test/orm/test_transaction.py
test/requirements.py

index a0b1ad9572c7aad5b4adcdaae90d6140e163e1b0..781911d6505c9792162089f0fc2257b581eb9b38 100644 (file)
     :version: 1.0.12
     :released:
 
+    .. change::
+        :tags: bug, engine, mysql
+        :tickets: 2696
+
+        Revisiting :ticket:`2696`, first released in 1.0.10, which attempts to
+        work around Python 2's lack of exception context reporting by emitting
+        a warning for an exception that was interrupted by a second exception
+        when attempting to roll back the already-failed transaction; this
+        issue continues to occur for MySQL backends in conjunction with a
+        savepoint that gets unexpectedly lost, which then causes a
+        "no such savepoint" error when the rollback is attempted, obscuring
+        what the original condition was.
+
+        The approach has been generalized to the Core "safe
+        reraise" function which takes place across the ORM and Core in any
+        place that a transaction is being rolled back in response to an error
+        which occurred trying to commit, including the context managers
+        provided by :class:`.Session` and :class:`.Connection`, and taking
+        place for operations such as a failure on "RELEASE SAVEPOINT".
+        Previously, the fix was only in place for a specific path within
+        the ORM flush/commit process; it now takes place for all transational
+        context managers as well.
+
     .. change::
         :tags: bug, sql
         :tickets: 3632
index fb14191ce909465b48a9df6c6adf9a80a024ec2e..aff64bf27d4616f8d21e29a78615893a320afddb 100644 (file)
@@ -412,23 +412,11 @@ class SessionTransaction(object):
             for subtransaction in stx._iterate_parents(upto=self):
                 subtransaction.close()
 
-        if _capture_exception:
-            captured_exception = sys.exc_info()[1]
-
         boundary = self
         if self._state in (ACTIVE, PREPARED):
             for transaction in self._iterate_parents():
                 if transaction._parent is None or transaction.nested:
-                    try:
-                        transaction._rollback_impl()
-                    except Exception:
-                        if _capture_exception:
-                            util.warn(
-                                "An exception raised during a Session "
-                                "persistence operation cannot be raised "
-                                "due to an additional ROLLBACK exception; "
-                                "the exception is: %s" % captured_exception)
-                        raise
+                    transaction._rollback_impl()
                     transaction._state = DEACTIVE
                     boundary = transaction
                     break
@@ -450,7 +438,7 @@ class SessionTransaction(object):
 
         self.close()
         if self._parent and _capture_exception:
-            self._parent._rollback_exception = captured_exception
+            self._parent._rollback_exception = sys.exc_info()[1]
 
         sess.dispatch.after_soft_rollback(sess, self)
 
index 70779543fe6796c8119f90def3c1a7a456208446..003cee81bf0cb79c00146d72840cce3d93df669c 100644 (file)
@@ -59,6 +59,13 @@ class safe_reraise(object):
             self._exc_info = None   # remove potential circular references
             compat.reraise(exc_type, exc_value, exc_tb)
         else:
+            if not compat.py3k and self._exc_info and self._exc_info[1]:
+                # emulate Py3K's behavior of telling us when an exception
+                # occurs in an exception handler.
+                warn(
+                    "An exception has occurred during handling of a "
+                    "previous exception.  The previous exception "
+                    "is:\n %s %s\n" % (self._exc_info[0], self._exc_info[1]))
             self._exc_info = None   # remove potential circular references
             compat.reraise(type_, value, traceback)
 
index c5e0077f1826381de61a796069d6b7ef77870ba6..978cbc18d3c68e4aa63aafab445a20ce6a3be202 100644 (file)
@@ -3,7 +3,7 @@ import sys
 
 from sqlalchemy import util, sql, exc, testing
 from sqlalchemy.testing import assert_raises, assert_raises_message, fixtures
-from sqlalchemy.testing import eq_, is_, ne_, fails_if, mock
+from sqlalchemy.testing import eq_, is_, ne_, fails_if, mock, expect_warnings
 from sqlalchemy.testing.util import picklers, gc_collect
 from sqlalchemy.util import classproperty, WeakSequence, get_callable_argspec
 from sqlalchemy.sql import column
@@ -2123,6 +2123,38 @@ class ReraiseTest(fixtures.TestBase):
             if testing.requires.python3.enabled:
                 is_(moe.__cause__, me)
 
+    @testing.requires.python2
+    def test_safe_reraise_py2k_warning(self):
+        class MyException(Exception):
+            pass
+
+        class MyOtherException(Exception):
+            pass
+
+        m1 = MyException("exc one")
+        m2 = MyOtherException("exc two")
+
+        def go2():
+            raise m2
+
+        def go():
+            try:
+                raise m1
+            except:
+                with util.safe_reraise():
+                    go2()
+
+        with expect_warnings(
+            "An exception has occurred during handling of a previous "
+            "exception.  The previous exception "
+            "is:.*MyException.*exc one"
+        ):
+            try:
+                go()
+                assert False
+            except MyOtherException:
+                pass
+
 
 class TestClassProperty(fixtures.TestBase):
 
index 7f8a7c97cd02903481547ee028fafc2bb7b7afe4..c81a7580f0f833670da0536d633e91016d5dfa6d 100644 (file)
@@ -218,6 +218,27 @@ class TransactionTest(fixtures.TestBase):
         finally:
             connection.close()
 
+    @testing.requires.python2
+    @testing.requires.savepoints_w_release
+    def test_savepoint_release_fails_warning(self):
+        with testing.db.connect() as connection:
+            connection.begin()
+
+            with expect_warnings(
+                "An exception has occurred during handling of a previous "
+                "exception.  The previous exception "
+                "is:.*..SQL\:.*RELEASE SAVEPOINT"
+            ):
+                def go():
+                    with connection.begin_nested() as savepoint:
+                        connection.dialect.do_release_savepoint(
+                            connection, savepoint._savepoint)
+                assert_raises_message(
+                    exc.DBAPIError,
+                    ".*SQL\:.*ROLLBACK TO SAVEPOINT",
+                    go
+                )
+
     def test_retains_through_options(self):
         connection = testing.db.connect()
         try:
index a51992a0da8701fe1bc835d31c19821981bd1297..718b1b82a54ae19c6ef83d73f73814f56c32499e 100644 (file)
@@ -657,8 +657,8 @@ class SessionTransactionTest(FixtureTest):
         assert session.transaction is not None, \
             'autocommit=False should start a new transaction'
 
-    @testing.skip_if("oracle", "oracle doesn't support release of savepoint")
-    @testing.requires.savepoints
+    @testing.requires.python2
+    @testing.requires.savepoints_w_release
     def test_report_primary_error_when_rollback_fails(self):
         User, users = self.classes.User, self.tables.users
 
@@ -666,7 +666,7 @@ class SessionTransactionTest(FixtureTest):
 
         session = Session(testing.db)
 
-        with expect_warnings(".*due to an additional ROLLBACK.*INSERT INTO"):
+        with expect_warnings(".*during handling of a previous exception.*"):
             session.begin_nested()
             savepoint = session.\
                 connection()._Connection__transaction._savepoint
index f7db7a4d42015d780492b87a966aec42b91d43ec..dfae7b285c224bf2affd63cd72697d1144aec3e2 100644 (file)
@@ -286,6 +286,10 @@ class DefaultRequirements(SuiteRequirements):
                     ("mysql", "<", (5, 0, 3)),
                     ], "savepoints not supported")
 
+    @property
+    def savepoints_w_release(self):
+        return self.savepoints + skip_if(
+            "oracle", "oracle doesn't support release of savepoint")
 
     @property
     def schemas(self):