summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/build/changelog/changelog_10.rst23
-rw-r--r--lib/sqlalchemy/orm/session.py16
-rw-r--r--lib/sqlalchemy/util/langhelpers.py7
-rw-r--r--test/base/test_utils.py34
-rw-r--r--test/engine/test_transaction.py21
-rw-r--r--test/orm/test_transaction.py6
-rw-r--r--test/requirements.py4
7 files changed, 93 insertions, 18 deletions
diff --git a/doc/build/changelog/changelog_10.rst b/doc/build/changelog/changelog_10.rst
index a0b1ad957..781911d65 100644
--- a/doc/build/changelog/changelog_10.rst
+++ b/doc/build/changelog/changelog_10.rst
@@ -20,6 +20,29 @@
: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
diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py
index 56513860a..0af8f60fb 100644
--- a/lib/sqlalchemy/orm/session.py
+++ b/lib/sqlalchemy/orm/session.py
@@ -408,23 +408,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
@@ -446,7 +434,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)
diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py
index 11aa9384d..a1780536a 100644
--- a/lib/sqlalchemy/util/langhelpers.py
+++ b/lib/sqlalchemy/util/langhelpers.py
@@ -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)
diff --git a/test/base/test_utils.py b/test/base/test_utils.py
index 6d162ff4d..53dbc12f9 100644
--- a/test/base/test_utils.py
+++ b/test/base/test_utils.py
@@ -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
@@ -2192,6 +2192,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):
diff --git a/test/engine/test_transaction.py b/test/engine/test_transaction.py
index 7f8a7c97c..c81a7580f 100644
--- a/test/engine/test_transaction.py
+++ b/test/engine/test_transaction.py
@@ -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:
diff --git a/test/orm/test_transaction.py b/test/orm/test_transaction.py
index 7efb5942b..c7b3315d9 100644
--- a/test/orm/test_transaction.py
+++ b/test/orm/test_transaction.py
@@ -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
diff --git a/test/requirements.py b/test/requirements.py
index 522a376e0..abc8ad5c2 100644
--- a/test/requirements.py
+++ b/test/requirements.py
@@ -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):