Merge lp:~gmb/launchpad/hook-up-stored-proc-boom-bug-585803 into lp:launchpad/db-devel
- hook-up-stored-proc-boom-bug-585803
- Merge into db-devel
Proposed by
Graham Binns
Status: | Merged |
---|---|
Approved by: | Graham Binns |
Approved revision: | no longer in the source branch. |
Merged at revision: | 9414 |
Proposed branch: | lp:~gmb/launchpad/hook-up-stored-proc-boom-bug-585803 |
Merge into: | lp:launchpad/db-devel |
Prerequisite: | lp:~gmb/launchpad/stored-proc-for-bug-heat-bug-582195 |
Diff against target: |
532 lines (+155/-198) 5 files modified
database/schema/trusted.sql (+2/-1) lib/canonical/launchpad/scripts/garbo.py (+22/-31) lib/lp/bugs/doc/bug-heat.txt (+114/-50) lib/lp/bugs/model/bug.py (+17/-14) lib/lp/bugs/tests/test_bugheat.py (+0/-102) |
To merge this branch: | bzr merge lp:~gmb/launchpad/hook-up-stored-proc-boom-bug-585803 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Curtis Hovey (community) | rc | Approve | |
Abel Deuring (community) | code | Approve | |
Review via email: mp+26191@code.launchpad.net |
Commit message
Bug heat is now updated directly rather than relying on CalculateBugHeatJob creation.
Description of the change
This branch converts all current sites where CalculateBugHea
I've removed all CalculateBugHeatJob creation callsites (except in tests, which I'll remove in a subsequent branch along with all the CalculateBugHeatJob infrastructure) and I've updated the BugHeatUpdater garbo job to call the calculate_
I've also removed the tests that covered the callsites that I've removed.
To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote : | # |
This is good to land and you have my RC to ensure this landed even if ec2 runs slowly.
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/trusted.sql' | |||
2 | --- database/schema/trusted.sql 2010-05-29 16:21:26 +0000 | |||
3 | +++ database/schema/trusted.sql 2010-05-29 16:21:27 +0000 | |||
4 | @@ -1696,7 +1696,8 @@ | |||
5 | 1696 | days_since_created = (datetime.utcnow() - date_created).days | 1696 | days_since_created = (datetime.utcnow() - date_created).days |
6 | 1697 | max_heat = get_max_heat_for_bug(bug_id) | 1697 | max_heat = get_max_heat_for_bug(bug_id) |
7 | 1698 | if max_heat is not None and days_since_created > 0: | 1698 | if max_heat is not None and days_since_created > 0: |
9 | 1699 | return total_heat + (max_heat * 0.25 / days_since_created) | 1699 | total_heat = ( |
10 | 1700 | total_heat + (max_heat * 0.25 / days_since_created)) | ||
11 | 1700 | 1701 | ||
12 | 1701 | return int(total_heat) | 1702 | return int(total_heat) |
13 | 1702 | $$; | 1703 | $$; |
14 | 1703 | 1704 | ||
15 | === added directory 'lib/canonical/launchpad/apidoc' | |||
16 | === modified file 'lib/canonical/launchpad/scripts/garbo.py' | |||
17 | --- lib/canonical/launchpad/scripts/garbo.py 2010-04-14 13:14:00 +0000 | |||
18 | +++ lib/canonical/launchpad/scripts/garbo.py 2010-05-29 16:21:27 +0000 | |||
19 | @@ -13,6 +13,7 @@ | |||
20 | 13 | import transaction | 13 | import transaction |
21 | 14 | from psycopg2 import IntegrityError | 14 | from psycopg2 import IntegrityError |
22 | 15 | from zope.component import getUtility | 15 | from zope.component import getUtility |
23 | 16 | from zope.security.proxy import removeSecurityProxy | ||
24 | 16 | from storm.locals import In, SQL, Max, Min | 17 | from storm.locals import In, SQL, Max, Min |
25 | 17 | 18 | ||
26 | 18 | from canonical.config import config | 19 | from canonical.config import config |
27 | @@ -30,6 +31,7 @@ | |||
28 | 30 | from canonical.launchpad.webapp.interfaces import ( | 31 | from canonical.launchpad.webapp.interfaces import ( |
29 | 31 | IStoreSelector, MAIN_STORE, MASTER_FLAVOR) | 32 | IStoreSelector, MAIN_STORE, MASTER_FLAVOR) |
30 | 32 | from lp.bugs.interfaces.bug import IBugSet | 33 | from lp.bugs.interfaces.bug import IBugSet |
31 | 34 | from lp.bugs.model.bug import Bug | ||
32 | 33 | from lp.bugs.model.bugattachment import BugAttachment | 35 | from lp.bugs.model.bugattachment import BugAttachment |
33 | 34 | from lp.bugs.interfaces.bugjob import ICalculateBugHeatJobSource | 36 | from lp.bugs.interfaces.bugjob import ICalculateBugHeatJobSource |
34 | 35 | from lp.bugs.model.bugnotification import BugNotification | 37 | from lp.bugs.model.bugnotification import BugNotification |
35 | @@ -536,50 +538,39 @@ | |||
36 | 536 | max_heat_age = config.calculate_bug_heat.max_heat_age | 538 | max_heat_age = config.calculate_bug_heat.max_heat_age |
37 | 537 | self.max_heat_age = max_heat_age | 539 | self.max_heat_age = max_heat_age |
38 | 538 | 540 | ||
39 | 541 | self.store = IMasterStore(Bug) | ||
40 | 542 | |||
41 | 543 | @property | ||
42 | 544 | def _outdated_bugs(self): | ||
43 | 545 | outdated_bugs = getUtility(IBugSet).getBugsWithOutdatedHeat( | ||
44 | 546 | self.max_heat_age) | ||
45 | 547 | # We remove the security proxy so that we can access the set() | ||
46 | 548 | # method of the result set. | ||
47 | 549 | return removeSecurityProxy(outdated_bugs) | ||
48 | 550 | |||
49 | 539 | def isDone(self): | 551 | def isDone(self): |
50 | 540 | """See `ITunableLoop`.""" | 552 | """See `ITunableLoop`.""" |
51 | 541 | # When the main loop has no more Bugs to process it sets | 553 | # When the main loop has no more Bugs to process it sets |
52 | 542 | # offset to None. Until then, it always has a numerical | 554 | # offset to None. Until then, it always has a numerical |
53 | 543 | # value. | 555 | # value. |
55 | 544 | return self.is_done | 556 | return self._outdated_bugs.is_empty() |
56 | 545 | 557 | ||
57 | 546 | def __call__(self, chunk_size): | 558 | def __call__(self, chunk_size): |
58 | 547 | """Retrieve a batch of Bugs and update their heat. | 559 | """Retrieve a batch of Bugs and update their heat. |
59 | 548 | 560 | ||
60 | 549 | See `ITunableLoop`. | 561 | See `ITunableLoop`. |
61 | 550 | """ | 562 | """ |
69 | 551 | # XXX 2010-01-08 gmb bug=198767: | 563 | # We multiply chunk_size by 1000 for the sake of doing updates |
70 | 552 | # We cast chunk_size to an integer to ensure that we're not | 564 | # quickly. |
71 | 553 | # trying to slice using floats or anything similarly | 565 | chunk_size = int(chunk_size * 1000) |
65 | 554 | # foolish. We shouldn't have to do this. | ||
66 | 555 | chunk_size = int(chunk_size) | ||
67 | 556 | start = self.offset | ||
68 | 557 | end = self.offset + chunk_size | ||
72 | 558 | 566 | ||
73 | 559 | transaction.begin() | 567 | transaction.begin() |
97 | 560 | bugs = getUtility(IBugSet).getBugsWithOutdatedHeat( | 568 | outdated_bugs = self._outdated_bugs[:chunk_size] |
98 | 561 | self.max_heat_age)[start:end] | 569 | self.log.debug("Updating heat for %s bugs" % outdated_bugs.count()) |
99 | 562 | 570 | outdated_bugs.set( | |
100 | 563 | bug_count = bugs.count() | 571 | heat=SQL('calculate_bug_heat(Bug.id)'), |
101 | 564 | if bug_count > 0: | 572 | heat_last_updated=datetime.now(pytz.utc)) |
102 | 565 | starting_id = bugs.first().id | 573 | |
80 | 566 | self.log.debug( | ||
81 | 567 | "Adding CalculateBugHeatJobs for %i Bugs (starting id: %i)" % | ||
82 | 568 | (bug_count, starting_id)) | ||
83 | 569 | else: | ||
84 | 570 | self.is_done = True | ||
85 | 571 | |||
86 | 572 | self.offset = None | ||
87 | 573 | for bug in bugs: | ||
88 | 574 | # We set the starting point of the next batch to the Bug | ||
89 | 575 | # id after the one we're looking at now. If there aren't any | ||
90 | 576 | # bugs this loop will run for 0 iterations and starting_id | ||
91 | 577 | # will remain set to None. | ||
92 | 578 | start += 1 | ||
93 | 579 | self.offset = start | ||
94 | 580 | self.log.debug("Adding CalculateBugHeatJob for bug %s" % bug.id) | ||
95 | 581 | getUtility(ICalculateBugHeatJobSource).create(bug) | ||
96 | 582 | self.total_processed += 1 | ||
103 | 583 | transaction.commit() | 574 | transaction.commit() |
104 | 584 | 575 | ||
105 | 585 | 576 | ||
106 | 586 | 577 | ||
107 | === modified file 'lib/lp/bugs/doc/bug-heat.txt' | |||
108 | --- lib/lp/bugs/doc/bug-heat.txt 2010-05-29 16:21:26 +0000 | |||
109 | +++ lib/lp/bugs/doc/bug-heat.txt 2010-05-29 16:21:27 +0000 | |||
110 | @@ -62,15 +62,11 @@ | |||
111 | 62 | 6 | 62 | 6 |
112 | 63 | 63 | ||
113 | 64 | 64 | ||
123 | 65 | Adjusting bug heat in transaction | 65 | Events which trigger bug heat updates |
124 | 66 | --------------------------------- | 66 | ------------------------------------- |
125 | 67 | 67 | ||
126 | 68 | Sometimes, when a bug changes, we want to see the changes reflected in | 68 | There are several events which will cause a bug's heat to be updated. |
127 | 69 | the bug's heat value immediately, without waiting for heat to be | 69 | First, as stated above, heat will be calculated when the bug is created. |
119 | 70 | recalculated. Currently we adjust heat immediately for bug privacy and | ||
120 | 71 | security. | ||
121 | 72 | |||
122 | 73 | >>> from canonical.database.sqlbase import flush_database_updates | ||
128 | 74 | 70 | ||
129 | 75 | >>> bug = factory.makeBug(owner=bug_owner) | 71 | >>> bug = factory.makeBug(owner=bug_owner) |
130 | 76 | >>> bug.heat | 72 | >>> bug.heat |
131 | @@ -80,18 +76,116 @@ | |||
132 | 80 | product against which it's filed, whose subscription is converted from | 76 | product against which it's filed, whose subscription is converted from |
133 | 81 | an indirect to a direct subscription. | 77 | an indirect to a direct subscription. |
134 | 82 | 78 | ||
135 | 79 | Marking a bug as private also gives it an extra 150 heat points. | ||
136 | 80 | |||
137 | 83 | >>> changed = bug.setPrivate(True, bug_owner) | 81 | >>> changed = bug.setPrivate(True, bug_owner) |
138 | 84 | >>> bug.heat | 82 | >>> bug.heat |
139 | 85 | 158 | 83 | 158 |
140 | 86 | 84 | ||
141 | 85 | Setting the bug as security related adds another 250 heat points. | ||
142 | 86 | |||
143 | 87 | >>> changed = bug.setSecurityRelated(True) | 87 | >>> changed = bug.setSecurityRelated(True) |
144 | 88 | >>> bug.heat | 88 | >>> bug.heat |
145 | 89 | 408 | 89 | 408 |
146 | 90 | 90 | ||
147 | 91 | Marking the bug public removes 150 heat points. | ||
148 | 92 | |||
149 | 91 | >>> changed = bug.setPrivate(False, bug_owner) | 93 | >>> changed = bug.setPrivate(False, bug_owner) |
150 | 92 | >>> bug.heat | 94 | >>> bug.heat |
151 | 93 | 258 | 95 | 258 |
152 | 94 | 96 | ||
153 | 97 | And marking it not security-related removes 250 points. | ||
154 | 98 | |||
155 | 99 | >>> changed = bug.setSecurityRelated(False) | ||
156 | 100 | >>> bug.heat | ||
157 | 101 | 8 | ||
158 | 102 | |||
159 | 103 | Adding a subscriber to the bug increases its heat by 2 points. | ||
160 | 104 | |||
161 | 105 | >>> new_subscriber = factory.makePerson() | ||
162 | 106 | >>> subscription = bug.subscribe(new_subscriber, new_subscriber) | ||
163 | 107 | >>> bug.heat | ||
164 | 108 | 10 | ||
165 | 109 | |||
166 | 110 | When a user unsubscribes, the bug loses 2 points of heat. | ||
167 | 111 | |||
168 | 112 | >>> bug.unsubscribe(new_subscriber, new_subscriber) | ||
169 | 113 | >>> bug.heat | ||
170 | 114 | 8 | ||
171 | 115 | |||
172 | 116 | Should a user mark themselves as affected by the bug, it will gain 4 | ||
173 | 117 | points of heat. | ||
174 | 118 | |||
175 | 119 | >>> bug.markUserAffected(new_subscriber) | ||
176 | 120 | >>> bug.heat | ||
177 | 121 | 12 | ||
178 | 122 | |||
179 | 123 | If a user who was previously affected marks themself as not affected, | ||
180 | 124 | the bug loses 4 points of heat. | ||
181 | 125 | |||
182 | 126 | >>> bug.markUserAffected(new_subscriber, False) | ||
183 | 127 | >>> bug.heat | ||
184 | 128 | 8 | ||
185 | 129 | |||
186 | 130 | If a user who wasn't affected by the bug marks themselve as explicitly | ||
187 | 131 | unaffected, the bug's heat doesn't change. | ||
188 | 132 | |||
189 | 133 | >>> unaffected_person = factory.makePerson() | ||
190 | 134 | >>> bug.markUserAffected(unaffected_person, False) | ||
191 | 135 | >>> bug.heat | ||
192 | 136 | 8 | ||
193 | 137 | |||
194 | 138 | Marking the bug as a duplicate will set its heat to zero, whilst also | ||
195 | 139 | adding 10 points of heat to the bug it duplicates, 6 points for the | ||
196 | 140 | duplication and 4 points for the subscribers that the duplicated bug | ||
197 | 141 | inherits. | ||
198 | 142 | |||
199 | 143 | >>> duplicated_bug = factory.makeBug() | ||
200 | 144 | >>> duplicated_bug.heat | ||
201 | 145 | 6 | ||
202 | 146 | |||
203 | 147 | >>> bug.markAsDuplicate(duplicated_bug) | ||
204 | 148 | >>> bug.heat | ||
205 | 149 | 0 | ||
206 | 150 | |||
207 | 151 | >>> duplicated_bug.heat | ||
208 | 152 | 16 | ||
209 | 153 | |||
210 | 154 | Unmarking the bug as a duplicate restores its heat and updates the | ||
211 | 155 | duplicated bug's heat. | ||
212 | 156 | |||
213 | 157 | >>> bug.markAsDuplicate(None) | ||
214 | 158 | >>> bug.heat | ||
215 | 159 | 8 | ||
216 | 160 | |||
217 | 161 | >>> duplicated_bug.heat | ||
218 | 162 | 6 | ||
219 | 163 | |||
220 | 164 | A number of other changes, handled by the Bug's addChange() method, will | ||
221 | 165 | cause heat to be recalculated, even if the heat itself may not actually | ||
222 | 166 | change. | ||
223 | 167 | |||
224 | 168 | For example, updating the bug's description calls the addChange() event, | ||
225 | 169 | and will cause the bug's heat to be recalculated. | ||
226 | 170 | |||
227 | 171 | We'll set the bug's heat to 0 first to demonstrate this. | ||
228 | 172 | |||
229 | 173 | >>> bug.setHeat(0) | ||
230 | 174 | >>> bug.heat | ||
231 | 175 | 0 | ||
232 | 176 | |||
233 | 177 | >>> from datetime import datetime, timedelta | ||
234 | 178 | >>> from pytz import timezone | ||
235 | 179 | |||
236 | 180 | >>> from lp.bugs.adapters.bugchange import BugDescriptionChange | ||
237 | 181 | >>> change = BugDescriptionChange( | ||
238 | 182 | ... when=datetime.now().replace(tzinfo=timezone('UTC')), | ||
239 | 183 | ... person=bug.owner, what_changed='description', | ||
240 | 184 | ... old_value=bug.description, new_value='Some text') | ||
241 | 185 | >>> bug.addChange(change) | ||
242 | 186 | >>> bug.heat | ||
243 | 187 | 8 | ||
244 | 188 | |||
245 | 95 | 189 | ||
246 | 96 | Getting bugs whose heat is outdated | 190 | Getting bugs whose heat is outdated |
247 | 97 | ----------------------------------- | 191 | ----------------------------------- |
248 | @@ -122,8 +216,6 @@ | |||
249 | 122 | getBugsWithOutdatedHeat() it will appear in the set returned by | 216 | getBugsWithOutdatedHeat() it will appear in the set returned by |
250 | 123 | getBugsWithOutdatedHeat(). | 217 | getBugsWithOutdatedHeat(). |
251 | 124 | 218 | ||
252 | 125 | >>> from datetime import datetime, timedelta | ||
253 | 126 | >>> from pytz import timezone | ||
254 | 127 | >>> old_heat_bug = factory.makeBug() | 219 | >>> old_heat_bug = factory.makeBug() |
255 | 128 | >>> old_heat_bug.setHeat( | 220 | >>> old_heat_bug.setHeat( |
256 | 129 | ... 0, datetime.now(timezone('UTC')) - timedelta(days=2)) | 221 | ... 0, datetime.now(timezone('UTC')) - timedelta(days=2)) |
257 | @@ -161,52 +253,24 @@ | |||
258 | 161 | >>> from canonical.launchpad.scripts.garbo import BugHeatUpdater | 253 | >>> from canonical.launchpad.scripts.garbo import BugHeatUpdater |
259 | 162 | >>> from canonical.launchpad.scripts import FakeLogger | 254 | >>> from canonical.launchpad.scripts import FakeLogger |
260 | 163 | 255 | ||
261 | 256 | We'll commit the transaction so that the BugHeatUpdater updates the | ||
262 | 257 | right bugs. | ||
263 | 258 | |||
264 | 259 | >>> transaction.commit() | ||
265 | 164 | >>> update_bug_heat = BugHeatUpdater(FakeLogger(), max_heat_age=1) | 260 | >>> update_bug_heat = BugHeatUpdater(FakeLogger(), max_heat_age=1) |
266 | 165 | 261 | ||
267 | 166 | BugHeatUpdater implements ITunableLoop and as such is callable. Calling | 262 | BugHeatUpdater implements ITunableLoop and as such is callable. Calling |
287 | 167 | it as a method will add jobs to calculate the heat of for all the bugs | 263 | it as a method will recalculate the heat for all the out-of-date bugs. |
288 | 168 | whose heat is more than seven days old. | 264 | |
289 | 169 | 265 | There are two bugs with heat more than a day old: | |
271 | 170 | Before update_bug_heat is called, we'll ensure that there are no waiting | ||
272 | 171 | jobs in the bug heat calculation queue. | ||
273 | 172 | |||
274 | 173 | >>> from lp.bugs.interfaces.bugjob import ICalculateBugHeatJobSource | ||
275 | 174 | >>> for calc_job in getUtility(ICalculateBugHeatJobSource).iterReady(): | ||
276 | 175 | ... calc_job.job.start() | ||
277 | 176 | ... calc_job.job.complete() | ||
278 | 177 | |||
279 | 178 | >>> ready_jobs = list(getUtility(ICalculateBugHeatJobSource).iterReady()) | ||
280 | 179 | >>> len(ready_jobs) | ||
281 | 180 | 0 | ||
282 | 181 | |||
283 | 182 | We need to commit here to ensure that the bugs we've created are | ||
284 | 183 | available to the update_bug_heat script. | ||
285 | 184 | |||
286 | 185 | >>> transaction.commit() | ||
290 | 186 | 266 | ||
291 | 187 | >>> getUtility(IBugSet).getBugsWithOutdatedHeat(1).count() | 267 | >>> getUtility(IBugSet).getBugsWithOutdatedHeat(1).count() |
292 | 188 | 2 | 268 | 2 |
293 | 189 | 269 | ||
314 | 190 | We need to run update_bug_heat() twice to ensure that both the bugs are | 270 | Calling our BugHeatUpdater will update the heat of those bugs. |
315 | 191 | updated. | 271 | |
316 | 192 | 272 | >>> update_bug_heat(chunk_size=1) | |
317 | 193 | >>> update_bug_heat(chunk_size=2) | 273 | DEBUG Updating heat for 2 bugs |
298 | 194 | DEBUG Adding CalculateBugHeatJobs for 2 Bugs (starting id: ...) | ||
299 | 195 | DEBUG Adding CalculateBugHeatJob for bug ... | ||
300 | 196 | DEBUG Adding CalculateBugHeatJob for bug ... | ||
301 | 197 | |||
302 | 198 | There will now be two CalculateBugHeatJobs in the queue. | ||
303 | 199 | |||
304 | 200 | >>> ready_jobs = list(getUtility(ICalculateBugHeatJobSource).iterReady()) | ||
305 | 201 | >>> len(ready_jobs) | ||
306 | 202 | 2 | ||
307 | 203 | |||
308 | 204 | Running them will update the bugs' heat. | ||
309 | 205 | |||
310 | 206 | >>> for calc_job in getUtility(ICalculateBugHeatJobSource).iterReady(): | ||
311 | 207 | ... calc_job.job.start() | ||
312 | 208 | ... calc_job.run() | ||
313 | 209 | ... calc_job.job.complete() | ||
318 | 210 | 274 | ||
319 | 211 | IBugSet.getBugsWithOutdatedHeat() will now return an empty set since all | 275 | IBugSet.getBugsWithOutdatedHeat() will now return an empty set since all |
320 | 212 | the bugs have been updated. | 276 | the bugs have been updated. |
321 | 213 | 277 | ||
322 | === modified file 'lib/lp/bugs/model/bug.py' | |||
323 | --- lib/lp/bugs/model/bug.py 2010-05-29 16:21:26 +0000 | |||
324 | +++ lib/lp/bugs/model/bug.py 2010-05-29 16:21:27 +0000 | |||
325 | @@ -73,7 +73,6 @@ | |||
326 | 73 | from lp.bugs.interfaces.bugactivity import IBugActivitySet | 73 | from lp.bugs.interfaces.bugactivity import IBugActivitySet |
327 | 74 | from lp.bugs.interfaces.bugattachment import ( | 74 | from lp.bugs.interfaces.bugattachment import ( |
328 | 75 | BugAttachmentType, IBugAttachmentSet) | 75 | BugAttachmentType, IBugAttachmentSet) |
329 | 76 | from lp.bugs.interfaces.bugjob import ICalculateBugHeatJobSource | ||
330 | 77 | from lp.bugs.interfaces.bugmessage import IBugMessageSet | 76 | from lp.bugs.interfaces.bugmessage import IBugMessageSet |
331 | 78 | from lp.bugs.interfaces.bugnomination import ( | 77 | from lp.bugs.interfaces.bugnomination import ( |
332 | 79 | NominationError, NominationSeriesObsoleteError) | 78 | NominationError, NominationSeriesObsoleteError) |
333 | @@ -490,8 +489,6 @@ | |||
334 | 490 | sub = BugSubscription( | 489 | sub = BugSubscription( |
335 | 491 | bug=self, person=person, subscribed_by=subscribed_by) | 490 | bug=self, person=person, subscribed_by=subscribed_by) |
336 | 492 | 491 | ||
337 | 493 | getUtility(ICalculateBugHeatJobSource).create(self) | ||
338 | 494 | |||
339 | 495 | # Ensure that the subscription has been flushed. | 492 | # Ensure that the subscription has been flushed. |
340 | 496 | Store.of(sub).flush() | 493 | Store.of(sub).flush() |
341 | 497 | 494 | ||
342 | @@ -501,6 +498,7 @@ | |||
343 | 501 | if suppress_notify is False: | 498 | if suppress_notify is False: |
344 | 502 | notify(ObjectCreatedEvent(sub, user=subscribed_by)) | 499 | notify(ObjectCreatedEvent(sub, user=subscribed_by)) |
345 | 503 | 500 | ||
346 | 501 | self.updateHeat() | ||
347 | 504 | return sub | 502 | return sub |
348 | 505 | 503 | ||
349 | 506 | def unsubscribe(self, person, unsubscribed_by): | 504 | def unsubscribe(self, person, unsubscribed_by): |
350 | @@ -525,6 +523,7 @@ | |||
351 | 525 | # flushed so that code running with implicit flushes | 523 | # flushed so that code running with implicit flushes |
352 | 526 | # disabled see the change. | 524 | # disabled see the change. |
353 | 527 | store.flush() | 525 | store.flush() |
354 | 526 | self.updateHeat() | ||
355 | 528 | return | 527 | return |
356 | 529 | 528 | ||
357 | 530 | def unsubscribeFromDupes(self, person, unsubscribed_by): | 529 | def unsubscribeFromDupes(self, person, unsubscribed_by): |
358 | @@ -803,7 +802,7 @@ | |||
359 | 803 | notification_data['text'], change.person, recipients, | 802 | notification_data['text'], change.person, recipients, |
360 | 804 | when) | 803 | when) |
361 | 805 | 804 | ||
363 | 806 | getUtility(ICalculateBugHeatJobSource).create(self) | 805 | self.updateHeat() |
364 | 807 | 806 | ||
365 | 808 | def expireNotifications(self): | 807 | def expireNotifications(self): |
366 | 809 | """See `IBug`.""" | 808 | """See `IBug`.""" |
367 | @@ -1459,7 +1458,7 @@ | |||
368 | 1459 | if dupe._getAffectedUser(user) is not None: | 1458 | if dupe._getAffectedUser(user) is not None: |
369 | 1460 | dupe.markUserAffected(user, affected) | 1459 | dupe.markUserAffected(user, affected) |
370 | 1461 | 1460 | ||
372 | 1462 | getUtility(ICalculateBugHeatJobSource).create(self) | 1461 | self.updateHeat() |
373 | 1463 | 1462 | ||
374 | 1464 | @property | 1463 | @property |
375 | 1465 | def readonly_duplicateof(self): | 1464 | def readonly_duplicateof(self): |
376 | @@ -1470,6 +1469,7 @@ | |||
377 | 1470 | """See `IBug`.""" | 1469 | """See `IBug`.""" |
378 | 1471 | field = DuplicateBug() | 1470 | field = DuplicateBug() |
379 | 1472 | field.context = self | 1471 | field.context = self |
380 | 1472 | current_duplicateof = self.duplicateof | ||
381 | 1473 | try: | 1473 | try: |
382 | 1474 | if duplicate_of is not None: | 1474 | if duplicate_of is not None: |
383 | 1475 | field._validate(duplicate_of) | 1475 | field._validate(duplicate_of) |
384 | @@ -1478,15 +1478,17 @@ | |||
385 | 1478 | raise InvalidDuplicateValue(validation_error) | 1478 | raise InvalidDuplicateValue(validation_error) |
386 | 1479 | 1479 | ||
387 | 1480 | if duplicate_of is not None: | 1480 | if duplicate_of is not None: |
392 | 1481 | # Create a job to update the heat of the master bug and set | 1481 | # Update the heat of the master bug and set this bug's heat |
393 | 1482 | # this bug's heat to 0 (since it's a duplicate, it shouldn't | 1482 | # to 0 (since it's a duplicate, it shouldn't have any heat |
394 | 1483 | # have any heat at all). | 1483 | # at all). |
395 | 1484 | getUtility(ICalculateBugHeatJobSource).create(duplicate_of) | 1484 | self.setHeat(0) |
396 | 1485 | duplicate_of.updateHeat() | ||
397 | 1486 | else: | ||
398 | 1487 | # Otherwise, recalculate this bug's heat, since it will be 0 | ||
399 | 1488 | # from having been a duplicate. We also update the bug that | ||
400 | 1489 | # was previously duplicated. | ||
401 | 1485 | self.updateHeat() | 1490 | self.updateHeat() |
406 | 1486 | else: | 1491 | current_duplicateof.updateHeat() |
403 | 1487 | # Otherwise, create a job to recalculate this bug's heat, | ||
404 | 1488 | # since it will be 0 from having been a duplicate. | ||
405 | 1489 | getUtility(ICalculateBugHeatJobSource).create(self) | ||
407 | 1490 | 1492 | ||
408 | 1491 | def setCommentVisibility(self, user, comment_number, visible): | 1493 | def setCommentVisibility(self, user, comment_number, visible): |
409 | 1492 | """See `IBug`.""" | 1494 | """See `IBug`.""" |
410 | @@ -1863,7 +1865,8 @@ | |||
411 | 1863 | Bug.heat_last_updated < last_updated_cutoff, | 1865 | Bug.heat_last_updated < last_updated_cutoff, |
412 | 1864 | Bug.heat_last_updated == None) | 1866 | Bug.heat_last_updated == None) |
413 | 1865 | 1867 | ||
415 | 1866 | return store.find(Bug, last_updated_clause).order_by('id') | 1868 | return store.find( |
416 | 1869 | Bug, Bug.duplicateof==None, last_updated_clause).order_by('id') | ||
417 | 1867 | 1870 | ||
418 | 1868 | 1871 | ||
419 | 1869 | class BugAffectsPerson(SQLBase): | 1872 | class BugAffectsPerson(SQLBase): |
420 | 1870 | 1873 | ||
421 | === modified file 'lib/lp/bugs/tests/test_bugheat.py' | |||
422 | --- lib/lp/bugs/tests/test_bugheat.py 2010-04-01 03:09:43 +0000 | |||
423 | +++ lib/lp/bugs/tests/test_bugheat.py 2010-05-29 16:21:27 +0000 | |||
424 | @@ -112,108 +112,6 @@ | |||
425 | 112 | self.assertIn(('bug_job_id', job.context.id), vars) | 112 | self.assertIn(('bug_job_id', job.context.id), vars) |
426 | 113 | self.assertIn(('bug_job_type', job.context.job_type.title), vars) | 113 | self.assertIn(('bug_job_type', job.context.job_type.title), vars) |
427 | 114 | 114 | ||
428 | 115 | def test_bug_changes_adds_job(self): | ||
429 | 116 | # Calling addChange() on a Bug will add a CalculateBugHeatJob | ||
430 | 117 | # for that bug to the queue. | ||
431 | 118 | self.assertEqual(0, self._getJobCount()) | ||
432 | 119 | |||
433 | 120 | change = BugDescriptionChange( | ||
434 | 121 | when=datetime.now().replace(tzinfo=pytz.timezone('UTC')), | ||
435 | 122 | person=self.bug.owner, what_changed='description', | ||
436 | 123 | old_value=self.bug.description, new_value='Some text') | ||
437 | 124 | self.bug.addChange(change) | ||
438 | 125 | |||
439 | 126 | # There will now be a job in the queue. | ||
440 | 127 | self.assertEqual(1, self._getJobCount()) | ||
441 | 128 | |||
442 | 129 | def test_subscribing_adds_job(self): | ||
443 | 130 | # Calling Bug.subscribe() will add a CalculateBugHeatJob for the | ||
444 | 131 | # Bug. | ||
445 | 132 | self.assertEqual(0, self._getJobCount()) | ||
446 | 133 | |||
447 | 134 | person = self.factory.makePerson() | ||
448 | 135 | self.bug.subscribe(person, person) | ||
449 | 136 | transaction.commit() | ||
450 | 137 | |||
451 | 138 | # There will now be a job in the queue. | ||
452 | 139 | self.assertEqual(1, self._getJobCount()) | ||
453 | 140 | |||
454 | 141 | def test_unsubscribing_adds_job(self): | ||
455 | 142 | # Calling Bug.unsubscribe() will add a CalculateBugHeatJob for the | ||
456 | 143 | # Bug. | ||
457 | 144 | self.assertEqual(0, self._getJobCount()) | ||
458 | 145 | |||
459 | 146 | self.bug.unsubscribe(self.bug.owner, self.bug.owner) | ||
460 | 147 | transaction.commit() | ||
461 | 148 | |||
462 | 149 | # There will now be a job in the queue. | ||
463 | 150 | self.assertEqual(1, self._getJobCount()) | ||
464 | 151 | |||
465 | 152 | def test_marking_affected_adds_job(self): | ||
466 | 153 | # Marking a user as affected by a bug adds a CalculateBugHeatJob | ||
467 | 154 | # for the bug. | ||
468 | 155 | self.assertEqual(0, self._getJobCount()) | ||
469 | 156 | |||
470 | 157 | person = self.factory.makePerson() | ||
471 | 158 | self.bug.markUserAffected(person) | ||
472 | 159 | |||
473 | 160 | # There will now be a job in the queue. | ||
474 | 161 | self.assertEqual(1, self._getJobCount()) | ||
475 | 162 | |||
476 | 163 | def test_marking_unaffected_adds_job(self): | ||
477 | 164 | # Marking a user as unaffected by a bug adds a CalculateBugHeatJob | ||
478 | 165 | # for the bug. | ||
479 | 166 | self.assertEqual(0, self._getJobCount()) | ||
480 | 167 | |||
481 | 168 | self.bug.markUserAffected(self.bug.owner, False) | ||
482 | 169 | |||
483 | 170 | # There will now be a job in the queue. | ||
484 | 171 | self.assertEqual(1, self._getJobCount()) | ||
485 | 172 | |||
486 | 173 | def test_bug_creation_creates_job(self): | ||
487 | 174 | # Creating a bug adds a CalculateBugHeatJob for the new bug. | ||
488 | 175 | self.assertEqual(0, self._getJobCount()) | ||
489 | 176 | |||
490 | 177 | new_bug = self.factory.makeBug() | ||
491 | 178 | |||
492 | 179 | # There will now be a job in the queue. | ||
493 | 180 | self.assertEqual(1, self._getJobCount()) | ||
494 | 181 | |||
495 | 182 | def test_marking_dupe_creates_job(self): | ||
496 | 183 | # Marking a bug as a duplicate of another bug creates a job to | ||
497 | 184 | # update the master bug. | ||
498 | 185 | new_bug = self.factory.makeBug() | ||
499 | 186 | new_bug.setHeat(42) | ||
500 | 187 | self._completeJobsAndAssertQueueEmpty() | ||
501 | 188 | |||
502 | 189 | new_bug.markAsDuplicate(self.bug) | ||
503 | 190 | |||
504 | 191 | # There will now be a job in the queue. | ||
505 | 192 | self.assertEqual(1, self._getJobCount()) | ||
506 | 193 | |||
507 | 194 | # And the job will be for the master bug. | ||
508 | 195 | bug_job = self._getJobs()[0] | ||
509 | 196 | self.assertEqual(bug_job.bug, self.bug) | ||
510 | 197 | |||
511 | 198 | # Also, the duplicate bug's heat will have been set to zero. | ||
512 | 199 | self.assertEqual(0, new_bug.heat) | ||
513 | 200 | |||
514 | 201 | def test_unmarking_dupe_creates_job(self): | ||
515 | 202 | # Unmarking a bug as a duplicate will create a | ||
516 | 203 | # CalculateBugHeatJob for the bug, since its heat will be 0 from | ||
517 | 204 | # having been marked as a duplicate. | ||
518 | 205 | new_bug = self.factory.makeBug() | ||
519 | 206 | new_bug.markAsDuplicate(self.bug) | ||
520 | 207 | self._completeJobsAndAssertQueueEmpty() | ||
521 | 208 | new_bug.markAsDuplicate(None) | ||
522 | 209 | |||
523 | 210 | # There will now be a job in the queue. | ||
524 | 211 | self.assertEqual(1, self._getJobCount()) | ||
525 | 212 | |||
526 | 213 | # And the job will be for the master bug. | ||
527 | 214 | bug_job = self._getJobs()[0] | ||
528 | 215 | self.assertEqual(bug_job.bug, new_bug) | ||
529 | 216 | |||
530 | 217 | 115 | ||
531 | 218 | class MaxHeatByTargetBase: | 116 | class MaxHeatByTargetBase: |
532 | 219 | """Base class for testing a bug target's max_bug_heat attribute.""" | 117 | """Base class for testing a bug target's max_bug_heat attribute.""" |
Hi Graham,
we discussed that it would be better to update the heat values in chunks, with initial chunk size of 1000. I like your implementation as in http:// pastebin. ubuntu. com/440470/
thanks for finally fixing the heat updater!