Merge lp:~stub/launchpad/branch-rewrite into lp:launchpad
- branch-rewrite
- Merge into devel
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Stuart Bishop | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 13863 | ||||
Proposed branch: | lp:~stub/launchpad/branch-rewrite | ||||
Merge into: | lp:launchpad | ||||
Prerequisite: | lp:~stub/launchpad/pgbouncer-fixture-noca | ||||
Diff against target: |
284 lines (+132/-12) 6 files modified
lib/canonical/config/tests/test_database_config.py (+2/-1) lib/canonical/launchpad/doc/canonical-config.txt (+2/-1) lib/lp/codehosting/tests/test_rewrite.py (+89/-7) lib/lp/testing/__init__.py (+24/-0) lib/lp/testing/tests/test_pgsql.py (+1/-1) scripts/branch-rewrite.py (+14/-2) |
||||
To merge this branch: | bzr merge lp:~stub/launchpad/branch-rewrite | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jeroen T. Vermeulen (community) | Approve | ||
Review via email: mp+73563@code.launchpad.net |
This proposal supersedes a proposal from 2011-08-29.
Commit message
[r=jtv][bug=836662] Make branch-rewrite.py survive database outages
Description of the change
= Summary =
branch-rewrite.py does not reconnect after a database outage.
== Proposed fix ==
Make it reconnect after a database outage.
Jeroen T. Vermeulen (jtv) wrote : Posted in a previous version of this proposal | # |
Stuart Bishop (stub) wrote : Posted in a previous version of this proposal | # |
On Wed, Aug 31, 2011 at 10:26 AM, Jeroen T. Vermeulen <email address hidden> wrote:
> === modified file 'lib/lp/
> +class TestBranchRewri
> + """Ensure branch-rewrite.py survives fastdowntime deploys."""
> + layer = LaunchpadScript
> +
> + def setUp(self):
> + super(TestBranc
> + self.pgbouncer = PGBouncerFixture()
> + self.addCleanup
> + self.pgbouncer.
>
> Couldn't those last three lines be replaced with a simple
>
> self.pgbouncer = self.useFixture
>
> ?
>
> In fact I'm not even sure it's worth a setUp with the super() dance, and carrying self.pgbouncer from setUp to the tests.
Yes, useFixture is nicer here. I've removed setUp() entirely and am
using useFixture in the tests.
> + def spawn(self):
> + script_file = os.path.join(
> + config.root, 'scripts', 'branch-
> +
> + self.rewriter_proc = subprocess.Popen(
> + [script_file], stdin=subproces
> + stderr=
>
> Again, is it worth keeping self.rewriter_proc on the test class? It just hides state. Why not make it a return value?
If I don't hide state, I have to maintain state and pass it to the
request() helper :-)
> Moreover, is this process guaranteed to clean itself up? If not, then please ensure that it is. For instance, you could add something like "self.addCleanu
subprocess module handles this for us as soon as things go out of
scope. But it doesn't hurt to be explicit so I've added the cleanup.
> + def request(self, query):
> + self.rewriter_
> + return self.rewriter_
>
> Do we know that the readline() will never hang under reasonable circumstances?
> + def test_disconnect
> + self.spawn()
>
> Sure "disconnect" is something that happens in this test, but is that one word really a good description of what you're testing here? Would it help to say something with "reconnects"?
I've change the test name to test_reconnects
> + # Everything should be working, and we get valid output.
> + out = self.request('foo')
> + assert out.endswith(
>
> Why use assert here? Normally you'd say self.assertEnds
Because I'm always forgetting which assert methods are approved,
standard, testutils extensions, LP extensions :-)
Fixed, along with the other occurrences.
> + self.pgbouncer.
> +
> + # Now with pgbouncer down, we should get NULL messages and
> + # stderr spam, and this keeps happening. We test more than
> + # once to ensure that we will keep trying to reconnect even
> + # after several failures.
> + for count in range(5):
> + out = self.request('foo')
>
> Is this race-free? I don't know if pgbouncer is a separate process or not; if it is, does stop() block until it's taken effect...
Stuart Bishop (stub) wrote : Posted in a previous version of this proposal | # |
readline() blocking shouldn't be a problem, but I've implemented a nonblocking readline helper and made use of it.
Jeroen T. Vermeulen (jtv) wrote : Posted in a previous version of this proposal | # |
I don't know enough to be of much help with the raise-or-swallow issue. What the best choice is there may depend on what goes on higher up in the call tree. If any kind of transactional integrity is expected across the failure, then the exception probably needs to be re-raised.
Jeroen T. Vermeulen (jtv) wrote : | # |
Thanks for taking the trouble.
A tip for nonblocking_
* deadline = start + timeout
* “now < start - timeout” becomes “now < deadline”
* “timeout - (now - start)” becomes “deadline - now”
(Both in their way involve computing the time remaining, but I'm not sure that making that explicit as well makes things any better.)
Jeroen
Preview Diff
1 | === modified file 'lib/canonical/config/tests/test_database_config.py' | |||
2 | --- lib/canonical/config/tests/test_database_config.py 2010-10-17 02:38:59 +0000 | |||
3 | +++ lib/canonical/config/tests/test_database_config.py 2011-09-03 05:39:35 +0000 | |||
4 | @@ -28,7 +28,8 @@ | |||
5 | 28 | self.assertEquals('librarian', config.librarian.dbuser) | 28 | self.assertEquals('librarian', config.librarian.dbuser) |
6 | 29 | 29 | ||
7 | 30 | dbconfig.setConfigSection('librarian') | 30 | dbconfig.setConfigSection('librarian') |
9 | 31 | expected_db = 'dbname=%s' % DatabaseLayer._db_fixture.dbname | 31 | expected_db = ( |
10 | 32 | 'dbname=%s host=localhost' % DatabaseLayer._db_fixture.dbname) | ||
11 | 32 | self.assertEquals(expected_db, dbconfig.rw_main_master) | 33 | self.assertEquals(expected_db, dbconfig.rw_main_master) |
12 | 33 | self.assertEquals('librarian', dbconfig.dbuser) | 34 | self.assertEquals('librarian', dbconfig.dbuser) |
13 | 34 | 35 | ||
14 | 35 | 36 | ||
15 | === modified file 'lib/canonical/launchpad/doc/canonical-config.txt' | |||
16 | --- lib/canonical/launchpad/doc/canonical-config.txt 2010-12-22 14:50:08 +0000 | |||
17 | +++ lib/canonical/launchpad/doc/canonical-config.txt 2011-09-03 05:39:35 +0000 | |||
18 | @@ -15,7 +15,8 @@ | |||
19 | 15 | 15 | ||
20 | 16 | >>> from canonical.config import config | 16 | >>> from canonical.config import config |
21 | 17 | >>> from canonical.testing.layers import DatabaseLayer | 17 | >>> from canonical.testing.layers import DatabaseLayer |
23 | 18 | >>> expected = 'dbname=%s' % DatabaseLayer._db_fixture.dbname | 18 | >>> expected = ( |
24 | 19 | ... 'dbname=%s host=localhost' % DatabaseLayer._db_fixture.dbname) | ||
25 | 19 | >>> expected == config.database.rw_main_master | 20 | >>> expected == config.database.rw_main_master |
26 | 20 | True | 21 | True |
27 | 21 | >>> config.database.db_statement_timeout is None | 22 | >>> config.database.db_statement_timeout is None |
28 | 22 | 23 | ||
29 | === modified file 'lib/lp/codehosting/tests/test_rewrite.py' | |||
30 | --- lib/lp/codehosting/tests/test_rewrite.py 2011-08-12 11:37:08 +0000 | |||
31 | +++ lib/lp/codehosting/tests/test_rewrite.py 2011-09-03 05:39:35 +0000 | |||
32 | @@ -1,4 +1,4 @@ | |||
34 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
35 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
36 | 3 | 3 | ||
37 | 4 | """Tests for the dynamic RewriteMap used to serve branches over HTTP.""" | 4 | """Tests for the dynamic RewriteMap used to serve branches over HTTP.""" |
38 | @@ -14,15 +14,21 @@ | |||
39 | 14 | from zope.security.proxy import removeSecurityProxy | 14 | from zope.security.proxy import removeSecurityProxy |
40 | 15 | 15 | ||
41 | 16 | from canonical.config import config | 16 | from canonical.config import config |
43 | 17 | from canonical.testing.layers import DatabaseFunctionalLayer | 17 | from canonical.testing.layers import ( |
44 | 18 | DatabaseFunctionalLayer, | ||
45 | 19 | DatabaseLayer, | ||
46 | 20 | ) | ||
47 | 18 | from lp.code.interfaces.codehosting import branch_id_alias | 21 | from lp.code.interfaces.codehosting import branch_id_alias |
48 | 19 | from lp.codehosting.rewrite import BranchRewriter | 22 | from lp.codehosting.rewrite import BranchRewriter |
49 | 20 | from lp.codehosting.vfs import branch_id_to_path | 23 | from lp.codehosting.vfs import branch_id_to_path |
50 | 21 | from lp.services.log.logger import BufferLogger | 24 | from lp.services.log.logger import BufferLogger |
51 | 22 | from lp.testing import ( | 25 | from lp.testing import ( |
52 | 23 | FakeTime, | 26 | FakeTime, |
53 | 27 | nonblocking_readline, | ||
54 | 28 | TestCase, | ||
55 | 24 | TestCaseWithFactory, | 29 | TestCaseWithFactory, |
56 | 25 | ) | 30 | ) |
57 | 31 | from lp.testing.fixture import PGBouncerFixture | ||
58 | 26 | 32 | ||
59 | 27 | 33 | ||
60 | 28 | class TestBranchRewriter(TestCaseWithFactory): | 34 | class TestBranchRewriter(TestCaseWithFactory): |
61 | @@ -177,7 +183,8 @@ | |||
62 | 177 | transaction.commit() | 183 | transaction.commit() |
63 | 178 | rewriter.rewriteLine('/' + branch.unique_name + '/.bzr/README') | 184 | rewriter.rewriteLine('/' + branch.unique_name + '/.bzr/README') |
64 | 179 | rewriter.rewriteLine('/' + branch.unique_name + '/.bzr/README') | 185 | rewriter.rewriteLine('/' + branch.unique_name + '/.bzr/README') |
66 | 180 | logging_output_lines = self.getLoggerOutput(rewriter).strip().split('\n') | 186 | logging_output_lines = self.getLoggerOutput( |
67 | 187 | rewriter).strip().split('\n') | ||
68 | 181 | self.assertEqual(2, len(logging_output_lines)) | 188 | self.assertEqual(2, len(logging_output_lines)) |
69 | 182 | self.assertIsNot( | 189 | self.assertIsNot( |
70 | 183 | None, | 190 | None, |
71 | @@ -194,7 +201,8 @@ | |||
72 | 194 | self.fake_time.advance( | 201 | self.fake_time.advance( |
73 | 195 | config.codehosting.branch_rewrite_cache_lifetime + 1) | 202 | config.codehosting.branch_rewrite_cache_lifetime + 1) |
74 | 196 | rewriter.rewriteLine('/' + branch.unique_name + '/.bzr/README') | 203 | rewriter.rewriteLine('/' + branch.unique_name + '/.bzr/README') |
76 | 197 | logging_output_lines = self.getLoggerOutput(rewriter).strip().split('\n') | 204 | logging_output_lines = self.getLoggerOutput( |
77 | 205 | rewriter).strip().split('\n') | ||
78 | 198 | self.assertEqual(2, len(logging_output_lines)) | 206 | self.assertEqual(2, len(logging_output_lines)) |
79 | 199 | self.assertIsNot( | 207 | self.assertIsNot( |
80 | 200 | None, | 208 | None, |
81 | @@ -246,7 +254,8 @@ | |||
82 | 246 | # buffering, write a complete line of output. | 254 | # buffering, write a complete line of output. |
83 | 247 | for input_line in input_lines: | 255 | for input_line in input_lines: |
84 | 248 | proc.stdin.write(input_line + '\n') | 256 | proc.stdin.write(input_line + '\n') |
86 | 249 | output_lines.append(proc.stdout.readline().rstrip('\n')) | 257 | output_lines.append( |
87 | 258 | nonblocking_readline(proc.stdout, 60).rstrip('\n')) | ||
88 | 250 | # If we create a new branch after the branch-rewrite.py script has | 259 | # If we create a new branch after the branch-rewrite.py script has |
89 | 251 | # connected to the database, or edit a branch name that has already | 260 | # connected to the database, or edit a branch name that has already |
90 | 252 | # been rewritten, both are rewritten successfully. | 261 | # been rewritten, both are rewritten successfully. |
91 | @@ -260,17 +269,90 @@ | |||
92 | 260 | 'file:///var/tmp/bazaar.launchpad.dev/mirrors/%s/.bzr/README' | 269 | 'file:///var/tmp/bazaar.launchpad.dev/mirrors/%s/.bzr/README' |
93 | 261 | % branch_id_to_path(new_branch.id)) | 270 | % branch_id_to_path(new_branch.id)) |
94 | 262 | proc.stdin.write(new_branch_input + '\n') | 271 | proc.stdin.write(new_branch_input + '\n') |
96 | 263 | output_lines.append(proc.stdout.readline().rstrip('\n')) | 272 | output_lines.append( |
97 | 273 | nonblocking_readline(proc.stdout, 60).rstrip('\n')) | ||
98 | 264 | 274 | ||
99 | 265 | edited_branch_input = '/%s/.bzr/README' % edited_branch.unique_name | 275 | edited_branch_input = '/%s/.bzr/README' % edited_branch.unique_name |
100 | 266 | expected_lines.append( | 276 | expected_lines.append( |
101 | 267 | 'file:///var/tmp/bazaar.launchpad.dev/mirrors/%s/.bzr/README' | 277 | 'file:///var/tmp/bazaar.launchpad.dev/mirrors/%s/.bzr/README' |
102 | 268 | % branch_id_to_path(edited_branch.id)) | 278 | % branch_id_to_path(edited_branch.id)) |
103 | 269 | proc.stdin.write(edited_branch_input + '\n') | 279 | proc.stdin.write(edited_branch_input + '\n') |
105 | 270 | output_lines.append(proc.stdout.readline().rstrip('\n')) | 280 | output_lines.append( |
106 | 281 | nonblocking_readline(proc.stdout, 60).rstrip('\n')) | ||
107 | 271 | 282 | ||
108 | 272 | os.kill(proc.pid, signal.SIGINT) | 283 | os.kill(proc.pid, signal.SIGINT) |
109 | 273 | err = proc.stderr.read() | 284 | err = proc.stderr.read() |
110 | 274 | # The script produces logging output, but not to stderr. | 285 | # The script produces logging output, but not to stderr. |
111 | 275 | self.assertEqual('', err) | 286 | self.assertEqual('', err) |
112 | 276 | self.assertEqual(expected_lines, output_lines) | 287 | self.assertEqual(expected_lines, output_lines) |
113 | 288 | |||
114 | 289 | |||
115 | 290 | class TestBranchRewriterScriptHandlesDisconnects(TestCase): | ||
116 | 291 | """Ensure branch-rewrite.py survives fastdowntime deploys.""" | ||
117 | 292 | layer = DatabaseLayer | ||
118 | 293 | |||
119 | 294 | def spawn(self): | ||
120 | 295 | script_file = os.path.join( | ||
121 | 296 | config.root, 'scripts', 'branch-rewrite.py') | ||
122 | 297 | |||
123 | 298 | self.rewriter_proc = subprocess.Popen( | ||
124 | 299 | [script_file], stdin=subprocess.PIPE, stdout=subprocess.PIPE, | ||
125 | 300 | stderr=subprocess.PIPE, bufsize=0) | ||
126 | 301 | |||
127 | 302 | self.addCleanup(self.rewriter_proc.terminate) | ||
128 | 303 | |||
129 | 304 | def request(self, query): | ||
130 | 305 | self.rewriter_proc.stdin.write(query + '\n') | ||
131 | 306 | self.rewriter_proc.stdin.flush() | ||
132 | 307 | |||
133 | 308 | # 60 second timeout as we might need to wait for the script to | ||
134 | 309 | # finish starting up. | ||
135 | 310 | result = nonblocking_readline(self.rewriter_proc.stdout, 60) | ||
136 | 311 | |||
137 | 312 | if result.endswith('\n'): | ||
138 | 313 | return result[:-1] | ||
139 | 314 | self.fail( | ||
140 | 315 | "Incomplete line or no result retrieved from subprocess: %s" | ||
141 | 316 | % repr(result.getvalue())) | ||
142 | 317 | |||
143 | 318 | def test_reconnects_when_disconnected(self): | ||
144 | 319 | pgbouncer = self.useFixture(PGBouncerFixture()) | ||
145 | 320 | |||
146 | 321 | self.spawn() | ||
147 | 322 | |||
148 | 323 | # Everything should be working, and we get valid output. | ||
149 | 324 | out = self.request('foo') | ||
150 | 325 | self.assertEndsWith(out, '/foo') | ||
151 | 326 | |||
152 | 327 | pgbouncer.stop() | ||
153 | 328 | |||
154 | 329 | # Now with pgbouncer down, we should get NULL messages and | ||
155 | 330 | # stderr spam, and this keeps happening. We test more than | ||
156 | 331 | # once to ensure that we will keep trying to reconnect even | ||
157 | 332 | # after several failures. | ||
158 | 333 | for count in range(5): | ||
159 | 334 | out = self.request('foo') | ||
160 | 335 | self.assertEqual(out, 'NULL') | ||
161 | 336 | |||
162 | 337 | pgbouncer.start() | ||
163 | 338 | |||
164 | 339 | # Everything should be working, and we get valid output. | ||
165 | 340 | out = self.request('foo') | ||
166 | 341 | self.assertEndsWith(out, '/foo') | ||
167 | 342 | |||
168 | 343 | def test_starts_with_db_down(self): | ||
169 | 344 | pgbouncer = self.useFixture(PGBouncerFixture()) | ||
170 | 345 | |||
171 | 346 | # Start with the database down. | ||
172 | 347 | pgbouncer.stop() | ||
173 | 348 | |||
174 | 349 | self.spawn() | ||
175 | 350 | |||
176 | 351 | for count in range(5): | ||
177 | 352 | out = self.request('foo') | ||
178 | 353 | self.assertEqual(out, 'NULL') | ||
179 | 354 | |||
180 | 355 | pgbouncer.start() | ||
181 | 356 | |||
182 | 357 | out = self.request('foo') | ||
183 | 358 | self.assertEndsWith(out, '/foo') | ||
184 | 277 | 359 | ||
185 | === modified file 'lib/lp/testing/__init__.py' | |||
186 | --- lib/lp/testing/__init__.py 2011-08-19 18:20:58 +0000 | |||
187 | +++ lib/lp/testing/__init__.py 2011-09-03 05:39:35 +0000 | |||
188 | @@ -29,6 +29,7 @@ | |||
189 | 29 | 'logout', | 29 | 'logout', |
190 | 30 | 'map_branch_contents', | 30 | 'map_branch_contents', |
191 | 31 | 'normalize_whitespace', | 31 | 'normalize_whitespace', |
192 | 32 | 'nonblocking_readline', | ||
193 | 32 | 'oauth_access_token_for', | 33 | 'oauth_access_token_for', |
194 | 33 | 'person_logged_in', | 34 | 'person_logged_in', |
195 | 34 | 'quote_jquery_expression', | 35 | 'quote_jquery_expression', |
196 | @@ -69,6 +70,7 @@ | |||
197 | 69 | import os | 70 | import os |
198 | 70 | from pprint import pformat | 71 | from pprint import pformat |
199 | 71 | import re | 72 | import re |
200 | 73 | from select import select | ||
201 | 72 | import shutil | 74 | import shutil |
202 | 73 | import subprocess | 75 | import subprocess |
203 | 74 | import sys | 76 | import sys |
204 | @@ -1325,3 +1327,25 @@ | |||
205 | 1325 | def extract_lp_cache(text): | 1327 | def extract_lp_cache(text): |
206 | 1326 | match = re.search(r'<script>LP.cache = (\{.*\});</script>', text) | 1328 | match = re.search(r'<script>LP.cache = (\{.*\});</script>', text) |
207 | 1327 | return simplejson.loads(match.group(1)) | 1329 | return simplejson.loads(match.group(1)) |
208 | 1330 | |||
209 | 1331 | |||
210 | 1332 | def nonblocking_readline(instream, timeout): | ||
211 | 1333 | """Non-blocking readline. | ||
212 | 1334 | |||
213 | 1335 | Files must provide a valid fileno() method. This is a test helper | ||
214 | 1336 | as it is inefficient and unlikely useful for production code. | ||
215 | 1337 | """ | ||
216 | 1338 | result = StringIO() | ||
217 | 1339 | start = now = time.time() | ||
218 | 1340 | deadline = start + timeout | ||
219 | 1341 | while (now < deadline and not result.getvalue().endswith('\n')): | ||
220 | 1342 | rlist = select([instream], [], [], deadline - now) | ||
221 | 1343 | if rlist: | ||
222 | 1344 | # Reading 1 character at a time is inefficient, but means | ||
223 | 1345 | # we don't need to implement put-back. | ||
224 | 1346 | next_char = os.read(instream.fileno(), 1) | ||
225 | 1347 | if next_char == "": | ||
226 | 1348 | break # EOF | ||
227 | 1349 | result.write(next_char) | ||
228 | 1350 | now = time.time() | ||
229 | 1351 | return result.getvalue() | ||
230 | 1328 | 1352 | ||
231 | === modified file 'lib/lp/testing/tests/test_pgsql.py' | |||
232 | --- lib/lp/testing/tests/test_pgsql.py 2011-02-19 13:50:19 +0000 | |||
233 | +++ lib/lp/testing/tests/test_pgsql.py 2011-09-03 05:39:35 +0000 | |||
234 | @@ -53,7 +53,7 @@ | |||
235 | 53 | fixture.setUp() | 53 | fixture.setUp() |
236 | 54 | self.addCleanup(fixture.dropDb) | 54 | self.addCleanup(fixture.dropDb) |
237 | 55 | self.addCleanup(fixture.tearDown) | 55 | self.addCleanup(fixture.tearDown) |
239 | 56 | expected_value = 'dbname=%s' % fixture.dbname | 56 | expected_value = 'dbname=%s host=localhost' % fixture.dbname |
240 | 57 | self.assertEqual(expected_value, dbconfig.rw_main_master) | 57 | self.assertEqual(expected_value, dbconfig.rw_main_master) |
241 | 58 | self.assertEqual(expected_value, dbconfig.rw_main_slave) | 58 | self.assertEqual(expected_value, dbconfig.rw_main_slave) |
242 | 59 | with ConfigUseFixture(BaseLayer.appserver_config_name): | 59 | with ConfigUseFixture(BaseLayer.appserver_config_name): |
243 | 60 | 60 | ||
244 | === modified file 'scripts/branch-rewrite.py' | |||
245 | --- scripts/branch-rewrite.py 2011-09-02 10:50:34 +0000 | |||
246 | +++ scripts/branch-rewrite.py 2011-09-03 05:39:35 +0000 | |||
247 | @@ -1,6 +1,6 @@ | |||
248 | 1 | #!/usr/bin/python -uS | 1 | #!/usr/bin/python -uS |
249 | 2 | # | 2 | # |
251 | 3 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 3 | # Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
252 | 4 | # GNU Affero General Public License version 3 (see the file LICENSE). | 4 | # GNU Affero General Public License version 3 (see the file LICENSE). |
253 | 5 | 5 | ||
254 | 6 | # pylint: disable-msg=W0403 | 6 | # pylint: disable-msg=W0403 |
255 | @@ -19,6 +19,8 @@ | |||
256 | 19 | 19 | ||
257 | 20 | from canonical.database.sqlbase import ISOLATION_LEVEL_AUTOCOMMIT | 20 | from canonical.database.sqlbase import ISOLATION_LEVEL_AUTOCOMMIT |
258 | 21 | from canonical.config import config | 21 | from canonical.config import config |
259 | 22 | from canonical.launchpad.interfaces.lpstorm import ISlaveStore | ||
260 | 23 | from lp.code.model.branch import Branch | ||
261 | 22 | from lp.codehosting.rewrite import BranchRewriter | 24 | from lp.codehosting.rewrite import BranchRewriter |
262 | 23 | from lp.services.log.loglevels import INFO, WARNING | 25 | from lp.services.log.loglevels import INFO, WARNING |
263 | 24 | from lp.services.scripts.base import LaunchpadScript | 26 | from lp.services.scripts.base import LaunchpadScript |
264 | @@ -60,9 +62,19 @@ | |||
265 | 60 | return | 62 | return |
266 | 61 | except KeyboardInterrupt: | 63 | except KeyboardInterrupt: |
267 | 62 | sys.exit() | 64 | sys.exit() |
269 | 63 | except: | 65 | except Exception: |
270 | 64 | self.logger.exception('Exception occurred:') | 66 | self.logger.exception('Exception occurred:') |
271 | 65 | print "NULL" | 67 | print "NULL" |
272 | 68 | # The exception might have been a DisconnectionError or | ||
273 | 69 | # similar. Cleanup such as database reconnection will | ||
274 | 70 | # not happen until the transaction is rolled back. | ||
275 | 71 | # XXX StuartBishop 2011-08-31 bug=819282: We are | ||
276 | 72 | # explicitly rolling back the store here as a workaround | ||
277 | 73 | # instead of using transaction.abort() | ||
278 | 74 | try: | ||
279 | 75 | ISlaveStore(Branch).rollback() | ||
280 | 76 | except Exception: | ||
281 | 77 | self.logger.exception('Exception occurred in rollback:') | ||
282 | 66 | 78 | ||
283 | 67 | 79 | ||
284 | 68 | if __name__ == '__main__': | 80 | if __name__ == '__main__': |
Hi Stuart,
Generally good, but I have a few things that look worth fixing.
=== modified file 'lib/lp/ codehosting/ tests/test_ rewrite. py' codehosting/ tests/test_ rewrite. py 2011-08-12 11:37:08 +0000 codehosting/ tests/test_ rewrite. py 2011-08-29 20:15:59 +0000
--- lib/lp/
+++ lib/lp/
@@ -177,7 +181,8 @@
transaction. commit( )
rewriter. rewriteLine( '/' + branch.unique_name + '/.bzr/README')
rewriter. rewriteLine( '/' + branch.unique_name + '/.bzr/README') output_ lines = self.getLoggerO utput(rewriter) .strip( ).split( '\n') output_ lines = self.getLoggerO utput( .strip( ).split( '\n')
- logging_
+ logging_
+ rewriter)
Cleaning out lint, I see. Thanks.
@@ -274,3 +280,62 @@
self. assertEqual( '', err)
self. assertEqual( expected_ lines, output_lines) terScriptHandle sDisconnects( TestCaseWithFac tory): Layer hRewriterScript HandlesDisconne cts, self).setUp() (self.pgbouncer .cleanUp) setUp()
# The script produces logging output, but not to stderr.
+
+
+class TestBranchRewri
+ """Ensure branch-rewrite.py survives fastdowntime deploys."""
+ layer = LaunchpadScript
+
+ def setUp(self):
+ super(TestBranc
+ self.pgbouncer = PGBouncerFixture()
+ self.addCleanup
+ self.pgbouncer.
Couldn't those last three lines be replaced with a simple
self.pgbouncer = self.useFixture (PGBouncerFixtu re())
?
In fact I'm not even sure it's worth a setUp with the super() dance, and carrying self.pgbouncer from setUp to the tests.
+ def spawn(self): rewrite. py') s.PIPE, stdout= subprocess. PIPE, subprocess. PIPE, bufsize=0)
+ script_file = os.path.join(
+ config.root, 'scripts', 'branch-
+
+ self.rewriter_proc = subprocess.Popen(
+ [script_file], stdin=subproces
+ stderr=
Again, is it worth keeping self.rewriter_proc on the test class? It just hides state. Why not make it a return value?
Moreover, is this process guaranteed to clean itself up? If not, then please ensure that it is. For instance, you could add something like "self.addCleanu p(kill, rewriter_proc)" to spawn().
+ def request(self, query): proc.stdin. write(query + '\n') proc.stdout. readline( ).rstrip( '\n')
+ self.rewriter_
+ return self.rewriter_
Do we know that the readline() will never hang under reasonable circumstances?
+ def test_disconnect (self):
+ self.spawn()
Sure "disconnect" is something that happens in this test, but is that one word really a good description of what you're testing here? Would it help to say something with "reconnects"?
+ # Everything should be working, and we get valid output. '/foo') , out
+ out = self.request('foo')
+ assert out.endswith(
Why use assert here? Normally you'd say self.assertEnds With(out, '/foo').
+ self.pgbouncer. stop()
+
+ # Now with pgbouncer down, we should get NULL messages and
+ # stderr spam, and this keeps happening. We test more than
+ # once to ensure that we will keep trying to reconnect even
+ # after several failures.
+ for count in range(5):
+ out = self.request('foo')
Is this race-free? I don't know ...