]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Failures on connect which raise dbapi.Error
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 1 Jul 2011 20:52:11 +0000 (16:52 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 1 Jul 2011 20:52:11 +0000 (16:52 -0400)
will forward the error to dialect.is_disconnect()
and set the "connection_invalidated" flag if
the dialect knows this to be a potentially
"retryable" condition.  Only Oracle ORA-01033
implemented for now.  [ticket:2201]

- Added ORA-01033 to disconnect codes, which
can be caught during a connection
event.  [ticket:2201]

CHANGES
lib/sqlalchemy/dialects/oracle/cx_oracle.py
lib/sqlalchemy/dialects/sqlite/pysqlite.py
lib/sqlalchemy/engine/strategies.py
test/engine/test_parseconnect.py

diff --git a/CHANGES b/CHANGES
index 4c2243f9fb1d297a0e69030eb3f3e53b734ddea8..44ee35d7867313af9c18c8a6cf556c1bcda72a66 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -66,8 +66,15 @@ CHANGES
     occur in the context of a statement 
     execution.
 
- - StatementException wrapping will display the
-   original exception class in the message.  
+  - StatementException wrapping will display the
+    original exception class in the message.  
+
+  - Failures on connect which raise dbapi.Error
+    will forward the error to dialect.is_disconnect()
+    and set the "connection_invalidated" flag if 
+    the dialect knows this to be a potentially
+    "retryable" condition.  Only Oracle ORA-01033
+    implemented for now.  [ticket:2201]
 
 - mssql
   - Adjusted the pyodbc dialect such that bound
@@ -83,6 +90,10 @@ CHANGES
     cx_oracle _Error.code to get at the code,
     [ticket:2200].  Also in 0.6.9.
 
+  - Added ORA-01033 to disconnect codes, which
+    can be caught during a connection 
+    event.  [ticket:2201]
+
 -ext
   - Fixed bug in the mutable extension whereby
     if the same type were used twice in one
index 4f0cdffcfc15b87f321ad218e97d49ac81c76ccd..29d9e2ab0a1e15e27b486c1f99a4ab3b61f2fad6 100644 (file)
@@ -689,11 +689,14 @@ class OracleDialect_cx_oracle(OracleDialect):
         error, = e.args
         if isinstance(e, self.dbapi.InterfaceError):
             return "not connected" in str(e)
-        else:
+        elif hasattr(error, 'code'):
             # ORA-00028: your session has been killed
             # ORA-03114: not connected to ORACLE
             # ORA-03113: end-of-file on communication channel
-            return error.code in (28, 3114, 3113)
+            # ORA-01033: ORACLE initialization or shutdown in progress
+            return error.code in (28, 3114, 3113, 1033)
+        else:
+            return False
 
     def create_xid(self):
         """create a two-phase transaction ID.
index 07df64712e2ef2e1d64e1a3683f706ba3d2d514a..c8739f242d41a0e6d71c9061830be4eb9154a46f 100644 (file)
@@ -289,6 +289,7 @@ class SQLiteDialect_pysqlite(SQLiteDialect):
         return ([filename], opts)
 
     def is_disconnect(self, e, connection, cursor):
-        return isinstance(e, self.dbapi.ProgrammingError) and "Cannot operate on a closed database." in str(e)
+        return isinstance(e, self.dbapi.ProgrammingError) and \
+                "Cannot operate on a closed database." in str(e)
 
 dialect = SQLiteDialect_pysqlite
index 25082443189caf25843656fe559a001db1f5d775..528bc3175753d97aa396c5c6ebcf0536525ae563 100644 (file)
@@ -85,7 +85,8 @@ class DefaultEngineStrategy(EngineStrategy):
                     # Py2K
                     import sys
                     raise exc.DBAPIError.instance(
-                                None, None, e, dialect.dbapi.Error), \
+                                None, None, e, dialect.dbapi.Error,
+                                connection_invalidated=dialect.is_disconnect(e, None, None)), \
                                 None, sys.exc_info()[2]
                     # end Py2K
 
index 3b3e09a7ad6306ed8d65a44fa2a5582211bca75d..1c1ab6aad1adab9bee330919fd9a1bb1dcb64751 100644 (file)
@@ -5,7 +5,7 @@ import sqlalchemy.engine.url as url
 from sqlalchemy import create_engine, engine_from_config, exc
 from sqlalchemy.engine import _coerce_config
 import sqlalchemy as tsa
-from test.lib import fixtures
+from test.lib import fixtures, testing
 
 class ParseConnectTest(fixtures.TestBase):
     def test_rfc1738(self):
@@ -175,7 +175,7 @@ pool_timeout=10
                           module=dbapi, _initialize=False)
         assert e.pool._recycle == 472
 
-    def test_badargs(self):
+    def test_bad_args(self):
         assert_raises(exc.ArgumentError, create_engine, 'foobar://',
                       module=mock_dbapi)
 
@@ -201,19 +201,49 @@ pool_timeout=10
         assert_raises(TypeError, create_engine, 'mysql+mysqldb://',
                       use_unicode=True, module=mock_dbapi)
 
+    @testing.requires.sqlite
+    def test_wraps_connect_in_dbapi(self):
         # sqlite uses SingletonThreadPool which doesnt have max_overflow
 
         assert_raises(TypeError, create_engine, 'sqlite://',
                       max_overflow=5, module=mock_sqlite_dbapi)
+        e = create_engine('sqlite://', connect_args={'use_unicode'
+                          : True}, convert_unicode=True)
         try:
-            e = create_engine('sqlite://', connect_args={'use_unicode'
-                              : True}, convert_unicode=True)
-        except ImportError:
-            # no sqlite
-            pass
-        else:
-            # raises DBAPIerror due to use_unicode not a sqlite arg
-            assert_raises(tsa.exc.DBAPIError, e.connect)
+            e.connect()
+        except tsa.exc.DBAPIError, de:
+            assert not de.connection_invalidated
+
+    def test_ensure_dialect_does_is_disconnect_no_conn(self):
+        """test that is_disconnect() doesn't choke if no connection, cursor given."""
+        dialect = testing.db.dialect
+        dbapi = dialect.dbapi
+        assert not dialect.is_disconnect(dbapi.OperationalError("test"), None, None)
+
+    @testing.requires.sqlite
+    def test_invalidate_on_connect(self):
+        """test that is_disconnect() is called during connect.
+        
+        interpretation of connection failures are not supported by
+        every backend.
+        
+        """
+        # pretend pysqlite throws the 
+        # "Cannot operate on a closed database." error
+        # on connect.   IRL we'd be getting Oracle's "shutdown in progress"
+
+        import sqlite3
+        class ThrowOnConnect(MockDBAPI):
+            dbapi = sqlite3
+            Error = sqlite3.Error
+            ProgrammingError = sqlite3.ProgrammingError
+            def connect(self, *args, **kw):
+                raise sqlite3.ProgrammingError("Cannot operate on a closed database.")
+        try:
+            create_engine('sqlite://', module=ThrowOnConnect()).connect()
+            assert False
+        except tsa.exc.DBAPIError, de:
+            assert de.connection_invalidated
 
     def test_urlattr(self):
         """test the url attribute on ``Engine``."""
@@ -264,6 +294,9 @@ pool_timeout=10
             )
 
 class MockDBAPI(object):
+    version_info = sqlite_version_info = 99, 9, 9
+    sqlite_version = '99.9.9'
+
     def __init__(self, **kwargs):
         self.kwargs = kwargs
         self.paramstyle = 'named'
@@ -293,5 +326,3 @@ class MockCursor(object):
 
 mock_dbapi = MockDBAPI()
 mock_sqlite_dbapi = msd = MockDBAPI()
-msd.version_info = msd.sqlite_version_info = 99, 9, 9
-msd.sqlite_version = '99.9.9'