Merge lp:~gmb/launchpad/stored-proc-for-bug-heat-bug-582195 into lp:launchpad/db-devel
- stored-proc-for-bug-heat-bug-582195
- Merge into db-devel
Status: | Merged |
---|---|
Merged at revision: | 9414 |
Proposed branch: | lp:~gmb/launchpad/stored-proc-for-bug-heat-bug-582195 |
Merge into: | lp:launchpad/db-devel |
Diff against target: |
387 lines (+211/-33) 6 files modified
database/schema/security.cfg (+1/-0) database/schema/trusted.sql (+115/-0) lib/lp/bugs/configure.zcml (+4/-2) lib/lp/bugs/doc/bug-heat.txt (+66/-20) lib/lp/bugs/interfaces/bug.py (+3/-0) lib/lp/bugs/model/bug.py (+22/-11) |
To merge this branch: | bzr merge lp:~gmb/launchpad/stored-proc-for-bug-heat-bug-582195 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Curtis Hovey (community) | rc | Approve | |
Björn Tillenius (community) | db | Approve | |
Stuart Bishop (community) | db | Approve | |
Canonical Launchpad Engineering | code | Pending | |
Review via email: mp+26064@code.launchpad.net |
This proposal supersedes a proposal from 2010-05-26.
Commit message
Add a stored procedure to update bug heat.
Description of the change
This branch takes the code in lib/lp/
The idea behind this is that we need to do bug heat calculations in the DB rather than in code, since the setup and teardown surrounding doing the calculations in code is far to slow and cumbersome.
I've added an updateHeat() method to IBug, which calls the new stored procedure. I've used this in setPrivate() and setSecurity() to replace the manual adding-
In psql:
SELECT calculate_
In iharness:
>>> from lp.bugs.
>>> bug_1 = getUtility(
>>> calculator = BugHeatCalculat
>>> calculator.
Björn Tillenius (bjornt) wrote : Posted in a previous version of this proposal | # |
Graham Binns (gmb) wrote : Posted in a previous version of this proposal | # |
2010/5/26 Björn Tillenius <email address hidden>:
> On Wed, May 26, 2010 at 10:50:49AM -0000, Graham Binns wrote:
>> The stored procedure is currently not hooked up to anything, but once
>> the patch is applied it's easy to check the results of the stored
>> procedure against the existing code using psql and an iharness session,
>> thus:
>
> Why not hook it up directly? It would be useful to have this tested,
> since it's hard to check that it's doing what it should be doing.
>
I was going to do it in a subsequent branch, but since it's now late
in stub's day anyway I'll do it in this one and resubmit.
--
Graham Binns | PGP Key: EC66FA7D
Graham Binns (gmb) wrote : | # |
Resubmitted. I've created an updateHeat() method on IBug which calls the stored procedure to update the bug's heat. I've hooked that up to all the sites which currently updated heat immediately (i.e. Bug.setPrivate() and setSecurity()). I'll start another branch to completely replace all the uses of CalculateBugHeatJob since that's likely to be several hundred lines long and would almost definitely exceed the limit were I to do it here.
Stuart Bishop (stub) wrote : | # |
On Wed, May 26, 2010 at 10:26 PM, Graham Binns <email address hidden> wrote:
> === added file 'database/
There is no need for a database patch - the stored procedure should be
added to trusted.sql (which is becoming unwieldy - I think I need to
split it up).
> +CREATE OR REPLACE FUNCTION calculate_
> +LANGUAGE plpythonu AS $$
> + from datetime import datetime
> +
> + class BugHeatConstants:
> + PRIVACY = 150
> + SECURITY = 250
> + DUPLICATE = 6
> + AFFECTED_USER = 4
> + SUBSCRIBER = 2
> +
> +
> + def get_max_
> + bug_tasks = plpy.execute(
> + "SELECT * FROM BugTask WHERE bug = %s" % bug_id)
> +
> + max_heats = []
> + for bug_task in bug_tasks:
> + if bug_task['product'] is not None:
> + product = plpy.execute(
> + "SELECT max_bug_heat FROM Product WHERE id = %s" %
> + bug_task[
> + max_heats.
> + elif bug_task[
> + distribution = plpy.execute(
> + "SELECT max_bug_heat FROM Distribution WHERE id = %s" %
> + bug_task[
> + max_heats.
> + elif bug_task[
> + product_series = plpy.execute("""
> + SELECT Product.
> + FROM ProductSeries, Product
> + WHERE ProductSeries.
> + AND ProductSeries.id = %s"""%
> + bug_task[
> + max_heats.
> + elif bug_task[
> + distro_series = plpy.execute("""
> + SELECT Distribution.
> + FROM DistroSeries, Distribution
> + WHERE DistroSeries.
> + AND DistroSeries.id = %s"""%
> + bug_task[
> + max_heats.
> + else:
> + pass
> +
> + return max(max_heats)
You can do this in a single query rather than one per task:
SELECT MAX(GREATEST(
FROM BugTask
LEFT OUTER JOIN ProductSeries ON BugTask.
LEFT OUTER JOIN Product ON (
BugTask.product = Product.id
OR ProductSeries.
LEFT OUTER JOIN DistroSeries ON BugTask.
LEFT OUTER JOIN Distribution ON (
BugTask.
OR DistroSeries.
WHERE
BugTask.bug = 1
> + # It would be nice to be able to just SELECT * here, but we need to
> + # format the timestamps so that datetime.strptime() won't choke on
> + # them.
Using epoch time would avoid the need for parsing strings (use
'EXTRACT(epoch FROM datecreated)' i...
Stuart Bishop (stub) wrote : | # |
On Wed, May 26, 2010 at 10:26 PM, Graham Binns <email address hidden> wrote:
> === added file 'database/
There is no need for a database patch - the stored procedure should be added to trusted.sql (which is becoming unwieldy - I think I need to split it up).
> +CREATE OR REPLACE FUNCTION calculate_
> +LANGUAGE plpythonu AS $$
> + from datetime import datetime
> +
> + class BugHeatConstants:
> + PRIVACY = 150
> + SECURITY = 250
> + DUPLICATE = 6
> + AFFECTED_USER = 4
> + SUBSCRIBER = 2
> +
> +
> + def get_max_
> + bug_tasks = plpy.execute(
> + "SELECT * FROM BugTask WHERE bug = %s" % bug_id)
> +
> + max_heats = []
> + for bug_task in bug_tasks:
> + if bug_task['product'] is not None:
> + product = plpy.execute(
> + "SELECT max_bug_heat FROM Product WHERE id = %s" %
> + bug_task[
> + max_heats.
> + elif bug_task[
> + distribution = plpy.execute(
> + "SELECT max_bug_heat FROM Distribution WHERE id = %s" %
> + bug_task[
> + max_heats.
> + elif bug_task[
> + product_series = plpy.execute("""
> + SELECT Product.
> + FROM ProductSeries, Product
> + WHERE ProductSeries.
> + AND ProductSeries.id = %s"""%
> + bug_task[
> + max_heats.
> + elif bug_task[
> + distro_series = plpy.execute("""
> + SELECT Distribution.
> + FROM DistroSeries, Distribution
> + WHERE DistroSeries.
> + AND DistroSeries.id = %s"""%
> + bug_task[
> + max_heats.
> + else:
> + pass
> +
> + return max(max_heats)
You can do this in a single query rather than one per task:
SELECT MAX(GREATEST(
FROM BugTask
LEFT OUTER JOIN ProductSeries ON BugTask.
LEFT OUTER JOIN Product ON (
BugTask.product = Product.id
OR ProductSeries.
LEFT OUTER JOIN DistroSeries ON BugTask.
LEFT OUTER JOIN Distribution ON (
BugTask.
OR DistroSeries.
WHERE
BugTask.bug = 1
> + # It would be nice to be able to just SELECT * here, but we need to
> + # format the timestamps so that datetime.strptime() won't choke on
> + # them.
Using epoch time would avoid the need for parsing strings (use 'EXTRACT(epoch FROM datecreated)' i...
Graham Binns (gmb) wrote : | # |
On Thu, May 27, 2010 at 05:13:27AM -0000, Stuart Bishop wrote:
> Review: Needs Fixing db
> On Wed, May 26, 2010 at 10:26 PM, Graham Binns <email address hidden> wrote:
>
> > === added file 'database/
>
> There is no need for a database patch - the stored procedure should be added to trusted.sql (which is becoming unwieldy - I think I need to split it up).
Righto, I've moved it.
>[snip]
>
> You can do this in a single query rather than one per task:
>
> SELECT MAX(GREATEST(
> FROM BugTask
> LEFT OUTER JOIN ProductSeries ON BugTask.
> LEFT OUTER JOIN Product ON (
> BugTask.product = Product.id
> OR ProductSeries.
> LEFT OUTER JOIN DistroSeries ON BugTask.
> LEFT OUTER JOIN Distribution ON (
> BugTask.
> OR DistroSeries.
> WHERE
> BugTask.bug = 1
>
Sweet; fixed.
> > + # It would be nice to be able to just SELECT * here, but we need to
> > + # format the timestamps so that datetime.strptime() won't choke on
> > + # them.
>
> Using epoch time would avoid the need for parsing strings (use
> 'EXTRACT(epoch FROM datecreated)' instead of 'TO_CHAR(...)'). It
> probably doesn't save any significant time though.
I've used epoch time instead (see below).
>
> > + if bug_data.nrows() == 0:
> > + return 0
>
> I'd return None or raise an exception here - the bug doesn't exist so
> the heat is undefined.
I've gone for raising an exception; is it worth defining an exception
subclass to use here or is Exception() okay?
> > + bug = bug_data[0]
> > + if bug['duplicateof'] is not None:
> > + return 0
>
> Again, None or the heat of the master bug. We should catch cases where
> we are attempting to calculate the bug heat of non-existent or
> duplicate bugs.
>
Okay. I'll update Bug.updateHeat() so that it doesn't try calling this
function if the bug is a duplicate and have this return None.
> > + if bug['private']:
> > + heat['privacy'] = BugHeatConstant
> > + if bug['security_
> > + heat['security'] = BugHeatConstant
> > +
> > + # Get the heat from subscribers.
> > + sub_count = plpy.execute("""
> > + SELECT bug, COUNT(id) AS sub_count
> > + FROM BugSubscription
> > + WHERE bug = %s
> > + GROUP BY bug
> > + """ % bug_id)
>
> This is unnecessarily complex. The simpler query is just "SELECT COUNT(*) FROM BugSubscription WHERE bug=%s".
>
Right; this is because I ripped it off from the query we were using to
update the bugs' heat manually :). Fixed now.
> > + if sub_count.nrows() > 0:
> > + heat['subscribers'] = (
> > + BugHeatConstant
>
> You don't need the nrows() guard, as the query will always return one row.
Ok.
> > + # Get the heat from subscribers via duplicates.
> > + subs_from_dupes = plpy.execute("""
> > + SELECT bug.duplicateof AS dupe_of,
> > + COUNT(bugsubscr
Stuart Bishop (stub) wrote : | # |
On Thu, May 27, 2010 at 6:42 PM, Graham Binns <email address hidden> wrote:
> I've gone for raising an exception; is it worth defining an exception
> subclass to use here or is Exception() okay?
Exception() is fine. Nothing outside this function would know about
your custom exception anyway.
>> You sure about that? Storm should be applying updates in order, so
>> when calculate_bug_heat is actually executed (at commit time or when
>> heat is next accessed) changes will be implicitly flushed.
>
> Hmm. I put the flush() in there because in testing it seemed like heat
> lagged behind where it should be. If I remove this, I get this result:
> So I think this is definitely necessary (or maybe it's just necessary
> for testing; I don't know. What are your recommendations?)
Your empirical results beat my fantasy, so leave it in.
Please change 'LANGUAGE plpythonu' to 'LANGUAGE plpythonu STABLE
RETURNS NULL ON NULL INPUT'
--
Stuart Bishop <email address hidden>
http://
Stuart Bishop (stub) : | # |
Graham Binns (gmb) wrote : | # |
On 27 May 2010 15:33, Stuart Bishop <email address hidden> wrote:
> On Thu, May 27, 2010 at 6:42 PM, Graham Binns <email address hidden> wrote:
>
>> I've gone for raising an exception; is it worth defining an exception
>> subclass to use here or is Exception() okay?
>
> Exception() is fine. Nothing outside this function would know about
> your custom exception anyway.
Cool.
>>> You sure about that? Storm should be applying updates in order, so
>>> when calculate_bug_heat is actually executed (at commit time or when
>>> heat is next accessed) changes will be implicitly flushed.
>>
>> Hmm. I put the flush() in there because in testing it seemed like heat
>> lagged behind where it should be. If I remove this, I get this result:
>
>> So I think this is definitely necessary (or maybe it's just necessary
>> for testing; I don't know. What are your recommendations?)
>
> Your empirical results beat my fantasy, so leave it in.
Fair enough.
> Please change 'LANGUAGE plpythonu' to 'LANGUAGE plpythonu STABLE
> RETURNS NULL ON NULL INPUT'
Done.
Björn Tillenius (bjornt) : | # |
Curtis Hovey (sinzui) wrote : | # |
Thanks for asking for the RC before PQM closes. I do not want to delay QA of this.
Preview Diff
1 | === modified file 'database/schema/security.cfg' |
2 | --- database/schema/security.cfg 2010-05-20 13:59:11 +0000 |
3 | +++ database/schema/security.cfg 2010-05-27 14:41:36 +0000 |
4 | @@ -15,6 +15,7 @@ |
5 | type=group |
6 | public.activity() = EXECUTE |
7 | public.person_sort_key(text, text) = EXECUTE |
8 | +public.calculate_bug_heat(integer) = EXECUTE |
9 | public.debversion_sort_key(text) = EXECUTE |
10 | public.null_count(anyarray) = EXECUTE |
11 | public.valid_name(text) = EXECUTE |
12 | |
13 | === modified file 'database/schema/trusted.sql' |
14 | --- database/schema/trusted.sql 2010-04-29 12:52:45 +0000 |
15 | +++ database/schema/trusted.sql 2010-05-27 14:41:36 +0000 |
16 | @@ -1585,3 +1585,118 @@ |
17 | RETURN NULL; -- Ignored - this is an AFTER trigger |
18 | END; |
19 | $$; |
20 | + |
21 | + |
22 | +CREATE OR REPLACE FUNCTION calculate_bug_heat(bug_id integer) RETURNS integer |
23 | +LANGUAGE plpythonu STABLE RETURNS NULL ON NULL INPUT AS $$ |
24 | + from datetime import datetime |
25 | + |
26 | + class BugHeatConstants: |
27 | + PRIVACY = 150 |
28 | + SECURITY = 250 |
29 | + DUPLICATE = 6 |
30 | + AFFECTED_USER = 4 |
31 | + SUBSCRIBER = 2 |
32 | + |
33 | + |
34 | + def get_max_heat_for_bug(bug_id): |
35 | + results = plpy.execute(""" |
36 | + SELECT MAX( |
37 | + GREATEST(Product.max_bug_heat, Distribution.max_bug_heat)) |
38 | + AS max_heat |
39 | + FROM BugTask |
40 | + LEFT OUTER JOIN ProductSeries ON |
41 | + BugTask.productseries = ProductSeries.id |
42 | + LEFT OUTER JOIN Product ON ( |
43 | + BugTask.product = Product.id |
44 | + OR ProductSeries.product = Product.id) |
45 | + LEFT OUTER JOIN DistroSeries ON |
46 | + BugTask.distroseries = DistroSeries.id |
47 | + LEFT OUTER JOIN Distribution ON ( |
48 | + BugTask.distribution = Distribution.id |
49 | + OR DistroSeries.distribution = Distribution.id) |
50 | + WHERE |
51 | + BugTask.bug = %s""" % bug_id) |
52 | + |
53 | + return results[0]['max_heat'] |
54 | + |
55 | + # It would be nice to be able to just SELECT * here, but we need the |
56 | + # timestamps to be in a format that datetime.fromtimestamp() will |
57 | + # understand. |
58 | + bug_data = plpy.execute(""" |
59 | + SELECT |
60 | + duplicateof, |
61 | + private, |
62 | + security_related, |
63 | + number_of_duplicates, |
64 | + users_affected_count, |
65 | + EXTRACT(epoch from datecreated) |
66 | + AS timestamp_date_created, |
67 | + EXTRACT(epoch from date_last_updated) |
68 | + AS timestamp_date_last_updated, |
69 | + EXTRACT(epoch from date_last_message) |
70 | + AS timestamp_date_last_message |
71 | + FROM Bug WHERE id = %s""" % bug_id) |
72 | + |
73 | + if bug_data.nrows() == 0: |
74 | + raise Exception("Bug %s doesn't exist." % bug_id) |
75 | + |
76 | + bug = bug_data[0] |
77 | + if bug['duplicateof'] is not None: |
78 | + return None |
79 | + |
80 | + heat = {} |
81 | + heat['dupes'] = ( |
82 | + BugHeatConstants.DUPLICATE * bug['number_of_duplicates']) |
83 | + heat['affected_users'] = ( |
84 | + BugHeatConstants.AFFECTED_USER * |
85 | + bug['users_affected_count']) |
86 | + |
87 | + if bug['private']: |
88 | + heat['privacy'] = BugHeatConstants.PRIVACY |
89 | + if bug['security_related']: |
90 | + heat['security'] = BugHeatConstants.SECURITY |
91 | + |
92 | + # Get the heat from subscribers, both direct and via duplicates. |
93 | + subs_from_dupes = plpy.execute(""" |
94 | + SELECT COUNT(DISTINCT BugSubscription.person) AS sub_count |
95 | + FROM BugSubscription, Bug |
96 | + WHERE Bug.id = BugSubscription.bug |
97 | + AND (Bug.id = %s OR Bug.duplicateof = %s)""" |
98 | + % (bug_id, bug_id)) |
99 | + |
100 | + heat['subcribers'] = ( |
101 | + BugHeatConstants.SUBSCRIBER |
102 | + * subs_from_dupes[0]['sub_count']) |
103 | + |
104 | + total_heat = sum(heat.values()) |
105 | + |
106 | + # Bugs decay over time. Every day the bug isn't touched its heat |
107 | + # decreases by 1%. |
108 | + date_last_updated = datetime.fromtimestamp( |
109 | + bug['timestamp_date_last_updated']) |
110 | + days_since_last_update = (datetime.utcnow() - date_last_updated).days |
111 | + total_heat = int(total_heat * (0.99 ** days_since_last_update)) |
112 | + |
113 | + if days_since_last_update > 0: |
114 | + # Bug heat increases by a quarter of the maximum bug heat |
115 | + # divided by the number of days since the bug's creation date. |
116 | + date_created = datetime.fromtimestamp( |
117 | + bug['timestamp_date_created']) |
118 | + |
119 | + if bug['timestamp_date_last_message'] is not None: |
120 | + date_last_message = datetime.fromtimestamp( |
121 | + bug['timestamp_date_last_message']) |
122 | + oldest_date = max(date_last_updated, date_last_message) |
123 | + else: |
124 | + date_last_message = None |
125 | + oldest_date = date_last_updated |
126 | + |
127 | + days_since_last_activity = (datetime.utcnow() - oldest_date).days |
128 | + days_since_created = (datetime.utcnow() - date_created).days |
129 | + max_heat = get_max_heat_for_bug(bug_id) |
130 | + if max_heat is not None and days_since_created > 0: |
131 | + return total_heat + (max_heat * 0.25 / days_since_created) |
132 | + |
133 | + return int(total_heat) |
134 | +$$; |
135 | |
136 | === modified file 'lib/lp/bugs/configure.zcml' |
137 | --- lib/lp/bugs/configure.zcml 2010-05-19 14:03:18 +0000 |
138 | +++ lib/lp/bugs/configure.zcml 2010-05-27 14:41:36 +0000 |
139 | @@ -691,7 +691,8 @@ |
140 | unlinkHWSubmission |
141 | linkBranch |
142 | unlinkBranch |
143 | - linkAttachment" |
144 | + linkAttachment |
145 | + updateHeat" |
146 | set_attributes=" |
147 | activity initial_message |
148 | activityscore |
149 | @@ -720,7 +721,8 @@ |
150 | permission="launchpad.Admin" |
151 | attributes=" |
152 | setCommentVisibility |
153 | - setHeat"/> |
154 | + setHeat" |
155 | + set_attributes="heat_last_updated" /> |
156 | </class> |
157 | <adapter |
158 | for="lp.bugs.interfaces.bug.IBug" |
159 | |
160 | === modified file 'lib/lp/bugs/doc/bug-heat.txt' |
161 | --- lib/lp/bugs/doc/bug-heat.txt 2010-04-14 12:55:44 +0000 |
162 | +++ lib/lp/bugs/doc/bug-heat.txt 2010-05-27 14:41:36 +0000 |
163 | @@ -5,50 +5,92 @@ |
164 | problematic a given bug is to the community and can be used to determine |
165 | which bugs should be tackled first. |
166 | |
167 | -A new bug will have a heat of zero. |
168 | +A bug's heat is calculated automatically when it is created. |
169 | |
170 | >>> bug_owner = factory.makePerson() |
171 | >>> bug = factory.makeBug(owner=bug_owner) |
172 | >>> bug.heat |
173 | - 0 |
174 | - |
175 | -It will also have a heat_last_updated of None. |
176 | - |
177 | - >>> print bug.heat_last_updated |
178 | - None |
179 | - |
180 | -The bug's heat can be set by calling its setHeat() method. |
181 | + 6 |
182 | + |
183 | +Its heat_last_updated time will be the same as its datecreated attribute. |
184 | + |
185 | + >>> bug.heat_last_updated == bug.datecreated |
186 | + True |
187 | + |
188 | +The bug's heat can be set by calling its setHeat() method. We'll commit |
189 | +the transaction first so that heat_last_updated gets changed. |
190 | + |
191 | + >>> import transaction |
192 | + >>> transaction.commit() |
193 | |
194 | >>> bug.setHeat(42) |
195 | >>> bug.heat |
196 | 42 |
197 | |
198 | -Its heat_last_updated will also have been set. |
199 | +Its heat_last_updated will also have been updated. |
200 | + |
201 | + >>> bug.heat_last_updated == bug.datecreated |
202 | + False |
203 | |
204 | >>> bug.heat_last_updated |
205 | datetime.datetime(..., tzinfo=<UTC>) |
206 | |
207 | |
208 | +Updating bug heat on-the-fly |
209 | +---------------------------- |
210 | + |
211 | +The IBug.updateHeat() method updates a Bug's heat using data already in |
212 | +the database. Where setHeat() can be used for setting bug heat to a |
213 | +specific value, updateHeat() uses a stored procedure in the database to |
214 | +calculate the heat overall. |
215 | + |
216 | +We'll create a new bug with a heat of 0 for the sake of testing. |
217 | + |
218 | + >>> bug_owner = factory.makePerson() |
219 | + >>> bug = factory.makeBug(owner=bug_owner) |
220 | + >>> bug.setHeat(0) |
221 | + |
222 | + >>> bug.heat |
223 | + 0 |
224 | + |
225 | +Calling updateHeat() will update the bug's heat. Since this new bug has |
226 | +one subscriber (the bug owner) and one affected user (ditto) its |
227 | +heat after update will be 6. |
228 | + |
229 | + >>> bug.updateHeat() |
230 | + >>> bug.heat |
231 | + 6 |
232 | + |
233 | + |
234 | Adjusting bug heat in transaction |
235 | --------------------------------- |
236 | |
237 | -Sometimes, when a bug changes, we want to see the changes reflected in the bug's |
238 | -heat value immidiately, without waiting for heat to be recalculated. Currently |
239 | -we adjust heat immidiately for bug privacy and security. |
240 | - |
241 | - >>> bug_owner = factory.makePerson() |
242 | +Sometimes, when a bug changes, we want to see the changes reflected in |
243 | +the bug's heat value immediately, without waiting for heat to be |
244 | +recalculated. Currently we adjust heat immediately for bug privacy and |
245 | +security. |
246 | + |
247 | + >>> from canonical.database.sqlbase import flush_database_updates |
248 | + |
249 | >>> bug = factory.makeBug(owner=bug_owner) |
250 | >>> bug.heat |
251 | - 0 |
252 | + 6 |
253 | + |
254 | +When the bug is marked private it gains a subscriber - the owner of the |
255 | +product against which it's filed, whose subscription is converted from |
256 | +an indirect to a direct subscription. |
257 | + |
258 | >>> changed = bug.setPrivate(True, bug_owner) |
259 | >>> bug.heat |
260 | - 150 |
261 | + 158 |
262 | + |
263 | >>> changed = bug.setSecurityRelated(True) |
264 | >>> bug.heat |
265 | - 400 |
266 | + 408 |
267 | + |
268 | >>> changed = bug.setPrivate(False, bug_owner) |
269 | >>> bug.heat |
270 | - 250 |
271 | + 258 |
272 | |
273 | |
274 | Getting bugs whose heat is outdated |
275 | @@ -97,6 +139,11 @@ |
276 | updated. |
277 | |
278 | >>> new_bug = factory.makeBug() |
279 | + |
280 | +We'll set the new bug's heat_last_updated to None manually. |
281 | + |
282 | + >>> new_bug.heat_last_updated = None |
283 | + |
284 | >>> outdated_bugs = getUtility(IBugSet).getBugsWithOutdatedHeat(1) |
285 | >>> outdated_bugs.count() |
286 | 2 |
287 | @@ -135,7 +182,6 @@ |
288 | We need to commit here to ensure that the bugs we've created are |
289 | available to the update_bug_heat script. |
290 | |
291 | - >>> import transaction |
292 | >>> transaction.commit() |
293 | |
294 | >>> getUtility(IBugSet).getBugsWithOutdatedHeat(1).count() |
295 | |
296 | === modified file 'lib/lp/bugs/interfaces/bug.py' |
297 | --- lib/lp/bugs/interfaces/bug.py 2010-04-29 17:49:19 +0000 |
298 | +++ lib/lp/bugs/interfaces/bug.py 2010-05-27 14:41:36 +0000 |
299 | @@ -800,6 +800,9 @@ |
300 | def setHeat(heat, timestamp=None): |
301 | """Set the heat for the bug.""" |
302 | |
303 | + def updateHeat(): |
304 | + """Update the heat for the bug.""" |
305 | + |
306 | class InvalidDuplicateValue(Exception): |
307 | """A bug cannot be set as the duplicate of another.""" |
308 | webservice_error(417) |
309 | |
310 | === modified file 'lib/lp/bugs/model/bug.py' |
311 | --- lib/lp/bugs/model/bug.py 2010-04-29 17:49:19 +0000 |
312 | +++ lib/lp/bugs/model/bug.py 2010-05-27 14:41:36 +0000 |
313 | @@ -35,7 +35,7 @@ |
314 | from sqlobject import SQLMultipleJoin, SQLRelatedJoin |
315 | from sqlobject import SQLObjectNotFound |
316 | from storm.expr import ( |
317 | - And, Count, Func, In, LeftJoin, Max, Not, Or, Select, SQLRaw, Union) |
318 | + And, Count, Func, In, LeftJoin, Max, Not, Or, Select, SQL, SQLRaw, Union) |
319 | from storm.store import EmptyResultSet, Store |
320 | |
321 | from lazr.lifecycle.event import ( |
322 | @@ -1355,11 +1355,7 @@ |
323 | |
324 | # Correct the heat for the bug immediately, so that we don't have |
325 | # to wait for the next calculation job for the adjusted heat. |
326 | - if private: |
327 | - self.setHeat(self.heat + BugHeatConstants.PRIVACY) |
328 | - else: |
329 | - self.setHeat(self.heat - BugHeatConstants.PRIVACY) |
330 | - |
331 | + self.updateHeat() |
332 | return True # Changed. |
333 | else: |
334 | return False # Not changed. |
335 | @@ -1371,10 +1367,7 @@ |
336 | |
337 | # Correct the heat for the bug immediately, so that we don't have |
338 | # to wait for the next calculation job for the adjusted heat. |
339 | - if security_related: |
340 | - self.setHeat(self.heat + BugHeatConstants.SECURITY) |
341 | - else: |
342 | - self.setHeat(self.heat - BugHeatConstants.SECURITY) |
343 | + self.updateHeat() |
344 | |
345 | return True # Changed |
346 | else: |
347 | @@ -1486,7 +1479,7 @@ |
348 | # this bug's heat to 0 (since it's a duplicate, it shouldn't |
349 | # have any heat at all). |
350 | getUtility(ICalculateBugHeatJobSource).create(duplicate_of) |
351 | - self.setHeat(0) |
352 | + self.updateHeat() |
353 | else: |
354 | # Otherwise, create a job to recalculate this bug's heat, |
355 | # since it will be 0 from having been a duplicate. |
356 | @@ -1573,6 +1566,21 @@ |
357 | for task in self.bugtasks: |
358 | task.target.recalculateMaxBugHeat() |
359 | |
360 | + def updateHeat(self): |
361 | + """See `IBug`.""" |
362 | + if self.duplicateof is not None: |
363 | + # If this bug is a duplicate we don't try to calculate its |
364 | + # heat. |
365 | + return |
366 | + |
367 | + # We need to flush the store first to ensure that changes are |
368 | + # reflected in the new bug heat total. |
369 | + store = Store.of(self) |
370 | + store.flush() |
371 | + |
372 | + self.heat = SQL("calculate_bug_heat(%s)" % sqlvalues(self)) |
373 | + self.heat_last_updated = UTC_NOW |
374 | + |
375 | @property |
376 | def attachments(self): |
377 | """See `IBug`.""" |
378 | @@ -1719,6 +1727,9 @@ |
379 | # Tell everyone. |
380 | notify(event) |
381 | |
382 | + # Calculate the bug's initial heat. |
383 | + bug.updateHeat() |
384 | + |
385 | return bug |
386 | |
387 | def createBugWithoutTarget(self, bug_params): |
On Wed, May 26, 2010 at 10:50:49AM -0000, Graham Binns wrote:
> The stored procedure is currently not hooked up to anything, but once
> the patch is applied it's easy to check the results of the stored
> procedure against the existing code using psql and an iharness session,
> thus:
Why not hook it up directly? It would be useful to have this tested,
since it's hard to check that it's doing what it should be doing.
-- /launchpad. net/~bjornt
Björn Tillenius | https:/