Merge lp:~jameinel/launchpad/suppress-generator-exit-726985 into lp:launchpad

Proposed by John A Meinel
Status: Merged
Approved by: Benji York
Approved revision: no longer in the source branch.
Merged at revision: 12556
Proposed branch: lp:~jameinel/launchpad/suppress-generator-exit-726985
Merge into: lp:launchpad
Diff against target: 84 lines (+43/-9)
2 files modified
lib/launchpad_loggerhead/app.py (+10/-2)
lib/launchpad_loggerhead/tests.py (+33/-7)
To merge this branch: bzr merge lp:~jameinel/launchpad/suppress-generator-exit-726985
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+52414@code.launchpad.net

Commit message

[r=benji] [bug=726985] Suppress bogus OOPS entries when clients disconnect early on streamed pages.

Description of the change

Adding another kind of exception for Loggerhead to treat as a non-OOPS.

There are only a few pages that do streamed bodies, but when one of those is stopped early, we get a GeneratorExit exception (when the generator loses its last reference.)

The test for this requires CPython's refcounting semantics (throw a GeneratorExit exception as soon as we call 'del app'). I'm pretty sure this is safe for Launchpad, but I figured I would mention it.

GeneratorExit itself never really cares any other context, so I don't think we could say that there is any reason to decide we really do want an oops here, rather than just always suppressing it. If we get 1 more of these, we should probably unify their logging, etc a bit. But for right now, 2 didn't seem to imply a trend.

Note that I was unable to actually reproduce the failure with custom "GET ../download/" and stopping early. So if we want, we can put this off until we actually see this being the #1 OOPS failure. Though according to wgrant, it was.

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

This branch looks good.

> The test for this requires CPython's refcounting semantics (throw a
> GeneratorExit exception as soon as we call 'del app'). I'm pretty sure
> this is safe for Launchpad, but I figured I would mention it.

Yeah, I agree that it's safe, especially because the failure mode would
result in a test failing (always good) while the app would continue to
work correctly.

> Note that I was unable to actually reproduce the failure with custom
> "GET ../download/" and stopping early. So if we want, we can put this
> off until we actually see this being the #1 OOPS failure. Though
> according to wgrant, it was.

I am a little concerned about not being unable to trigger the behavior
we're trying to remedy "for real". This may indicate that we're not
really fixing the problem, but the change as given appears to be a pure
win, so there's no reason not to do it.

review: Approve (benji)
Revision history for this message
Benji York (benji) :
review: Approve (code)

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-02-09 00:43:53 +0000
3+++ lib/launchpad_loggerhead/app.py 2011-03-07 13:48:19 +0000
4@@ -356,8 +356,16 @@
5 # just because the connection was closed prematurely.
6 logger = logging.getLogger('lp-loggerhead')
7 logger.info('Caught socket exception from %s: %s %s'
8- % (environ.get('REMOTE_ADDR', '<unknown>'),
9- e.__class__, e,))
10+ % (environ.get('REMOTE_ADDR', '<unknown>'),
11+ e.__class__, e,))
12+ return
13+ except GeneratorExit, e:
14+ # This generally means a client closed early during a streaming
15+ # body. Nothing to worry about. GeneratorExit doesn't usually have
16+ # any context associated with it, so not worth printing to the log.
17+ logger = logging.getLogger('lp-loggerhead')
18+ logger.info('Caught GeneratorExit from %s'
19+ % (environ.get('REMOTE_ADDR', '<unknown>')))
20 return
21 except:
22 error_page_sent = wrapped.generate_oops(environ, error_utility)
23
24=== modified file 'lib/launchpad_loggerhead/tests.py'
25--- lib/launchpad_loggerhead/tests.py 2011-02-23 17:26:26 +0000
26+++ lib/launchpad_loggerhead/tests.py 2011-03-07 13:48:19 +0000
27@@ -203,17 +203,26 @@
28 raise socket.error(errno.EPIPE, 'Connection closed')
29 return fail_write
30
31+ def multi_yielding_app(self, environ, start_response):
32+ writer = start_response('200 OK', {})
33+ yield 'content\n'
34+ yield 'I want\n'
35+ yield 'to give to the user\n'
36+
37+ def _get_default_environ(self):
38+ return {'wsgi.version': (1, 0),
39+ 'wsgi.url_scheme': 'http',
40+ 'PATH_INFO': '/test/path',
41+ 'REQUEST_METHOD': 'GET',
42+ 'SERVER_NAME': 'localhost',
43+ 'SERVER_PORT': '8080',
44+ }
45+
46 def wrap_and_run(self, app, failing_write=False):
47 app = oops_middleware(app)
48 # Just random env data, rather than setting up a whole wsgi stack just
49 # to pass in values for this dict
50- environ = {'wsgi.version': (1, 0),
51- 'wsgi.url_scheme': 'http',
52- 'PATH_INFO': '/test/path',
53- 'REQUEST_METHOD': 'GET',
54- 'SERVER_NAME': 'localhost',
55- 'SERVER_PORT': '8080',
56- }
57+ environ = self._get_default_environ()
58 if failing_write:
59 result = list(app(environ, self.failing_start_response))
60 else:
61@@ -242,6 +251,23 @@
62 self.assertContainsRe(self.log_stream.getvalue(),
63 'Caught socket exception from <unknown>:.*Connection closed')
64
65+ def test_stopping_early_no_oops(self):
66+ # See bug #726985.
67+ # If content is being streamed, and the pipe closes, we'll get a
68+ # 'GeneratorExit', because it is closed before finishing. This doesn't
69+ # need to become an OOPS.
70+ self.catchLogEvents()
71+ app = oops_middleware(self.multi_yielding_app)
72+ environ = self._get_default_environ()
73+ result = app(environ, self.noop_start_response)
74+ self.assertEqual('content\n', result.next())
75+ # At this point, we intentionally kill the app and the response, so
76+ # that they will get GeneratorExit
77+ del app, result
78+ self.assertEqual([], self.oopses)
79+ self.assertContainsRe(self.log_stream.getvalue(),
80+ 'Caught GeneratorExit from <unknown>')
81+
82
83 def test_suite():
84 return unittest.TestLoader().loadTestsFromName(__name__)