Merge lp:~elachuni/rnr-server/app-timeout into lp:rnr-server

Proposed by Anthony Lenton
Status: Merged
Approved by: Łukasz Czyżykowski
Approved revision: 214
Merged at revision: 214
Proposed branch: lp:~elachuni/rnr-server/app-timeout
Merge into: lp:rnr-server
Diff against target: 268 lines (+140/-8)
8 files modified
django_project/config_dev/config/main.cfg (+2/-1)
src/reviewsapp/middleware/exception.py (+1/-0)
src/reviewsapp/middleware/timeouts.py (+50/-0)
src/reviewsapp/schema.py (+1/-0)
src/reviewsapp/tests/test_middleware.py (+52/-6)
src/reviewsapp/tests/test_views.py (+20/-0)
src/reviewsapp/urls.py (+2/-1)
src/reviewsapp/views/__init__.py (+12/-0)
To merge this branch: bzr merge lp:~elachuni/rnr-server/app-timeout
Reviewer Review Type Date Requested Status
Ratings and Reviews Developers Pending
Review via email: mp+98748@code.launchpad.net

Commit message

Added SoftTimeoutMiddleware to trigger an oops report before haproxy times out

Description of the change

This branch adds a "Soft Timeout" middleware to keep an oops report when a request exceeds a configurable length of time. The oops report will include the total time used by the request, and all sql queries performed.

To allow QA to test the timeout middleware I also provided a 'delay' view that can delay the response for any amount before returning.

While I was adding tests for the middleware I found that ExceptionMiddlewareTestCase hadn't been added to __all__, so those tests weren't being run at all :/ And, they failed miserably for several reasons, so I also made those pass.

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'django_project/config_dev/config/main.cfg'
2--- django_project/config_dev/config/main.cfg 2011-09-08 13:55:52 +0000
3+++ django_project/config_dev/config/main.cfg 2012-03-21 22:21:27 +0000
4@@ -52,7 +52,8 @@
5 django.template.loaders.app_directories.load_template_source
6 django.template.loaders.eggs.load_template_source
7
8-middleware_classes = django.middleware.http.ConditionalGetMiddleware
9+middleware_classes = reviewsapp.middleware.timeouts.SoftTimeoutMiddleware
10+ django.middleware.http.ConditionalGetMiddleware
11 django.middleware.cache.UpdateCacheMiddleware
12 django.middleware.common.CommonMiddleware
13 django.contrib.csrf.middleware.CsrfMiddleware
14
15=== modified file 'src/reviewsapp/middleware/exception.py'
16--- src/reviewsapp/middleware/exception.py 2011-03-30 16:32:11 +0000
17+++ src/reviewsapp/middleware/exception.py 2012-03-21 22:21:27 +0000
18@@ -104,6 +104,7 @@
19 template_debug = settings.TEMPLATE_DEBUG
20 settings.TEMPLATE_DEBUG = True
21 try:
22+ logging.warn(reporter.get_traceback_html())
23 logging.warn(traceback.format_exc())
24 for query in connection.queries:
25 logging.warn("time: %(time)s sql: %(sql)s" % query)
26
27=== added file 'src/reviewsapp/middleware/timeouts.py'
28--- src/reviewsapp/middleware/timeouts.py 1970-01-01 00:00:00 +0000
29+++ src/reviewsapp/middleware/timeouts.py 2012-03-21 22:21:27 +0000
30@@ -0,0 +1,50 @@
31+# This file is part of Software Center Ratings and Reviews
32+# Copyright (C) 2010 Canonical Ltd.
33+#
34+# This program is free software: you can redistribute it and/or modify
35+# it under the terms of the GNU Affero General Public License as
36+# published by the Free Software Foundation, either version 3 of the
37+# License, or (at your option) any later version.
38+#
39+# This program is distributed in the hope that it will be useful,
40+# but WITHOUT ANY WARRANTY; without even the implied warranty of
41+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
42+# GNU Affero General Public License for more details.
43+#
44+# You should have received a copy of the GNU Affero General Public License
45+# along with this program. If not, see <http://www.gnu.org/licenses/>.
46+
47+"""A response middleware that uses oops reports for soft timeouts."""
48+
49+from __future__ import absolute_import
50+
51+__metaclass__ = type
52+__all__ = [
53+ 'SoftTimeoutMiddleware',
54+ ]
55+
56+import logging
57+from time import time
58+
59+from django.conf import settings
60+from django.core.exceptions import MiddlewareNotUsed
61+from django.db import connection
62+
63+
64+class SoftTimeoutMiddleware(object):
65+ def __init__(self):
66+ if settings.SOFT_REQUEST_TIMEOUT == 0:
67+ raise MiddlewareNotUsed()
68+
69+ def process_request(self, request):
70+ request._start_time = time()
71+
72+ def process_response(self, request, response):
73+ if hasattr(request, '_start_time'):
74+ total = time() - request._start_time
75+ if total > settings.SOFT_REQUEST_TIMEOUT:
76+ logging.warn('Request took %.3f seconds to return' % total)
77+ for query in connection.queries:
78+ logging.warn("time: %(time)s sql: %(sql)s" % query)
79+ request.environ['dump-oops'] = 'yes'
80+ return response
81
82=== modified file 'src/reviewsapp/schema.py'
83--- src/reviewsapp/schema.py 2012-01-27 12:55:57 +0000
84+++ src/reviewsapp/schema.py 2012-03-21 22:21:27 +0000
85@@ -25,6 +25,7 @@
86 serve_static_media = BoolConfigOption()
87 extra_pythonpath = LinesConfigOption(item=StringConfigOption())
88 default_timeout = IntConfigOption(default=None)
89+ soft_request_timeout = IntConfigOption(default=20)
90
91 # openid
92 openid = ConfigSection()
93
94=== modified file 'src/reviewsapp/tests/test_middleware.py'
95--- src/reviewsapp/tests/test_middleware.py 2012-01-25 16:13:06 +0000
96+++ src/reviewsapp/tests/test_middleware.py 2012-03-21 22:21:27 +0000
97@@ -21,17 +21,25 @@
98 __metaclass__ = type
99 __all__ = [
100 'StatsLoggingTestCase',
101+ 'ExceptionMiddlewareTestCase',
102+ 'SoftTimeoutMiddlewareTestCase',
103 ]
104
105 import logging
106+import sys
107 import time
108
109+from django.http import HttpRequest
110+from django.conf import settings
111 from django.core.urlresolvers import reverse
112 from django.test import TestCase
113 from mock import patch, MagicMock, Mock
114
115 from reviewsapp.middleware.stats import StatisticsLoggingMiddleware
116-from reviewsapp.middleware.exception import LogExceptionMiddleware
117+from reviewsapp.middleware.exception import (
118+ LogExceptionMiddleware,
119+ SanitizedExceptionReporter,
120+ )
121 from reviewsapp.tests.factory import TestCaseWithFactory
122 from reviewsapp.tests.helpers import patch_settings
123
124@@ -184,6 +192,22 @@
125 self.assertFalse('CPU' in logged[0])
126
127
128+class MockRequest(HttpRequest):
129+
130+ def __init__(self, path):
131+ super(HttpRequest, self).__init__()
132+ self.path = path
133+
134+ class MockSession(dict):
135+ def flush(self):
136+ pass
137+
138+ self.session = MockSession()
139+
140+ def get_full_path(self):
141+ return self.path
142+
143+
144 class ExceptionMiddlewareTestCase(TestCase):
145
146 POST_PASSWORD = 'P1!2@3#'
147@@ -227,8 +251,8 @@
148 try:
149 x = 2 / 0
150 except:
151- reporter = self.middleware.get_reporter(self.request,
152- *sys.exc_info())
153+ reporter = SanitizedExceptionReporter(self.request,
154+ *sys.exc_info())
155 html = reporter.get_traceback_html()
156 self.assertFalse(self.PRIVATE_SETTING in html)
157 self.assertTrue(self.PUBLIC_SETTING in html)
158@@ -238,14 +262,15 @@
159 FRAME_PASSWORD = self.FRAME_VAR
160 public_variable = 2 / 0
161 except:
162- reporter = self.middleware.get_reporter(self.request,
163- *sys.exc_info())
164+ reporter = SanitizedExceptionReporter(self.request,
165+ *sys.exc_info())
166 html = reporter.get_traceback_html()
167 self.assertFalse(self.FRAME_VAR in html)
168 self.assertTrue('public_variable' in html)
169
170 def test_request_sanitization(self):
171- req = self.middleware.sanitize_request(self.request)
172+ reporter = SanitizedExceptionReporter(self.request, None, None, None)
173+ req = reporter.request
174 # Make sure sensitive data is no longer in the request
175 self.assertFalse(self.POST_PASSWORD in req.POST.values())
176 self.assertFalse(self.POST_SECRET in req.POST.values())
177@@ -263,3 +288,24 @@
178 # Make sure public data is unaffected
179 self.assertEquals(req.POST['post_public'], self.POST_PUBLIC)
180 self.assertEquals(req.GET['get_public'], self.GET_PUBLIC)
181+
182+
183+class SoftTimeoutMiddlewareTestCase(TestCase):
184+ @patch('django.test.client.RequestFactory._base_environ')
185+ def test_delay_triggers_oops_report(self, mock_environ):
186+ environ = {
187+ 'REQUEST_METHOD': 'GET',
188+ 'SERVER_NAME': 'testserver',
189+ 'SERVER_PORT': '80',
190+ }
191+
192+ def get_environ(**request):
193+ environ.update(request)
194+ return environ
195+ mock_environ.side_effect = get_environ
196+ # soft_request_timeout is an IntConfigOption, but a float works too
197+ with patch_settings(ENABLE_ARTIFICIAL_OOPS=True,
198+ SOFT_REQUEST_TIMEOUT=0.1):
199+ response = self.client.get('/reviews/delay/0.1/')
200+
201+ self.assertTrue('dump-oops' in environ)
202
203=== modified file 'src/reviewsapp/tests/test_views.py'
204--- src/reviewsapp/tests/test_views.py 2012-01-25 16:13:06 +0000
205+++ src/reviewsapp/tests/test_views.py 2012-03-21 22:21:27 +0000
206@@ -531,3 +531,23 @@
207 '/reviews/die/')
208
209 reload(reviewsapp.urls)
210+
211+ def test_delay(self):
212+ with patch_settings(ENABLE_ARTIFICIAL_OOPS=True):
213+ # We need to reload urls.py to recalculate all urls
214+ reload(reviewsapp.urls)
215+
216+ response = self.client.get('/reviews/delay/0.1/')
217+
218+ reload(reviewsapp.urls)
219+ self.assertContains(response, 'Slept for 0.1 seconds')
220+
221+ def test_delay_invalid_amount(self):
222+ with patch_settings(ENABLE_ARTIFICIAL_OOPS=True):
223+ # We need to reload urls.py to recalculate all urls
224+ reload(reviewsapp.urls)
225+
226+ response = self.client.get('/reviews/delay/0.1.2/')
227+
228+ reload(reviewsapp.urls)
229+ self.assertContains(response, 'Invalid delay amount "0.1.2"')
230
231=== modified file 'src/reviewsapp/urls.py'
232--- src/reviewsapp/urls.py 2011-04-20 16:47:48 +0000
233+++ src/reviewsapp/urls.py 2012-03-21 22:21:27 +0000
234@@ -61,5 +61,6 @@
235
236 if getattr(settings, 'ENABLE_ARTIFICIAL_OOPS', False):
237 urlpatterns += patterns('reviewsapp.views',
238- (r'^die/', 'die'),
239+ (r'^die/$', 'die'),
240+ (r'^delay/(?P<amount>[\d.]+)/$', 'delay'),
241 )
242
243=== modified file 'src/reviewsapp/views/__init__.py'
244--- src/reviewsapp/views/__init__.py 2011-04-20 16:47:48 +0000
245+++ src/reviewsapp/views/__init__.py 2012-03-21 22:21:27 +0000
246@@ -22,6 +22,9 @@
247 'die'
248 ]
249
250+from time import sleep
251+
252+from django.http import HttpResponse
253 from django.shortcuts import render_to_response
254
255 from reviewsapp.models import ReviewModeration
256@@ -45,3 +48,12 @@
257 def die(request):
258 """Just trigger an oops report"""
259 raise ZeroDivisionError()
260+
261+
262+def delay(request, amount):
263+ try:
264+ amount = float(amount)
265+ except ValueError:
266+ return HttpResponse('Invalid delay amount "%s"' % amount)
267+ sleep(amount)
268+ return HttpResponse('Slept for %s seconds' % amount)

Subscribers

People subscribed via source and target branches