summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2013-04-11 19:10:02 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2013-04-11 19:10:02 -0400
commitee7bb9c1744b4b09263ef32032afd38f694df136 (patch)
tree5f381ca1c3869c130e4f2a86092ca07b99a0ebbf
parentf35987d31a009a6f181e2d4060e2f6274f02cbe7 (diff)
downloadsqlalchemy-ee7bb9c1744b4b09263ef32032afd38f694df136.tar.gz
Improvements to Connection auto-invalidation
handling. If a non-disconnect error occurs, but leads to a delayed disconnect error within error handling (happens with MySQL), the disconnect condition is detected. The Connection can now also be closed when in an invalid state, meaning it will raise "closed" on next usage, and additionally the "close with result" feature will work even if the autorollback in an error handling routine fails and regardless of whether the condition is a disconnect or not. [ticket:2695]
-rw-r--r--doc/build/changelog/changelog_08.rst15
-rw-r--r--lib/sqlalchemy/engine/base.py56
-rw-r--r--test/engine/test_reconnect.py136
3 files changed, 169 insertions, 38 deletions
diff --git a/doc/build/changelog/changelog_08.rst b/doc/build/changelog/changelog_08.rst
index cdd96e01e..50d31a094 100644
--- a/doc/build/changelog/changelog_08.rst
+++ b/doc/build/changelog/changelog_08.rst
@@ -8,6 +8,21 @@
.. change::
:tags: bug, sql
+ :tickets: 2695
+
+ Improvements to Connection auto-invalidation
+ handling. If a non-disconnect error occurs,
+ but leads to a delayed disconnect error within error
+ handling (happens with MySQL), the disconnect condition
+ is detected. The Connection can now also be closed
+ when in an invalid state, meaning it will raise "closed"
+ on next usage, and additionally the "close with result"
+ feature will work even if the autorollback in an error
+ handling routine fails and regardless of whether the
+ condition is a disconnect or not.
+
+ .. change::
+ :tags: bug, sql
:tickets: 2702
A major fix to the way in which a select() object produces
diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py
index 6242f0816..e40af6219 100644
--- a/lib/sqlalchemy/engine/base.py
+++ b/lib/sqlalchemy/engine/base.py
@@ -62,6 +62,7 @@ class Connection(Connectable):
self.__savepoint_seq = 0
self.__branch = _branch
self.__invalid = False
+ self.__can_reconnect = True
if _dispatch:
self.dispatch = _dispatch
elif engine._has_events:
@@ -213,8 +214,8 @@ class Connection(Connectable):
def closed(self):
"""Return True if this connection is closed."""
- return not self.__invalid and '_Connection__connection' \
- not in self.__dict__
+ return '_Connection__connection' not in self.__dict__ \
+ and not self.__can_reconnect
@property
def invalidated(self):
@@ -232,7 +233,7 @@ class Connection(Connectable):
return self._revalidate_connection()
def _revalidate_connection(self):
- if self.__invalid:
+ if self.__can_reconnect and self.__invalid:
if self.__transaction is not None:
raise exc.InvalidRequestError(
"Can't reconnect until invalid "
@@ -577,15 +578,15 @@ class Connection(Connectable):
and will allow no further operations.
"""
-
try:
conn = self.__connection
except AttributeError:
- return
- if not self.__branch:
- conn.close()
- self.__invalid = False
- del self.__connection
+ pass
+ else:
+ if not self.__branch:
+ conn.close()
+ del self.__connection
+ self.__can_reconnect = False
self.__transaction = None
def scalar(self, object, *multiparams, **params):
@@ -972,13 +973,22 @@ class Connection(Connectable):
if isinstance(e, (SystemExit, KeyboardInterrupt)):
raise
+ _reentrant_error = False
+ _is_disconnect = False
+
def _handle_dbapi_exception(self,
e,
statement,
parameters,
cursor,
context):
- if getattr(self, '_reentrant_error', False):
+
+ if not self._is_disconnect:
+ self._is_disconnect = isinstance(e, self.dialect.dbapi.Error) and \
+ not self.closed and \
+ self.dialect.is_disconnect(e, self.__connection, cursor)
+
+ if self._reentrant_error:
# Py3K
#raise exc.DBAPIError.instance(statement, parameters, e,
# self.dialect.dbapi.Error) from e
@@ -1006,21 +1016,10 @@ class Connection(Connectable):
e)
context.handle_dbapi_exception(e)
- is_disconnect = isinstance(e, self.dialect.dbapi.Error) and \
- self.dialect.is_disconnect(e, self.__connection, cursor)
-
- if is_disconnect:
- dbapi_conn_wrapper = self.connection
- self.invalidate(e)
- if not hasattr(dbapi_conn_wrapper, '_pool') or \
- dbapi_conn_wrapper._pool is self.engine.pool:
- self.engine.dispose()
- else:
+ if not self._is_disconnect:
if cursor:
self._safe_close_cursor(cursor)
self._autorollback()
- if self.should_close_with_result:
- self.close()
if not should_wrap:
return
@@ -1031,7 +1030,7 @@ class Connection(Connectable):
# parameters,
# e,
# self.dialect.dbapi.Error,
- # connection_invalidated=is_disconnect) \
+ # connection_invalidated=self._is_disconnect) \
# from e
# Py2K
raise exc.DBAPIError.instance(
@@ -1039,12 +1038,21 @@ class Connection(Connectable):
parameters,
e,
self.dialect.dbapi.Error,
- connection_invalidated=is_disconnect), \
+ connection_invalidated=self._is_disconnect), \
None, sys.exc_info()[2]
# end Py2K
finally:
del self._reentrant_error
+ if self._is_disconnect:
+ del self._is_disconnect
+ dbapi_conn_wrapper = self.connection
+ self.invalidate(e)
+ if not hasattr(dbapi_conn_wrapper, '_pool') or \
+ dbapi_conn_wrapper._pool is self.engine.pool:
+ self.engine.dispose()
+ if self.should_close_with_result:
+ self.close()
# poor man's multimethod/generic function thingy
executors = {
diff --git a/test/engine/test_reconnect.py b/test/engine/test_reconnect.py
index 6b283654b..86f646f33 100644
--- a/test/engine/test_reconnect.py
+++ b/test/engine/test_reconnect.py
@@ -11,7 +11,10 @@ from sqlalchemy import exc
from sqlalchemy.testing import fixtures
from sqlalchemy.testing.engines import testing_engine
-class MockDisconnect(Exception):
+class MockError(Exception):
+ pass
+
+class MockDisconnect(MockError):
pass
class MockDBAPI(object):
@@ -20,17 +23,23 @@ class MockDBAPI(object):
self.connections = weakref.WeakKeyDictionary()
def connect(self, *args, **kwargs):
return MockConnection(self)
- def shutdown(self):
+ def shutdown(self, explode='execute'):
for c in self.connections:
- c.explode[0] = True
- Error = MockDisconnect
+ c.explode = explode
+ Error = MockError
class MockConnection(object):
def __init__(self, dbapi):
dbapi.connections[self] = True
- self.explode = [False]
+ self.explode = ""
def rollback(self):
- pass
+ if self.explode == 'rollback':
+ raise MockDisconnect("Lost the DB connection on rollback")
+ if self.explode == 'rollback_no_disconnect':
+ raise MockError(
+ "something broke on rollback but we didn't lose the connection")
+ else:
+ return
def commit(self):
pass
def cursor(self):
@@ -43,8 +52,14 @@ class MockCursor(object):
self.explode = parent.explode
self.description = ()
def execute(self, *args, **kwargs):
- if self.explode[0]:
- raise MockDisconnect("Lost the DB connection")
+ if self.explode == 'execute':
+ raise MockDisconnect("Lost the DB connection on execute")
+ elif self.explode in ('execute_no_disconnect', ):
+ raise MockError(
+ "something broke on execute but we didn't lose the connection")
+ elif self.explode in ('rollback', 'rollback_no_disconnect'):
+ raise MockError(
+ "something broke on execute but we didn't lose the connection")
else:
return
def close(self):
@@ -167,12 +182,10 @@ class MockReconnectTest(fixtures.TestBase):
dbapi.shutdown()
- # raises error
- try:
- conn.execute(select([1]))
- assert False
- except tsa.exc.DBAPIError:
- pass
+ assert_raises(
+ tsa.exc.DBAPIError,
+ conn.execute, select([1])
+ )
assert not conn.closed
assert conn.invalidated
@@ -186,6 +199,101 @@ class MockReconnectTest(fixtures.TestBase):
assert not conn.invalidated
assert len(dbapi.connections) == 1
+ def test_invalidated_close(self):
+ conn = db.connect()
+
+ dbapi.shutdown()
+
+ assert_raises(
+ tsa.exc.DBAPIError,
+ conn.execute, select([1])
+ )
+
+ conn.close()
+ assert conn.closed
+ assert conn.invalidated
+ assert_raises_message(
+ tsa.exc.StatementError,
+ "This Connection is closed",
+ conn.execute, select([1])
+ )
+
+ def test_noreconnect_execute_plus_closewresult(self):
+ conn = db.connect(close_with_result=True)
+
+ dbapi.shutdown("execute_no_disconnect")
+
+ # raises error
+ assert_raises_message(
+ tsa.exc.DBAPIError,
+ "something broke on execute but we didn't lose the connection",
+ conn.execute, select([1])
+ )
+
+ assert conn.closed
+ assert not conn.invalidated
+
+ def test_noreconnect_rollback_plus_closewresult(self):
+ conn = db.connect(close_with_result=True)
+
+ dbapi.shutdown("rollback_no_disconnect")
+
+ # raises error
+ assert_raises_message(
+ tsa.exc.DBAPIError,
+ "something broke on rollback but we didn't lose the connection",
+ conn.execute, select([1])
+ )
+
+ assert conn.closed
+ assert not conn.invalidated
+
+ assert_raises_message(
+ tsa.exc.StatementError,
+ "This Connection is closed",
+ conn.execute, select([1])
+ )
+
+ def test_reconnect_on_reentrant(self):
+ conn = db.connect()
+
+ conn.execute(select([1]))
+
+ assert len(dbapi.connections) == 1
+
+ dbapi.shutdown("rollback")
+
+ # raises error
+ assert_raises_message(
+ tsa.exc.DBAPIError,
+ "Lost the DB connection on rollback",
+ conn.execute, select([1])
+ )
+
+ assert not conn.closed
+ assert conn.invalidated
+
+ def test_reconnect_on_reentrant_plus_closewresult(self):
+ conn = db.connect(close_with_result=True)
+
+ dbapi.shutdown("rollback")
+
+ # raises error
+ assert_raises_message(
+ tsa.exc.DBAPIError,
+ "Lost the DB connection on rollback",
+ conn.execute, select([1])
+ )
+
+ assert conn.closed
+ assert conn.invalidated
+
+ assert_raises_message(
+ tsa.exc.StatementError,
+ "This Connection is closed",
+ conn.execute, select([1])
+ )
+
class CursorErrTest(fixtures.TestBase):
def setup(self):