diff options
20 files changed, 63 insertions, 30 deletions
diff --git a/lib/sqlalchemy/connectors/mxodbc.py b/lib/sqlalchemy/connectors/mxodbc.py index f467234ca..5573dda40 100644 --- a/lib/sqlalchemy/connectors/mxodbc.py +++ b/lib/sqlalchemy/connectors/mxodbc.py @@ -106,9 +106,9 @@ class MxODBCConnector(Connector): opts.pop('database', None) return (args,), opts - def is_disconnect(self, e): - # eGenix recommends checking connection.closed here, - # but how can we get a handle on the current connection? + def is_disconnect(self, e, connection, cursor): + # TODO: eGenix recommends checking connection.closed here + # Does that detect dropped connections ? if isinstance(e, self.dbapi.ProgrammingError): return "connection already closed" in str(e) elif isinstance(e, self.dbapi.Error): diff --git a/lib/sqlalchemy/connectors/pyodbc.py b/lib/sqlalchemy/connectors/pyodbc.py index c66a8a8ae..3f6d6cb5f 100644 --- a/lib/sqlalchemy/connectors/pyodbc.py +++ b/lib/sqlalchemy/connectors/pyodbc.py @@ -81,7 +81,7 @@ class PyODBCConnector(Connector): connectors.extend(['%s=%s' % (k,v) for k,v in keys.iteritems()]) return [[";".join (connectors)], connect_args] - def is_disconnect(self, e): + def is_disconnect(self, e, connection, cursor): if isinstance(e, self.dbapi.ProgrammingError): return "The cursor's connection has been closed." in str(e) or \ 'Attempt to use a closed connection.' in str(e) diff --git a/lib/sqlalchemy/connectors/zxJDBC.py b/lib/sqlalchemy/connectors/zxJDBC.py index a9ff5ec95..20bf9d9cf 100644 --- a/lib/sqlalchemy/connectors/zxJDBC.py +++ b/lib/sqlalchemy/connectors/zxJDBC.py @@ -46,7 +46,7 @@ class ZxJDBCConnector(Connector): self.jdbc_driver_name], opts] - def is_disconnect(self, e): + def is_disconnect(self, e, connection, cursor): if not isinstance(e, self.dbapi.ProgrammingError): return False e = str(e) diff --git a/lib/sqlalchemy/dialects/firebird/kinterbasdb.py b/lib/sqlalchemy/dialects/firebird/kinterbasdb.py index 216fec270..ebb7805ae 100644 --- a/lib/sqlalchemy/dialects/firebird/kinterbasdb.py +++ b/lib/sqlalchemy/dialects/firebird/kinterbasdb.py @@ -153,7 +153,7 @@ class FBDialect_kinterbasdb(FBDialect): else: return tuple([int(x) for x in m.group(1, 2, 3)] + ['interbase']) - def is_disconnect(self, e): + def is_disconnect(self, e, connection, cursor): if isinstance(e, (self.dbapi.OperationalError, self.dbapi.ProgrammingError)): msg = str(e) diff --git a/lib/sqlalchemy/dialects/informix/informixdb.py b/lib/sqlalchemy/dialects/informix/informixdb.py index c81983816..1b6833af7 100644 --- a/lib/sqlalchemy/dialects/informix/informixdb.py +++ b/lib/sqlalchemy/dialects/informix/informixdb.py @@ -62,7 +62,7 @@ class InformixDialect_informixdb(InformixDialect): v = VERSION_RE.split(connection.connection.dbms_version) return (int(v[1]), int(v[2]), v[3]) - def is_disconnect(self, e): + def is_disconnect(self, e, connection, cursor): if isinstance(e, self.dbapi.OperationalError): return 'closed the connection' in str(e) \ or 'connection not open' in str(e) diff --git a/lib/sqlalchemy/dialects/mssql/adodbapi.py b/lib/sqlalchemy/dialects/mssql/adodbapi.py index 355214d89..f2d945de2 100644 --- a/lib/sqlalchemy/dialects/mssql/adodbapi.py +++ b/lib/sqlalchemy/dialects/mssql/adodbapi.py @@ -62,7 +62,7 @@ class MSDialect_adodbapi(MSDialect): connectors.append("Integrated Security=SSPI") return [[";".join (connectors)], {}] - def is_disconnect(self, e): + def is_disconnect(self, e, connection, cursor): return isinstance(e, self.dbapi.adodbapi.DatabaseError) and \ "'connection failure'" in str(e) diff --git a/lib/sqlalchemy/dialects/mssql/pymssql.py b/lib/sqlalchemy/dialects/mssql/pymssql.py index 192e63366..8bc0ad95b 100644 --- a/lib/sqlalchemy/dialects/mssql/pymssql.py +++ b/lib/sqlalchemy/dialects/mssql/pymssql.py @@ -95,7 +95,7 @@ class MSDialect_pymssql(MSDialect): opts['host'] = "%s:%s" % (opts['host'], port) return [[], opts] - def is_disconnect(self, e): + def is_disconnect(self, e, connection, cursor): for msg in ( "Error 10054", "Not connected to any MS SQL server", diff --git a/lib/sqlalchemy/dialects/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py index b495cc36e..882e13d2e 100644 --- a/lib/sqlalchemy/dialects/mysql/base.py +++ b/lib/sqlalchemy/dialects/mysql/base.py @@ -1711,7 +1711,7 @@ class MySQLDialect(default.DefaultDialect): resultset = connection.execute("XA RECOVER") return [row['data'][0:row['gtrid_length']] for row in resultset] - def is_disconnect(self, e): + def is_disconnect(self, e, connection, cursor): if isinstance(e, self.dbapi.OperationalError): return self._extract_error_code(e) in \ (2006, 2013, 2014, 2045, 2055) diff --git a/lib/sqlalchemy/dialects/mysql/mysqlconnector.py b/lib/sqlalchemy/dialects/mysql/mysqlconnector.py index d3ec1f5cf..035ebe459 100644 --- a/lib/sqlalchemy/dialects/mysql/mysqlconnector.py +++ b/lib/sqlalchemy/dialects/mysql/mysqlconnector.py @@ -118,7 +118,7 @@ class MySQLDialect_mysqlconnector(MySQLDialect): def _extract_error_code(self, exception): return exception.errno - def is_disconnect(self, e): + def is_disconnect(self, e, connection, cursor): errnos = (2006, 2013, 2014, 2045, 2055, 2048) exceptions = (self.dbapi.OperationalError,self.dbapi.InterfaceError) if isinstance(e, exceptions): diff --git a/lib/sqlalchemy/dialects/mysql/oursql.py b/lib/sqlalchemy/dialects/mysql/oursql.py index d3ef839b1..8caa1eaec 100644 --- a/lib/sqlalchemy/dialects/mysql/oursql.py +++ b/lib/sqlalchemy/dialects/mysql/oursql.py @@ -195,7 +195,7 @@ class MySQLDialect_oursql(MySQLDialect): execution_options(_oursql_plain_query=True), table, charset, full_name) - def is_disconnect(self, e): + def is_disconnect(self, e, connection, cursor): if isinstance(e, self.dbapi.ProgrammingError): return e.errno is None and 'cursor' not in e.args[1] and e.args[1].endswith('closed') else: diff --git a/lib/sqlalchemy/dialects/oracle/cx_oracle.py b/lib/sqlalchemy/dialects/oracle/cx_oracle.py index bc1c87703..b00adcd63 100644 --- a/lib/sqlalchemy/dialects/oracle/cx_oracle.py +++ b/lib/sqlalchemy/dialects/oracle/cx_oracle.py @@ -680,7 +680,7 @@ class OracleDialect_cx_oracle(OracleDialect): for x in connection.connection.version.split('.') ) - def is_disconnect(self, e): + def is_disconnect(self, e, connection, cursor): if isinstance(e, self.dbapi.InterfaceError): return "not connected" in str(e) else: diff --git a/lib/sqlalchemy/dialects/postgresql/pg8000.py b/lib/sqlalchemy/dialects/postgresql/pg8000.py index d3c2f1d50..c4f00eabe 100644 --- a/lib/sqlalchemy/dialects/postgresql/pg8000.py +++ b/lib/sqlalchemy/dialects/postgresql/pg8000.py @@ -108,7 +108,7 @@ class PGDialect_pg8000(PGDialect): opts.update(url.query) return ([], opts) - def is_disconnect(self, e): + def is_disconnect(self, e, connection, cursor): return "connection is closed" in str(e) dialect = PGDialect_pg8000 diff --git a/lib/sqlalchemy/dialects/postgresql/psycopg2.py b/lib/sqlalchemy/dialects/postgresql/psycopg2.py index 50ea9d437..10d6e0269 100644 --- a/lib/sqlalchemy/dialects/postgresql/psycopg2.py +++ b/lib/sqlalchemy/dialects/postgresql/psycopg2.py @@ -227,6 +227,7 @@ class PGDialect_psycopg2(PGDialect): execution_ctx_cls = PGExecutionContext_psycopg2 statement_compiler = PGCompiler_psycopg2 preparer = PGIdentifierPreparer_psycopg2 + psycopg2_version = (0, 0) colspecs = util.update_copy( PGDialect.colspecs, @@ -243,6 +244,11 @@ class PGDialect_psycopg2(PGDialect): self.server_side_cursors = server_side_cursors self.use_native_unicode = use_native_unicode self.supports_unicode_binds = use_native_unicode + if self.dbapi and hasattr(self.dbapi, '__version__'): + m = re.match(r'(\d+)\.(\d+)\.(\d+)?', + self.dbapi.__version__) + if m: + self.psycopg2_version = tuple(map(int, m.group(1, 2, 3))) @classmethod def dbapi(cls): @@ -295,7 +301,7 @@ class PGDialect_psycopg2(PGDialect): opts.update(url.query) return ([], opts) - def is_disconnect(self, e): + def is_disconnect(self, e, connection, cursor): if isinstance(e, self.dbapi.OperationalError): # these error messages from libpq: interfaces/libpq/fe-misc.c. # TODO: these are sent through gettext in libpq and we can't diff --git a/lib/sqlalchemy/dialects/postgresql/pypostgresql.py b/lib/sqlalchemy/dialects/postgresql/pypostgresql.py index dd22fcb33..a137a6240 100644 --- a/lib/sqlalchemy/dialects/postgresql/pypostgresql.py +++ b/lib/sqlalchemy/dialects/postgresql/pypostgresql.py @@ -67,7 +67,7 @@ class PGDialect_pypostgresql(PGDialect): opts.update(url.query) return ([], opts) - def is_disconnect(self, e): + def is_disconnect(self, e, connection, cursor): return "connection is closed" in str(e) dialect = PGDialect_pypostgresql diff --git a/lib/sqlalchemy/dialects/sqlite/pysqlite.py b/lib/sqlalchemy/dialects/sqlite/pysqlite.py index 14cfa93d9..646c5b86f 100644 --- a/lib/sqlalchemy/dialects/sqlite/pysqlite.py +++ b/lib/sqlalchemy/dialects/sqlite/pysqlite.py @@ -238,7 +238,7 @@ class SQLiteDialect_pysqlite(SQLiteDialect): return ([filename], opts) - def is_disconnect(self, e): + def is_disconnect(self, e, connection, cursor): return isinstance(e, self.dbapi.ProgrammingError) and "Cannot operate on a closed database." in str(e) dialect = SQLiteDialect_pysqlite diff --git a/lib/sqlalchemy/dialects/sybase/pysybase.py b/lib/sqlalchemy/dialects/sybase/pysybase.py index fed792817..e12cf07dd 100644 --- a/lib/sqlalchemy/dialects/sybase/pysybase.py +++ b/lib/sqlalchemy/dialects/sybase/pysybase.py @@ -87,7 +87,7 @@ class SybaseDialect_pysybase(SybaseDialect): # (12, 5, 0, 0) return (vers / 1000, vers % 1000 / 100, vers % 100 / 10, vers % 10) - def is_disconnect(self, e): + def is_disconnect(self, e, connection, cursor): if isinstance(e, (self.dbapi.OperationalError, self.dbapi.ProgrammingError)): msg = str(e) diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index b78a30537..f6c974136 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -502,7 +502,7 @@ class Dialect(object): raise NotImplementedError() - def is_disconnect(self, e): + def is_disconnect(self, e, connection, cursor): """Return True if the given DB-API error indicates an invalid connection""" @@ -1518,7 +1518,7 @@ class Connection(Connectable): if context: context.handle_dbapi_exception(e) - is_disconnect = self.dialect.is_disconnect(e) + is_disconnect = self.dialect.is_disconnect(e, self.__connection, cursor) if is_disconnect: self.invalidate(e) self.engine.dispose() diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index aa75a2853..e669b305e 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -324,7 +324,7 @@ class DefaultDialect(base.Dialect): def do_execute(self, cursor, statement, parameters, context=None): cursor.execute(statement, parameters) - def is_disconnect(self, e): + def is_disconnect(self, e, connection, cursor): return False def reset_isolation_level(self, dbapi_conn): diff --git a/lib/sqlalchemy/pool.py b/lib/sqlalchemy/pool.py index 5150d282c..7201bccf3 100644 --- a/lib/sqlalchemy/pool.py +++ b/lib/sqlalchemy/pool.py @@ -425,11 +425,7 @@ class _ConnectionFairy(object): self._close() def cursor(self, *args, **kwargs): - try: - return self.connection.cursor(*args, **kwargs) - except Exception, e: - self.invalidate(e=e) - raise + return self.connection.cursor(*args, **kwargs) def __getattr__(self, key): return getattr(self.connection, key) diff --git a/test/engine/test_reconnect.py b/test/engine/test_reconnect.py index 31a2b705a..9b3a1b4db 100644 --- a/test/engine/test_reconnect.py +++ b/test/engine/test_reconnect.py @@ -58,7 +58,7 @@ class MockReconnectTest(TestBase): module=dbapi, _initialize=False) # monkeypatch disconnect checker - db.dialect.is_disconnect = lambda e: isinstance(e, MockDisconnect) + db.dialect.is_disconnect = lambda e, conn, cursor: isinstance(e, MockDisconnect) def test_reconnect(self): """test that an 'is_disconnect' condition will invalidate the @@ -259,6 +259,22 @@ class RealReconnectTest(TestBase): conn.close() + def test_ensure_is_disconnect_gets_connection(self): + def is_disconnect(e, conn, cursor): + # connection is still present + assert conn.connection is not None + # the error usually occurs on connection.cursor(), + # though MySQLdb we get a non-working cursor. + # assert cursor is None + + engine.dialect.is_disconnect = is_disconnect + conn = engine.connect() + engine.test_shutdown() + assert_raises( + tsa.exc.DBAPIError, + conn.execute, select([1]) + ) + def test_invalidate_twice(self): conn = engine.connect() conn.invalidate() @@ -280,7 +296,7 @@ class RealReconnectTest(TestBase): p1 = engine.pool - def is_disconnect(e): + def is_disconnect(e, conn, cursor): return True engine.dialect.is_disconnect = is_disconnect @@ -374,13 +390,28 @@ class RecycleTest(TestBase): def test_basic(self): for threadlocal in False, True: - engine = engines.reconnecting_engine(options={'pool_recycle' - : 1, 'pool_threadlocal': threadlocal}) + engine = engines.reconnecting_engine( + options={'pool_threadlocal': threadlocal}) + conn = engine.contextual_connect() eq_(conn.execute(select([1])).scalar(), 1) conn.close() + + # set the pool recycle down to 1. + # we aren't doing this inline with the + # engine create since cx_oracle takes way + # too long to create the 1st connection and don't + # want to build a huge delay into this test. + + engine.pool._recycle = 1 + + # kill the DB connection engine.test_shutdown() + + # wait until past the recycle period time.sleep(2) + + # can connect, no exception conn = engine.contextual_connect() eq_(conn.execute(select([1])).scalar(), 1) conn.close() |