launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #05881
[Merge] lp:~lifeless/python-oops-wsgi/timeout into lp:python-oops-wsgi
Robert Collins has proposed merging lp:~lifeless/python-oops-wsgi/timeout into lp:python-oops-wsgi.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~lifeless/python-oops-wsgi/timeout/+merge/85056
This will let us get slow-performance data out of bazaar.launchpad.net.
--
https://code.launchpad.net/~lifeless/python-oops-wsgi/timeout/+merge/85056
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~lifeless/python-oops-wsgi/timeout into lp:python-oops-wsgi.
=== modified file 'NEWS'
--- NEWS 2011-12-09 03:24:22 +0000
+++ NEWS 2011-12-09 04:09:23 +0000
@@ -9,6 +9,10 @@
* Added a Django helper to ease integration in older non-WSGI compatible Django
versions. (James Henstridge, Robert Collins)
+* Passing ``soft_start_timeout`` to ``make_app`` allows the creation of OOPS
+ reports when the wrapped application is slow to start its response.
+ (Robert Collins)
+
0.0.7
-----
=== modified file 'oops_wsgi/middleware.py'
--- oops_wsgi/middleware.py 2011-11-13 21:57:57 +0000
+++ oops_wsgi/middleware.py 2011-12-09 04:09:23 +0000
@@ -19,6 +19,7 @@
import socket
import sys
+import time
from paste.request import construct_url
@@ -46,9 +47,13 @@
}
+class SoftRequestTimeout(Exception):
+ """Soft request timeout expired"""
+
+
def make_app(app, config, template=default_error_template,
content_type='text/html', error_render=None, oops_on_status=None,
- map_environ=None, tracker=None):
+ map_environ=None, tracker=None, soft_start_timeout=None):
"""Construct a middleware around app that will forward errors via config.
Any errors encountered by the app will be forwarded to config and an error
@@ -86,6 +91,13 @@
See generator_tracker for the reference tracker used in regular WSGI
environments. generator_tracker is used by default or when
tracker=None.
+ :param soft_start_timeout: A duration in milliseconds for the creation of
+ reports on slow requests. If this is set and the duration between
+ calling into the app and start_response being called is greater than
+ the timeout value, then an OOPS will be created and the OOPS id added
+ to the response HTTP headers as normal. A backtrace leading into the
+ middleware is generated (this can be informative as start_response is
+ a callback) and the exception type is set to SoftRequestTimeout.
:return: A WSGI app.
"""
def oops_middleware(environ, start_response):
@@ -100,6 +112,8 @@
"""
environ['oops.report'] = {}
environ['oops.context'] = {}
+ if soft_start_timeout:
+ start_time = time.time()
state = {}
def make_context(exc_info=None):
context = dict(url=construct_url(environ), wsgi_environ=environ)
@@ -125,9 +139,11 @@
if exc_info is not None:
# The app is explicitly signalling an error (rather than
# returning a page describing the error). Capture that and then
- # forward to the containing element verbatim. In future we might
- # choose to add the OOPS id to the headers (but we can't fiddle
- # the body as it is being generated by the contained app.
+ # forward to the containing element untouched except for the
+ # addition of the X-Oops-Id header. We don't touch the body
+ # because the application is handling the error and generating
+ # the body itself. We may in future provide an option to
+ # replace the body in this situation.
report = config.create(make_context(exc_info=exc_info))
ids = config.publish(report)
try:
@@ -139,12 +155,24 @@
finally:
del exc_info
else:
+ do_oops = False
if oops_on_status:
for sniff_status in oops_on_status:
if status.startswith(sniff_status):
- report = config.create(make_context())
- report['HTTP_STATUS'] = status.split(' ')[0]
- config.publish(report)
+ do_oops = True
+ if (soft_start_timeout and
+ (time.time()-start_time)*1000 > soft_start_timeout):
+ try:
+ raise SoftRequestTimeout(
+ "Start_response over timeout %s."
+ % soft_start_timeout)
+ except SoftRequestTimeout:
+ exc_info = sys.exc_info()
+ do_oops = True
+ if do_oops:
+ report = config.create(make_context(exc_info=exc_info))
+ report['HTTP_STATUS'] = status.split(' ')[0]
+ config.publish(report)
state['response'] = (status, headers)
return oops_write
try:
=== modified file 'oops_wsgi/tests/test_middleware.py'
--- oops_wsgi/tests/test_middleware.py 2011-11-13 21:57:57 +0000
+++ oops_wsgi/tests/test_middleware.py 2011-12-09 04:09:23 +0000
@@ -21,6 +21,7 @@
import socket
import sys
from textwrap import dedent
+import time
from oops import (
Config,
@@ -57,6 +58,8 @@
self.checkkeys = checkkeys or {}
def match(self, report):
+ if type(report) is not dict:
+ return Mismatch("Report is not a dict: '%s'" % (report,))
sentinel = object()
mismatches = []
for key, value in self.checkkeys.items():
@@ -448,6 +451,8 @@
self.assertEqual('bar', self.calls[0]['foo'])
def test_sniff_404_not_published_does_not_error(self):
+ # If the OOPS is not published, the start_response just passes through
+ # as though we were not sniffing responses.
def inner(environ, start_response):
start_response('404 MISSING', [])
yield 'pretty 404'
@@ -507,6 +512,39 @@
app = make_app(myapp, Config(), tracker=mytracker)
self.assertEqual('tracker', app({}, lambda status, headers:None))
+ def test_soft_start_timeout_below_threshold(self):
+ # When a request responds below the timeout, no OOPS is made.
+ def myapp(environ, start_response):
+ start_response('200 OK', [])
+ yield 'success'
+ config = self.config_for_oopsing()
+ body = self.wrap_and_run(
+ myapp, config, params=dict(soft_start_timeout=1000))
+ self.assertEqual('success', body)
+ expected_start_response = ('200 OK', [])
+ self.assertThat(self.calls, MatchesListwise([
+ Equals(expected_start_response),
+ ]))
+
+ def test_soft_start_timeout_above_threshold(self):
+ # When a request responds above the timeout, an OOPS is made with a
+ # SoftTimeout exception and backtrace.
+ def myapp(environ, start_response):
+ time.sleep(0.002)
+ start_response('200 OK', [])
+ yield 'success'
+ config = self.config_for_oopsing()
+ body = self.wrap_and_run(
+ myapp, config, params=dict(soft_start_timeout=1))
+ self.assertEqual('success', body)
+ expected_start_response = ('200 OK', [])
+ self.assertThat(self.calls, MatchesListwise([
+ # An OOPS is logged with the observed response code.
+ MatchesOOPS({'type': 'SoftRequestTimeout'}),
+ Equals(expected_start_response),
+ ]))
+
+
class TestGeneratorTracker(TestCase):