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
=== modified file 'lib/canonical/launchpad/webapp/adapter.py'
--- lib/canonical/launchpad/webapp/adapter.py 2011-08-22 19:06:39 +0000
+++ lib/canonical/launchpad/webapp/adapter.py 2011-08-26 13:16:48 +0000
@@ -661,6 +661,15 @@
661 threading.currentThread().lp_last_sql_statement = statement661 threading.currentThread().lp_last_sql_statement = statement
662 request_starttime = getattr(_local, 'request_start_time', None)662 request_starttime = getattr(_local, 'request_start_time', None)
663 if request_starttime is None:663 if request_starttime is None:
664 if (sql_trace is not None or
665 self._debug_sql or
666 self._debug_sql_extra):
667 # Stash some information for logging at the end of the
668 # request.
669 connection._lp_statement_info = (
670 time(),
671 'SQL-%s' % connection._database.name,
672 statement_to_log)
664 return673 return
665 action = get_request_timeline(get_current_browser_request()).start(674 action = get_request_timeline(get_current_browser_request()).start(
666 'SQL-%s' % connection._database.name, statement_to_log)675 'SQL-%s' % connection._database.name, statement_to_log)
@@ -673,15 +682,27 @@
673 # action may be None if the tracer was installed after the682 # action may be None if the tracer was installed after the
674 # statement was submitted.683 # statement was submitted.
675 action.finish()684 action.finish()
676 # Do data reporting for the [start|stop]_sql_traceback_logging685 # Do data reporting for the [start|stop]_sql_traceback_logging
677 # feature, as exposed by ++profile++sqltrace, and/or for stderr,686 # feature, as exposed by ++profile++sqltrace, and/or for stderr,
678 # as exposed by LP_DEBUG_SQL_EXTRA and LP_DEBUG_SQL.687 # as exposed by LP_DEBUG_SQL_EXTRA and LP_DEBUG_SQL.
679 sql_trace = getattr(_local, 'sql_trace', None)688 sql_trace = getattr(_local, 'sql_trace', None)
680 if sql_trace and sql_trace[-1]['sql'] is None:689 if sql_trace is not None or self._debug_sql or self._debug_sql_extra:
681 sql_trace[-1]['sql'] = action.logTuple()690 data = None
682 if self._debug_sql or self._debug_sql_extra:691 if action is not None:
683 sys.stderr.write('%d-%d@%s %s\n' % action.logTuple())692 data = action.logTuple()
684 sys.stderr.write("-" * 70 + "\n")693 else:
694 info = getattr(connection, '_lp_statement_info', None)
695 if info is not None:
696 start, dbname, statement = info
697 # Times are in milliseconds, to mirror actions.
698 duration = int((time() - start) * 1000)
699 data = (0, duration, dbname, statement)
700 if data is not None:
701 if sql_trace and sql_trace[-1]['sql'] is None:
702 sql_trace[-1]['sql'] = data
703 if self._debug_sql or self._debug_sql_extra:
704 sys.stderr.write('%d-%d@%s %s\n' % data)
705 sys.stderr.write("-" * 70 + "\n")
685706
686 def connection_raw_execute_error(self, connection, raw_cursor,707 def connection_raw_execute_error(self, connection, raw_cursor,
687 statement, params, error):708 statement, params, error):
688709
=== modified file 'lib/lp/services/profile/profile.py'
--- lib/lp/services/profile/profile.py 2011-08-22 19:06:39 +0000
+++ lib/lp/services/profile/profile.py 2011-08-26 13:16:48 +0000
@@ -16,12 +16,13 @@
16import heapq16import heapq
17import os17import os
18import pstats18import pstats
19import StringIO
20import sys
19import threading21import threading
20import StringIO
2122
22from bzrlib import lsprof23from bzrlib import lsprof
23import oops_datedir_repo.serializer_rfc82224import oops_datedir_repo.serializer_rfc822
24from zope.pagetemplate.pagetemplatefile import PageTemplateFile25from z3c.pt.pagetemplate import PageTemplateFile
25from zope.app.publication.interfaces import IEndRequestEvent26from zope.app.publication.interfaces import IEndRequestEvent
26from zope.component import (27from zope.component import (
27 adapter,28 adapter,
@@ -29,6 +30,7 @@
29 )30 )
30from zope.contenttype.parse import parse31from zope.contenttype.parse import parse
31from zope.error.interfaces import IErrorReportingUtility32from zope.error.interfaces import IErrorReportingUtility
33from zope.exceptions.exceptionformatter import format_exception
32from zope.traversing.namespace import view34from zope.traversing.namespace import view
3335
34from canonical.config import config36from canonical.config import config
@@ -344,7 +346,7 @@
344 del prof_stats346 del prof_stats
345 trace = None347 trace = None
346 if 'sqltrace' in actions:348 if 'sqltrace' in actions:
347 trace = da.stop_sql_traceback_logging()349 trace = da.stop_sql_traceback_logging() or ()
348 # The trace is a list of dicts, each with the keys "sql" and350 # The trace is a list of dicts, each with the keys "sql" and
349 # "stack". "sql" is a tuple of start time, stop time, database351 # "stack". "sql" is a tuple of start time, stop time, database
350 # name (with a "SQL-" prefix), and sql statement. "stack" is a352 # name (with a "SQL-" prefix), and sql statement. "stack" is a
@@ -451,7 +453,14 @@
451 if actions and is_html:453 if actions and is_html:
452 # Hack the new HTML in at the end of the page.454 # Hack the new HTML in at the end of the page.
453 encoding = content_type_params.get('charset', 'utf-8')455 encoding = content_type_params.get('charset', 'utf-8')
454 added_html = template(**template_context).encode(encoding)456 try:
457 added_html = template(**template_context).encode(encoding)
458 except (SystemExit, KeyboardInterrupt):
459 raise
460 except:
461 error = ''.join(format_exception(*sys.exc_info(), as_html=True))
462 added_html = (
463 '<div class="profiling_info">' + error + '</div>')
455 existing_html = request.response.consumeBody()464 existing_html = request.response.consumeBody()
456 e_start, e_close_body, e_end = existing_html.rpartition(465 e_start, e_close_body, e_end = existing_html.rpartition(
457 '</body>')466 '</body>')
458467
=== modified file 'lib/lp/services/stacktrace.py'
--- lib/lp/services/stacktrace.py 2011-08-22 19:06:39 +0000
+++ lib/lp/services/stacktrace.py 2011-08-26 13:16:48 +0000
@@ -20,6 +20,18 @@
20import traceback20import traceback
2121
22DEBUG_EXCEPTION_FORMATTER = False22DEBUG_EXCEPTION_FORMATTER = False
23EXPLOSIVE_ERRORS = (SystemExit, MemoryError, KeyboardInterrupt)
24
25
26def _try_except(callable, *args, **kwargs):
27 try:
28 return callable(*args, **kwargs)
29 except EXPLOSIVE_ERRORS:
30 raise
31 except:
32 if DEBUG_EXCEPTION_FORMATTER:
33 traceback.print_exc()
34 # return None
2335
2436
25def _get_frame(f):37def _get_frame(f):
@@ -85,7 +97,7 @@
85 item.append(supp['extra']) # We do not include a prefix.97 item.append(supp['extra']) # We do not include a prefix.
86 if info:98 if info:
87 item.append(_fmt(info))99 item.append(_fmt(info))
88 except (SystemExit, KeyboardInterrupt):100 except EXPLOSIVE_ERRORS:
89 raise101 raise
90 except:102 except:
91 # The values above may not stringify properly, or who knows what103 # The values above may not stringify properly, or who knows what
@@ -119,19 +131,11 @@
119131
120def _get_frame_data(f, lineno):132def _get_frame_data(f, lineno):
121 "Given a frame and a lineno, return data for each item of extract_*."133 "Given a frame and a lineno, return data for each item of extract_*."
122 co = f.f_code
123 filename = co.co_filename
124 name = co.co_name
125 linecache.checkcache(filename)
126 line = linecache.getline(filename, lineno, f.f_globals)
127 if line:
128 line = line.strip()
129 else:
130 line = None
131 # Adapted from zope.exceptions.134 # Adapted from zope.exceptions.
135 try_except = _try_except # This is a micro-optimization.
132 modname = f.f_globals.get('__name__')136 modname = f.f_globals.get('__name__')
133 # Output a traceback supplement, if any.137 # Output a traceback supplement, if any.
134 supplement = info = None138 supplement_dict = info = None
135 if '__traceback_supplement__' in f.f_locals:139 if '__traceback_supplement__' in f.f_locals:
136 # Use the supplement defined in the function.140 # Use the supplement defined in the function.
137 tbs = f.f_locals['__traceback_supplement__']141 tbs = f.f_locals['__traceback_supplement__']
@@ -143,42 +147,50 @@
143 if tbs is not None:147 if tbs is not None:
144 factory = tbs[0]148 factory = tbs[0]
145 args = tbs[1:]149 args = tbs[1:]
146 try:150 supplement = try_except(factory, *args)
147 supplement = factory(*args)151 if supplement is not None:
148 except (SystemExit, KeyboardInterrupt):
149 raise
150 except:
151 if DEBUG_EXCEPTION_FORMATTER:
152 traceback.print_exc()
153 # else just swallow the exception.
154 else:
155 # It might be nice if supplements could be dicts, for simplicity.152 # It might be nice if supplements could be dicts, for simplicity.
156 # Historically, though, they are objects.153 # Historically, though, they are objects.
157 # We will turn the supplement into a dict, so that we have154 # We will turn the supplement into a dict of strings, so that we
158 # "getInfo" pre-processed and so that we are not holding on to155 # have "getInfo" pre-processed and so that we are not holding on
159 # anything from the frame.156 # to anything from the frame.
160 extra = None157 extra = None
161 getInfo = getattr(supplement, 'getInfo', None)158 getInfo = getattr(supplement, 'getInfo', None)
162 if getInfo is not None:159 if getInfo is not None:
163 try:160 extra = try_except(getInfo)
164 extra = getInfo()161 extra = try_except(str, extra) if extra is not None else None
165 except (SystemExit, KeyboardInterrupt):162 warnings = []
166 raise163 # The outer try-except is for the iteration.
167 except:164 try:
168 if DEBUG_EXCEPTION_FORMATTER:165 for warning in getattr(supplement, 'warnings', ()):
169 traceback.print_exc()166 if warning is not None:
170 # else just swallow the exception.167 warning = try_except(str, warning)
171 supplement = dict(168 if warning is not None:
172 # The "_url" suffix is historical.169 warnings.append(warning)
173 source_url=getattr(supplement, 'source_url', None),170 except EXPLOSIVE_ERRORS:
174 line=getattr(supplement, 'line', None),171 raise
175 column=getattr(supplement, 'column', None),172 except:
176 expression=getattr(supplement, 'expression', None),173 if DEBUG_EXCEPTION_FORMATTER:
177 warnings=getattr(supplement, 'warnings', ()),174 traceback.print_exc()
178 extra=extra)175 supplement_dict = dict(warnings=warnings, extra=extra)
176 for key in ('source_url', 'line', 'column', 'expression'):
177 value = getattr(supplement, key, None)
178 if value is not None:
179 value = try_except(str, value)
180 supplement_dict[key] = value
179 info = f.f_locals.get('__traceback_info__', None)181 info = f.f_locals.get('__traceback_info__', None)
182 info = try_except(str, info) if info is not None else None
180 # End part adapted from zope.exceptions.183 # End part adapted from zope.exceptions.
181 return (filename, lineno, name, line, modname, supplement, info)184 co = f.f_code
185 filename = co.co_filename
186 name = co.co_name
187 linecache.checkcache(filename)
188 line = linecache.getline(filename, lineno, f.f_globals)
189 if line:
190 line = line.strip()
191 else:
192 line = None
193 return (filename, lineno, name, line, modname, supplement_dict, info)
182194
183195
184def extract_stack(f=None, limit=None):196def extract_stack(f=None, limit=None):
@@ -194,8 +206,9 @@
194 limit = _get_limit(limit)206 limit = _get_limit(limit)
195 list = []207 list = []
196 n = 0208 n = 0
209 get_frame_data = _get_frame_data # This is a micro-optimization.
197 while f is not None and (limit is None or n < limit):210 while f is not None and (limit is None or n < limit):
198 list.append(_get_frame_data(f, f.f_lineno))211 list.append(get_frame_data(f, f.f_lineno))
199 f = f.f_back212 f = f.f_back
200 n = n + 1213 n = n + 1
201 list.reverse()214 list.reverse()
@@ -220,8 +233,9 @@
220 limit = _get_limit(limit)233 limit = _get_limit(limit)
221 list = []234 list = []
222 n = 0235 n = 0
236 get_frame_data = _get_frame_data # This is a micro-optimization.
223 while tb is not None and (limit is None or n < limit):237 while tb is not None and (limit is None or n < limit):
224 list.append(_get_frame_data(tb.tb_frame, tb.tb_lineno))238 list.append(get_frame_data(tb.tb_frame, tb.tb_lineno))
225 tb = tb.tb_next239 tb = tb.tb_next
226 n = n + 1240 n = n + 1
227 return list241 return list
228242
=== modified file 'lib/lp/services/tests/test_stacktrace.py'
--- lib/lp/services/tests/test_stacktrace.py 2011-08-22 19:06:39 +0000
+++ lib/lp/services/tests/test_stacktrace.py 2011-08-26 13:16:48 +0000
@@ -16,6 +16,8 @@
16# the tests to pass.16# the tests to pass.
17MY_LINE_NUMBER = 1717MY_LINE_NUMBER = 17
1818
19MY_FILE_NAME = __file__[:__file__.rindex('.py')] + '.py'
20
1921
20class Supplement:22class Supplement:
21 def __init__(self, kwargs):23 def __init__(self, kwargs):
@@ -35,6 +37,12 @@
35 return sys._getframe()37 return sys._getframe()
3638
3739
40class BadString:
41
42 def __str__(self):
43 raise ValueError()
44
45
38class TestStacktrace(TestCase):46class TestStacktrace(TestCase):
3947
40 layer = BaseLayer48 layer = BaseLayer
@@ -71,7 +79,7 @@
71 def test_get_frame_data_standard(self):79 def test_get_frame_data_standard(self):
72 filename, lineno, name, line, modname, supplement, info = (80 filename, lineno, name, line, modname, supplement, info = (
73 stacktrace._get_frame_data(get_frame(), MY_LINE_NUMBER))81 stacktrace._get_frame_data(get_frame(), MY_LINE_NUMBER))
74 self.assertEqual(__file__, filename)82 self.assertEqual(MY_FILE_NAME, filename)
75 self.assertEqual(MY_LINE_NUMBER, lineno)83 self.assertEqual(MY_LINE_NUMBER, lineno)
76 self.assertEqual('get_frame', name)84 self.assertEqual('get_frame', name)
77 self.assertStartsWith(line, 'MY_LINE_NUMBER = ')85 self.assertStartsWith(line, 'MY_LINE_NUMBER = ')
@@ -91,7 +99,7 @@
91 get_frame(supplement={}), MY_LINE_NUMBER))99 get_frame(supplement={}), MY_LINE_NUMBER))
92 self.assertEqual(100 self.assertEqual(
93 dict(source_url=None, line=None, column=None, expression=None,101 dict(source_url=None, line=None, column=None, expression=None,
94 warnings=(), extra=None),102 warnings=[], extra=None),
95 supplement)103 supplement)
96104
97 def test_get_frame_data_supplement_and_info(self):105 def test_get_frame_data_supplement_and_info(self):
@@ -101,7 +109,7 @@
101 MY_LINE_NUMBER))109 MY_LINE_NUMBER))
102 self.assertEqual(110 self.assertEqual(
103 dict(source_url=None, line=None, column=None, expression=None,111 dict(source_url=None, line=None, column=None, expression=None,
104 warnings=(), extra=None),112 warnings=[], extra=None),
105 supplement)113 supplement)
106 self.assertEqual('foo bar', info)114 self.assertEqual('foo bar', info)
107115
@@ -119,9 +127,9 @@
119 )),127 )),
120 MY_LINE_NUMBER))128 MY_LINE_NUMBER))
121 self.assertEqual(129 self.assertEqual(
122 dict(source_url='/foo/bar.pt', line=42, column=84,130 dict(source_url='/foo/bar.pt', line='42', column='84',
123 expression='tal:define="foo view/foo"',131 expression='tal:define="foo view/foo"',
124 warnings=('watch out', 'pass auf'),132 warnings=['watch out', 'pass auf'],
125 extra='read all about it'),133 extra='read all about it'),
126 supplement)134 supplement)
127135
@@ -134,7 +142,7 @@
134 MY_LINE_NUMBER))142 MY_LINE_NUMBER))
135 self.assertEqual(143 self.assertEqual(
136 dict(source_url=None, line=None, column=None, expression=None,144 dict(source_url=None, line=None, column=None, expression=None,
137 warnings=(), extra=None),145 warnings=[], extra=None),
138 supplement)146 supplement)
139147
140 def test_get_frame_data_supplement_bad_getInfo_with_traceback(self):148 def test_get_frame_data_supplement_bad_getInfo_with_traceback(self):
@@ -153,16 +161,47 @@
153 stacktrace.DEBUG_EXCEPTION_FORMATTER = False161 stacktrace.DEBUG_EXCEPTION_FORMATTER = False
154 self.assertEqual(162 self.assertEqual(
155 dict(source_url=None, line=None, column=None, expression=None,163 dict(source_url=None, line=None, column=None, expression=None,
156 warnings=(), extra=None),164 warnings=[], extra=None),
157 supplement)165 supplement)
158 self.assertIn('boo_hiss', stderr.getvalue())166 self.assertIn('boo_hiss', stderr.getvalue())
159167
168 def test_get_frame_data_broken_str(self):
169 bad = BadString()
170 filename, lineno, name, line, modname, supplement, info = (
171 stacktrace._get_frame_data(
172 get_frame(
173 supplement=dict(
174 source_url=bad,
175 line=bad,
176 column=bad,
177 expression=bad,
178 warnings=('watch out', bad),
179 getInfo=lambda: bad
180 ),
181 info=bad),
182 MY_LINE_NUMBER))
183 self.assertEqual(
184 dict(source_url=None, line=None, column=None,
185 expression=None, warnings=['watch out'], extra=None),
186 supplement)
187 self.assertIs(None, info)
188
189 def test_get_frame_data_broken_warnings(self):
190 filename, lineno, name, line, modname, supplement, info = (
191 stacktrace._get_frame_data(
192 get_frame(
193 supplement=dict(
194 warnings=object()
195 )),
196 MY_LINE_NUMBER))
197 self.assertEqual([], supplement['warnings'])
198
160 def test_extract_stack(self):199 def test_extract_stack(self):
161 extracted = stacktrace.extract_stack(get_frame())200 extracted = stacktrace.extract_stack(get_frame())
162 self.assertTrue(len(extracted) > 1)201 self.assertTrue(len(extracted) > 1)
163 filename, lineno, name, line, modname, supplement, info = (202 filename, lineno, name, line, modname, supplement, info = (
164 extracted[-1])203 extracted[-1])
165 self.assertEqual(__file__, filename)204 self.assertEqual(MY_FILE_NAME, filename)
166 self.assertIsInstance(lineno, int)205 self.assertIsInstance(lineno, int)
167 self.assertEqual('get_frame', name)206 self.assertEqual('get_frame', name)
168 self.assertEqual('return sys._getframe()', line)207 self.assertEqual('return sys._getframe()', line)
@@ -179,7 +218,7 @@
179 self.assertEqual(1, len(extracted))218 self.assertEqual(1, len(extracted))
180 filename, lineno, name, line, modname, supplement, info = (219 filename, lineno, name, line, modname, supplement, info = (
181 extracted[0])220 extracted[0])
182 self.assertEqual(__file__, filename)221 self.assertEqual(MY_FILE_NAME, filename)
183 self.assertIsInstance(lineno, int)222 self.assertIsInstance(lineno, int)
184 self.assertEqual('test_extract_tb', name)223 self.assertEqual('test_extract_tb', name)
185 self.assertEqual('raise ValueError()', line)224 self.assertEqual('raise ValueError()', line)
@@ -195,7 +234,7 @@
195 self.assertEndsWith(line, '\n')234 self.assertEndsWith(line, '\n')
196 line = formatted[-1].split('\n')235 line = formatted[-1].split('\n')
197 self.assertStartsWith(236 self.assertStartsWith(
198 line[0], ' File "' + __file__ + '", line ')237 line[0], ' File "' + MY_FILE_NAME + '", line ')
199 self.assertEndsWith(line[0], ', in get_frame')238 self.assertEndsWith(line[0], ', in get_frame')
200 self.assertEqual(' return sys._getframe()', line[1])239 self.assertEqual(' return sys._getframe()', line[1])
201240
@@ -218,7 +257,7 @@
218 self.assertEndsWith(line, '\n')257 self.assertEndsWith(line, '\n')
219 line = formatted[-1].split('\n')258 line = formatted[-1].split('\n')
220 self.assertStartsWith(259 self.assertStartsWith(
221 line[0], ' File "' + __file__ + '", line ')260 line[0], ' File "' + MY_FILE_NAME + '", line ')
222 self.assertEndsWith(line[0], ', in get_frame')261 self.assertEndsWith(line[0], ', in get_frame')
223 self.assertEqual(' return sys._getframe()', line[1])262 self.assertEqual(' return sys._getframe()', line[1])
224 self.assertEqual(' - /foo/bar.pt', line[2])263 self.assertEqual(' - /foo/bar.pt', line[2])
@@ -234,13 +273,8 @@
234 self.assertEqual(' - I am the Walrus', line[8])273 self.assertEqual(' - I am the Walrus', line[8])
235274
236 def test_format_list_extra_errors(self):275 def test_format_list_extra_errors(self):
237 extracted = stacktrace.extract_stack(276 extracted = stacktrace.extract_stack(get_frame(supplement=dict()))
238 get_frame(277 extracted[-1][-2]['warnings'] = object() # This should never happen.
239 supplement=dict(
240 # This will cause an error because it is not iterable.
241 warnings=object()
242 ))
243 )
244 stderr = sys.stderr = StringIO.StringIO()278 stderr = sys.stderr = StringIO.StringIO()
245 self.assertFalse(stacktrace.DEBUG_EXCEPTION_FORMATTER)279 self.assertFalse(stacktrace.DEBUG_EXCEPTION_FORMATTER)
246 stacktrace.DEBUG_EXCEPTION_FORMATTER = True280 stacktrace.DEBUG_EXCEPTION_FORMATTER = True