diff options
-rw-r--r-- | doc/build/changelog/changelog_08.rst | 15 | ||||
-rw-r--r-- | lib/sqlalchemy/engine/base.py | 56 | ||||
-rw-r--r-- | test/engine/test_reconnect.py | 136 |
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): |