Merge lp:~gmb/launchpad/stored-proc-for-bug-heat-bug-582195 into lp:launchpad/db-devel

Proposed by Graham Binns
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
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/bugs/scripts/bugheat.py and converts it into a plpython stored procedure.

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-and-subtracting of bug heat that we currently do at those callsites.

In psql:

  SELECT calculate_bug_heat(1);

In iharness:

  >>> from lp.bugs.scripts.bugheat import BugHeatCalculator
  >>> bug_1 = getUtility(IBugSet).get(1)
  >>> calculator = BugHeatCalculator(bug_1)
  >>> calculator.getBugHeat()

To post a comment you must log in.
Revision history for this message
Björn Tillenius (bjornt) wrote : Posted in a previous version of this proposal

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.

--
Björn Tillenius | https://launchpad.net/~bjornt

Revision history for this message
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

Revision history for this message
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.

Revision history for this message
Stuart Bishop (stub) wrote :
Download full text (7.2 KiB)

On Wed, May 26, 2010 at 10:26 PM, Graham Binns <email address hidden> wrote:

> === added file 'database/schema/patch-2207-99-0.sql'

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_bug_heat(bug_id integer) RETURNS integer
> +LANGUAGE plpythonu AS $$
> +    from datetime import datetime
> +
> +    class BugHeatConstants:
> +        PRIVACY = 150
> +        SECURITY = 250
> +        DUPLICATE = 6
> +        AFFECTED_USER = 4
> +        SUBSCRIBER = 2
> +
> +
> +    def get_max_heat_for_bug(bug_id):
> +        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['product'])[0]
> +                max_heats.append(product['max_bug_heat'])
> +            elif bug_task['distribution']:
> +                distribution = plpy.execute(
> +                    "SELECT max_bug_heat FROM Distribution WHERE id = %s" %
> +                    bug_task['distribution'])[0]
> +                max_heats.append(distribution['max_bug_heat'])
> +            elif bug_task['productseries'] is not None:
> +                product_series = plpy.execute("""
> +                    SELECT Product.max_bug_heat
> +                      FROM ProductSeries, Product
> +                     WHERE ProductSeries.Product = Product.id
> +                       AND ProductSeries.id = %s"""%
> +                    bug_task['productseries'])[0]
> +                max_heats.append(product_series['max_bug_heat'])
> +            elif bug_task['distroseries']:
> +                distro_series = plpy.execute("""
> +                    SELECT Distribution.max_bug_heat
> +                      FROM DistroSeries, Distribution
> +                     WHERE DistroSeries.Distribution = Distribution.id
> +                       AND DistroSeries.id = %s"""%
> +                    bug_task['distroseries'])[0]
> +                max_heats.append(distro_series['max_bug_heat'])
> +            else:
> +                pass
> +
> +        return max(max_heats)

You can do this in a single query rather than one per task:

SELECT MAX(GREATEST(Product.max_bug_heat, Distribution.max_bug_heat))
FROM BugTask
LEFT OUTER JOIN ProductSeries ON BugTask.productseries = ProductSeries.id
LEFT OUTER JOIN Product ON (
    BugTask.product = Product.id
    OR ProductSeries.product = Product.id)
LEFT OUTER JOIN DistroSeries ON BugTask.distroseries = DistroSeries.id
LEFT OUTER JOIN Distribution ON (
    BugTask.distribution = Distribution.id
    OR DistroSeries.distribution = Distribution.id)
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...

Read more...

Revision history for this message
Stuart Bishop (stub) wrote :
Download full text (7.2 KiB)

On Wed, May 26, 2010 at 10:26 PM, Graham Binns <email address hidden> wrote:

> === added file 'database/schema/patch-2207-99-0.sql'

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_bug_heat(bug_id integer) RETURNS integer
> +LANGUAGE plpythonu AS $$
> + from datetime import datetime
> +
> + class BugHeatConstants:
> + PRIVACY = 150
> + SECURITY = 250
> + DUPLICATE = 6
> + AFFECTED_USER = 4
> + SUBSCRIBER = 2
> +
> +
> + def get_max_heat_for_bug(bug_id):
> + 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['product'])[0]
> + max_heats.append(product['max_bug_heat'])
> + elif bug_task['distribution']:
> + distribution = plpy.execute(
> + "SELECT max_bug_heat FROM Distribution WHERE id = %s" %
> + bug_task['distribution'])[0]
> + max_heats.append(distribution['max_bug_heat'])
> + elif bug_task['productseries'] is not None:
> + product_series = plpy.execute("""
> + SELECT Product.max_bug_heat
> + FROM ProductSeries, Product
> + WHERE ProductSeries.Product = Product.id
> + AND ProductSeries.id = %s"""%
> + bug_task['productseries'])[0]
> + max_heats.append(product_series['max_bug_heat'])
> + elif bug_task['distroseries']:
> + distro_series = plpy.execute("""
> + SELECT Distribution.max_bug_heat
> + FROM DistroSeries, Distribution
> + WHERE DistroSeries.Distribution = Distribution.id
> + AND DistroSeries.id = %s"""%
> + bug_task['distroseries'])[0]
> + max_heats.append(distro_series['max_bug_heat'])
> + else:
> + pass
> +
> + return max(max_heats)

You can do this in a single query rather than one per task:

SELECT MAX(GREATEST(Product.max_bug_heat, Distribution.max_bug_heat))
FROM BugTask
LEFT OUTER JOIN ProductSeries ON BugTask.productseries = ProductSeries.id
LEFT OUTER JOIN Product ON (
    BugTask.product = Product.id
    OR ProductSeries.product = Product.id)
LEFT OUTER JOIN DistroSeries ON BugTask.distroseries = DistroSeries.id
LEFT OUTER JOIN Distribution ON (
    BugTask.distribution = Distribution.id
    OR DistroSeries.distribution = Distribution.id)
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...

Read more...

review: Needs Fixing (db)
Revision history for this message
Graham Binns (gmb) wrote :
Download full text (6.9 KiB)

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/schema/patch-2207-99-0.sql'
>
> 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(Product.max_bug_heat, Distribution.max_bug_heat))
> FROM BugTask
> LEFT OUTER JOIN ProductSeries ON BugTask.productseries = ProductSeries.id
> LEFT OUTER JOIN Product ON (
> BugTask.product = Product.id
> OR ProductSeries.product = Product.id)
> LEFT OUTER JOIN DistroSeries ON BugTask.distroseries = DistroSeries.id
> LEFT OUTER JOIN Distribution ON (
> BugTask.distribution = Distribution.id
> OR DistroSeries.distribution = Distribution.id)
> 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'] = BugHeatConstants.PRIVACY
> > + if bug['security_related']:
> > + heat['security'] = BugHeatConstants.SECURITY
> > +
> > + # 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'] = (
> > + BugHeatConstants.SUBSCRIBER * sub_count[0]['sub_count'])
>
> 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(bugsubscription.id) AS...

Read more...

Revision history for this message
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://www.stuartbishop.net/

Revision history for this message
Stuart Bishop (stub) :
review: Approve (db)
Revision history for this message
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.

Revision history for this message
Björn Tillenius (bjornt) :
review: Approve (db)
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thanks for asking for the RC before PQM closes. I do not want to delay QA of this.

review: Approve (rc)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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):

Subscribers

People subscribed via source and target branches

to status/vote changes: