Merge lp:~james-w/python-timeline-django/start-end-request into lp:python-timeline-django

Proposed by James Westby
Status: Merged
Approved by: Natalia Bidart
Approved revision: 20
Merged at revision: 19
Proposed branch: lp:~james-w/python-timeline-django/start-end-request
Merge into: lp:python-timeline-django
Diff against target: 234 lines (+135/-3)
5 files modified
timeline_django/hooks.py (+14/-0)
timeline_django/signals.py (+5/-0)
timeline_django/tests/test_hooks.py (+69/-0)
timeline_django/tests/test_wsgi.py (+37/-1)
timeline_django/wsgi.py (+10/-2)
To merge this branch: bzr merge lp:~james-w/python-timeline-django/start-end-request
Reviewer Review Type Date Requested Status
Natalia Bidart (community) Approve
Review via email: mp+182128@code.launchpad.net

Commit message

Add signals for the start of the wsgi request and start of the response.

The django signal for request-finished fires when the client has consumed
the response. However, when generating an oops it can be done before this
meaning that the timeline doesn't have a reference for an end point.
This defines a response-started signal that fires when start_response
is called in the wsgi stack, making it more likely that there will be
an end point in the timeline.

Description of the change

Hi,

This is best described by the commit message, but should get us oopses
that better reflect the duration of the request.

Thanks,

James

To post a comment you must log in.
Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Looks good!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'timeline_django/hooks.py'
2--- timeline_django/hooks.py 2013-08-24 19:44:21 +0000
3+++ timeline_django/hooks.py 2013-08-26 15:18:27 +0000
4@@ -19,6 +19,8 @@
5 import django.core.signals
6 import django.db.backends.signals
7
8+from . import signals
9+
10
11 class TimelineReceiver(object):
12 """Converts django signals in to `Timeline` entries.
13@@ -30,6 +32,8 @@
14 REQUEST_CATEGORY = "request"
15 REQUEST_EXCEPTION_CATEGORY = "request-exception"
16 CONNECTION_CREATED_CATEGORY = "connection-created"
17+ WSGI_REQUEST_CATEGORY = "wsgi_request-start"
18+ WSGI_RESPONSE_CATEGORY = "wsgi_response-start"
19
20 def __init__(self, timeline_factory):
21 """Create a TimelineReceiver.
22@@ -99,9 +103,19 @@
23 self._do_instantaneous_action(self.CONNECTION_CREATED_CATEGORY,
24 connection_name)
25
26+ def wsgi_request_started(self, sender, **kwargs):
27+ self._do_instantaneous_action(self.WSGI_REQUEST_CATEGORY,
28+ self._model_name(sender))
29+
30+ def wsgi_response_started(self, sender, **kwargs):
31+ self._do_instantaneous_action(self.WSGI_RESPONSE_CATEGORY,
32+ self._model_name(sender))
33+
34 def connect_to_signals(self):
35 """Connect the callbacks to their corresponding signals."""
36 django.core.signals.request_started.connect(self.request_started, weak=False)
37 django.core.signals.request_finished.connect(self.request_finished, weak=False)
38 django.core.signals.got_request_exception.connect(self.got_request_exception, weak=False)
39 django.db.backends.signals.connection_created.connect(self.connection_created, weak=False)
40+ signals.wsgi_request_started.connect(self.wsgi_request_started, weak=False)
41+ signals.wsgi_response_started.connect(self.wsgi_response_started, weak=False)
42
43=== added file 'timeline_django/signals.py'
44--- timeline_django/signals.py 1970-01-01 00:00:00 +0000
45+++ timeline_django/signals.py 2013-08-26 15:18:27 +0000
46@@ -0,0 +1,5 @@
47+from django.dispatch import Signal
48+
49+
50+wsgi_request_started = Signal(providing_args=['environ'])
51+wsgi_response_started = Signal(providing_args=['environ'])
52
53=== modified file 'timeline_django/tests/test_hooks.py'
54--- timeline_django/tests/test_hooks.py 2013-08-24 19:44:21 +0000
55+++ timeline_django/tests/test_hooks.py 2013-08-26 15:18:27 +0000
56@@ -21,6 +21,7 @@
57 from testtools import TestCase
58 from timeline import Timeline
59
60+from .. import signals
61 from ..hooks import TimelineReceiver
62
63
64@@ -138,6 +139,64 @@
65 receiver.got_request_exception(TestModel)
66 self.assertEqual(timedelta(), timeline.actions[0].duration)
67
68+ def test_wsgi_request_started_with_no_timeline(self):
69+ receiver = TimelineReceiver(lambda: None)
70+ # Mustn't crash due to trying to use a None timeline
71+ receiver.wsgi_request_started(TestModel)
72+
73+ def test_wsgi_request_started_adds_action(self):
74+ receiver, timeline = self.get_timeline_receiver()
75+ receiver.wsgi_request_started(TestModel)
76+ self.assertEqual(1, len(timeline.actions))
77+
78+ def test_wsgi_request_started_sets_wsgi_request_as_category(self):
79+ receiver, timeline = self.get_timeline_receiver()
80+ receiver.wsgi_request_started(TestModel)
81+ # -start is appended as this is a nestable action
82+ self.assertEqual('wsgi_request-start', timeline.actions[0].category)
83+
84+ def test_wsgi_request_started_sets_sender_as_detail(self):
85+ receiver, timeline = self.get_timeline_receiver()
86+ receiver.wsgi_request_started(TestModel)
87+ self.assertEqual(TestModel.NAME, timeline.actions[0].detail)
88+
89+ def test_wsgi_request_started_finishes_action(self):
90+ receiver, timeline = self.get_timeline_receiver()
91+ receiver.wsgi_request_started(TestModel)
92+ # If this passes it means the action was completed as an
93+ # instantaneous action. This is so nesting can be done,
94+ # as the action may well trigger SQL
95+ self.assertEqual(timedelta(), timeline.actions[0].duration)
96+
97+ def test_wsgi_response_started_with_no_timeline(self):
98+ receiver = TimelineReceiver(lambda: None)
99+ # Mustn't crash due to trying to use a None timeline
100+ receiver.wsgi_response_started(TestModel)
101+
102+ def test_wsgi_response_started_adds_action(self):
103+ receiver, timeline = self.get_timeline_receiver()
104+ receiver.wsgi_response_started(TestModel)
105+ self.assertEqual(1, len(timeline.actions))
106+
107+ def test_wsgi_response_started_sets_wsgi_response_as_category(self):
108+ receiver, timeline = self.get_timeline_receiver()
109+ receiver.wsgi_response_started(TestModel)
110+ # -start is appended as this is a nestable action
111+ self.assertEqual('wsgi_response-start', timeline.actions[0].category)
112+
113+ def test_wsgi_response_started_sets_sender_as_detail(self):
114+ receiver, timeline = self.get_timeline_receiver()
115+ receiver.wsgi_response_started(TestModel)
116+ self.assertEqual(TestModel.NAME, timeline.actions[0].detail)
117+
118+ def test_wsgi_response_started_finishes_action(self):
119+ receiver, timeline = self.get_timeline_receiver()
120+ receiver.wsgi_response_started(TestModel)
121+ # If this passes it means the action was completed as an
122+ # instantaneous action. This is so nesting can be done,
123+ # as the action may well trigger SQL
124+ self.assertEqual(timedelta(), timeline.actions[0].duration)
125+
126 def test_connection_created_with_no_timeline(self):
127 receiver = TimelineReceiver(lambda: None)
128 connection = TestConnection()
129@@ -204,3 +263,13 @@
130 signal, receiver = self.register_signal(django.db.backends.signals, 'connection_created')
131 self.assertEqual(1, len(signal.receivers))
132 self.assertEqual(receiver.connection_created, signal.receivers[0][1])
133+
134+ def test_register_signals_registers_wsgi_request_started(self):
135+ signal, receiver = self.register_signal(signals, 'wsgi_request_started')
136+ self.assertEqual(1, len(signal.receivers))
137+ self.assertEqual(receiver.wsgi_request_started, signal.receivers[0][1])
138+
139+ def test_register_signals_registers_wsgi_response_started(self):
140+ signal, receiver = self.register_signal(signals, 'wsgi_response_started')
141+ self.assertEqual(1, len(signal.receivers))
142+ self.assertEqual(receiver.wsgi_response_started, signal.receivers[0][1])
143
144=== modified file 'timeline_django/tests/test_wsgi.py'
145--- timeline_django/tests/test_wsgi.py 2012-09-22 09:00:56 +0000
146+++ timeline_django/tests/test_wsgi.py 2013-08-26 15:18:27 +0000
147@@ -15,10 +15,15 @@
148
149 import threading
150
151+from django.dispatch import Signal
152+
153 from testtools import TestCase
154 from testtools.matchers import Is
155
156-from .. import wsgi
157+from .. import (
158+ signals,
159+ wsgi,
160+)
161
162
163 class TestWSGI(TestCase):
164@@ -38,3 +43,34 @@
165 out = app(environ, start_response)
166 self.assertEqual(["foo"], out)
167 self.assertThat(wsgi.get_environ(), Is(environ))
168+
169+ def patch_signal(self, name):
170+ signal = Signal()
171+ self.patch(signals, name, signal)
172+ return signal
173+
174+ def test_sends_wsgi_request_started_signal(self):
175+ signal = self.patch_signal('wsgi_request_started')
176+ called_environs = []
177+ def capture_signal(sender, environ, signal=None):
178+ called_environs.append(environ)
179+ signal.connect(capture_signal)
180+ wrapped_app = wsgi.make_app(lambda environ, start_response: None)
181+ environ = object()
182+ wrapped_app(environ, None)
183+ self.assertEqual([environ], called_environs)
184+
185+ def test_sends_wsgi_response_started_signal(self):
186+ signal = self.patch_signal('wsgi_response_started')
187+ called_environs = []
188+ def capture_signal(sender, environ, signal=None):
189+ called_environs.append(environ)
190+ signal.connect(capture_signal)
191+ def app(environ, start_response):
192+ start_response()
193+ wrapped_app = wsgi.make_app(app)
194+ environ = object()
195+ def start_response(*args, **kwargs):
196+ pass
197+ wrapped_app(environ, start_response)
198+ self.assertEqual([environ], called_environs)
199
200=== modified file 'timeline_django/wsgi.py'
201--- timeline_django/wsgi.py 2012-09-22 09:00:56 +0000
202+++ timeline_django/wsgi.py 2013-08-26 15:18:27 +0000
203@@ -17,6 +17,8 @@
204
205 import threading
206
207+from timeline_django import signals
208+
209
210 _thread_local = threading.local()
211
212@@ -34,7 +36,7 @@
213 def make_app(application):
214 """Create a WSGI application that stores the environ in a thread-local.
215
216- This is useful when dealing with non-WSGI code or code that runs in
217+ This is useful when dealing with non-WSGI code or code that runs in
218 global contexts such as ORMs, and you wish to work with something in the
219 WSGI environ.
220
221@@ -43,6 +45,12 @@
222 :return: A WSGI app wrapping application.
223 """
224 def save_environ(environ, start_response):
225+ signals.wsgi_request_started.send(sender=save_environ,
226+ environ=environ)
227 _thread_local.environ = environ
228- return application(environ, start_response)
229+ def signalled_start_response(*args, **kwargs):
230+ signals.wsgi_response_started.send(sender=save_environ,
231+ environ=environ)
232+ return start_response(*args, **kwargs)
233+ return application(environ, signalled_start_response)
234 return save_environ

Subscribers

People subscribed via source and target branches