← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~stub/launchpad/pgbouncer-fixture into lp:launchpad

 

Stuart Bishop has proposed merging lp:~stub/launchpad/pgbouncer-fixture into lp:launchpad with lp:~stub/launchpad/disco as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~stub/launchpad/pgbouncer-fixture/+merge/73283

= Summary =

We need to test disconnection handling for fastdowntime deployments.

== Proposed fix ==

Add a pgbouncer fixture. When the fixture is installed, database connections are made via the pgbouncer proxy. pgbouncer can be started and stopped at will, duplicating what happens on staging and production during fastdowntime deployments.

== Pre-implementation notes ==

== Implementation details ==

The pgbouncer debian package needs to be added to our developer dependencies before this branch can land.

== Tests ==

== Demo and Q/A ==


= Launchpad lint =

There is old lint in pgsql.py I have not fixed.


Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/testing/pgsql.py
  lib/lp/testing/tests/test_fixture.py
  versions.cfg
  setup.py
  lib/lp/testing/fixture.py
  configs/testrunner/launchpad-lazr.conf

./lib/lp/testing/pgsql.py
      90: Line exceeds 78 characters.
     270: Line exceeds 78 characters.
     281: Line exceeds 78 characters.
     282: Line exceeds 78 characters.
     346: Line exceeds 78 characters.
     350: Line exceeds 78 characters.
      47: E261 at least two spaces before inline comment
      90: E501 line too long (80 characters)
     133: E302 expected 2 blank lines, found 0
     136: E302 expected 2 blank lines, found 1
     142: E302 expected 2 blank lines, found 1
     151: E261 at least two spaces before inline comment
     329: E225 missing whitespace around operator
     341: E225 missing whitespace around operator
     346: E501 line too long (88 characters)
     350: E501 line too long (83 characters)
     351: E225 missing whitespace around operator
     365: E261 at least two spaces before inline comment
./configs/testrunner/launchpad-lazr.conf
     140: Line exceeds 78 characters.
-- 
https://code.launchpad.net/~stub/launchpad/pgbouncer-fixture/+merge/73283
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~stub/launchpad/pgbouncer-fixture into lp:launchpad.
=== modified file 'configs/testrunner/launchpad-lazr.conf'
--- configs/testrunner/launchpad-lazr.conf	2011-08-14 23:11:45 +0000
+++ configs/testrunner/launchpad-lazr.conf	2011-08-29 19:48:27 +0000
@@ -45,12 +45,12 @@
 error_dir: /var/tmp/codehosting.test
 
 [database]
-rw_main_master: dbname=launchpad_ftest
-rw_main_slave:  dbname=launchpad_ftest
+rw_main_master: dbname=launchpad_ftest host=localhost
+rw_main_slave:  dbname=launchpad_ftest host=localhost
 # Use our _template databases here just so that we have different values from
 # the rw_* configs.
-ro_main_master: dbname=launchpad_ftest_template
-ro_main_slave:  dbname=launchpad_ftest_template
+ro_main_master: dbname=launchpad_ftest_template host=localhost
+ro_main_slave:  dbname=launchpad_ftest_template host=localhost
 randomise_select_results: true
 
 [error_reports]

=== modified file 'lib/lp/testing/fixture.py'
--- lib/lp/testing/fixture.py	2011-07-05 15:05:21 +0000
+++ lib/lp/testing/fixture.py	2011-08-29 19:48:27 +0000
@@ -11,9 +11,15 @@
     'ZopeViewReplacementFixture',
     ]
 
+from ConfigParser import SafeConfigParser
+import os.path
 from textwrap import dedent
 
-from fixtures import Fixture
+from fixtures import (
+    EnvironmentVariableFixture,
+    Fixture,
+    )
+import pgbouncer.fixture
 import rabbitfixture.server
 from zope.component import (
     getGlobalSiteManager,
@@ -27,6 +33,8 @@
     undefineChecker,
     )
 
+from canonical.config import config
+
 
 class RabbitServer(rabbitfixture.server.RabbitServer):
     """A RabbitMQ server fixture with Launchpad-specific config.
@@ -46,6 +54,55 @@
             """ % self.config.port)
 
 
+class PGBouncerFixture(pgbouncer.fixture.PGBouncerFixture):
+    def __init__(self):
+        super(PGBouncerFixture, self).__init__()
+
+        # Known databases
+        from canonical.testing.layers import DatabaseLayer
+        dbnames = [
+            DatabaseLayer._db_fixture.dbname,
+            DatabaseLayer._db_template_fixture.dbname,
+            'session_ftest',
+            'launchpad_empty',
+            ]
+        for dbname in dbnames:
+            self.databases[dbname] = 'dbname=%s port=5432 host=localhost' % (
+                dbname,)
+
+        # Known users, pulled from security.cfg
+        security_cfg_path = os.path.join(
+            config.root, 'database', 'schema', 'security.cfg')
+        security_cfg_config = SafeConfigParser({})
+        security_cfg_config.read([security_cfg_path])
+        for section_name in security_cfg_config.sections():
+            self.users[section_name] = 'trusted'
+        self.users[os.environ['USER']] = 'trusted'
+
+    def setUp(self):
+        super(PGBouncerFixture, self).setUp()
+
+        from canonical.testing.layers import reconnect_stores
+
+        # Abuse the PGPORT environment variable to get things connecting
+        # via pgbouncer. Otherwise, we would need to temporarily
+        # overwrite the database connection strings in the config.
+        pgport_fixture = EnvironmentVariableFixture('PGPORT', str(self.port))
+        pghost_fixture = EnvironmentVariableFixture('PGHOST', 'localhost')
+
+        # reconnect_store cleanup added first so it is run last, after
+        # the environment variables have been reset.
+        self.addCleanup(reconnect_stores)
+        self.addCleanup(pgport_fixture.cleanUp)
+        self.addCleanup(pghost_fixture.cleanUp)
+
+        pgport_fixture.setUp()
+        pghost_fixture.setUp()
+
+        # Reset database connections so they go through pgbouncer.
+        reconnect_stores()
+
+
 class ZopeAdapterFixture(Fixture):
     """A fixture to register and unregister an adapter."""
 

=== modified file 'lib/lp/testing/pgsql.py'
--- lib/lp/testing/pgsql.py	2011-07-01 07:11:27 +0000
+++ lib/lp/testing/pgsql.py	2011-08-29 19:48:27 +0000
@@ -41,7 +41,10 @@
     def close(self):
         if self in PgTestSetup.connections:
             PgTestSetup.connections.remove(self)
-            self.real_connection.close()
+            try:
+                self.real_connection.close()
+            except psycopg2.InterfaceError:
+                pass # Already closed, killed etc. Ignore.
 
     def rollback(self, InterfaceError=psycopg2.InterfaceError):
         # In our test suites, rollback ends up being called twice in some

=== modified file 'lib/lp/testing/tests/test_fixture.py'
--- lib/lp/testing/tests/test_fixture.py	2011-07-05 15:05:21 +0000
+++ lib/lp/testing/tests/test_fixture.py	2011-08-29 19:48:27 +0000
@@ -8,6 +8,7 @@
 from textwrap import dedent
 
 from fixtures import EnvironmentVariableFixture
+from storm.exceptions import DisconnectionError
 from zope.component import (
     adapts,
     queryAdapter,
@@ -17,9 +18,12 @@
     Interface,
     )
 
-from canonical.testing.layers import BaseLayer
+from canonical.launchpad.interfaces.lpstorm import IMasterStore
+from canonical.testing.layers import BaseLayer, LaunchpadZopelessLayer
+from lp.registry.model.person import Person
 from lp.testing import TestCase
 from lp.testing.fixture import (
+    PGBouncerFixture,
     RabbitServer,
     ZopeAdapterFixture,
     )
@@ -89,3 +93,59 @@
             self.assertIsInstance(adapter, FooToBar)
         # The adapter is no longer registered.
         self.assertIs(None, queryAdapter(context, IBar))
+
+
+class TestPGBouncerFixture(TestCase):
+    layer = LaunchpadZopelessLayer
+
+    def is_connected(self):
+        # First rollback any existing transaction to ensure we attempt
+        # to reconnect. We currently rollback the store explicitely
+        # rather than call transaction.abort() due to Bug #819282.
+        store = IMasterStore(Person)
+        store.rollback()
+
+        try:
+            store.find(Person).first()
+            return True
+        except DisconnectionError:
+            return False
+
+    def test_stop_and_start(self):
+        # Database is working.
+        assert self.is_connected()
+
+        # And database with the fixture is working too.
+        pgbouncer = PGBouncerFixture()
+        with PGBouncerFixture() as pgbouncer:
+            assert self.is_connected()
+
+            # pgbouncer is transparant. To confirm we are connecting via
+            # pgbouncer, we need to shut it down and confirm our
+            # connections are dropped.
+            pgbouncer.stop()
+            assert not self.is_connected()
+
+            # If we restart it, things should be back to normal.
+            pgbouncer.start()
+            assert self.is_connected()
+
+        # Database is still working.
+        assert self.is_connected()
+
+    def test_stop_no_start(self):
+        # Database is working.
+        assert self.is_connected()
+
+        # And database with the fixture is working too.
+        with PGBouncerFixture() as pgbouncer:
+            assert self.is_connected()
+
+            # pgbouncer is transparant. To confirm we are connecting via
+            # pgbouncer, we need to shut it down and confirm our
+            # connections are dropped.
+            pgbouncer.stop()
+            assert not self.is_connected()
+
+        # Database is still working.
+        assert self.is_connected()

=== modified file 'setup.py'
--- setup.py	2011-08-22 18:13:58 +0000
+++ setup.py	2011-08-29 19:48:27 +0000
@@ -60,6 +60,7 @@
         'oops_datedir_repo',
         'oops_wsgi',
         'paramiko',
+        'pgbouncer',
         'psycopg2',
         'python-memcached',
         'pyasn1',

=== modified file 'versions.cfg'
--- versions.cfg	2011-08-25 10:11:02 +0000
+++ versions.cfg	2011-08-29 19:48:27 +0000
@@ -55,6 +55,7 @@
 paramiko = 1.7.4
 Paste = 1.7.2
 PasteDeploy = 1.3.3
+pgbouncer = 0.0.2
 plone.recipe.command = 1.1
 psycopg2 = 2.2.2
 pyasn1 = 0.0.9a