Merge lp:~lifeless/launchpad/oops into lp:launchpad

Proposed by Robert Collins
Status: Merged
Approved by: Robert Collins
Approved revision: no longer in the source branch.
Merged at revision: 11497
Proposed branch: lp:~lifeless/launchpad/oops
Merge into: lp:launchpad
Diff against target: 824 lines (+463/-70)
17 files modified
lib/canonical/launchpad/pagetests/basics/page-request-summaries.txt (+7/-6)
lib/canonical/launchpad/pagetests/basics/user-requested-oops.txt (+3/-3)
lib/canonical/launchpad/webapp/adapter.py (+53/-44)
lib/canonical/launchpad/webapp/errorlog.py (+13/-7)
lib/canonical/launchpad/webapp/ftests/test_adapter.txt (+12/-0)
lib/canonical/launchpad/webapp/servers.py (+3/-3)
lib/canonical/launchpad/webapp/tests/test_publication.py (+2/-1)
lib/canonical/launchpad/webapp/tests/test_user_requested_oops.py (+0/-5)
lib/lp/app/browser/tests/base-layout.txt (+1/-1)
lib/lp/services/timeline/__init__.py (+14/-0)
lib/lp/services/timeline/requesttimeline.py (+51/-0)
lib/lp/services/timeline/tests/__init__.py (+4/-0)
lib/lp/services/timeline/tests/test_requesttimeline.py (+58/-0)
lib/lp/services/timeline/tests/test_timedaction.py (+61/-0)
lib/lp/services/timeline/tests/test_timeline.py (+50/-0)
lib/lp/services/timeline/timedaction.py (+69/-0)
lib/lp/services/timeline/timeline.py (+62/-0)
To merge this branch: bzr merge lp:~lifeless/launchpad/oops
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+34272@code.launchpad.net

Commit message

Create lp.services.timeline for accruing/reporting on external/blocking actions.

Description of the change

Create a more generic environment for accruing/reporting on external/blocking actions.

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

This was not one of those rubber-stamp reviews. :-)

The biggest sticking point, if a small one, was the dead code left in there to document the expected future state of the code. Please resolve that situation as soon as possible, of course.

We found that Timeline.baseline was at risk of ambiguity, because the Timeline could in principle be created on-demand in the middle of handling a request. We considered always having a "Start timeline" action, but decided against it. Ultimately you went with the option of creating the Timeline early on when handling a request, so that the baseline is always more or less the time we start handling the request.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/pagetests/basics/page-request-summaries.txt'
--- lib/canonical/launchpad/pagetests/basics/page-request-summaries.txt 2009-09-18 18:04:16 +0000
+++ lib/canonical/launchpad/pagetests/basics/page-request-summaries.txt 2010-09-03 03:44:47 +0000
@@ -1,12 +1,13 @@
1Launchpad pages end with a comment that displays how many queries we've issued1Launchpad pages end with a comment that displays how many actions outside of
2to render the page. Note that we say "at least" because unfortunately2the regular python code we've issued to satisfy the request. Note that we say "at
3some queries may be issued after the page may finish rendering -- the3least" because unfortunately some queries may be issued after the page may
4authoritative source is in OOPS reports or in the web app's stderr.4finish rendering -- the authoritative source is in OOPS reports or in the web
5app's stderr.
56
6 >>> browser.open('http://launchpad.dev/')7 >>> browser.open('http://launchpad.dev/')
7 >>> print browser.contents8 >>> print browser.contents
8 <!DOCTYPE...9 <!DOCTYPE...
9 ...<!--... At least ... queries issued in ... seconds ...-->...10 ...<!--... At least ... actions issued in ... seconds ...-->...
1011
11It's available for any page:12It's available for any page:
1213
@@ -14,4 +15,4 @@
14 >>> browser.open('http://launchpad.dev/~mark/')15 >>> browser.open('http://launchpad.dev/~mark/')
15 >>> print browser.contents16 >>> print browser.contents
16 <!DOCTYPE...17 <!DOCTYPE...
17 ...<!--... At least ... queries issued in ... seconds ...-->...18 ...<!--... At least ... actions issued in ... seconds ...-->...
1819
=== modified file 'lib/canonical/launchpad/pagetests/basics/user-requested-oops.txt'
--- lib/canonical/launchpad/pagetests/basics/user-requested-oops.txt 2010-06-17 16:56:04 +0000
+++ lib/canonical/launchpad/pagetests/basics/user-requested-oops.txt 2010-09-03 03:44:47 +0000
@@ -3,7 +3,7 @@
3OOPSes happen from time to time in Launchpad, and they contain lots of3OOPSes happen from time to time in Launchpad, and they contain lots of
4interesting information for helping debug. Sometimes though it is useful to4interesting information for helping debug. Sometimes though it is useful to
5get some of this information even if there hasn't been a problem. The major5get some of this information even if there hasn't been a problem. The major
6use case for this is to get a dump of the database queries that a page does.6use case for this is to get a dump of the blocking activities that a page does.
77
8A user can request an oops for a page by using the ++oops++ namespace in any8A user can request an oops for a page by using the ++oops++ namespace in any
9page traversal.9page traversal.
@@ -15,7 +15,7 @@
15 >>> (page, summary) = browser.contents.split('</html>')15 >>> (page, summary) = browser.contents.split('</html>')
16 >>> print summary16 >>> print summary
17 <!-- ...17 <!-- ...
18 At least ... queries issued in ... seconds OOPS-...18 At least ... actions issued in ... seconds OOPS-...
19 <BLANKLINE>19 <BLANKLINE>
20 r...20 r...
21 -->21 -->
@@ -26,7 +26,7 @@
26 >>> (page, summary) = browser.contents.split('</html>')26 >>> (page, summary) = browser.contents.split('</html>')
27 >>> print summary27 >>> print summary
28 <!-- ...28 <!-- ...
29 At least ... queries issued in ... seconds OOPS-...29 At least ... actions issued in ... seconds OOPS-...
30 <BLANKLINE>30 <BLANKLINE>
31 r...31 r...
32 -->32 -->
3333
=== modified file 'lib/canonical/launchpad/webapp/adapter.py'
--- lib/canonical/launchpad/webapp/adapter.py 2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/webapp/adapter.py 2010-09-03 03:44:47 +0000
@@ -6,6 +6,7 @@
66
7__metaclass__ = type7__metaclass__ = type
88
9import datetime
9import os10import os
10import re11import re
11import sys12import sys
@@ -22,6 +23,7 @@
22 ISOLATION_LEVEL_SERIALIZABLE,23 ISOLATION_LEVEL_SERIALIZABLE,
23 QueryCanceledError,24 QueryCanceledError,
24 )25 )
26import pytz
25from storm.database import register_scheme27from storm.database import register_scheme
26from storm.databases.postgres import (28from storm.databases.postgres import (
27 Postgres,29 Postgres,
@@ -63,7 +65,12 @@
63 SLAVE_FLAVOR,65 SLAVE_FLAVOR,
64 )66 )
65from canonical.launchpad.webapp.opstats import OpStats67from canonical.launchpad.webapp.opstats import OpStats
66from canonical.lazr.utils import safe_hasattr68from canonical.lazr.utils import get_current_browser_request, safe_hasattr
69from lp.services.timeline.timeline import Timeline
70from lp.services.timeline.requesttimeline import (
71 get_request_timeline,
72 set_request_timeline,
73 )
6774
6875
69__all__ = [76__all__ = [
@@ -80,6 +87,8 @@
80 ]87 ]
8188
8289
90UTC = pytz.utc
91
83classImplements(TimeoutError, IRequestExpired)92classImplements(TimeoutError, IRequestExpired)
8493
8594
@@ -123,9 +132,9 @@
123 pass132 pass
124133
125 def afterCompletion(self, txn):134 def afterCompletion(self, txn):
126 now = time()135 action = get_request_timeline(get_current_browser_request()).start(
127 _log_statement(136 "SQL-nostore", 'Transaction completed, status: %s' % txn.status)
128 now, now, None, 'Transaction completed, status: %s' % txn.status)137 action.finish()
129138
130139
131def set_request_started(140def set_request_started(
@@ -151,10 +160,14 @@
151 if starttime is None:160 if starttime is None:
152 starttime = time()161 starttime = time()
153 _local.request_start_time = starttime162 _local.request_start_time = starttime
154 if request_statements is None:163 request = get_current_browser_request()
155 _local.request_statements = []164 if request_statements is not None:
156 else:165 # Specify a specific sequence object for the timeline.
157 _local.request_statements = request_statements166 set_request_timeline(request, Timeline(request_statements))
167 else:
168 # Ensure a timeline is created, so that time offset for actions is
169 # reasonable.
170 set_request_timeline(request, Timeline())
158 _local.current_statement_timeout = None171 _local.current_statement_timeout = None
159 _local.enable_timeout = enable_timeout172 _local.enable_timeout = enable_timeout
160 if txn is not None:173 if txn is not None:
@@ -167,9 +180,9 @@
167 """180 """
168 if getattr(_local, 'request_start_time', None) is None:181 if getattr(_local, 'request_start_time', None) is None:
169 warnings.warn('clear_request_started() called outside of a request')182 warnings.warn('clear_request_started() called outside of a request')
170
171 _local.request_start_time = None183 _local.request_start_time = None
172 _local.request_statements = []184 request = get_current_browser_request()
185 set_request_timeline(request, Timeline())
173 commit_logger = getattr(_local, 'commit_logger', None)186 commit_logger = getattr(_local, 'commit_logger', None)
174 if commit_logger is not None:187 if commit_logger is not None:
175 _local.commit_logger.txn.unregisterSynch(_local.commit_logger)188 _local.commit_logger.txn.unregisterSynch(_local.commit_logger)
@@ -179,7 +192,8 @@
179def summarize_requests():192def summarize_requests():
180 """Produce human-readable summary of requests issued so far."""193 """Produce human-readable summary of requests issued so far."""
181 secs = get_request_duration()194 secs = get_request_duration()
182 statements = getattr(_local, 'request_statements', [])195 request = get_current_browser_request()
196 timeline = get_request_timeline(request)
183 from canonical.launchpad.webapp.errorlog import (197 from canonical.launchpad.webapp.errorlog import (
184 maybe_record_user_requested_oops)198 maybe_record_user_requested_oops)
185 oopsid = maybe_record_user_requested_oops()199 oopsid = maybe_record_user_requested_oops()
@@ -187,14 +201,15 @@
187 oops_str = ""201 oops_str = ""
188 else:202 else:
189 oops_str = " %s" % oopsid203 oops_str = " %s" % oopsid
190 log = "%s queries issued in %.2f seconds%s" % (204 log = "%s queries/external actions issued in %.2f seconds%s" % (
191 len(statements), secs, oops_str)205 len(timeline.actions), secs, oops_str)
192 return log206 return log
193207
194208
195def store_sql_statements_and_request_duration(event):209def store_sql_statements_and_request_duration(event):
210 actions = get_request_timeline(get_current_browser_request()).actions
196 event.request.setInWSGIEnvironment(211 event.request.setInWSGIEnvironment(
197 'launchpad.sqlstatements', len(get_request_statements()))212 'launchpad.nonpythonactions', len(actions))
198 event.request.setInWSGIEnvironment(213 event.request.setInWSGIEnvironment(
199 'launchpad.requestduration', get_request_duration())214 'launchpad.requestduration', get_request_duration())
200215
@@ -205,7 +220,16 @@
205 The list is composed of (starttime, endtime, db_id, statement) tuples.220 The list is composed of (starttime, endtime, db_id, statement) tuples.
206 Times are given in milliseconds since the start of the request.221 Times are given in milliseconds since the start of the request.
207 """222 """
208 return getattr(_local, 'request_statements', [])223 result = []
224 request = get_current_browser_request()
225 for action in get_request_timeline(request).actions:
226 if not action.category.startswith("SQL-"):
227 continue
228 # Can't show incomplete requests in this API
229 if action.duration is None:
230 continue
231 result.append(action.logTuple())
232 return result
209233
210234
211def get_request_start_time():235def get_request_start_time():
@@ -224,29 +248,6 @@
224 return now - starttime248 return now - starttime
225249
226250
227def _log_statement(starttime, endtime, connection_wrapper, statement):
228 """Log that a database statement was executed."""
229 request_starttime = getattr(_local, 'request_start_time', None)
230 if request_starttime is None:
231 return
232
233 # convert times to integer millisecond values
234 starttime = int((starttime - request_starttime) * 1000)
235 endtime = int((endtime - request_starttime) * 1000)
236 # A string containing no whitespace that lets us identify which Store
237 # is being used.
238 if connection_wrapper is not None:
239 database_identifier = connection_wrapper._database.name
240 else:
241 database_identifier = None
242 _local.request_statements.append(
243 (starttime, endtime, database_identifier, statement))
244
245 # store the last executed statement as an attribute on the current
246 # thread
247 threading.currentThread().lp_last_sql_statement = statement
248
249
250def _check_expired(timeout):251def _check_expired(timeout):
251 """Checks whether the current request has passed the given timeout."""252 """Checks whether the current request has passed the given timeout."""
252 if timeout is None or not getattr(_local, 'enable_timeout', True):253 if timeout is None or not getattr(_local, 'enable_timeout', True):
@@ -530,15 +531,23 @@
530 if self._debug_sql or self._debug_sql_extra:531 if self._debug_sql or self._debug_sql_extra:
531 sys.stderr.write(statement + "\n")532 sys.stderr.write(statement + "\n")
532 sys.stderr.write("-" * 70 + "\n")533 sys.stderr.write("-" * 70 + "\n")
533534 # store the last executed statement as an attribute on the current
534 now = time()535 # thread
535 connection._lp_statement_start_time = now536 threading.currentThread().lp_last_sql_statement = statement
537 request_starttime = getattr(_local, 'request_start_time', None)
538 if request_starttime is None:
539 return
540 action = get_request_timeline(get_current_browser_request()).start(
541 'SQL-%s' % connection._database.name, statement)
542 connection._lp_statement_action = action
536543
537 def connection_raw_execute_success(self, connection, raw_cursor,544 def connection_raw_execute_success(self, connection, raw_cursor,
538 statement, params):545 statement, params):
539 end = time()546 action = getattr(connection, '_lp_statement_action', None)
540 start = getattr(connection, '_lp_statement_start_time', end)547 if action is not None:
541 _log_statement(start, end, connection, statement)548 # action may be None if the tracer was installed the statement was
549 # submitted.
550 action.finish()
542551
543 def connection_raw_execute_error(self, connection, raw_cursor,552 def connection_raw_execute_error(self, connection, raw_cursor,
544 statement, params, error):553 statement, params, error):
545554
=== modified file 'lib/canonical/launchpad/webapp/errorlog.py'
--- lib/canonical/launchpad/webapp/errorlog.py 2010-08-26 02:47:21 +0000
+++ lib/canonical/launchpad/webapp/errorlog.py 2010-09-03 03:44:47 +0000
@@ -31,7 +31,7 @@
31from canonical.launchpad.layers import WebServiceLayer31from canonical.launchpad.layers import WebServiceLayer
32from canonical.launchpad.webapp.adapter import (32from canonical.launchpad.webapp.adapter import (
33 get_request_duration,33 get_request_duration,
34 get_request_statements,34 get_request_start_time,
35 soft_timeout_expired,35 soft_timeout_expired,
36 )36 )
37from canonical.launchpad.webapp.interfaces import (37from canonical.launchpad.webapp.interfaces import (
@@ -42,7 +42,7 @@
42from canonical.launchpad.webapp.opstats import OpStats42from canonical.launchpad.webapp.opstats import OpStats
43from canonical.lazr.utils import safe_hasattr43from canonical.lazr.utils import safe_hasattr
44from lp.services.log.uniquefileallocator import UniqueFileAllocator44from lp.services.log.uniquefileallocator import UniqueFileAllocator
4545from lp.services.timeline.requesttimeline import get_request_timeline
4646
47UTC = pytz.utc47UTC = pytz.utc
4848
@@ -451,11 +451,17 @@
451 strurl = _safestr(url)451 strurl = _safestr(url)
452452
453 duration = get_request_duration()453 duration = get_request_duration()
454454 # In principle the timeline is per-request, but see bug=623199 -
455 statements = sorted(455 # at this point the request is optional, but get_request_timeline
456 (start, end, _safestr(database_id), _safestr(statement))456 # does not care; when it starts caring, we will always have a
457 for (start, end, database_id, statement)457 # request object (or some annotations containing object).
458 in get_request_statements())458 # RBC 20100901
459 timeline = get_request_timeline(request)
460 statements = []
461 for action in timeline.actions:
462 start, end, category, detail = action.logTuple()
463 statements.append(
464 (start, end, _safestr(category), _safestr(detail)))
459465
460 oopsid, filename = self.log_namer.newId(now)466 oopsid, filename = self.log_namer.newId(now)
461467
462468
=== modified file 'lib/canonical/launchpad/webapp/ftests/test_adapter.txt'
--- lib/canonical/launchpad/webapp/ftests/test_adapter.txt 2010-03-25 11:58:42 +0000
+++ lib/canonical/launchpad/webapp/ftests/test_adapter.txt 2010-09-03 03:44:47 +0000
@@ -9,6 +9,7 @@
9 >>> import threading9 >>> import threading
10 >>> import transaction10 >>> import transaction
11 >>> from zope.component import getUtility11 >>> from zope.component import getUtility
12 >>> from lazr.restful.utils import get_current_browser_request
12 >>> from storm.zope.interfaces import IZStorm13 >>> from storm.zope.interfaces import IZStorm
13 >>> from canonical.launchpad.webapp.interfaces import (14 >>> from canonical.launchpad.webapp.interfaces import (
14 ... IStoreSelector, MAIN_STORE, MASTER_FLAVOR)15 ... IStoreSelector, MAIN_STORE, MASTER_FLAVOR)
@@ -17,6 +18,7 @@
17 >>> from canonical.launchpad.webapp.adapter import (18 >>> from canonical.launchpad.webapp.adapter import (
18 ... clear_request_started, get_request_statements,19 ... clear_request_started, get_request_statements,
19 ... set_request_started)20 ... set_request_started)
21 >>> from lp.services.timeline.requesttimeline import get_request_timeline
2022
21There are several possible database connections available via the23There are several possible database connections available via the
22IStoreSelector utility.24IStoreSelector utility.
@@ -50,6 +52,16 @@
50 SELECT 152 SELECT 1
51 SELECT 253 SELECT 2
5254
55A timeline is created too:
56
57 >>> timeline = get_request_timeline(get_current_browser_request())
58 >>> for action in timeline.actions:
59 ... if not action.category.startswith("SQL-"):
60 ... continue
61 ... print action.detail
62 SELECT 1
63 SELECT 2
64
53After we complete the request, the statement log is cleared:65After we complete the request, the statement log is cleared:
5466
55 >>> clear_request_started()67 >>> clear_request_started()
5668
=== modified file 'lib/canonical/launchpad/webapp/servers.py'
--- lib/canonical/launchpad/webapp/servers.py 2010-09-03 03:12:39 +0000
+++ lib/canonical/launchpad/webapp/servers.py 2010-09-03 03:44:47 +0000
@@ -977,7 +977,7 @@
977 request string (1st line of request)977 request string (1st line of request)
978 response status978 response status
979 response bytes written979 response bytes written
980 number of sql statements980 number of nonpython statements (sql, email, memcache, rabbit etc)
981 request duration981 request duration
982 number of ticks during traversal982 number of ticks during traversal
983 number of ticks during publication983 number of ticks during publication
@@ -998,7 +998,7 @@
998 bytes_written = task.bytes_written998 bytes_written = task.bytes_written
999 userid = cgi_env.get('launchpad.userid', '')999 userid = cgi_env.get('launchpad.userid', '')
1000 pageid = cgi_env.get('launchpad.pageid', '')1000 pageid = cgi_env.get('launchpad.pageid', '')
1001 sql_statements = cgi_env.get('launchpad.sqlstatements', 0)1001 nonpython_actions = cgi_env.get('launchpad.nonpythonactions', 0)
1002 request_duration = cgi_env.get('launchpad.requestduration', 0)1002 request_duration = cgi_env.get('launchpad.requestduration', 0)
1003 traversal_ticks = cgi_env.get('launchpad.traversalticks', 0)1003 traversal_ticks = cgi_env.get('launchpad.traversalticks', 0)
1004 publication_ticks = cgi_env.get('launchpad.publicationticks', 0)1004 publication_ticks = cgi_env.get('launchpad.publicationticks', 0)
@@ -1016,7 +1016,7 @@
1016 first_line,1016 first_line,
1017 status,1017 status,
1018 bytes_written,1018 bytes_written,
1019 sql_statements,1019 nonpython_actions,
1020 request_duration,1020 request_duration,
1021 traversal_ticks,1021 traversal_ticks,
1022 publication_ticks,1022 publication_ticks,
10231023
=== modified file 'lib/canonical/launchpad/webapp/tests/test_publication.py'
--- lib/canonical/launchpad/webapp/tests/test_publication.py 2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/webapp/tests/test_publication.py 2010-09-03 03:44:47 +0000
@@ -292,7 +292,8 @@
292 next_oops = error_reporting_utility.getLastOopsReport()292 next_oops = error_reporting_utility.getLastOopsReport()
293293
294 # Ensure the OOPS mentions the correct exception294 # Ensure the OOPS mentions the correct exception
295 self.assertTrue(repr(next_oops).find("DisconnectionError") != -1)295 self.assertTrue(repr(next_oops).find("DisconnectionError") != -1,
296 "next_oops was %r" % next_oops)
296297
297 # Ensure the OOPS is correctly marked as informational only.298 # Ensure the OOPS is correctly marked as informational only.
298 self.assertEqual(next_oops.informational, 'True')299 self.assertEqual(next_oops.informational, 'True')
299300
=== modified file 'lib/canonical/launchpad/webapp/tests/test_user_requested_oops.py'
--- lib/canonical/launchpad/webapp/tests/test_user_requested_oops.py 2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/webapp/tests/test_user_requested_oops.py 2010-09-03 03:44:47 +0000
@@ -4,7 +4,6 @@
44
5__metaclass__ = type5__metaclass__ = type
66
7import unittest
87
9from lazr.restful.utils import get_current_browser_request8from lazr.restful.utils import get_current_browser_request
10from zope.component import getUtility9from zope.component import getUtility
@@ -84,7 +83,3 @@
84 self.assertEqual(last_oops.type, 'UserRequestOops')83 self.assertEqual(last_oops.type, 'UserRequestOops')
85 self.assertEqual(last_oops.informational, 'True')84 self.assertEqual(last_oops.informational, 'True')
8685
87
88def test_suite():
89 return unittest.TestLoader().loadTestsFromName(__name__)
90
9186
=== modified file 'lib/lp/app/browser/tests/base-layout.txt'
--- lib/lp/app/browser/tests/base-layout.txt 2010-08-17 15:05:44 +0000
+++ lib/lp/app/browser/tests/base-layout.txt 2010-09-03 03:44:47 +0000
@@ -134,7 +134,7 @@
134 Has global search: False134 Has global search: False
135 Has application tabs: False135 Has application tabs: False
136 Has side portlets: False136 Has side portlets: False
137 At least ... queries issued in ... seconds137 At least ... queries... issued in ... seconds
138 Features: {}138 Features: {}
139 in scopes {}139 in scopes {}
140 r...140 r...
141141
=== added directory 'lib/lp/services/timeline'
=== added file 'lib/lp/services/timeline/__init__.py'
--- lib/lp/services/timeline/__init__.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/timeline/__init__.py 2010-09-03 03:44:47 +0000
@@ -0,0 +1,14 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""lp.services.timeline provides a timeline for varied actions.
5
6This is used as part of determining where time goes in a request.
7
8NOTE that it is not LP's timeline-view for products, though they are similar in
9intent and concept (If a better name presents itself, this package may be
10renamed).
11
12Because this is part of lp.services, packages in this namespace can only use
13general LAZR or library functionality.
14"""
015
=== added file 'lib/lp/services/timeline/requesttimeline.py'
--- lib/lp/services/timeline/requesttimeline.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/timeline/requesttimeline.py 2010-09-03 03:44:47 +0000
@@ -0,0 +1,51 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Manage a Timeline for a request."""
5
6__all__ = [
7 'get_request_timeline',
8 'set_request_timeline',
9 ]
10
11__metaclass__ = type
12
13# XXX RobertCollins 2010-09-01 bug=623199: Undesirable but pragmatic.
14# Because of this bug, rather than using request.annotations we have
15# to work in with the webapp.adapter request model, which is
16# different to that used by get_current_browser_request.
17from canonical.launchpad import webapp
18from timeline import Timeline
19
20
21def get_request_timeline(request):
22 """Get a `Timeline` for request.
23
24 This should ideally return the request.annotations['timeline'], creating it
25 if necessary. However due to bug 623199 it instead using the adapter
26 context for 'requests'. Once bug 623199 is fixed it will instead use the
27 request annotations.
28
29 :param request: A zope/launchpad request object.
30 :return: A lp.services.timeline.timeline.Timeline object for the request.
31 """
32 try:
33 return webapp.adapter._local.request_timeline
34 except AttributeError:
35 return set_request_timeline(request, Timeline())
36 # Disabled code path: bug 623199, ideally we would use this code path.
37 return request.annotations.setdefault('timeline', Timeline())
38
39
40def set_request_timeline(request, timeline):
41 """Explicitly set a timeline for request.
42
43 This is used by code which wants to manually assemble a timeline.
44
45 :param request: A zope/launchpad request object.
46 :param timeline: A Timeline.
47 """
48 webapp.adapter._local.request_timeline = timeline
49 return timeline
50 # Disabled code path: bug 623199, ideally we would use this code path.
51 request.annotations['timeline'] = timeline
052
=== added directory 'lib/lp/services/timeline/tests'
=== added file 'lib/lp/services/timeline/tests/__init__.py'
--- lib/lp/services/timeline/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/timeline/tests/__init__.py 2010-09-03 03:44:47 +0000
@@ -0,0 +1,4 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for lp.services.timeline."""
05
=== added file 'lib/lp/services/timeline/tests/test_requesttimeline.py'
--- lib/lp/services/timeline/tests/test_requesttimeline.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/timeline/tests/test_requesttimeline.py 2010-09-03 03:44:47 +0000
@@ -0,0 +1,58 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests of requesttimeline."""
5
6__metaclass__ = type
7
8import testtools
9from zope.publisher.browser import TestRequest
10
11from canonical.launchpad import webapp
12from lp.services.timeline.requesttimeline import (
13 get_request_timeline,
14 set_request_timeline,
15 )
16from lp.services.timeline.timeline import OverlappingActionError, Timeline
17
18
19class TestRequestTimeline(testtools.TestCase):
20
21 # These disabled tests are for the desired API using request annotations.
22 # bug=623199 describes some issues with why this doesn't work.
23 def disabled_test_new_request_get_request_timeline_works(self):
24 req = TestRequest()
25 timeline = get_request_timeline(req)
26 self.assertIsInstance(timeline, Timeline)
27
28 def disabled_test_same_timeline_repeated_calls(self):
29 req = TestRequest()
30 timeline = get_request_timeline(req)
31 self.assertEqual(timeline, get_request_timeline(req))
32
33 def disabled_test_set_timeline(self):
34 req = TestRequest()
35 timeline = Timeline()
36 set_request_timeline(req, timeline)
37 self.assertEqual(timeline, get_request_timeline(req))
38
39 # Tests while adapter._local contains the timeline --start---
40 # Delete when bug=623199 is fixed and the timeline store moved to
41 # annotations.
42 def test_new_request_get_request_timeline_uses_webapp(self):
43 req = TestRequest()
44 timeline = get_request_timeline(req)
45 self.assertIsInstance(timeline, Timeline)
46 self.assertTrue(webapp.adapter._local.request_timeline is timeline)
47
48 def test_same_timeline_repeated_calls(self):
49 req = TestRequest()
50 timeline = get_request_timeline(req)
51 self.assertEqual(timeline, get_request_timeline(req))
52
53 def test_set_timeline(self):
54 req = TestRequest()
55 timeline = Timeline()
56 set_request_timeline(req, timeline)
57 self.assertEqual(timeline, get_request_timeline(req))
58 # Tests while adapter._local contains the timeline ---end---
059
=== added file 'lib/lp/services/timeline/tests/test_timedaction.py'
--- lib/lp/services/timeline/tests/test_timedaction.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/timeline/tests/test_timedaction.py 2010-09-03 03:44:47 +0000
@@ -0,0 +1,61 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests of the TimedAction class."""
5
6__metaclass__ = type
7
8import datetime
9
10import testtools
11
12from lp.services.timeline.timedaction import TimedAction
13from lp.services.timeline.timeline import Timeline
14
15
16class TestTimedAction(testtools.TestCase):
17
18 def test_starts_now(self):
19 action = TimedAction("Sending mail", None)
20 self.assertIsInstance(action.start, datetime.datetime)
21
22 def test_finish_sets_duration(self):
23 action = TimedAction("Sending mail", None)
24 self.assertEqual(None, action.duration)
25 action.finish()
26 self.assertIsInstance(action.duration, datetime.timedelta)
27
28 def test__init__sets_category(self):
29 action = TimedAction("Sending mail", None)
30 self.assertEqual("Sending mail", action.category)
31
32 def test__init__sets_detail(self):
33 action = TimedAction(None, "fred.jones@example.com")
34 self.assertEqual("fred.jones@example.com", action.detail)
35
36 def test_logTuple(self):
37 timeline = Timeline()
38 action = TimedAction("foo", "bar", timeline)
39 # Set variable for deterministic results
40 action.start = timeline.baseline + datetime.timedelta(0, 0, 0, 2)
41 action.duration = datetime.timedelta(0, 0, 0, 4)
42 log_tuple = action.logTuple()
43 self.assertEqual(4, len(log_tuple), "!= 4 elements %s" % (log_tuple,))
44 self.assertAlmostEqual(2, log_tuple[0])
45 self.assertAlmostEqual(4, log_tuple[1])
46 self.assertEqual("foo", log_tuple[2])
47 self.assertEqual("bar", log_tuple[3])
48
49 def test_logTupleIncomplete(self):
50 # Things that start and hit a timeout *may* not get recorded as
51 # finishing in normal operation.
52 timeline = Timeline()
53 action = TimedAction("foo", "bar", timeline)
54 # Set variable for deterministic results
55 action.start = timeline.baseline + datetime.timedelta(0, 0, 0, 2)
56 log_tuple = action.logTuple()
57 self.assertEqual(4, len(log_tuple), "!= 4 elements %s" % (log_tuple,))
58 self.assertAlmostEqual(2, log_tuple[0])
59 self.assertAlmostEqual(999999, log_tuple[1])
60 self.assertEqual("foo", log_tuple[2])
61 self.assertEqual("bar", log_tuple[3])
062
=== added file 'lib/lp/services/timeline/tests/test_timeline.py'
--- lib/lp/services/timeline/tests/test_timeline.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/timeline/tests/test_timeline.py 2010-09-03 03:44:47 +0000
@@ -0,0 +1,50 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests of the Timeline class."""
5
6__metaclass__ = type
7
8import datetime
9
10import testtools
11
12from lp.services.timeline.timedaction import TimedAction
13from lp.services.timeline.timeline import OverlappingActionError, Timeline
14
15
16class TestTimeline(testtools.TestCase):
17
18 def test_start_returns_action(self):
19 timeline = Timeline()
20 action = timeline.start("Sending mail", "Noone")
21 self.assertIsInstance(action, TimedAction)
22 self.assertEqual("Sending mail", action.category)
23 self.assertEqual("Noone", action.detail)
24 self.assertEqual(None, action.duration)
25 self.assertEqual(timeline, action.timeline)
26
27 def test_can_supply_list(self):
28 actions = "foo"
29 timeline = Timeline(actions)
30 self.assertEqual(actions, timeline.actions)
31
32 def test_start_with_unfinished_action_fails(self):
33 # A design constraint of timeline says that overlapping actions are not
34 # permitted. See the Timeline docstrings.
35 timeline = Timeline()
36 action = timeline.start("Sending mail", "Noone")
37 self.assertRaises(OverlappingActionError, timeline.start,
38 "Sending mail", "Noone")
39
40 def test_start_after_finish_works(self):
41 timeline = Timeline()
42 action = timeline.start("Sending mail", "Noone")
43 action.finish()
44 action = timeline.start("Sending mail", "Noone")
45 action.finish()
46 self.assertEqual(2, len(timeline.actions))
47
48 def test_baseline(self):
49 timeline = Timeline()
50 self.assertIsInstance(timeline.baseline, datetime.datetime)
051
=== added file 'lib/lp/services/timeline/timedaction.py'
--- lib/lp/services/timeline/timedaction.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/timeline/timedaction.py 2010-09-03 03:44:47 +0000
@@ -0,0 +1,69 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Time a single categorised action."""
5
6
7__all__ = ['TimedAction']
8
9__metaclass__ = type
10
11
12import datetime
13
14import pytz
15
16
17UTC = pytz.utc
18
19
20class TimedAction:
21 """An individual action which has been timed.
22
23 :ivar timeline: The timeline that this action took place within.
24 :ivar start: A datetime object with tz for the start of the action.
25 :ivar duration: A timedelta for the duration of the action. None for
26 actions which have not completed.
27 :ivar category: The category of the action. E.g. "sql".
28 :ivar detail: The detail about the action. E.g. "SELECT COUNT(*) ..."
29 """
30
31 def __init__(self, category, detail, timeline=None):
32 """Create a TimedAction.
33
34 New TimedActions have a start but no duration.
35
36 :param category: The category for the action.
37 :param detail: The detail about the action being timed.
38 :param timeline: The timeline for the action.
39 """
40 self.start = datetime.datetime.now(UTC)
41 self.duration = None
42 self.category = category
43 self.detail = detail
44 self.timeline = timeline
45
46 def logTuple(self):
47 """Return a 4-tuple suitable for errorlog's use."""
48 offset = self._td_to_ms(self.start - self.timeline.baseline)
49 if self.duration is None:
50 # This action wasn't finished: give it a duration that will stand
51 # out. This is pretty normal when action ends are recorded by
52 # callbacks rather than stack-like structures. E.g. storm
53 # tracers in launchpad:
54 # log-trace START : starts action
55 # timeout-trace START : raises
56 # log-trace FINISH is never called.
57 length = 999999
58 else:
59 length = self._td_to_ms(self.duration)
60 return (offset, length, self.category, self.detail)
61
62 def _td_to_ms(self, td):
63 """Tweak on a backport from python 2.7"""
64 return (td.microseconds + (
65 td.seconds + td.days * 24 * 3600) * 10**6) / 10**3
66
67 def finish(self):
68 """Mark the TimedAction as finished."""
69 self.duration = datetime.datetime.now(UTC) - self.start
070
=== added file 'lib/lp/services/timeline/timeline.py'
--- lib/lp/services/timeline/timeline.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/timeline/timeline.py 2010-09-03 03:44:47 +0000
@@ -0,0 +1,62 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Coordinate a sequence of non overlapping TimedActionss."""
5
6__all__ = ['Timeline']
7
8__metaclass__ = type
9
10import datetime
11
12from pytz import utc as UTC
13
14from timedaction import TimedAction
15
16
17class OverlappingActionError(Exception):
18 """A new action was attempted without finishing the prior one."""
19 # To make analysis easy we do not permit overlapping actions: each
20 # action that is being timed and accrued must complete before the next
21 # is started. This means, for instance, that sending mail cannot do SQL
22 # queries, as both are timed and accrued. OTOH it makes analysis and
23 # serialisation of timelines simpler, and for the current use cases in
24 # Launchpad this is sufficient. This constraint should not be considered
25 # sacrosanct - if, in future, we desire timelines with overlapping actions,
26 # as long as the OOPS analysis code is extended to generate sensible
27 # reports in those situations, this can be changed.
28
29
30class Timeline:
31 """A sequence of TimedActions.
32
33 This is used for collecting expensive/external actions inside Launchpad
34 requests.
35
36 :ivar actions: The actions.
37 :ivar baseline: The point the timeline starts at.
38 """
39
40 def __init__(self, actions=None):
41 """Create a Timeline.
42
43 :param actions: An optional object to use to store the timeline. This
44 must implement the list protocol.
45 """
46 if actions is None:
47 actions = []
48 self.actions = actions
49 self.baseline = datetime.datetime.now(UTC)
50
51 def start(self, category, detail):
52 """Create a new TimedAction at the end of the timeline.
53
54 :param category: the category for the action.
55 :param detail: The detail for the action.
56 :return: A TimedAction for that category and detail.
57 """
58 result = TimedAction(category, detail, self)
59 if self.actions and self.actions[-1].duration is None:
60 raise OverlappingActionError(self.actions[-1], result)
61 self.actions.append(result)
62 return result