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
1=== modified file 'configs/testrunner/launchpad-lazr.conf'
2--- configs/testrunner/launchpad-lazr.conf 2011-08-14 23:11:45 +0000
3+++ configs/testrunner/launchpad-lazr.conf 2011-08-31 17:28:39 +0000
4@@ -45,12 +45,12 @@
5 error_dir: /var/tmp/codehosting.test
6
7 [database]
8-rw_main_master: dbname=launchpad_ftest
9-rw_main_slave: dbname=launchpad_ftest
10+rw_main_master: dbname=launchpad_ftest host=localhost
11+rw_main_slave: dbname=launchpad_ftest host=localhost
12 # Use our _template databases here just so that we have different values from
13 # the rw_* configs.
14-ro_main_master: dbname=launchpad_ftest_template
15-ro_main_slave: dbname=launchpad_ftest_template
16+ro_main_master: dbname=launchpad_ftest_template host=localhost
17+ro_main_slave: dbname=launchpad_ftest_template host=localhost
18 randomise_select_results: true
19
20 [error_reports]
21
22=== modified file 'lib/lp/testing/fixture.py'
23--- lib/lp/testing/fixture.py 2011-07-05 15:05:21 +0000
24+++ lib/lp/testing/fixture.py 2011-08-31 17:28:39 +0000
25@@ -11,9 +11,15 @@
26 'ZopeViewReplacementFixture',
27 ]
28
29+from ConfigParser import SafeConfigParser
30+import os.path
31 from textwrap import dedent
32
33-from fixtures import Fixture
34+from fixtures import (
35+ EnvironmentVariableFixture,
36+ Fixture,
37+ )
38+import pgbouncer.fixture
39 import rabbitfixture.server
40 from zope.component import (
41 getGlobalSiteManager,
42@@ -27,6 +33,8 @@
43 undefineChecker,
44 )
45
46+from canonical.config import config
47+
48
49 class RabbitServer(rabbitfixture.server.RabbitServer):
50 """A RabbitMQ server fixture with Launchpad-specific config.
51@@ -46,6 +54,55 @@
52 """ % self.config.port)
53
54
55+class PGBouncerFixture(pgbouncer.fixture.PGBouncerFixture):
56+ """Inserts a controllable pgbouncer instance in front of PostgreSQL.
57+
58+ The pgbouncer proxy can be shutdown and restarted at will, simulating
59+ database outages and fastdowntime deployments.
60+ """
61+
62+ def __init__(self):
63+ super(PGBouncerFixture, self).__init__()
64+
65+ # Known databases
66+ from canonical.testing.layers import DatabaseLayer
67+ dbnames = [
68+ DatabaseLayer._db_fixture.dbname,
69+ DatabaseLayer._db_template_fixture.dbname,
70+ 'session_ftest',
71+ 'launchpad_empty',
72+ ]
73+ for dbname in dbnames:
74+ self.databases[dbname] = 'dbname=%s port=5432 host=localhost' % (
75+ dbname,)
76+
77+ # Known users, pulled from security.cfg
78+ security_cfg_path = os.path.join(
79+ config.root, 'database', 'schema', 'security.cfg')
80+ security_cfg_config = SafeConfigParser({})
81+ security_cfg_config.read([security_cfg_path])
82+ for section_name in security_cfg_config.sections():
83+ self.users[section_name] = 'trusted'
84+ self.users[section_name + '_ro'] = 'trusted'
85+ self.users[os.environ['USER']] = 'trusted'
86+
87+ def setUp(self):
88+ super(PGBouncerFixture, self).setUp()
89+
90+ # reconnect_store cleanup added first so it is run last, after
91+ # the environment variables have been reset.
92+ from canonical.testing.layers import reconnect_stores
93+ self.addCleanup(reconnect_stores)
94+
95+ # Abuse the PGPORT environment variable to get things connecting
96+ # via pgbouncer. Otherwise, we would need to temporarily
97+ # overwrite the database connection strings in the config.
98+ self.useFixture(EnvironmentVariableFixture('PGPORT', str(self.port)))
99+
100+ # Reset database connections so they go through pgbouncer.
101+ reconnect_stores()
102+
103+
104 class ZopeAdapterFixture(Fixture):
105 """A fixture to register and unregister an adapter."""
106
107
108=== modified file 'lib/lp/testing/pgsql.py'
109--- lib/lp/testing/pgsql.py 2011-07-01 07:11:27 +0000
110+++ lib/lp/testing/pgsql.py 2011-08-31 17:28:39 +0000
111@@ -1,4 +1,4 @@
112-# Copyright 2009 Canonical Ltd. This software is licensed under the
113+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
114 # GNU Affero General Public License version 3 (see the file LICENSE).
115
116 '''
117@@ -41,7 +41,10 @@
118 def close(self):
119 if self in PgTestSetup.connections:
120 PgTestSetup.connections.remove(self)
121- self.real_connection.close()
122+ try:
123+ self.real_connection.close()
124+ except psycopg2.InterfaceError:
125+ pass # Already closed, killed etc. Ignore.
126
127 def rollback(self, InterfaceError=psycopg2.InterfaceError):
128 # In our test suites, rollback ends up being called twice in some
129@@ -186,8 +189,8 @@
130 # available.
131 # Avoid circular imports
132 section = """[database]
133-rw_main_master: dbname=%s
134-rw_main_slave: dbname=%s
135+rw_main_master: dbname=%s host=localhost
136+rw_main_slave: dbname=%s host=localhost
137
138 """ % (self.dbname, self.dbname)
139 if BaseLayer.config_fixture is not None:
140
141=== modified file 'lib/lp/testing/tests/test_fixture.py'
142--- lib/lp/testing/tests/test_fixture.py 2011-07-05 15:05:21 +0000
143+++ lib/lp/testing/tests/test_fixture.py 2011-08-31 17:28:39 +0000
144@@ -8,6 +8,7 @@
145 from textwrap import dedent
146
147 from fixtures import EnvironmentVariableFixture
148+from storm.exceptions import DisconnectionError
149 from zope.component import (
150 adapts,
151 queryAdapter,
152@@ -17,9 +18,12 @@
153 Interface,
154 )
155
156-from canonical.testing.layers import BaseLayer
157+from canonical.launchpad.interfaces.lpstorm import IMasterStore
158+from canonical.testing.layers import BaseLayer, LaunchpadZopelessLayer
159+from lp.registry.model.person import Person
160 from lp.testing import TestCase
161 from lp.testing.fixture import (
162+ PGBouncerFixture,
163 RabbitServer,
164 ZopeAdapterFixture,
165 )
166@@ -89,3 +93,59 @@
167 self.assertIsInstance(adapter, FooToBar)
168 # The adapter is no longer registered.
169 self.assertIs(None, queryAdapter(context, IBar))
170+
171+
172+class TestPGBouncerFixture(TestCase):
173+ layer = LaunchpadZopelessLayer
174+
175+ def is_connected(self):
176+ # First rollback any existing transaction to ensure we attempt
177+ # to reconnect. We currently rollback the store explicitely
178+ # rather than call transaction.abort() due to Bug #819282.
179+ store = IMasterStore(Person)
180+ store.rollback()
181+
182+ try:
183+ store.find(Person).first()
184+ return True
185+ except DisconnectionError:
186+ return False
187+
188+ def test_stop_and_start(self):
189+ # Database is working.
190+ assert self.is_connected()
191+
192+ # And database with the fixture is working too.
193+ pgbouncer = PGBouncerFixture()
194+ with PGBouncerFixture() as pgbouncer:
195+ assert self.is_connected()
196+
197+ # pgbouncer is transparant. To confirm we are connecting via
198+ # pgbouncer, we need to shut it down and confirm our
199+ # connections are dropped.
200+ pgbouncer.stop()
201+ assert not self.is_connected()
202+
203+ # If we restart it, things should be back to normal.
204+ pgbouncer.start()
205+ assert self.is_connected()
206+
207+ # Database is still working.
208+ assert self.is_connected()
209+
210+ def test_stop_no_start(self):
211+ # Database is working.
212+ assert self.is_connected()
213+
214+ # And database with the fixture is working too.
215+ with PGBouncerFixture() as pgbouncer:
216+ assert self.is_connected()
217+
218+ # pgbouncer is transparant. To confirm we are connecting via
219+ # pgbouncer, we need to shut it down and confirm our
220+ # connections are dropped.
221+ pgbouncer.stop()
222+ assert not self.is_connected()
223+
224+ # Database is working again.
225+ assert self.is_connected()
226
227=== modified file 'setup.py'
228--- setup.py 2011-08-22 18:13:58 +0000
229+++ setup.py 2011-08-31 17:28:39 +0000
230@@ -60,6 +60,7 @@
231 'oops_datedir_repo',
232 'oops_wsgi',
233 'paramiko',
234+ 'pgbouncer',
235 'psycopg2',
236 'python-memcached',
237 'pyasn1',
238
239=== modified file 'versions.cfg'
240--- versions.cfg 2011-08-25 10:11:02 +0000
241+++ versions.cfg 2011-08-31 17:28:39 +0000
242@@ -55,6 +55,7 @@
243 paramiko = 1.7.4
244 Paste = 1.7.2
245 PasteDeploy = 1.3.3
246+pgbouncer = 0.0.2
247 plone.recipe.command = 1.1
248 psycopg2 = 2.2.2
249 pyasn1 = 0.0.9a