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
=== modified file 'django_project/config_dev/config/main.cfg'
--- django_project/config_dev/config/main.cfg 2011-09-08 13:55:52 +0000
+++ django_project/config_dev/config/main.cfg 2012-03-21 22:21:27 +0000
@@ -52,7 +52,8 @@
52 django.template.loaders.app_directories.load_template_source52 django.template.loaders.app_directories.load_template_source
53 django.template.loaders.eggs.load_template_source53 django.template.loaders.eggs.load_template_source
5454
55middleware_classes = django.middleware.http.ConditionalGetMiddleware55middleware_classes = reviewsapp.middleware.timeouts.SoftTimeoutMiddleware
56 django.middleware.http.ConditionalGetMiddleware
56 django.middleware.cache.UpdateCacheMiddleware57 django.middleware.cache.UpdateCacheMiddleware
57 django.middleware.common.CommonMiddleware58 django.middleware.common.CommonMiddleware
58 django.contrib.csrf.middleware.CsrfMiddleware59 django.contrib.csrf.middleware.CsrfMiddleware
5960
=== modified file 'src/reviewsapp/middleware/exception.py'
--- src/reviewsapp/middleware/exception.py 2011-03-30 16:32:11 +0000
+++ src/reviewsapp/middleware/exception.py 2012-03-21 22:21:27 +0000
@@ -104,6 +104,7 @@
104 template_debug = settings.TEMPLATE_DEBUG104 template_debug = settings.TEMPLATE_DEBUG
105 settings.TEMPLATE_DEBUG = True105 settings.TEMPLATE_DEBUG = True
106 try:106 try:
107 logging.warn(reporter.get_traceback_html())
107 logging.warn(traceback.format_exc())108 logging.warn(traceback.format_exc())
108 for query in connection.queries:109 for query in connection.queries:
109 logging.warn("time: %(time)s sql: %(sql)s" % query)110 logging.warn("time: %(time)s sql: %(sql)s" % query)
110111
=== added file 'src/reviewsapp/middleware/timeouts.py'
--- src/reviewsapp/middleware/timeouts.py 1970-01-01 00:00:00 +0000
+++ src/reviewsapp/middleware/timeouts.py 2012-03-21 22:21:27 +0000
@@ -0,0 +1,50 @@
1# This file is part of Software Center Ratings and Reviews
2# Copyright (C) 2010 Canonical Ltd.
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU Affero General Public License as
6# published by the Free Software Foundation, either version 3 of the
7# License, or (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17"""A response middleware that uses oops reports for soft timeouts."""
18
19from __future__ import absolute_import
20
21__metaclass__ = type
22__all__ = [
23 'SoftTimeoutMiddleware',
24 ]
25
26import logging
27from time import time
28
29from django.conf import settings
30from django.core.exceptions import MiddlewareNotUsed
31from django.db import connection
32
33
34class SoftTimeoutMiddleware(object):
35 def __init__(self):
36 if settings.SOFT_REQUEST_TIMEOUT == 0:
37 raise MiddlewareNotUsed()
38
39 def process_request(self, request):
40 request._start_time = time()
41
42 def process_response(self, request, response):
43 if hasattr(request, '_start_time'):
44 total = time() - request._start_time
45 if total > settings.SOFT_REQUEST_TIMEOUT:
46 logging.warn('Request took %.3f seconds to return' % total)
47 for query in connection.queries:
48 logging.warn("time: %(time)s sql: %(sql)s" % query)
49 request.environ['dump-oops'] = 'yes'
50 return response
051
=== modified file 'src/reviewsapp/schema.py'
--- src/reviewsapp/schema.py 2012-01-27 12:55:57 +0000
+++ src/reviewsapp/schema.py 2012-03-21 22:21:27 +0000
@@ -25,6 +25,7 @@
25 serve_static_media = BoolConfigOption()25 serve_static_media = BoolConfigOption()
26 extra_pythonpath = LinesConfigOption(item=StringConfigOption())26 extra_pythonpath = LinesConfigOption(item=StringConfigOption())
27 default_timeout = IntConfigOption(default=None)27 default_timeout = IntConfigOption(default=None)
28 soft_request_timeout = IntConfigOption(default=20)
2829
29 # openid30 # openid
30 openid = ConfigSection()31 openid = ConfigSection()
3132
=== modified file 'src/reviewsapp/tests/test_middleware.py'
--- src/reviewsapp/tests/test_middleware.py 2012-01-25 16:13:06 +0000
+++ src/reviewsapp/tests/test_middleware.py 2012-03-21 22:21:27 +0000
@@ -21,17 +21,25 @@
21__metaclass__ = type21__metaclass__ = type
22__all__ = [22__all__ = [
23 'StatsLoggingTestCase',23 'StatsLoggingTestCase',
24 'ExceptionMiddlewareTestCase',
25 'SoftTimeoutMiddlewareTestCase',
24 ]26 ]
2527
26import logging28import logging
29import sys
27import time30import time
2831
32from django.http import HttpRequest
33from django.conf import settings
29from django.core.urlresolvers import reverse34from django.core.urlresolvers import reverse
30from django.test import TestCase35from django.test import TestCase
31from mock import patch, MagicMock, Mock36from mock import patch, MagicMock, Mock
3237
33from reviewsapp.middleware.stats import StatisticsLoggingMiddleware38from reviewsapp.middleware.stats import StatisticsLoggingMiddleware
34from reviewsapp.middleware.exception import LogExceptionMiddleware39from reviewsapp.middleware.exception import (
40 LogExceptionMiddleware,
41 SanitizedExceptionReporter,
42 )
35from reviewsapp.tests.factory import TestCaseWithFactory43from reviewsapp.tests.factory import TestCaseWithFactory
36from reviewsapp.tests.helpers import patch_settings44from reviewsapp.tests.helpers import patch_settings
3745
@@ -184,6 +192,22 @@
184 self.assertFalse('CPU' in logged[0])192 self.assertFalse('CPU' in logged[0])
185193
186194
195class MockRequest(HttpRequest):
196
197 def __init__(self, path):
198 super(HttpRequest, self).__init__()
199 self.path = path
200
201 class MockSession(dict):
202 def flush(self):
203 pass
204
205 self.session = MockSession()
206
207 def get_full_path(self):
208 return self.path
209
210
187class ExceptionMiddlewareTestCase(TestCase):211class ExceptionMiddlewareTestCase(TestCase):
188212
189 POST_PASSWORD = 'P1!2@3#'213 POST_PASSWORD = 'P1!2@3#'
@@ -227,8 +251,8 @@
227 try:251 try:
228 x = 2 / 0252 x = 2 / 0
229 except:253 except:
230 reporter = self.middleware.get_reporter(self.request,254 reporter = SanitizedExceptionReporter(self.request,
231 *sys.exc_info())255 *sys.exc_info())
232 html = reporter.get_traceback_html()256 html = reporter.get_traceback_html()
233 self.assertFalse(self.PRIVATE_SETTING in html)257 self.assertFalse(self.PRIVATE_SETTING in html)
234 self.assertTrue(self.PUBLIC_SETTING in html)258 self.assertTrue(self.PUBLIC_SETTING in html)
@@ -238,14 +262,15 @@
238 FRAME_PASSWORD = self.FRAME_VAR262 FRAME_PASSWORD = self.FRAME_VAR
239 public_variable = 2 / 0263 public_variable = 2 / 0
240 except:264 except:
241 reporter = self.middleware.get_reporter(self.request,265 reporter = SanitizedExceptionReporter(self.request,
242 *sys.exc_info())266 *sys.exc_info())
243 html = reporter.get_traceback_html()267 html = reporter.get_traceback_html()
244 self.assertFalse(self.FRAME_VAR in html)268 self.assertFalse(self.FRAME_VAR in html)
245 self.assertTrue('public_variable' in html)269 self.assertTrue('public_variable' in html)
246270
247 def test_request_sanitization(self):271 def test_request_sanitization(self):
248 req = self.middleware.sanitize_request(self.request)272 reporter = SanitizedExceptionReporter(self.request, None, None, None)
273 req = reporter.request
249 # Make sure sensitive data is no longer in the request274 # Make sure sensitive data is no longer in the request
250 self.assertFalse(self.POST_PASSWORD in req.POST.values())275 self.assertFalse(self.POST_PASSWORD in req.POST.values())
251 self.assertFalse(self.POST_SECRET in req.POST.values())276 self.assertFalse(self.POST_SECRET in req.POST.values())
@@ -263,3 +288,24 @@
263 # Make sure public data is unaffected288 # Make sure public data is unaffected
264 self.assertEquals(req.POST['post_public'], self.POST_PUBLIC)289 self.assertEquals(req.POST['post_public'], self.POST_PUBLIC)
265 self.assertEquals(req.GET['get_public'], self.GET_PUBLIC)290 self.assertEquals(req.GET['get_public'], self.GET_PUBLIC)
291
292
293class SoftTimeoutMiddlewareTestCase(TestCase):
294 @patch('django.test.client.RequestFactory._base_environ')
295 def test_delay_triggers_oops_report(self, mock_environ):
296 environ = {
297 'REQUEST_METHOD': 'GET',
298 'SERVER_NAME': 'testserver',
299 'SERVER_PORT': '80',
300 }
301
302 def get_environ(**request):
303 environ.update(request)
304 return environ
305 mock_environ.side_effect = get_environ
306 # soft_request_timeout is an IntConfigOption, but a float works too
307 with patch_settings(ENABLE_ARTIFICIAL_OOPS=True,
308 SOFT_REQUEST_TIMEOUT=0.1):
309 response = self.client.get('/reviews/delay/0.1/')
310
311 self.assertTrue('dump-oops' in environ)
266312
=== modified file 'src/reviewsapp/tests/test_views.py'
--- src/reviewsapp/tests/test_views.py 2012-01-25 16:13:06 +0000
+++ src/reviewsapp/tests/test_views.py 2012-03-21 22:21:27 +0000
@@ -531,3 +531,23 @@
531 '/reviews/die/')531 '/reviews/die/')
532532
533 reload(reviewsapp.urls)533 reload(reviewsapp.urls)
534
535 def test_delay(self):
536 with patch_settings(ENABLE_ARTIFICIAL_OOPS=True):
537 # We need to reload urls.py to recalculate all urls
538 reload(reviewsapp.urls)
539
540 response = self.client.get('/reviews/delay/0.1/')
541
542 reload(reviewsapp.urls)
543 self.assertContains(response, 'Slept for 0.1 seconds')
544
545 def test_delay_invalid_amount(self):
546 with patch_settings(ENABLE_ARTIFICIAL_OOPS=True):
547 # We need to reload urls.py to recalculate all urls
548 reload(reviewsapp.urls)
549
550 response = self.client.get('/reviews/delay/0.1.2/')
551
552 reload(reviewsapp.urls)
553 self.assertContains(response, 'Invalid delay amount "0.1.2"')
534554
=== modified file 'src/reviewsapp/urls.py'
--- src/reviewsapp/urls.py 2011-04-20 16:47:48 +0000
+++ src/reviewsapp/urls.py 2012-03-21 22:21:27 +0000
@@ -61,5 +61,6 @@
6161
62if getattr(settings, 'ENABLE_ARTIFICIAL_OOPS', False):62if getattr(settings, 'ENABLE_ARTIFICIAL_OOPS', False):
63 urlpatterns += patterns('reviewsapp.views',63 urlpatterns += patterns('reviewsapp.views',
64 (r'^die/', 'die'),64 (r'^die/$', 'die'),
65 (r'^delay/(?P<amount>[\d.]+)/$', 'delay'),
65 )66 )
6667
=== modified file 'src/reviewsapp/views/__init__.py'
--- src/reviewsapp/views/__init__.py 2011-04-20 16:47:48 +0000
+++ src/reviewsapp/views/__init__.py 2012-03-21 22:21:27 +0000
@@ -22,6 +22,9 @@
22 'die'22 'die'
23 ]23 ]
2424
25from time import sleep
26
27from django.http import HttpResponse
25from django.shortcuts import render_to_response28from django.shortcuts import render_to_response
2629
27from reviewsapp.models import ReviewModeration30from reviewsapp.models import ReviewModeration
@@ -45,3 +48,12 @@
45def die(request):48def die(request):
46 """Just trigger an oops report"""49 """Just trigger an oops report"""
47 raise ZeroDivisionError()50 raise ZeroDivisionError()
51
52
53def delay(request, amount):
54 try:
55 amount = float(amount)
56 except ValueError:
57 return HttpResponse('Invalid delay amount "%s"' % amount)
58 sleep(amount)
59 return HttpResponse('Slept for %s seconds' % amount)

Subscribers

People subscribed via source and target branches