Merge lp:~jameinel/launchpad/loggerhead-732481 into lp:launchpad

Proposed by John A Meinel
Status: Merged
Approved by: Robert Collins
Approved revision: no longer in the source branch.
Merged at revision: 12583
Proposed branch: lp:~jameinel/launchpad/loggerhead-732481
Merge into: lp:launchpad
Diff against target: 183 lines (+56/-16)
2 files modified
lib/launchpad_loggerhead/app.py (+13/-9)
lib/launchpad_loggerhead/tests.py (+43/-7)
To merge this branch: bzr merge lp:~jameinel/launchpad/loggerhead-732481
Reviewer Review Type Date Requested Status
Robert Collins (community) Approve
Review via email: mp+52902@code.launchpad.net

Commit message

[r=lifeless][bug=732481] [bug=732496] oops_middleware should call start_response for success, even if there is no body.

Description of the change

This fixes bug #732481. We had a logic snafu. The oops_middleware proxied the 'start_response' call until actual content was written. So that if a bug triggered between the time 'start_response' was called, and when actual content was written, it would have a chance to write its own oops response.

However, it would then only call start_response once it actually started writing content. But now that HEAD requests weren't writing any content, it wasn't actually calling start_response. The WSGI layer would then see that the app returned successfully and would start trying to write the empty-string, but would die because start_response was never called.

Long term, we really could use some black-box tests that actually 'start_loggerhead' and then make a couple requests to it. Probably 1 request on every major page, which would have helped prevent a silly regression like this. But I don't have a good feeling for how to set up a test like that.

I did make sure to do manual "HEAD" requests against a page. Which showed both that it used to fail, and that it is now fixed. As did the 'test_no_body_calls_start_response'

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/launchpad_loggerhead/app.py'
2--- lib/launchpad_loggerhead/app.py 2011-03-07 13:08:14 +0000
3+++ lib/launchpad_loggerhead/app.py 2011-03-10 18:44:14 +0000
4@@ -295,12 +295,15 @@
5 self.response_start += (exc_info,)
6 return self.write_wrapper
7
8- def really_start(self):
9+ def ensure_started(self):
10+ if not self.body_started and self.response_start is not None:
11+ self._really_start()
12+
13+ def _really_start(self):
14 self._write_callable = self._real_start_response(*self.response_start)
15
16 def write_wrapper(self, data):
17- if not self.body_started:
18- self.really_start()
19+ self.ensure_started()
20 self._write_callable(data)
21
22 def generate_oops(self, environ, error_utility):
23@@ -312,7 +315,7 @@
24 oopsid = report_oops(environ, error_utility)
25 if self.body_started:
26 return False
27- write = self._real_start_response(_error_status, _error_headers)
28+ write = self.start_response(_error_status, _error_headers)
29 write(_oops_html_template % {'oopsid': oopsid})
30 return True
31
32@@ -341,14 +344,15 @@
33 # Start processing this request, build the app
34 app_iter = iter(app(environ, wrapped.start_response))
35 # Start yielding the response
36- while True:
37+ stopping = False
38+ while not stopping:
39 try:
40 data = app_iter.next()
41 except StopIteration:
42- return
43- if not wrapped.body_started:
44- wrapped.really_start()
45- yield data
46+ stopping = True
47+ wrapped.ensure_started()
48+ if not stopping:
49+ yield data
50 except httpserver.SocketErrors, e:
51 # The Paste WSGIHandler suppresses these exceptions.
52 # Generally it means something like 'EPIPE' because the
53
54=== modified file 'lib/launchpad_loggerhead/tests.py'
55--- lib/launchpad_loggerhead/tests.py 2011-03-07 13:08:14 +0000
56+++ lib/launchpad_loggerhead/tests.py 2011-03-10 18:44:14 +0000
57@@ -21,7 +21,11 @@
58 from canonical.launchpad.webapp.errorlog import ErrorReport, ErrorReportEvent
59 from canonical.launchpad.webapp.vhosts import allvhosts
60 from canonical.testing.layers import DatabaseFunctionalLayer
61-from launchpad_loggerhead.app import RootApp, oops_middleware
62+from launchpad_loggerhead.app import (
63+ _oops_html_template,
64+ oops_middleware,
65+ RootApp,
66+ )
67 from launchpad_loggerhead.session import SessionHandler
68 from lp.testing import TestCase
69
70@@ -152,6 +156,10 @@
71
72 class TestOopsMiddleware(TestCase):
73
74+ def setUp(self):
75+ super(TestOopsMiddleware, self).setUp()
76+ self.start_response_called = False
77+
78 def assertContainsRe(self, haystack, needle_re, flags=0):
79 """Assert that a contains something matching a regular expression."""
80 # There is: self.assertTextMatchesExpressionIgnoreWhitespace
81@@ -188,10 +196,12 @@
82 yield None
83 raise socket.error(errno.EPIPE, 'Connection closed')
84
85- def noop_start_response(self, status, response_headers, exc_info=None):
86- def noop_write(chunk):
87- pass
88- return noop_write
89+ def logging_start_response(self, status, response_headers, exc_info=None):
90+ self._response_chunks = []
91+ def _write(chunk):
92+ self._response_chunks.append(chunk)
93+ self.start_response_called = True
94+ return _write
95
96 def success_app(self, environ, start_response):
97 writer = start_response('200 OK', {})
98@@ -201,6 +211,7 @@
99 def failing_start_response(self, status, response_headers, exc_info=None):
100 def fail_write(chunk):
101 raise socket.error(errno.EPIPE, 'Connection closed')
102+ self.start_response_called = True
103 return fail_write
104
105 def multi_yielding_app(self, environ, start_response):
106@@ -209,6 +220,10 @@
107 yield 'I want\n'
108 yield 'to give to the user\n'
109
110+ def no_body_app(self, environ, start_response):
111+ writer = start_response('200 OK', {})
112+ return []
113+
114 def _get_default_environ(self):
115 return {'wsgi.version': (1, 0),
116 'wsgi.url_scheme': 'http',
117@@ -226,7 +241,7 @@
118 if failing_write:
119 result = list(app(environ, self.failing_start_response))
120 else:
121- result = list(app(environ, self.noop_start_response))
122+ result = list(app(environ, self.logging_start_response))
123 return result
124
125 def test_exception_triggers_oops(self):
126@@ -236,6 +251,11 @@
127 self.assertEqual(1, len(self.oopses))
128 oops = self.oopses[0]
129 self.assertEqual('RuntimeError', oops.type)
130+ # runtime_failing_app doesn't call start_response, but oops_middleware
131+ # does because it tries to send the OOPS information to the user.
132+ self.assertTrue(self.start_response_called)
133+ self.assertEqual(_oops_html_template % {'oopsid': oops.id},
134+ ''.join(self._response_chunks))
135
136 def test_ignores_socket_exceptions(self):
137 self.catchLogEvents()
138@@ -243,6 +263,9 @@
139 self.assertEqual(0, len(self.oopses))
140 self.assertContainsRe(self.log_stream.getvalue(),
141 'Caught socket exception from <unknown>:.*Connection closed')
142+ # start_response doesn't get called because the app fails first,
143+ # and oops_middleware knows not to do anything with a closed socket.
144+ self.assertFalse(self.start_response_called)
145
146 def test_ignores_writer_failures(self):
147 self.catchLogEvents()
148@@ -250,6 +273,8 @@
149 self.assertEqual(0, len(self.oopses))
150 self.assertContainsRe(self.log_stream.getvalue(),
151 'Caught socket exception from <unknown>:.*Connection closed')
152+ # success_app calls start_response, so this should get passed on.
153+ self.assertTrue(self.start_response_called)
154
155 def test_stopping_early_no_oops(self):
156 # See bug #726985.
157@@ -259,7 +284,7 @@
158 self.catchLogEvents()
159 app = oops_middleware(self.multi_yielding_app)
160 environ = self._get_default_environ()
161- result = app(environ, self.noop_start_response)
162+ result = app(environ, self.logging_start_response)
163 self.assertEqual('content\n', result.next())
164 # At this point, we intentionally kill the app and the response, so
165 # that they will get GeneratorExit
166@@ -267,6 +292,17 @@
167 self.assertEqual([], self.oopses)
168 self.assertContainsRe(self.log_stream.getvalue(),
169 'Caught GeneratorExit from <unknown>')
170+ # Body content was yielded, we must have called start_response
171+ self.assertTrue(self.start_response_called)
172+
173+ def test_no_body_calls_start_response(self):
174+ # See bug #732481, even if we don't have a body, if we have headers to
175+ # send, we must call start_response
176+ result = self.wrap_and_run(self.no_body_app)
177+ self.assertEqual([], result)
178+ self.assertTrue(self.start_response_called)
179+ # Output content is empty because of no_body_app
180+ self.assertEqual('', ''.join(self._response_chunks))
181
182
183 def test_suite():