Merge lp:~gary/launchpad/sqlprofiler into lp:launchpad

Proposed by Gary Poster
Status: Merged
Approved by: Gary Poster
Approved revision: no longer in the source branch.
Merged at revision: 13808
Proposed branch: lp:~gary/launchpad/sqlprofiler
Merge into: lp:launchpad
Diff against target: 432 lines (+151/-73)
4 files modified
lib/canonical/launchpad/webapp/adapter.py (+30/-9)
lib/lp/services/profile/profile.py (+13/-4)
lib/lp/services/stacktrace.py (+56/-42)
lib/lp/services/tests/test_stacktrace.py (+52/-18)
To merge this branch: bzr merge lp:~gary/launchpad/sqlprofiler
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+72765@code.launchpad.net

Commit message

[bug=833972,834466] [r=benji][bug=833972] Fix ++profile++sqltrace for homepage, others

Description of the change

This branch fixes a problem observed in the QA for bug 832215: ++profile++sqltrace would cause Apache to fall over if used on some pages, such as the Launchpad home page. The problem was that strings need to be rendered immediately. This branch does that, as well as adding a quick hack to see a traceback if something like this happens again when trying to see a ++profile++ page.

lint is happy.

Tests are there for all of the core stacktrace changes. I did not add tests for the emergency hatch in ++profile++ traceback rendering, which I added for diagnosis of this problem and left in because it was convenient.

QA: go to https://qastaging.launchpad.net/++profile++sqltrace and actually see that it works. If it doesn't, I suggest that we still mark the branch qa-ok even as we mark the failure in the bug: this should not affect production.

Thank you

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

This branch looks good. Here are a couple of little things:

I would add MemoryError to the (SystemExit, KeyboardInterrupt)
exceptions. Since that tuple is repeated several times you might want
to define a "constant" for it as well.

I don't understand the comment near the end of the diff: "This should
never happen."

review: Approve (code)
Revision history for this message
Gary Poster (gary) wrote :

Change made, thanks.

comment: because non-iterating warnings are caught earlier in the pipeline of functions now, the rendering code should not actually encounter this.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/webapp/adapter.py'
2--- lib/canonical/launchpad/webapp/adapter.py 2011-08-22 19:06:39 +0000
3+++ lib/canonical/launchpad/webapp/adapter.py 2011-08-26 13:16:48 +0000
4@@ -661,6 +661,15 @@
5 threading.currentThread().lp_last_sql_statement = statement
6 request_starttime = getattr(_local, 'request_start_time', None)
7 if request_starttime is None:
8+ if (sql_trace is not None or
9+ self._debug_sql or
10+ self._debug_sql_extra):
11+ # Stash some information for logging at the end of the
12+ # request.
13+ connection._lp_statement_info = (
14+ time(),
15+ 'SQL-%s' % connection._database.name,
16+ statement_to_log)
17 return
18 action = get_request_timeline(get_current_browser_request()).start(
19 'SQL-%s' % connection._database.name, statement_to_log)
20@@ -673,15 +682,27 @@
21 # action may be None if the tracer was installed after the
22 # statement was submitted.
23 action.finish()
24- # Do data reporting for the [start|stop]_sql_traceback_logging
25- # feature, as exposed by ++profile++sqltrace, and/or for stderr,
26- # as exposed by LP_DEBUG_SQL_EXTRA and LP_DEBUG_SQL.
27- sql_trace = getattr(_local, 'sql_trace', None)
28- if sql_trace and sql_trace[-1]['sql'] is None:
29- sql_trace[-1]['sql'] = action.logTuple()
30- if self._debug_sql or self._debug_sql_extra:
31- sys.stderr.write('%d-%d@%s %s\n' % action.logTuple())
32- sys.stderr.write("-" * 70 + "\n")
33+ # Do data reporting for the [start|stop]_sql_traceback_logging
34+ # feature, as exposed by ++profile++sqltrace, and/or for stderr,
35+ # as exposed by LP_DEBUG_SQL_EXTRA and LP_DEBUG_SQL.
36+ sql_trace = getattr(_local, 'sql_trace', None)
37+ if sql_trace is not None or self._debug_sql or self._debug_sql_extra:
38+ data = None
39+ if action is not None:
40+ data = action.logTuple()
41+ else:
42+ info = getattr(connection, '_lp_statement_info', None)
43+ if info is not None:
44+ start, dbname, statement = info
45+ # Times are in milliseconds, to mirror actions.
46+ duration = int((time() - start) * 1000)
47+ data = (0, duration, dbname, statement)
48+ if data is not None:
49+ if sql_trace and sql_trace[-1]['sql'] is None:
50+ sql_trace[-1]['sql'] = data
51+ if self._debug_sql or self._debug_sql_extra:
52+ sys.stderr.write('%d-%d@%s %s\n' % data)
53+ sys.stderr.write("-" * 70 + "\n")
54
55 def connection_raw_execute_error(self, connection, raw_cursor,
56 statement, params, error):
57
58=== modified file 'lib/lp/services/profile/profile.py'
59--- lib/lp/services/profile/profile.py 2011-08-22 19:06:39 +0000
60+++ lib/lp/services/profile/profile.py 2011-08-26 13:16:48 +0000
61@@ -16,12 +16,13 @@
62 import heapq
63 import os
64 import pstats
65+import StringIO
66+import sys
67 import threading
68-import StringIO
69
70 from bzrlib import lsprof
71 import oops_datedir_repo.serializer_rfc822
72-from zope.pagetemplate.pagetemplatefile import PageTemplateFile
73+from z3c.pt.pagetemplate import PageTemplateFile
74 from zope.app.publication.interfaces import IEndRequestEvent
75 from zope.component import (
76 adapter,
77@@ -29,6 +30,7 @@
78 )
79 from zope.contenttype.parse import parse
80 from zope.error.interfaces import IErrorReportingUtility
81+from zope.exceptions.exceptionformatter import format_exception
82 from zope.traversing.namespace import view
83
84 from canonical.config import config
85@@ -344,7 +346,7 @@
86 del prof_stats
87 trace = None
88 if 'sqltrace' in actions:
89- trace = da.stop_sql_traceback_logging()
90+ trace = da.stop_sql_traceback_logging() or ()
91 # The trace is a list of dicts, each with the keys "sql" and
92 # "stack". "sql" is a tuple of start time, stop time, database
93 # name (with a "SQL-" prefix), and sql statement. "stack" is a
94@@ -451,7 +453,14 @@
95 if actions and is_html:
96 # Hack the new HTML in at the end of the page.
97 encoding = content_type_params.get('charset', 'utf-8')
98- added_html = template(**template_context).encode(encoding)
99+ try:
100+ added_html = template(**template_context).encode(encoding)
101+ except (SystemExit, KeyboardInterrupt):
102+ raise
103+ except:
104+ error = ''.join(format_exception(*sys.exc_info(), as_html=True))
105+ added_html = (
106+ '<div class="profiling_info">' + error + '</div>')
107 existing_html = request.response.consumeBody()
108 e_start, e_close_body, e_end = existing_html.rpartition(
109 '</body>')
110
111=== modified file 'lib/lp/services/stacktrace.py'
112--- lib/lp/services/stacktrace.py 2011-08-22 19:06:39 +0000
113+++ lib/lp/services/stacktrace.py 2011-08-26 13:16:48 +0000
114@@ -20,6 +20,18 @@
115 import traceback
116
117 DEBUG_EXCEPTION_FORMATTER = False
118+EXPLOSIVE_ERRORS = (SystemExit, MemoryError, KeyboardInterrupt)
119+
120+
121+def _try_except(callable, *args, **kwargs):
122+ try:
123+ return callable(*args, **kwargs)
124+ except EXPLOSIVE_ERRORS:
125+ raise
126+ except:
127+ if DEBUG_EXCEPTION_FORMATTER:
128+ traceback.print_exc()
129+ # return None
130
131
132 def _get_frame(f):
133@@ -85,7 +97,7 @@
134 item.append(supp['extra']) # We do not include a prefix.
135 if info:
136 item.append(_fmt(info))
137- except (SystemExit, KeyboardInterrupt):
138+ except EXPLOSIVE_ERRORS:
139 raise
140 except:
141 # The values above may not stringify properly, or who knows what
142@@ -119,19 +131,11 @@
143
144 def _get_frame_data(f, lineno):
145 "Given a frame and a lineno, return data for each item of extract_*."
146- co = f.f_code
147- filename = co.co_filename
148- name = co.co_name
149- linecache.checkcache(filename)
150- line = linecache.getline(filename, lineno, f.f_globals)
151- if line:
152- line = line.strip()
153- else:
154- line = None
155 # Adapted from zope.exceptions.
156+ try_except = _try_except # This is a micro-optimization.
157 modname = f.f_globals.get('__name__')
158 # Output a traceback supplement, if any.
159- supplement = info = None
160+ supplement_dict = info = None
161 if '__traceback_supplement__' in f.f_locals:
162 # Use the supplement defined in the function.
163 tbs = f.f_locals['__traceback_supplement__']
164@@ -143,42 +147,50 @@
165 if tbs is not None:
166 factory = tbs[0]
167 args = tbs[1:]
168- try:
169- supplement = factory(*args)
170- except (SystemExit, KeyboardInterrupt):
171- raise
172- except:
173- if DEBUG_EXCEPTION_FORMATTER:
174- traceback.print_exc()
175- # else just swallow the exception.
176- else:
177+ supplement = try_except(factory, *args)
178+ if supplement is not None:
179 # It might be nice if supplements could be dicts, for simplicity.
180 # Historically, though, they are objects.
181- # We will turn the supplement into a dict, so that we have
182- # "getInfo" pre-processed and so that we are not holding on to
183- # anything from the frame.
184+ # We will turn the supplement into a dict of strings, so that we
185+ # have "getInfo" pre-processed and so that we are not holding on
186+ # to anything from the frame.
187 extra = None
188 getInfo = getattr(supplement, 'getInfo', None)
189 if getInfo is not None:
190- try:
191- extra = getInfo()
192- except (SystemExit, KeyboardInterrupt):
193- raise
194- except:
195- if DEBUG_EXCEPTION_FORMATTER:
196- traceback.print_exc()
197- # else just swallow the exception.
198- supplement = dict(
199- # The "_url" suffix is historical.
200- source_url=getattr(supplement, 'source_url', None),
201- line=getattr(supplement, 'line', None),
202- column=getattr(supplement, 'column', None),
203- expression=getattr(supplement, 'expression', None),
204- warnings=getattr(supplement, 'warnings', ()),
205- extra=extra)
206+ extra = try_except(getInfo)
207+ extra = try_except(str, extra) if extra is not None else None
208+ warnings = []
209+ # The outer try-except is for the iteration.
210+ try:
211+ for warning in getattr(supplement, 'warnings', ()):
212+ if warning is not None:
213+ warning = try_except(str, warning)
214+ if warning is not None:
215+ warnings.append(warning)
216+ except EXPLOSIVE_ERRORS:
217+ raise
218+ except:
219+ if DEBUG_EXCEPTION_FORMATTER:
220+ traceback.print_exc()
221+ supplement_dict = dict(warnings=warnings, extra=extra)
222+ for key in ('source_url', 'line', 'column', 'expression'):
223+ value = getattr(supplement, key, None)
224+ if value is not None:
225+ value = try_except(str, value)
226+ supplement_dict[key] = value
227 info = f.f_locals.get('__traceback_info__', None)
228+ info = try_except(str, info) if info is not None else None
229 # End part adapted from zope.exceptions.
230- return (filename, lineno, name, line, modname, supplement, info)
231+ co = f.f_code
232+ filename = co.co_filename
233+ name = co.co_name
234+ linecache.checkcache(filename)
235+ line = linecache.getline(filename, lineno, f.f_globals)
236+ if line:
237+ line = line.strip()
238+ else:
239+ line = None
240+ return (filename, lineno, name, line, modname, supplement_dict, info)
241
242
243 def extract_stack(f=None, limit=None):
244@@ -194,8 +206,9 @@
245 limit = _get_limit(limit)
246 list = []
247 n = 0
248+ get_frame_data = _get_frame_data # This is a micro-optimization.
249 while f is not None and (limit is None or n < limit):
250- list.append(_get_frame_data(f, f.f_lineno))
251+ list.append(get_frame_data(f, f.f_lineno))
252 f = f.f_back
253 n = n + 1
254 list.reverse()
255@@ -220,8 +233,9 @@
256 limit = _get_limit(limit)
257 list = []
258 n = 0
259+ get_frame_data = _get_frame_data # This is a micro-optimization.
260 while tb is not None and (limit is None or n < limit):
261- list.append(_get_frame_data(tb.tb_frame, tb.tb_lineno))
262+ list.append(get_frame_data(tb.tb_frame, tb.tb_lineno))
263 tb = tb.tb_next
264 n = n + 1
265 return list
266
267=== modified file 'lib/lp/services/tests/test_stacktrace.py'
268--- lib/lp/services/tests/test_stacktrace.py 2011-08-22 19:06:39 +0000
269+++ lib/lp/services/tests/test_stacktrace.py 2011-08-26 13:16:48 +0000
270@@ -16,6 +16,8 @@
271 # the tests to pass.
272 MY_LINE_NUMBER = 17
273
274+MY_FILE_NAME = __file__[:__file__.rindex('.py')] + '.py'
275+
276
277 class Supplement:
278 def __init__(self, kwargs):
279@@ -35,6 +37,12 @@
280 return sys._getframe()
281
282
283+class BadString:
284+
285+ def __str__(self):
286+ raise ValueError()
287+
288+
289 class TestStacktrace(TestCase):
290
291 layer = BaseLayer
292@@ -71,7 +79,7 @@
293 def test_get_frame_data_standard(self):
294 filename, lineno, name, line, modname, supplement, info = (
295 stacktrace._get_frame_data(get_frame(), MY_LINE_NUMBER))
296- self.assertEqual(__file__, filename)
297+ self.assertEqual(MY_FILE_NAME, filename)
298 self.assertEqual(MY_LINE_NUMBER, lineno)
299 self.assertEqual('get_frame', name)
300 self.assertStartsWith(line, 'MY_LINE_NUMBER = ')
301@@ -91,7 +99,7 @@
302 get_frame(supplement={}), MY_LINE_NUMBER))
303 self.assertEqual(
304 dict(source_url=None, line=None, column=None, expression=None,
305- warnings=(), extra=None),
306+ warnings=[], extra=None),
307 supplement)
308
309 def test_get_frame_data_supplement_and_info(self):
310@@ -101,7 +109,7 @@
311 MY_LINE_NUMBER))
312 self.assertEqual(
313 dict(source_url=None, line=None, column=None, expression=None,
314- warnings=(), extra=None),
315+ warnings=[], extra=None),
316 supplement)
317 self.assertEqual('foo bar', info)
318
319@@ -119,9 +127,9 @@
320 )),
321 MY_LINE_NUMBER))
322 self.assertEqual(
323- dict(source_url='/foo/bar.pt', line=42, column=84,
324+ dict(source_url='/foo/bar.pt', line='42', column='84',
325 expression='tal:define="foo view/foo"',
326- warnings=('watch out', 'pass auf'),
327+ warnings=['watch out', 'pass auf'],
328 extra='read all about it'),
329 supplement)
330
331@@ -134,7 +142,7 @@
332 MY_LINE_NUMBER))
333 self.assertEqual(
334 dict(source_url=None, line=None, column=None, expression=None,
335- warnings=(), extra=None),
336+ warnings=[], extra=None),
337 supplement)
338
339 def test_get_frame_data_supplement_bad_getInfo_with_traceback(self):
340@@ -153,16 +161,47 @@
341 stacktrace.DEBUG_EXCEPTION_FORMATTER = False
342 self.assertEqual(
343 dict(source_url=None, line=None, column=None, expression=None,
344- warnings=(), extra=None),
345+ warnings=[], extra=None),
346 supplement)
347 self.assertIn('boo_hiss', stderr.getvalue())
348
349+ def test_get_frame_data_broken_str(self):
350+ bad = BadString()
351+ filename, lineno, name, line, modname, supplement, info = (
352+ stacktrace._get_frame_data(
353+ get_frame(
354+ supplement=dict(
355+ source_url=bad,
356+ line=bad,
357+ column=bad,
358+ expression=bad,
359+ warnings=('watch out', bad),
360+ getInfo=lambda: bad
361+ ),
362+ info=bad),
363+ MY_LINE_NUMBER))
364+ self.assertEqual(
365+ dict(source_url=None, line=None, column=None,
366+ expression=None, warnings=['watch out'], extra=None),
367+ supplement)
368+ self.assertIs(None, info)
369+
370+ def test_get_frame_data_broken_warnings(self):
371+ filename, lineno, name, line, modname, supplement, info = (
372+ stacktrace._get_frame_data(
373+ get_frame(
374+ supplement=dict(
375+ warnings=object()
376+ )),
377+ MY_LINE_NUMBER))
378+ self.assertEqual([], supplement['warnings'])
379+
380 def test_extract_stack(self):
381 extracted = stacktrace.extract_stack(get_frame())
382 self.assertTrue(len(extracted) > 1)
383 filename, lineno, name, line, modname, supplement, info = (
384 extracted[-1])
385- self.assertEqual(__file__, filename)
386+ self.assertEqual(MY_FILE_NAME, filename)
387 self.assertIsInstance(lineno, int)
388 self.assertEqual('get_frame', name)
389 self.assertEqual('return sys._getframe()', line)
390@@ -179,7 +218,7 @@
391 self.assertEqual(1, len(extracted))
392 filename, lineno, name, line, modname, supplement, info = (
393 extracted[0])
394- self.assertEqual(__file__, filename)
395+ self.assertEqual(MY_FILE_NAME, filename)
396 self.assertIsInstance(lineno, int)
397 self.assertEqual('test_extract_tb', name)
398 self.assertEqual('raise ValueError()', line)
399@@ -195,7 +234,7 @@
400 self.assertEndsWith(line, '\n')
401 line = formatted[-1].split('\n')
402 self.assertStartsWith(
403- line[0], ' File "' + __file__ + '", line ')
404+ line[0], ' File "' + MY_FILE_NAME + '", line ')
405 self.assertEndsWith(line[0], ', in get_frame')
406 self.assertEqual(' return sys._getframe()', line[1])
407
408@@ -218,7 +257,7 @@
409 self.assertEndsWith(line, '\n')
410 line = formatted[-1].split('\n')
411 self.assertStartsWith(
412- line[0], ' File "' + __file__ + '", line ')
413+ line[0], ' File "' + MY_FILE_NAME + '", line ')
414 self.assertEndsWith(line[0], ', in get_frame')
415 self.assertEqual(' return sys._getframe()', line[1])
416 self.assertEqual(' - /foo/bar.pt', line[2])
417@@ -234,13 +273,8 @@
418 self.assertEqual(' - I am the Walrus', line[8])
419
420 def test_format_list_extra_errors(self):
421- extracted = stacktrace.extract_stack(
422- get_frame(
423- supplement=dict(
424- # This will cause an error because it is not iterable.
425- warnings=object()
426- ))
427- )
428+ extracted = stacktrace.extract_stack(get_frame(supplement=dict()))
429+ extracted[-1][-2]['warnings'] = object() # This should never happen.
430 stderr = sys.stderr = StringIO.StringIO()
431 self.assertFalse(stacktrace.DEBUG_EXCEPTION_FORMATTER)
432 stacktrace.DEBUG_EXCEPTION_FORMATTER = True