launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #02875
[Merge] lp:~jameinel/launchpad/suppress-generator-exit-726985 into lp:launchpad
John A Meinel has proposed merging lp:~jameinel/launchpad/suppress-generator-exit-726985 into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
#726985 codebrowse OOPSes with GeneratorExit when connection closed early
https://bugs.launchpad.net/bugs/726985
For more details, see:
https://code.launchpad.net/~jameinel/launchpad/suppress-generator-exit-726985/+merge/52414
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.
--
https://code.launchpad.net/~jameinel/launchpad/suppress-generator-exit-726985/+merge/52414
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jameinel/launchpad/suppress-generator-exit-726985 into lp:launchpad.
=== modified file 'lib/launchpad_loggerhead/app.py'
--- lib/launchpad_loggerhead/app.py 2011-02-09 00:43:53 +0000
+++ lib/launchpad_loggerhead/app.py 2011-03-07 13:48:19 +0000
@@ -356,8 +356,16 @@
# just because the connection was closed prematurely.
logger = logging.getLogger('lp-loggerhead')
logger.info('Caught socket exception from %s: %s %s'
- % (environ.get('REMOTE_ADDR', '<unknown>'),
- e.__class__, e,))
+ % (environ.get('REMOTE_ADDR', '<unknown>'),
+ e.__class__, e,))
+ return
+ except GeneratorExit, e:
+ # This generally means a client closed early during a streaming
+ # body. Nothing to worry about. GeneratorExit doesn't usually have
+ # any context associated with it, so not worth printing to the log.
+ logger = logging.getLogger('lp-loggerhead')
+ logger.info('Caught GeneratorExit from %s'
+ % (environ.get('REMOTE_ADDR', '<unknown>')))
return
except:
error_page_sent = wrapped.generate_oops(environ, error_utility)
=== modified file 'lib/launchpad_loggerhead/tests.py'
--- lib/launchpad_loggerhead/tests.py 2011-02-23 17:26:26 +0000
+++ lib/launchpad_loggerhead/tests.py 2011-03-07 13:48:19 +0000
@@ -203,17 +203,26 @@
raise socket.error(errno.EPIPE, 'Connection closed')
return fail_write
+ def multi_yielding_app(self, environ, start_response):
+ writer = start_response('200 OK', {})
+ yield 'content\n'
+ yield 'I want\n'
+ yield 'to give to the user\n'
+
+ def _get_default_environ(self):
+ return {'wsgi.version': (1, 0),
+ 'wsgi.url_scheme': 'http',
+ 'PATH_INFO': '/test/path',
+ 'REQUEST_METHOD': 'GET',
+ 'SERVER_NAME': 'localhost',
+ 'SERVER_PORT': '8080',
+ }
+
def wrap_and_run(self, app, failing_write=False):
app = oops_middleware(app)
# Just random env data, rather than setting up a whole wsgi stack just
# to pass in values for this dict
- environ = {'wsgi.version': (1, 0),
- 'wsgi.url_scheme': 'http',
- 'PATH_INFO': '/test/path',
- 'REQUEST_METHOD': 'GET',
- 'SERVER_NAME': 'localhost',
- 'SERVER_PORT': '8080',
- }
+ environ = self._get_default_environ()
if failing_write:
result = list(app(environ, self.failing_start_response))
else:
@@ -242,6 +251,23 @@
self.assertContainsRe(self.log_stream.getvalue(),
'Caught socket exception from <unknown>:.*Connection closed')
+ def test_stopping_early_no_oops(self):
+ # See bug #726985.
+ # If content is being streamed, and the pipe closes, we'll get a
+ # 'GeneratorExit', because it is closed before finishing. This doesn't
+ # need to become an OOPS.
+ self.catchLogEvents()
+ app = oops_middleware(self.multi_yielding_app)
+ environ = self._get_default_environ()
+ result = app(environ, self.noop_start_response)
+ self.assertEqual('content\n', result.next())
+ # At this point, we intentionally kill the app and the response, so
+ # that they will get GeneratorExit
+ del app, result
+ self.assertEqual([], self.oopses)
+ self.assertContainsRe(self.log_stream.getvalue(),
+ 'Caught GeneratorExit from <unknown>')
+
def test_suite():
return unittest.TestLoader().loadTestsFromName(__name__)