Merge lp:~vila/udd/608563-requeue-all-of-type into lp:udd

Proposed by Vincent Ladeuil
Status: Merged
Merged at revision: 413
Proposed branch: lp:~vila/udd/608563-requeue-all-of-type
Merge into: lp:udd
Diff against target: 203 lines (+121/-20)
2 files modified
icommon.py (+42/-20)
tests.py (+79/-0)
To merge this branch: bzr merge lp:~vila/udd/608563-requeue-all-of-type
Reviewer Review Type Date Requested Status
James Westby Approve
Review via email: mp+53527@code.launchpad.net

Description of the change

Basically the fix is only:

@@ -686,7 +706,7 @@
                             % self.FAILURES_TABLE).fetchall()
                     for row in rows:
                         this_raw_reason = row[1].encode("ascii", "replace")
- this_sig = self.failure_signature(raw_reason)
+ this_sig = self.failure_signature(this_raw_reason)
                         if this_sig == sig:
                             self._retry(c, row[0], sig, row[2],
                                     priority=priority)

I've added some tangentially related tests and found a couple of minor issues while investigating which I'd like to land too.

To post a comment you must log in.
420. By Vincent Ladeuil

Mark the bug as fixed

Revision history for this message
Vincent Ladeuil (vila) wrote :

Hmm, I was so surprised when I fixed the bug that I forgot to write a proper cover letter, here it is.

requeue_package.py --all-of-type <pkg_name> had a bug where all failed imports were requeued instead of only the ones with the same failure signature. This was due to a typo that makes the signature comparison always use the signature from <pkg_name>.

This patches fixes the typo and makes the signature more precise (since the full traceback is stored in the db this should only make things clearer and doesn't require updating the db itself).

There were also some dusty code in failure_signature that I got rid off (there can't be '\n' in the trace lines since they are produced by splitlines(), annotate and some more following across files revealed that this was needed when the traceback was acquired with readlines()).

Finally there was a bogus test against running_sentinel which *contains* a '\n' that should be ignored when comparing the signature from the db.

421. By Vincent Ladeuil

Explain the traceback crude parsing.

Revision history for this message
James Westby (james-w) wrote :

Excellent, thanks for fixing this.

Thanks,

James

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'icommon.py'
2--- icommon.py 2011-03-15 09:00:36 +0000
3+++ icommon.py 2011-03-16 10:29:18 +0000
4@@ -406,8 +406,8 @@
5 emailed integer default 0,
6 constraint isprimary PRIMARY KEY
7 (package))''' % FAILURES_TABLE
8- FAILURES_TABLE_FIND = '''select * from %s where package=?''' % FAILURES_TABLE
9- FAILURES_TABLE_DELETE = '''delete from %s where package=?''' % FAILURES_TABLE
10+ FAILURES_TABLE_FIND = 'select * from %s where package=?' % FAILURES_TABLE
11+ FAILURES_TABLE_DELETE = 'delete from %s where package=?' % FAILURES_TABLE
12
13 OLD_FAILURES_TABLE = "old_failures"
14 OLD_FAILURES_TABLE_CREATE = '''create table if not exists %s
15@@ -633,7 +633,7 @@
16 try:
17 row = c.execute(self.FAILURES_TABLE_FIND, (package,)).fetchone()
18 if row is None:
19- return row
20+ return None
21 return row[1]
22 finally:
23 self.conn.rollback()
24@@ -641,24 +641,49 @@
25
26 def failure_signature(self, raw_reason):
27 trace = raw_reason.splitlines()
28- sig = ''
29 if len(trace) == 1:
30- if trace[0] == running_sentinel:
31+ if trace[0] == running_sentinel[:-1]: # Get rid of the final '\n'
32 return None
33 # sometimes, Python exceptions do not have file references
34 m = re.match('(\w+): ', trace[0])
35 if m:
36 return m.group(1)
37 else:
38- return trace[0].strip().replace("\n", " ")
39+ return trace[0].strip()
40 elif len(trace) < 3:
41- return " ".join(trace).strip().replace("\n", " ")
42+ return " ".join(trace).strip()
43
44+ # If the failure reason is a traceback (which should always be the
45+ # case, the running_sentinel check above taking care of the still
46+ # running imports), we build the traceback signature and capture the
47+ # exception type
48+ sig = ''
49+ exc_type = ''
50+ exc_type_coming = False
51+ file_line_seen = False
52 for l in trace:
53 if l.startswith(' File'):
54+ # Keep the method/function name
55 sig += ':' + l.split()[-1]
56-
57- return trace[-1].split(':')[0].replace("\n", " ") + sig
58+ file_line_seen = True
59+ elif file_line_seen:
60+ # We've seen the 'File...' line, so we are now seeing the code
61+ # line
62+ exc_type_coming = True
63+ file_line_seen = False
64+ elif exc_type_coming:
65+ # We've seen the code line so we may find the exception line
66+ # itself now.
67+
68+ # We have no way to know if we are at the last line of the
69+ # traceback, but we can't rely on the execption always
70+ # displaying a single line either, so the last captured
71+ # exception will do.
72+ exc_type = l.split(':')[0]
73+ exc_type_coming = False
74+
75+ sig = exc_type + sig
76+ return sig
77
78 def retry(self, package, force=False, priority=False, auto=False,
79 all=False):
80@@ -686,7 +711,7 @@
81 % self.FAILURES_TABLE).fetchall()
82 for row in rows:
83 this_raw_reason = row[1].encode("ascii", "replace")
84- this_sig = self.failure_signature(raw_reason)
85+ this_sig = self.failure_signature(this_raw_reason)
86 if this_sig == sig:
87 self._retry(c, row[0], sig, row[2],
88 priority=priority)
89@@ -705,17 +730,14 @@
90 if row[4] > self.MAX_AUTO_RETRY_COUNT:
91 print ("Warning: %s has failed %d times in the same way"
92 % (package, row[4]))
93- c.execute('update %s set package=?, reason=?, '
94- 'when_failed=?, last_failed=?, failure_count=? '
95- 'where package=?'
96- % self.OLD_FAILURES_TABLE,
97- (package, signature, timestamp, timestamp, row[4]+1,
98- package))
99+ failure_count = row[4]+1
100 else:
101- c.execute('update %s set package=?, reason=?, when_failed=?, '
102- 'last_failed=?, failure_count=? where package=?'
103- % self.OLD_FAILURES_TABLE,
104- (package, signature, timestamp, timestamp, 1, package))
105+ failure_count = 1
106+ c.execute('update %s set package=?, reason=?, when_failed=?, '
107+ 'last_failed=?, failure_count=? where package=?'
108+ % self.OLD_FAILURES_TABLE,
109+ (package, signature, timestamp, timestamp, failure_count,
110+ package))
111 else:
112 c.execute('insert into %s values (?, ?, ?, ?, ?)'
113 % self.OLD_FAILURES_TABLE,
114
115=== modified file 'tests.py'
116--- tests.py 2011-02-23 18:23:37 +0000
117+++ tests.py 2011-03-16 10:29:18 +0000
118@@ -229,6 +229,85 @@
119 self.check_rows(0, 0, 2)
120
121
122+class StatusDatabaseTests(tests.TestCase):
123+
124+ def setUp(self):
125+ super(StatusDatabaseTests, self).setUp()
126+ self.db = icommon.StatusDatabase(":memory:")
127+
128+ def test_no_failures_in_empty_db(self):
129+ reasons, package_info = self.db.summarise_failures()
130+ self.assertEquals({}, reasons)
131+ self.assertEquals([], package_info)
132+
133+ def test_no_unemailed_failures_in_empty_db(self):
134+ self.assertEquals([], self.db.unemailed_failures())
135+
136+ def assertSignature(self, expected, raw):
137+ self.assertEquals(expected, self.db.failure_signature(raw))
138+
139+ def test_running_signature(self):
140+ # Running imports use a special failure signature
141+ self.assertSignature(None, icommon.running_sentinel)
142+
143+ def test_one_line_signature(self):
144+ self.assertSignature('AssertionError', 'AssertionError: xx')
145+
146+ def test_multi_line_signature(self):
147+ # We use a real-life example here, the apparent duplication of method
148+ # names is due to calls to the base class methods
149+ self.assertSignature(
150+ 'OperationalError:do_one_step:do_one_step'
151+ ':collect_terminated_threads'
152+ ':collect_terminated_threads:collect'
153+ ':finish_job:_set_failure',
154+ '''Traceback (most recent call last):
155+ File "/srv/package-import.canonical.com/new/scripts/mass_import.py", line 234, in do_one_step
156+ super(ImportDriver, self).do_one_step()
157+ File "/srv/package-import.canonical.com/new/scripts/icommon.py", line 2091, in do_one_step
158+ self.collect_terminated_threads()
159+ File "/srv/package-import.canonical.com/new/scripts/mass_import.py", line 253, in collect_terminated_threads
160+ super(ImportDriver, self).collect_terminated_threads()
161+ File "/srv/package-import.canonical.com/new/scripts/icommon.py", line 2119, in collect_terminated_threads
162+ t.collect()
163+ File "/srv/package-import.canonical.com/new/scripts/mass_import.py", line 162, in collect
164+ unicode_output.encode("utf-8", "replace"))
165+ File "/srv/package-import.canonical.com/new/scripts/icommon.py", line 552, in finish_job
166+ self._set_failure(c, package, output, now)
167+ File "/srv/package-import.canonical.com/new/scripts/icommon.py", line 485, in _set_failure
168+ % self.FAILURES_TABLE, (package, reason, now))
169+OperationalError: table failures has 4 columns but 3 values were supplied
170+''')
171+
172+ def test_regular_traceback(self):
173+ self.assertSignature(
174+ 'bzrlib.errors.NoSuchTag:lookup_tag',
175+ '''Traceback (most recent call last):
176+ File "/usr/lib/python2.5/site-packages/bzrlib/tag.py", line 109, in lookup_tag
177+ raise errors.NoSuchTag(tag_name)
178+bzrlib.errors.NoSuchTag: No such tag: upstream-4.6.2
179+''')
180+
181+ def test_traceback_with_verbose_execption(self):
182+ # make sure we get bzrlib.errors.NoFinalPath without being tricked by:
183+ # the final empty line, nor the 'file-id:' and 'root trans-id' lines.
184+ self.assertSignature(
185+ 'bzrlib.errors.NoFinalPath:get_path:_determine_path:final_name',
186+ ''''Traceback (most recent call last):
187+ File "/usr/lib/python2.5/site-packages/bzrlib/transform.py", line 2368, in get_path
188+ self._known_paths[trans_id] = self._determine_path(trans_id)
189+ File "/usr/lib/python2.5/site-packages/bzrlib/transform.py", line 2358, in _determine_path
190+ name = self.transform.final_name(trans_id)
191+ File "/usr/lib/python2.5/site-packages/bzrlib/transform.py", line 470, in final_name
192+ raise NoFinalPath(trans_id, self)
193+bzrlib.errors.NoFinalPath: No final name for trans_id 'new-18'
194+file-id: None
195+root trans-id: 'new-0'
196+
197+''')
198+
199+
200+
201 class FindEarliestMergeTests(tests.TestCaseWithTransport):
202
203 def test_tip_revision(self):

Subscribers

People subscribed via source and target branches