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

Proposed by Stuart Bishop
Status: Merged
Approved by: William Grant
Approved revision: no longer in the source branch.
Merged at revision: 13863
Proposed branch: lp:~stub/launchpad/pgbouncer-fixture
Merge into: lp:launchpad
Prerequisite: lp:~stub/launchpad/disco
Diff against target: 249 lines (+132/-10)
6 files modified
configs/testrunner/launchpad-lazr.conf (+4/-4)
lib/lp/testing/fixture.py (+58/-1)
lib/lp/testing/pgsql.py (+7/-4)
lib/lp/testing/tests/test_fixture.py (+61/-1)
setup.py (+1/-0)
versions.cfg (+1/-0)
To merge this branch: bzr merge lp:~stub/launchpad/pgbouncer-fixture
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+73283@code.launchpad.net

Description of the change

= 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.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)
Revision history for this message
Robert Collins (lifeless) wrote :

One tweak:
+ 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()

->
+ # reconnect_store cleanup added first so it is run last, after
+ # the environment variables have been reset.
+ self.addCleanup(reconnect_stores)
+ self.useFixture(EnvironmentVariableFixture('PGPORT', str(self.port)))
+ self.useFixture(EnvironmentVariableFixture('PGHOST', 'localhost'))

:)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== 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-31 17:28:39 +0000
@@ -45,12 +45,12 @@
45error_dir: /var/tmp/codehosting.test45error_dir: /var/tmp/codehosting.test
4646
47[database]47[database]
48rw_main_master: dbname=launchpad_ftest48rw_main_master: dbname=launchpad_ftest host=localhost
49rw_main_slave: dbname=launchpad_ftest49rw_main_slave: dbname=launchpad_ftest host=localhost
50# Use our _template databases here just so that we have different values from50# Use our _template databases here just so that we have different values from
51# the rw_* configs.51# the rw_* configs.
52ro_main_master: dbname=launchpad_ftest_template52ro_main_master: dbname=launchpad_ftest_template host=localhost
53ro_main_slave: dbname=launchpad_ftest_template53ro_main_slave: dbname=launchpad_ftest_template host=localhost
54randomise_select_results: true54randomise_select_results: true
5555
56[error_reports]56[error_reports]
5757
=== 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-31 17:28:39 +0000
@@ -11,9 +11,15 @@
11 'ZopeViewReplacementFixture',11 'ZopeViewReplacementFixture',
12 ]12 ]
1313
14from ConfigParser import SafeConfigParser
15import os.path
14from textwrap import dedent16from textwrap import dedent
1517
16from fixtures import Fixture18from fixtures import (
19 EnvironmentVariableFixture,
20 Fixture,
21 )
22import pgbouncer.fixture
17import rabbitfixture.server23import rabbitfixture.server
18from zope.component import (24from zope.component import (
19 getGlobalSiteManager,25 getGlobalSiteManager,
@@ -27,6 +33,8 @@
27 undefineChecker,33 undefineChecker,
28 )34 )
2935
36from canonical.config import config
37
3038
31class RabbitServer(rabbitfixture.server.RabbitServer):39class RabbitServer(rabbitfixture.server.RabbitServer):
32 """A RabbitMQ server fixture with Launchpad-specific config.40 """A RabbitMQ server fixture with Launchpad-specific config.
@@ -46,6 +54,55 @@
46 """ % self.config.port)54 """ % self.config.port)
4755
4856
57class PGBouncerFixture(pgbouncer.fixture.PGBouncerFixture):
58 """Inserts a controllable pgbouncer instance in front of PostgreSQL.
59
60 The pgbouncer proxy can be shutdown and restarted at will, simulating
61 database outages and fastdowntime deployments.
62 """
63
64 def __init__(self):
65 super(PGBouncerFixture, self).__init__()
66
67 # Known databases
68 from canonical.testing.layers import DatabaseLayer
69 dbnames = [
70 DatabaseLayer._db_fixture.dbname,
71 DatabaseLayer._db_template_fixture.dbname,
72 'session_ftest',
73 'launchpad_empty',
74 ]
75 for dbname in dbnames:
76 self.databases[dbname] = 'dbname=%s port=5432 host=localhost' % (
77 dbname,)
78
79 # Known users, pulled from security.cfg
80 security_cfg_path = os.path.join(
81 config.root, 'database', 'schema', 'security.cfg')
82 security_cfg_config = SafeConfigParser({})
83 security_cfg_config.read([security_cfg_path])
84 for section_name in security_cfg_config.sections():
85 self.users[section_name] = 'trusted'
86 self.users[section_name + '_ro'] = 'trusted'
87 self.users[os.environ['USER']] = 'trusted'
88
89 def setUp(self):
90 super(PGBouncerFixture, self).setUp()
91
92 # reconnect_store cleanup added first so it is run last, after
93 # the environment variables have been reset.
94 from canonical.testing.layers import reconnect_stores
95 self.addCleanup(reconnect_stores)
96
97 # Abuse the PGPORT environment variable to get things connecting
98 # via pgbouncer. Otherwise, we would need to temporarily
99 # overwrite the database connection strings in the config.
100 self.useFixture(EnvironmentVariableFixture('PGPORT', str(self.port)))
101
102 # Reset database connections so they go through pgbouncer.
103 reconnect_stores()
104
105
49class ZopeAdapterFixture(Fixture):106class ZopeAdapterFixture(Fixture):
50 """A fixture to register and unregister an adapter."""107 """A fixture to register and unregister an adapter."""
51108
52109
=== 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-31 17:28:39 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4'''4'''
@@ -41,7 +41,10 @@
41 def close(self):41 def close(self):
42 if self in PgTestSetup.connections:42 if self in PgTestSetup.connections:
43 PgTestSetup.connections.remove(self)43 PgTestSetup.connections.remove(self)
44 self.real_connection.close()44 try:
45 self.real_connection.close()
46 except psycopg2.InterfaceError:
47 pass # Already closed, killed etc. Ignore.
4548
46 def rollback(self, InterfaceError=psycopg2.InterfaceError):49 def rollback(self, InterfaceError=psycopg2.InterfaceError):
47 # In our test suites, rollback ends up being called twice in some50 # In our test suites, rollback ends up being called twice in some
@@ -186,8 +189,8 @@
186 # available.189 # available.
187 # Avoid circular imports190 # Avoid circular imports
188 section = """[database]191 section = """[database]
189rw_main_master: dbname=%s192rw_main_master: dbname=%s host=localhost
190rw_main_slave: dbname=%s193rw_main_slave: dbname=%s host=localhost
191194
192""" % (self.dbname, self.dbname)195""" % (self.dbname, self.dbname)
193 if BaseLayer.config_fixture is not None:196 if BaseLayer.config_fixture is not None:
194197
=== 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-31 17:28:39 +0000
@@ -8,6 +8,7 @@
8from textwrap import dedent8from textwrap import dedent
99
10from fixtures import EnvironmentVariableFixture10from fixtures import EnvironmentVariableFixture
11from storm.exceptions import DisconnectionError
11from zope.component import (12from zope.component import (
12 adapts,13 adapts,
13 queryAdapter,14 queryAdapter,
@@ -17,9 +18,12 @@
17 Interface,18 Interface,
18 )19 )
1920
20from canonical.testing.layers import BaseLayer21from canonical.launchpad.interfaces.lpstorm import IMasterStore
22from canonical.testing.layers import BaseLayer, LaunchpadZopelessLayer
23from lp.registry.model.person import Person
21from lp.testing import TestCase24from lp.testing import TestCase
22from lp.testing.fixture import (25from lp.testing.fixture import (
26 PGBouncerFixture,
23 RabbitServer,27 RabbitServer,
24 ZopeAdapterFixture,28 ZopeAdapterFixture,
25 )29 )
@@ -89,3 +93,59 @@
89 self.assertIsInstance(adapter, FooToBar)93 self.assertIsInstance(adapter, FooToBar)
90 # The adapter is no longer registered.94 # The adapter is no longer registered.
91 self.assertIs(None, queryAdapter(context, IBar))95 self.assertIs(None, queryAdapter(context, IBar))
96
97
98class TestPGBouncerFixture(TestCase):
99 layer = LaunchpadZopelessLayer
100
101 def is_connected(self):
102 # First rollback any existing transaction to ensure we attempt
103 # to reconnect. We currently rollback the store explicitely
104 # rather than call transaction.abort() due to Bug #819282.
105 store = IMasterStore(Person)
106 store.rollback()
107
108 try:
109 store.find(Person).first()
110 return True
111 except DisconnectionError:
112 return False
113
114 def test_stop_and_start(self):
115 # Database is working.
116 assert self.is_connected()
117
118 # And database with the fixture is working too.
119 pgbouncer = PGBouncerFixture()
120 with PGBouncerFixture() as pgbouncer:
121 assert self.is_connected()
122
123 # pgbouncer is transparant. To confirm we are connecting via
124 # pgbouncer, we need to shut it down and confirm our
125 # connections are dropped.
126 pgbouncer.stop()
127 assert not self.is_connected()
128
129 # If we restart it, things should be back to normal.
130 pgbouncer.start()
131 assert self.is_connected()
132
133 # Database is still working.
134 assert self.is_connected()
135
136 def test_stop_no_start(self):
137 # Database is working.
138 assert self.is_connected()
139
140 # And database with the fixture is working too.
141 with PGBouncerFixture() as pgbouncer:
142 assert self.is_connected()
143
144 # pgbouncer is transparant. To confirm we are connecting via
145 # pgbouncer, we need to shut it down and confirm our
146 # connections are dropped.
147 pgbouncer.stop()
148 assert not self.is_connected()
149
150 # Database is working again.
151 assert self.is_connected()
92152
=== modified file 'setup.py'
--- setup.py 2011-08-22 18:13:58 +0000
+++ setup.py 2011-08-31 17:28:39 +0000
@@ -60,6 +60,7 @@
60 'oops_datedir_repo',60 'oops_datedir_repo',
61 'oops_wsgi',61 'oops_wsgi',
62 'paramiko',62 'paramiko',
63 'pgbouncer',
63 'psycopg2',64 'psycopg2',
64 'python-memcached',65 'python-memcached',
65 'pyasn1',66 'pyasn1',
6667
=== modified file 'versions.cfg'
--- versions.cfg 2011-08-25 10:11:02 +0000
+++ versions.cfg 2011-08-31 17:28:39 +0000
@@ -55,6 +55,7 @@
55paramiko = 1.7.455paramiko = 1.7.4
56Paste = 1.7.256Paste = 1.7.2
57PasteDeploy = 1.3.357PasteDeploy = 1.3.3
58pgbouncer = 0.0.2
58plone.recipe.command = 1.159plone.recipe.command = 1.1
59psycopg2 = 2.2.260psycopg2 = 2.2.2
60pyasn1 = 0.0.9a61pyasn1 = 0.0.9a