On 17/09/2007, James Henstridge <[EMAIL PROTECTED]> wrote:
> Hi,
>
> I've updated my database disconnect branch, and think it is just about
> ready.  The branch is attached to bug 94986, and the branch is at r178
> (it looks like Launchpad hasn't yet published the latest changes).

As the branch still hasn't published, here's bundle of the change made
against trunk.

James.
# Bazaar merge directive format 2 (Bazaar 0.90)
# revision_id: [EMAIL PROTECTED]
#   hhb42uxgysux6lcl
# target_branch: http://bazaar.launchpad.net/%7Estorm/storm/trunk/
# testament_sha1: 6dbb0be16a0d387415b4c85a456612e907970a77
# timestamp: 2007-09-17 21:01:08 +0800
# source_branch: sftp://devpad.canonical.com/code/jamesh/storm\
#   /reconnect
# base_revision_id: [EMAIL PROTECTED]
#   fvghwn1552shwijz
# 
# Begin patch
=== added file 'tests/databases/proxy.py'
--- tests/databases/proxy.py	1970-01-01 00:00:00 +0000
+++ tests/databases/proxy.py	2007-09-17 08:17:13 +0000
@@ -0,0 +1,105 @@
+
+import os
+import select
+import socket
+import SocketServer
+import threading
+
+TIMEOUT = 0.1
+
+
+class ProxyRequestHandler(SocketServer.BaseRequestHandler):
+    """A request handler that proxies traffic to another TCP port."""
+
+    def __init__(self, request, client_address, server):
+        self._generation = server._generation
+        SocketServer.BaseRequestHandler.__init__(
+            self, request, client_address, server)
+
+    def handle(self):
+        dst = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        dst.connect(self.server.proxy_dest)
+
+        readers = [self.request, dst]
+        while readers:
+            rlist, wlist, xlist = select.select(readers, [], [], TIMEOUT)
+
+            # If the server generation has been incremented, close the
+            # connection.
+            if self._generation != self.server._generation:
+                return
+
+            if self.request in rlist:
+                chunk = os.read(self.request.fileno(), 1024)
+                dst.send(chunk)
+                if chunk == '':
+                    readers.remove(self.request)
+                    dst.shutdown(socket.SHUT_WR)
+
+            if dst in rlist:
+                chunk = os.read(dst.fileno(), 1024)
+                self.request.send(chunk)
+                if chunk == '':
+                    readers.remove(dst)
+                    self.request.shutdown(socket.SHUT_WR)
+
+class ProxyTCPServer(SocketServer.ThreadingTCPServer):
+
+    allow_reuse_address = True
+
+    def __init__(self, proxy_dest):
+        SocketServer.ThreadingTCPServer.__init__(
+            self, ('127.0.0.1', 0), ProxyRequestHandler)
+        self.proxy_dest = proxy_dest
+        self._start_lock = threading.Lock()
+        self._thread = None
+        self._generation = 0
+        self._running = False
+        self.start()
+
+    def __del__(self):
+        self.close()
+
+    def close(self):
+        if self._running:
+            self.stop()
+
+    def start(self):
+        assert not self._running, 'Server should not be running'
+        self._thread = threading.Thread(target=self._run)
+        self._thread.setDaemon(True)
+
+        self._running = True
+        self._start_lock.acquire()
+        self._thread.start()
+        # Wait for server to start
+        self._start_lock.acquire()
+        self._start_lock.release()
+
+    def _run(self):
+        self.server_activate()
+        self.socket.settimeout(TIMEOUT)
+        self._start_lock.release()
+        while self._running:
+            try:
+                self.handle_request()
+            except socket.timeout:
+                pass
+
+    def stop(self):
+        assert self._running, 'Server should be running'
+        # Increment server generation, and wait for thread to stop.
+        self._generation += 1
+        self._running = False
+        self._thread.join()
+
+        # Recreate socket, to kill listen queue.  As we've allowed
+        # address reuse, this should work.
+        self.socket.close()
+        self.socket = socket.socket(self.address_family,
+                                    self.socket_type)
+        self.server_bind()
+
+    def restart(self):
+        self.stop()
+        self.start()

=== modified file 'storm/database.py'
--- storm/database.py	2007-08-08 19:01:45 +0000
+++ storm/database.py	2007-09-17 08:52:24 +0000
@@ -27,7 +27,7 @@
 
 from storm.expr import Expr, State, compile
 from storm.variables import Variable
-from storm.exceptions import ClosedError
+from storm.exceptions import ClosedError, DatabaseError, DisconnectionError
 from storm.uri import URI
 import storm
 
@@ -47,6 +47,7 @@
     def __init__(self, connection, raw_cursor):
         self._connection = connection # Ensures deallocation order.
         self._raw_cursor = raw_cursor
+        self._generation = connection._generation
         if raw_cursor.arraysize == 1:
             # Default of 1 is silly.
             self._raw_cursor.arraysize = 10
@@ -74,7 +75,8 @@
 
         @return: A converted row or None, if no data is left.
         """
-        row = self._raw_cursor.fetchone()
+        assert self._generation == self._connection._generation
+        row = self._connection._check_disconnect(self._raw_cursor.fetchone)
         if row is not None:
             return tuple(self.from_database(row))
         return None
@@ -85,7 +87,8 @@
         The results will be converted to an appropriate format via
         L{from_database}.
         """
-        result = self._raw_cursor.fetchall()
+        assert self._generation == self._connection._generation
+        result = self._connection._check_disconnect(self._raw_cursor.fetchall)
         if result:
             return [tuple(self.from_database(row)) for row in result]
         return result
@@ -97,7 +100,8 @@
         L{from_database}.
         """
         while True:
-            results = self._raw_cursor.fetchmany()
+            results = self._connection._check_disconnect(
+                self._raw_cursor.fetchmany)
             if not results:
                 break
             for result in results:
@@ -147,10 +151,12 @@
     compile = compile
 
     _closed = False
+    _is_dead = False
+    _generation = 0
 
-    def __init__(self, database, raw_connection):
+    def __init__(self, database):
         self._database = database # Ensures deallocation order.
-        self._raw_connection = raw_connection
+        self._raw_connection = self._database._connect()
 
     def __del__(self):
         """Close the connection."""
@@ -172,6 +178,7 @@
         """
         if self._closed:
             raise ClosedError("Connection is closed")
+        self._ensure_connected()
         if isinstance(statement, Expr):
             if params is not None:
                 raise ValueError("Can't pass parameters with expressions")
@@ -179,9 +186,10 @@
             statement = self.compile(statement, state)
             params = state.parameters
         statement = convert_param_marks(statement, "?", self.param_mark)
-        raw_cursor = self.raw_execute(statement, params)
+        raw_cursor = self._check_disconnect(
+            self.raw_execute, statement, params)
         if noresult:
-            raw_cursor.close()
+            self._check_disconnect(raw_cursor.close)
             return None
         return self.result_factory(self, raw_cursor)
 
@@ -194,11 +202,20 @@
 
     def commit(self):
         """Commit the connection."""
-        self._raw_connection.commit()
+        self._ensure_connected()
+        self._check_disconnect(self._raw_connection.commit)
 
     def rollback(self):
         """Rollback the connection."""
-        self._raw_connection.rollback()
+        if self._raw_connection is not None:
+            try:
+                self._raw_connection.rollback()
+            except DatabaseError, exc:
+                if self._is_disconnection(exc):
+                    self._raw_connection = None
+                else:
+                    raise
+        self._is_dead = False
 
     @staticmethod
     def to_database(params):
@@ -247,6 +264,48 @@
             raw_cursor.execute(statement, params)
         return raw_cursor
 
+    def _ensure_connected(self):
+        """Ensure that we are connected to the database.
+
+        If the connection is marked as dead, or if we can't reconnect,
+        then raise DisconnectionError.
+
+        If we need to reconnect, the connection generation number is
+        incremented.
+        """
+        if self._is_dead:
+            raise DisconnectionError('Already disconnected')
+        if self._raw_connection is not None:
+            return
+        try:
+            self._raw_connection = self._database._connect()
+            self._generation += 1
+        except DatabaseError, exc:
+            self._is_dead = True
+            self._raw_connection = None
+            raise DisconnectionError(str(exc))
+
+    def _is_disconnection(self, exc):
+        """Check whether the given exception value represents a
+        database disconnection.
+
+        This should be overridden by backends to detect whichever
+        exception values are used to represent this condition.
+        """
+        return False
+
+    def _check_disconnect(self, function, *args, **kwargs):
+        """Run the given function, checking for database disconnections."""
+        try:
+            return function(*args, **kwargs)
+        except DatabaseError, exc:
+            if self._is_disconnection(exc):
+                self._is_dead = True
+                self._raw_connection = None
+                raise DisconnectionError(str(exc))
+            else:
+                raise
+
     def preset_primary_key(self, primary_columns, primary_variables):
         """Process primary variables before an insert happens.
 
@@ -261,8 +320,7 @@
     This should be subclassed for individual database backends.
 
     @cvar connection_factory: A callable which will take this database
-        and a raw connection and should return an instance of
-        L{Connection}.
+        and should return an instance of L{Connection}.
     """
 
     connection_factory = Connection
@@ -270,12 +328,22 @@
     def connect(self):
         """Create a connection to the database.
 
-        This should be overriden in subclasses to do any
-        database-specific connection setup. It should call
-        C{self.connection_factory} to allow for ease of customization.
+        It calls C{self.connection_factory} to allow for ease of
+        customization.
 
         @return: An instance of L{Connection}.
         """
+        return self.connection_factory(self)
+
+    def _connect(self):
+        """Create a raw database connection.
+
+        This is used by L{Connection} objects to connect to the
+        database.  It should be overriden in subclasses to do any
+        database-specific connection setup.
+
+        @return: A DB-API connection object.
+        """
         raise NotImplementedError
 
 

=== modified file 'storm/databases/mysql.py'
--- storm/databases/mysql.py	2007-08-08 15:02:34 +0000
+++ storm/databases/mysql.py	2007-09-05 07:27:57 +0000
@@ -118,9 +118,9 @@
         self._connect_kwargs["conv"] = self._converters
         self._connect_kwargs["use_unicode"] = True
 
-    def connect(self):
+    def _connect(self):
         raw_connection = MySQLdb.connect(**self._connect_kwargs)
-        return self.connection_factory(self, raw_connection)
+        return raw_connection
 
 
 create_from_uri = MySQL

=== modified file 'storm/databases/postgres.py'
--- storm/databases/postgres.py	2007-08-08 15:02:34 +0000
+++ storm/databases/postgres.py	2007-09-17 08:17:13 +0000
@@ -166,6 +166,17 @@
             else:
                 yield param
 
+    def _is_disconnection(self, exc):
+        msg = exc.args[0]
+        # XXX: 2007-09-17 jamesh
+        # I have no idea why I am seeing the last exception message
+        # after upgrading to Gutsy.
+        return (msg.startswith('server closed the connection unexpectedly') or
+                msg.startswith('could not connect to server') or
+                msg.startswith('no connection to the server') or
+                msg.startswith('connection not open') or
+                msg.startswith('losed the connection unexpectedly'))
+
 
 class Postgres(Database):
 
@@ -176,12 +187,12 @@
             raise DatabaseModuleError("'psycopg2' module not found")
         self._dsn = make_dsn(uri)
 
-    def connect(self):
+    def _connect(self):
         raw_connection = psycopg2.connect(self._dsn)
         raw_connection.set_client_encoding("UTF8")
         raw_connection.set_isolation_level(
             psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE)
-        return self.connection_factory(self, raw_connection)
+        return raw_connection
 
 
 create_from_uri = Postgres

=== modified file 'storm/databases/sqlite.py'
--- storm/databases/sqlite.py	2007-08-08 15:02:34 +0000
+++ storm/databases/sqlite.py	2007-09-05 07:27:57 +0000
@@ -167,11 +167,11 @@
         self._filename = uri.database or ":memory:"
         self._timeout = float(uri.options.get("timeout", 5))
 
-    def connect(self):
+    def _connect(self):
         # See the story at the end to understand why we set isolation_level.
         raw_connection = sqlite.connect(self._filename, timeout=self._timeout,
                                         isolation_level=None)
-        return self.connection_factory(self, raw_connection)
+        return raw_connection
 
 
 create_from_uri = SQLite

=== modified file 'storm/exceptions.py'
--- storm/exceptions.py	2007-08-07 18:36:04 +0000
+++ storm/exceptions.py	2007-09-07 03:48:00 +0000
@@ -50,6 +50,9 @@
 class ClosedError(StormError):
     pass
 
+class DisconnectionError(StormError):
+    pass
+
 class FeatureError(StormError):
     pass
 

=== modified file 'tests/database.py'
--- tests/database.py	2007-08-08 15:02:34 +0000
+++ tests/database.py	2007-09-17 08:52:24 +0000
@@ -90,6 +90,15 @@
         return result
 
 
+class FakeConnection(object):
+
+    def __init__(self):
+        self._generation = 0
+
+    def _check_disconnect(self, _function, *args, **kwargs):
+        return _function(*args, **kwargs)
+
+
 class DatabaseTest(TestHelper):
 
     def setUp(self):
@@ -107,7 +116,8 @@
         self.executed = []
         self.database = Database()
         self.raw_connection = RawConnection(self.executed)
-        self.connection = Connection(self.database, self.raw_connection)
+        self.database._connect = lambda: self.raw_connection
+        self.connection = Connection(self.database)
 
     def test_execute(self):
         result = self.connection.execute("something")
@@ -131,7 +141,7 @@
     def test_execute_convert_param_style(self):
         class MyConnection(Connection):
             param_mark = "%s"
-        connection = MyConnection(self.database, RawConnection(self.executed))
+        connection = MyConnection(self.database)
         result = connection.execute("'?' ? '?' ? '?'")
         self.assertEquals(self.executed, [("'?' %s '?' %s '?'", marker)])
 
@@ -203,7 +213,7 @@
         TestHelper.setUp(self)
         self.executed = []
         self.raw_cursor = RawCursor(executed=self.executed)
-        self.result = Result(None, self.raw_cursor)
+        self.result = Result(FakeConnection(), self.raw_cursor)
 
     def test_get_one(self):
         self.assertEquals(self.result.get_one(), ("fetchone0",))
@@ -217,7 +227,7 @@
         self.assertEquals(self.result.get_all(), [])
 
     def test_iter(self):
-        result = Result(None, RawCursor(2))
+        result = Result(FakeConnection(), RawCursor(2))
         self.assertEquals([item for item in result],
                           [("fetchmany0",), ("fetchmany1",), ("fetchmany2",),
                            ("fetchmany3",), ("fetchmany4",)])
@@ -256,13 +266,13 @@
         """When the arraysize is 1, change it to a better value."""
         raw_cursor = RawCursor()
         self.assertEquals(raw_cursor.arraysize, 1)
-        result = Result(None, raw_cursor)
+        result = Result(FakeConnection(), raw_cursor)
         self.assertEquals(raw_cursor.arraysize, 10)
 
     def test_preserve_arraysize(self):
         """When the arraysize is not 1, preserve it."""
         raw_cursor = RawCursor(arraysize=123)
-        result = Result(None, raw_cursor)
+        result = Result(FakeConnection(), raw_cursor)
         self.assertEquals(raw_cursor.arraysize, 123)
 
 

=== modified file 'tests/databases/base.py'
--- tests/databases/base.py	2007-08-08 15:02:34 +0000
+++ tests/databases/base.py	2007-09-17 08:17:13 +0000
@@ -21,7 +21,6 @@
 #
 from datetime import datetime, date, time
 import cPickle as pickle
-import thread
 import shutil
 import sys
 import os
@@ -32,7 +31,8 @@
                              DecimalVariable)
 from storm.variables import DateTimeVariable, DateVariable, TimeVariable
 from storm.database import *
-from storm.exceptions import DatabaseModuleError, OperationalError
+from storm.exceptions import (
+    DatabaseModuleError, DisconnectionError, OperationalError)
 
 from tests.helper import MakePath
 
@@ -395,3 +395,86 @@
             del sys.modules["_fake_"]
 
             sys.modules.update(dbapi_modules)
+
+
+class DatabaseDisconnectionTest(object):
+
+    def setUp(self):
+        super(DatabaseDisconnectionTest, self).setUp()
+        self.create_database_and_proxy()
+        self.create_connection()
+
+    def tearDown(self):
+        self.drop_database()
+        self.proxy.close()
+        super(DatabaseDisconnectionTest, self).tearDown()
+
+    def create_database_and_proxy(self):
+        """Set up the TCP proxy and database object.
+
+        The TCP proxy should forward requests on to the database.  The
+        database object should point at the TCP proxy.
+        """
+        raise NotImplementedError
+
+    def create_connection(self):
+        self.connection = self.database.connect()
+
+    def drop_database(self):
+        pass
+
+    def test_proxy_works(self):
+        # Ensure that we can talk to the database through the proxy.
+        result = self.connection.execute("SELECT 1")
+        self.assertEqual(result.get_one(), (1,))
+
+    def test_catch_disconnect_on_execute(self):
+        # Test that database disconnections get caught on execute().
+        result = self.connection.execute("SELECT 1")
+        self.assertTrue(result.get_one())
+        self.proxy.restart()
+        self.assertRaises(DisconnectionError,
+                          self.connection.execute, "SELECT 1")
+
+    def test_catch_disconnect_on_commit(self):
+        # Test that database disconnections get caught on commit().
+        result = self.connection.execute("SELECT 1")
+        self.assertTrue(result.get_one())
+        self.proxy.restart()
+        self.assertRaises(DisconnectionError, self.connection.commit)
+
+    def test_connection_stays_disconnected_in_transaction(self):
+        # Test that subsequent operations in the transaction also
+        # raise DisconnectionError.
+        result = self.connection.execute("SELECT 1")
+        self.assertTrue(result.get_one())
+        self.proxy.restart()
+        self.assertRaises(DisconnectionError,
+                          self.connection.execute, "SELECT 1")
+        self.assertRaises(DisconnectionError,
+                          self.connection.execute, "SELECT 1")
+
+    def test_reconnect_after_rollback(self):
+        # Test that we reconnect after rolling back the connection.
+        result = self.connection.execute("SELECT 1")
+        self.assertTrue(result.get_one())
+        self.proxy.restart()
+        self.assertRaises(DisconnectionError,
+                          self.connection.execute, "SELECT 1")
+        self.connection.rollback()
+        result = self.connection.execute("SELECT 1")
+        self.assertTrue(result.get_one())
+
+    def test_catch_disconnect_on_reconnect(self):
+        # Test that we raise DisconnectionError if unable to reconnect
+        # on a new transaction.
+        result = self.connection.execute("SELECT 1")
+        self.assertTrue(result.get_one())
+        self.proxy.stop()
+        self.assertRaises(DisconnectionError,
+                          self.connection.execute, "SELECT 1")
+        # Rollback the connection, but because the proxy is still
+        # down, we get a DisconnectionError again.
+        self.connection.rollback()
+        self.assertRaises(DisconnectionError,
+                          self.connection.execute, "SELECT 1")

=== modified file 'tests/databases/postgres.py'
--- tests/databases/postgres.py	2007-08-07 23:15:09 +0000
+++ tests/databases/postgres.py	2007-09-17 08:17:13 +0000
@@ -28,7 +28,9 @@
 from storm.variables import ListVariable, IntVariable, Variable
 from storm.expr import Union, Select, Alias, SQLRaw, State, Sequence
 
-from tests.databases.base import DatabaseTest, UnsupportedDatabaseTest
+from tests.databases.base import (
+    DatabaseTest, DatabaseDisconnectionTest, UnsupportedDatabaseTest)
+from tests.databases.proxy import ProxyTCPServer
 from tests.helper import TestHelper, MakePath
 
 
@@ -215,3 +217,16 @@
     
     dbapi_module_names = ["psycopg2"]
     db_module_name = "postgres"
+
+
+class PostgresDisconnectionTest(DatabaseDisconnectionTest, TestHelper):
+
+    def is_supported(self):
+        return bool(os.environ.get("STORM_POSTGRES_URI"))
+
+    def create_database_and_proxy(self):
+        uri = URI(os.environ["STORM_POSTGRES_URI"])
+        self.proxy = ProxyTCPServer((uri.host or '127.0.0.1',
+                                     uri.port or 5432))
+        uri.host, uri.port = self.proxy.server_address
+        self.database = create_database(uri)

# Begin bundle
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
UkSi1KGDADp3KmxM+I9PBtn/4u5IpwoSEX7HzgA=
-- 
storm mailing list
[email protected]
Modify settings or unsubscribe at: 
https://lists.ubuntu.com/mailman/listinfo/storm

Reply via email to