Merge lp:~allenap/launchpad/dynamic-batch-size-bug-546085 into lp:launchpad/db-devel
- dynamic-batch-size-bug-546085
- Merge into db-devel
| Status: | Merged |
|---|---|
| Approved by: | Gavin Panella on 2010-04-07 |
| Approved revision: | no longer in the source branch. |
| Merged at revision: | not available |
| Proposed branch: | lp:~allenap/launchpad/dynamic-batch-size-bug-546085 |
| Merge into: | lp:launchpad/db-devel |
| Diff against target: |
595 lines (+202/-125) 11 files modified
lib/lp/bugs/doc/checkwatches.txt (+6/-3) lib/lp/bugs/doc/externalbugtracker-debbugs.txt (+3/-3) lib/lp/bugs/doc/externalbugtracker.txt (+5/-2) lib/lp/bugs/externalbugtracker/__init__.py (+4/-1) lib/lp/bugs/externalbugtracker/base.py (+5/-1) lib/lp/bugs/externalbugtracker/debbugs.py (+8/-7) lib/lp/bugs/scripts/checkwatches/tests/test_scheduler.py (+110/-0) lib/lp/bugs/scripts/checkwatches/tests/test_updater.py (+15/-0) lib/lp/bugs/scripts/checkwatches/updater.py (+40/-16) lib/lp/bugs/tests/externalbugtracker.py (+4/-2) lib/lp/bugs/tests/test_bugwatch.py (+2/-90) |
| To merge this branch: | bzr merge lp:~allenap/launchpad/dynamic-batch-size-bug-546085 |
| Related bugs: |
| Reviewer | Review Type | Date Requested | Status |
|---|---|---|---|
| Edwin Grubbs (community) | code | 2010-03-31 | Approve on 2010-03-31 |
|
Review via email:
|
|||
Commit Message
In checkwatches, calculate a sensible batch size at run-time based on the number of watches there are for a bug tracker.
Description of the Change
When checkwatches runs it observes a batch_size attribute on the object representing the remote system. This is fine, but for bug trackers with an especially large number of bug watches the batch size is a bit too low. This branch makes batch_size equal to 2% of the number of bug watches recorded for a bug tracker, or 100, whichever is greater. For GNOME Bugs this works out at about 300 (GNOME Bugs is the largest active bug tracker registered in Launchpad, with >15000 bug watches).
I've also moved the test for BugWatchScheduler into the lp.bugs.
| Gavin Panella (allenap) wrote : | # |
Post-review diff, constructed from revisions 9177, 9178 and 9180.
| 1 | --- lib/lp/bugs/externalbugtracker/__init__.py 2009-06-25 00:40:31 +0000 |
| 2 | +++ lib/lp/bugs/externalbugtracker/__init__.py 2010-04-07 14:25:18 +0000 |
| 3 | @@ -7,7 +7,7 @@ |
| 4 | |
| 5 | __metaclass__ = type |
| 6 | __all__ = [ |
| 7 | - 'get_external_bugtracker', |
| 8 | + 'BATCH_SIZE_UNLIMITED', |
| 9 | 'BugNotFound', |
| 10 | 'BugTrackerConnectError', |
| 11 | 'BugWatchUpdateError', |
| 12 | @@ -30,6 +30,7 @@ |
| 13 | 'UnparseableBugData', |
| 14 | 'UnparseableBugTrackerVersion', |
| 15 | 'UnsupportedBugTrackerVersion', |
| 16 | + 'get_external_bugtracker', |
| 17 | ] |
| 18 | |
| 19 | from lp.bugs.externalbugtracker.base import * |
| 20 | @@ -41,6 +42,8 @@ |
| 21 | from lp.bugs.externalbugtracker.rt import * |
| 22 | from lp.bugs.externalbugtracker.trac import * |
| 23 | from lp.bugs.interfaces.bugtracker import BugTrackerType |
| 24 | + |
| 25 | + |
| 26 | BUG_TRACKER_CLASSES = { |
| 27 | BugTrackerType.BUGZILLA: Bugzilla, |
| 28 | BugTrackerType.DEBBUGS: DebBugs, |
| 29 | --- lib/lp/bugs/externalbugtracker/base.py 2010-03-30 12:19:11 +0000 |
| 30 | +++ lib/lp/bugs/externalbugtracker/base.py 2010-04-07 14:25:18 +0000 |
| 31 | @@ -5,6 +5,7 @@ |
| 32 | |
| 33 | __metaclass__ = type |
| 34 | __all__ = [ |
| 35 | + 'BATCH_SIZE_UNLIMITED', |
| 36 | 'BugNotFound', |
| 37 | 'BugTrackerAuthenticationError', |
| 38 | 'BugTrackerConnectError', |
| 39 | @@ -39,6 +40,9 @@ |
| 40 | # The user agent we send in our requests |
| 41 | LP_USER_AGENT = "Launchpad Bugscraper/0.2 (https://bugs.launchpad.net/)" |
| 42 | |
| 43 | +# To signify that all bug watches should be checked in a single run. |
| 44 | +BATCH_SIZE_UNLIMITED = 0 |
| 45 | + |
| 46 | |
| 47 | # |
| 48 | # Errors. |
| 49 | --- lib/lp/bugs/externalbugtracker/debbugs.py 2010-03-26 17:32:38 +0000 |
| 50 | +++ lib/lp/bugs/externalbugtracker/debbugs.py 2010-04-07 14:25:18 +0000 |
| 51 | @@ -22,18 +22,19 @@ |
| 52 | |
| 53 | from canonical.config import config |
| 54 | from canonical.database.sqlbase import commit |
| 55 | +from canonical.launchpad.interfaces.message import IMessageSet |
| 56 | +from canonical.launchpad.mail import simple_sendmail |
| 57 | +from canonical.launchpad.webapp import urlsplit |
| 58 | + |
| 59 | from lp.bugs.externalbugtracker import ( |
| 60 | - BugNotFound, BugTrackerConnectError, ExternalBugTracker, |
| 61 | - InvalidBugId, UnknownRemoteStatusError) |
| 62 | -from canonical.launchpad.interfaces.message import IMessageSet |
| 63 | + BATCH_SIZE_UNLIMITED, BugNotFound, BugTrackerConnectError, |
| 64 | + ExternalBugTracker, InvalidBugId, UnknownRemoteStatusError) |
| 65 | +from lp.bugs.externalbugtracker.isolation import ensure_no_transaction |
| 66 | from lp.bugs.interfaces.bugtask import BugTaskImportance, BugTaskStatus |
| 67 | from lp.bugs.interfaces.externalbugtracker import ( |
| 68 | ISupportsBugImport, ISupportsCommentImport, ISupportsCommentPushing, |
| 69 | UNKNOWN_REMOTE_IMPORTANCE) |
| 70 | -from canonical.launchpad.mail import simple_sendmail |
| 71 | from lp.bugs.scripts import debbugs |
| 72 | -from canonical.launchpad.webapp import urlsplit |
| 73 | -from lp.bugs.externalbugtracker.isolation import ensure_no_transaction |
| 74 | |
| 75 | |
| 76 | debbugsstatusmap = {'open': BugTaskStatus.NEW, |
| 77 | @@ -61,7 +62,7 @@ |
| 78 | # Because we keep a local copy of debbugs, we remove the batch_size |
| 79 | # limit so that all debbugs watches that need checking will be |
| 80 | # checked each time checkwatches runs. |
| 81 | - batch_size = 0 |
| 82 | + batch_size = BATCH_SIZE_UNLIMITED |
| 83 | |
| 84 | def __init__(self, baseurl, db_location=None): |
| 85 | super(DebBugs, self).__init__(baseurl) |
| 86 | --- lib/lp/bugs/scripts/checkwatches/updater.py 2010-03-31 10:51:17 +0000 |
| 87 | +++ lib/lp/bugs/scripts/checkwatches/updater.py 2010-04-07 14:25:18 +0000 |
| 88 | @@ -61,16 +61,15 @@ |
| 89 | |
| 90 | from lp.bugs import externalbugtracker |
| 91 | from lp.bugs.externalbugtracker import ( |
| 92 | - BugNotFound, BugTrackerConnectError, BugWatchUpdateError, |
| 93 | - BugWatchUpdateWarning, InvalidBugId, PrivateRemoteBug, |
| 94 | - UnknownBugTrackerTypeError, UnknownRemoteStatusError, UnparseableBugData, |
| 95 | - UnparseableBugTrackerVersion, UnsupportedBugTrackerVersion) |
| 96 | -from lp.bugs.externalbugtracker.bugzilla import ( |
| 97 | - BugzillaAPI) |
| 98 | + BATCH_SIZE_UNLIMITED, BugNotFound, BugTrackerConnectError, |
| 99 | + BugWatchUpdateError, BugWatchUpdateWarning, InvalidBugId, |
| 100 | + PrivateRemoteBug, UnknownBugTrackerTypeError, UnknownRemoteStatusError, |
| 101 | + UnparseableBugData, UnparseableBugTrackerVersion, |
| 102 | + UnsupportedBugTrackerVersion) |
| 103 | +from lp.bugs.externalbugtracker.bugzilla import BugzillaAPI |
| 104 | from lp.bugs.externalbugtracker.isolation import check_no_transaction |
| 105 | from lp.bugs.interfaces.bug import IBugSet |
| 106 | -from lp.bugs.interfaces.externalbugtracker import ( |
| 107 | - ISupportsBackLinking) |
| 108 | +from lp.bugs.interfaces.externalbugtracker import ISupportsBackLinking |
| 109 | from lp.services.limitedlist import LimitedList |
| 110 | from lp.services.scripts.base import LaunchpadCronScript |
| 111 | |
| 112 | @@ -250,17 +249,17 @@ |
| 113 | def suggest_batch_size(remote_system, num_watches): |
| 114 | """Suggest a value for batch_size if it's not set. |
| 115 | |
| 116 | - Givend the number of bug watches for an `remote_system`, this sets |
| 117 | - a suggested batch size on it. If `remote_system` already has a |
| 118 | - batch size set, this does not override it. |
| 119 | + Given the number of bug watches for a `remote_system`, this sets a |
| 120 | + suggested batch size on it. If `remote_system` already has a batch |
| 121 | + size set, this does not override it. |
| 122 | |
| 123 | :param remote_system: An `ExternalBugTracker`. |
| 124 | :param num_watches: The number of watches for `remote_system`. |
| 125 | """ |
| 126 | if remote_system.batch_size is None: |
| 127 | remote_system.batch_size = max( |
| 128 | - SUGGESTED_BATCH_SIZE_MIN, int( |
| 129 | - SUGGESTED_BATCH_SIZE_PROPORTION * num_watches)) |
| 130 | + SUGGESTED_BATCH_SIZE_MIN, |
| 131 | + int(SUGGESTED_BATCH_SIZE_PROPORTION * num_watches)) |
| 132 | |
| 133 | |
| 134 | class BugWatchUpdater(object): |
| 135 | @@ -402,7 +401,12 @@ |
| 136 | `BaseScheduler`. If no scheduler is given, `SerialScheduler` |
| 137 | will be used, which simply runs the jobs in order. |
| 138 | """ |
| 139 | - self.log.debug("Using a global batch size of %s" % batch_size) |
| 140 | + if batch_size is None: |
| 141 | + self.log.debug("No global batch size specified.") |
| 142 | + elif batch_size == BATCH_SIZE_UNLIMITED: |
| 143 | + self.log.debug("Using an unlimited global batch size.") |
| 144 | + else: |
| 145 | + self.log.debug("Using a global batch size of %s" % batch_size) |
| 146 | |
| 147 | # Default to using the very simple SerialScheduler. |
| 148 | if scheduler is None: |
| 149 | @@ -681,11 +685,6 @@ |
| 150 | # by the ExternalBugTracker. |
| 151 | batch_size = remotesystem.batch_size |
| 152 | |
| 153 | - if batch_size == 0: |
| 154 | - # A batch_size of 0 means that there's no batch size limit |
| 155 | - # for this bug tracker. |
| 156 | - batch_size = None |
| 157 | - |
| 158 | with self.transaction: |
| 159 | old_bug_watches = set( |
| 160 | bug_watch for bug_watch in bug_watches |
| 161 | @@ -718,7 +717,7 @@ |
| 162 | # are actually some bugs that we're interested in so as to |
| 163 | # avoid unnecessary network traffic. |
| 164 | if server_time is not None and len(remote_old_ids) > 0: |
| 165 | - if batch_size is None: |
| 166 | + if batch_size == BATCH_SIZE_UNLIMITED: |
| 167 | remote_old_ids_to_check = ( |
| 168 | remotesystem.getModifiedRemoteBugs( |
| 169 | remote_old_ids, oldest_lastchecked)) |
| 170 | @@ -746,7 +745,7 @@ |
| 171 | remote_ids_to_check = chain( |
| 172 | remote_ids_with_comments, remote_new_ids, remote_old_ids_to_check) |
| 173 | |
| 174 | - if batch_size is not None: |
| 175 | + if batch_size != BATCH_SIZE_UNLIMITED: |
| 176 | # Some remote bug IDs may appear in more than one list so |
| 177 | # we must filter the list before slicing. |
| 178 | remote_ids_to_check = islice( |
| 179 | --- lib/lp/bugs/doc/checkwatches.txt 2010-03-30 17:25:52 +0000 |
| 180 | +++ lib/lp/bugs/doc/checkwatches.txt 2010-04-07 16:08:16 +0000 |
| 181 | @@ -45,7 +45,7 @@ |
| 182 | |
| 183 | >>> print err |
| 184 | INFO creating lockfile |
| 185 | - DEBUG Using a global batch size of None |
| 186 | + DEBUG No global batch size specified. |
| 187 | DEBUG Skipping updating Ubuntu Bugzilla watches. |
| 188 | DEBUG No watches to update on http://bugs.debian.org |
| 189 | DEBUG No watches to update on mailto:bugs@example.com |
| 190 | @@ -274,7 +274,7 @@ |
| 191 | ... updater.log = original_log |
| 192 | ... externalbugtracker.Roundup.batch_size = batch_size |
| 193 | ... urllib2.urlopen = urlopen |
| 194 | - DEBUG Using a global batch size of None |
| 195 | + DEBUG No global batch size specified. |
| 196 | INFO Updating 5 watches for 5 bugs on http://bugs.example.com |
| 197 | ERROR Connection timed out when updating ... (OOPS-...) |
| 198 | |
| 199 | @@ -412,7 +412,8 @@ |
| 200 | >>> from lp.bugs.interfaces.externalbugtracker import ( |
| 201 | ... ISupportsCommentImport, ISupportsCommentPushing, |
| 202 | ... ISupportsBackLinking) |
| 203 | - >>> from lp.bugs.externalbugtracker.base import ExternalBugTracker |
| 204 | + >>> from lp.bugs.externalbugtracker.base import ( |
| 205 | + ... BATCH_SIZE_UNLIMITED, ExternalBugTracker) |
| 206 | |
| 207 | >>> nowish = datetime.now(utc) |
| 208 | >>> class UselessExternalBugTracker(ExternalBugTracker): |
| 209 | @@ -421,6 +422,8 @@ |
| 210 | ... ISupportsBackLinking, ISupportsCommentImport, |
| 211 | ... ISupportsCommentPushing) |
| 212 | ... |
| 213 | + ... batch_size = BATCH_SIZE_UNLIMITED |
| 214 | + ... |
| 215 | ... def initializeRemoteBugDB(self, bug_ids): |
| 216 | ... # This just exists to stop errors from being raised. |
| 217 | ... pass |
| 218 | --- lib/lp/bugs/doc/externalbugtracker-debbugs.txt 2010-03-26 13:48:53 +0000 |
| 219 | +++ lib/lp/bugs/doc/externalbugtracker-debbugs.txt 2010-04-07 16:08:16 +0000 |
| 220 | @@ -14,7 +14,7 @@ |
| 221 | You can specify the db_location explicitly: |
| 222 | |
| 223 | >>> from lp.bugs.externalbugtracker import ( |
| 224 | - ... DebBugs) |
| 225 | + ... BATCH_SIZE_UNLIMITED, DebBugs) |
| 226 | >>> from canonical.testing import LaunchpadZopelessLayer |
| 227 | >>> txn = LaunchpadZopelessLayer.txn |
| 228 | >>> external_debbugs = DebBugs( |
| 229 | @@ -42,8 +42,8 @@ |
| 230 | worry about batching bug watch updates for performance reasons, so |
| 231 | DebBugs instances don't have a batch_size limit. |
| 232 | |
| 233 | - >>> print external_debbugs.batch_size |
| 234 | - None |
| 235 | + >>> external_debbugs.batch_size == BATCH_SIZE_UNLIMITED |
| 236 | + True |
| 237 | |
| 238 | |
| 239 | == Retrieving bug status from the debbugs database == |
| 240 | --- lib/lp/bugs/tests/externalbugtracker.py 2009-12-23 12:14:59 +0000 |
| 241 | +++ lib/lp/bugs/tests/externalbugtracker.py 2010-04-07 16:08:16 +0000 |
| 242 | @@ -24,8 +24,8 @@ |
| 243 | from canonical.config import config |
| 244 | from canonical.database.sqlbase import commit, ZopelessTransactionManager |
| 245 | from lp.bugs.externalbugtracker import ( |
| 246 | - BugNotFound, BugTrackerConnectError, Bugzilla, DebBugs, |
| 247 | - ExternalBugTracker, Mantis, RequestTracker, Roundup, SourceForge, |
| 248 | + BATCH_SIZE_UNLIMITED, BugNotFound, BugTrackerConnectError, Bugzilla, |
| 249 | + DebBugs, ExternalBugTracker, Mantis, RequestTracker, Roundup, SourceForge, |
| 250 | Trac) |
| 251 | from lp.bugs.externalbugtracker.trac import ( |
| 252 | FAULT_TICKET_NOT_FOUND, LP_PLUGIN_BUG_IDS_ONLY, LP_PLUGIN_FULL, |
| 253 | @@ -152,6 +152,8 @@ |
| 254 | implementation, though it doesn't actually do anything. |
| 255 | """ |
| 256 | |
| 257 | + batch_size = BATCH_SIZE_UNLIMITED |
| 258 | + |
| 259 | def __init__(self, baseurl='http://example.com/'): |
| 260 | super(TestExternalBugTracker, self).__init__(baseurl) |
| 261 |
| Gavin Panella (allenap) wrote : | # |
Conversation re. additional changes from IRC:
<EdwinGrubbs> allenap: I'm surprised that you added a line to tests/externalb
<allenap> EdwinGrubbs: It kind of does the same as lines 153 to 157 used to do.
<allenap> EdwinGrubbs: ... because most calls into updateBugWatches() and _getRemoteIdsTo
<EdwinGrubbs> allenap: ok, that makes sense. Looks good.
Preview Diff
| 1 | === modified file 'lib/lp/bugs/doc/checkwatches.txt' |
| 2 | --- lib/lp/bugs/doc/checkwatches.txt 2010-04-06 21:36:16 +0000 |
| 3 | +++ lib/lp/bugs/doc/checkwatches.txt 2010-04-08 15:38:28 +0000 |
| 4 | @@ -45,7 +45,7 @@ |
| 5 | |
| 6 | >>> print err |
| 7 | INFO Creating lockfile: /var/lock/launchpad-checkwatches.lock |
| 8 | - DEBUG Using a global batch size of None |
| 9 | + DEBUG No global batch size specified. |
| 10 | DEBUG Skipping updating Ubuntu Bugzilla watches. |
| 11 | DEBUG No watches to update on http://bugs.debian.org |
| 12 | DEBUG No watches to update on mailto:bugs@example.com |
| 13 | @@ -274,7 +274,7 @@ |
| 14 | ... updater.log = original_log |
| 15 | ... externalbugtracker.Roundup.batch_size = batch_size |
| 16 | ... urllib2.urlopen = urlopen |
| 17 | - DEBUG Using a global batch size of None |
| 18 | + DEBUG No global batch size specified. |
| 19 | INFO Updating 5 watches for 5 bugs on http://bugs.example.com |
| 20 | ERROR Connection timed out when updating ... (OOPS-...) |
| 21 | |
| 22 | @@ -412,7 +412,8 @@ |
| 23 | >>> from lp.bugs.interfaces.externalbugtracker import ( |
| 24 | ... ISupportsCommentImport, ISupportsCommentPushing, |
| 25 | ... ISupportsBackLinking) |
| 26 | - >>> from lp.bugs.externalbugtracker.base import ExternalBugTracker |
| 27 | + >>> from lp.bugs.externalbugtracker.base import ( |
| 28 | + ... BATCH_SIZE_UNLIMITED, ExternalBugTracker) |
| 29 | |
| 30 | >>> nowish = datetime.now(utc) |
| 31 | >>> class UselessExternalBugTracker(ExternalBugTracker): |
| 32 | @@ -421,6 +422,8 @@ |
| 33 | ... ISupportsBackLinking, ISupportsCommentImport, |
| 34 | ... ISupportsCommentPushing) |
| 35 | ... |
| 36 | + ... batch_size = BATCH_SIZE_UNLIMITED |
| 37 | + ... |
| 38 | ... def initializeRemoteBugDB(self, bug_ids): |
| 39 | ... # This just exists to stop errors from being raised. |
| 40 | ... pass |
| 41 | |
| 42 | === modified file 'lib/lp/bugs/doc/externalbugtracker-debbugs.txt' |
| 43 | --- lib/lp/bugs/doc/externalbugtracker-debbugs.txt 2010-03-26 13:48:53 +0000 |
| 44 | +++ lib/lp/bugs/doc/externalbugtracker-debbugs.txt 2010-04-08 15:38:28 +0000 |
| 45 | @@ -14,7 +14,7 @@ |
| 46 | You can specify the db_location explicitly: |
| 47 | |
| 48 | >>> from lp.bugs.externalbugtracker import ( |
| 49 | - ... DebBugs) |
| 50 | + ... BATCH_SIZE_UNLIMITED, DebBugs) |
| 51 | >>> from canonical.testing import LaunchpadZopelessLayer |
| 52 | >>> txn = LaunchpadZopelessLayer.txn |
| 53 | >>> external_debbugs = DebBugs( |
| 54 | @@ -42,8 +42,8 @@ |
| 55 | worry about batching bug watch updates for performance reasons, so |
| 56 | DebBugs instances don't have a batch_size limit. |
| 57 | |
| 58 | - >>> print external_debbugs.batch_size |
| 59 | - None |
| 60 | + >>> external_debbugs.batch_size == BATCH_SIZE_UNLIMITED |
| 61 | + True |
| 62 | |
| 63 | |
| 64 | == Retrieving bug status from the debbugs database == |
| 65 | |
| 66 | === modified file 'lib/lp/bugs/doc/externalbugtracker.txt' |
| 67 | --- lib/lp/bugs/doc/externalbugtracker.txt 2010-03-26 15:12:59 +0000 |
| 68 | +++ lib/lp/bugs/doc/externalbugtracker.txt 2010-04-08 15:38:28 +0000 |
| 69 | @@ -1042,13 +1042,16 @@ |
| 70 | |
| 71 | _getRemoteIdsToCheck() will interpret a batch_size parameter of 0 as an |
| 72 | instruction to ignore the batch size limitation altogether and just return all |
| 73 | -the IDs that need checking. |
| 74 | +the IDs that need checking. The constant BATCH_SIZE_UNLIMITED should |
| 75 | +be used in place of using 0 verbatim. |
| 76 | + |
| 77 | + >>> from lp.bugs.externalbugtracker import BATCH_SIZE_UNLIMITED |
| 78 | |
| 79 | >>> ids = bug_watch_updater._getRemoteIdsToCheck( |
| 80 | ... external_bugtracker, |
| 81 | ... unchecked_watches + watches_with_comments + old_watches, |
| 82 | ... external_bugtracker.getCurrentDBTime(), utc_now, |
| 83 | - ... batch_size=0) |
| 84 | + ... batch_size=BATCH_SIZE_UNLIMITED) |
| 85 | >>> print sorted(ids['remote_ids_to_check']) |
| 86 | [u'0', u'1', u'2', u'3', u'4', u'5', u'6', u'7', u'8', u'9'] |
| 87 | |
| 88 | |
| 89 | === modified file 'lib/lp/bugs/externalbugtracker/__init__.py' |
| 90 | --- lib/lp/bugs/externalbugtracker/__init__.py 2009-06-25 00:40:31 +0000 |
| 91 | +++ lib/lp/bugs/externalbugtracker/__init__.py 2010-04-08 15:38:28 +0000 |
| 92 | @@ -7,7 +7,7 @@ |
| 93 | |
| 94 | __metaclass__ = type |
| 95 | __all__ = [ |
| 96 | - 'get_external_bugtracker', |
| 97 | + 'BATCH_SIZE_UNLIMITED', |
| 98 | 'BugNotFound', |
| 99 | 'BugTrackerConnectError', |
| 100 | 'BugWatchUpdateError', |
| 101 | @@ -30,6 +30,7 @@ |
| 102 | 'UnparseableBugData', |
| 103 | 'UnparseableBugTrackerVersion', |
| 104 | 'UnsupportedBugTrackerVersion', |
| 105 | + 'get_external_bugtracker', |
| 106 | ] |
| 107 | |
| 108 | from lp.bugs.externalbugtracker.base import * |
| 109 | @@ -41,6 +42,8 @@ |
| 110 | from lp.bugs.externalbugtracker.rt import * |
| 111 | from lp.bugs.externalbugtracker.trac import * |
| 112 | from lp.bugs.interfaces.bugtracker import BugTrackerType |
| 113 | + |
| 114 | + |
| 115 | BUG_TRACKER_CLASSES = { |
| 116 | BugTrackerType.BUGZILLA: Bugzilla, |
| 117 | BugTrackerType.DEBBUGS: DebBugs, |
| 118 | |
| 119 | === modified file 'lib/lp/bugs/externalbugtracker/base.py' |
| 120 | --- lib/lp/bugs/externalbugtracker/base.py 2010-03-26 15:12:59 +0000 |
| 121 | +++ lib/lp/bugs/externalbugtracker/base.py 2010-04-08 15:38:28 +0000 |
| 122 | @@ -5,6 +5,7 @@ |
| 123 | |
| 124 | __metaclass__ = type |
| 125 | __all__ = [ |
| 126 | + 'BATCH_SIZE_UNLIMITED', |
| 127 | 'BugNotFound', |
| 128 | 'BugTrackerAuthenticationError', |
| 129 | 'BugTrackerConnectError', |
| 130 | @@ -39,6 +40,9 @@ |
| 131 | # The user agent we send in our requests |
| 132 | LP_USER_AGENT = "Launchpad Bugscraper/0.2 (https://bugs.launchpad.net/)" |
| 133 | |
| 134 | +# To signify that all bug watches should be checked in a single run. |
| 135 | +BATCH_SIZE_UNLIMITED = 0 |
| 136 | + |
| 137 | |
| 138 | # |
| 139 | # Errors. |
| 140 | @@ -133,7 +137,7 @@ |
| 141 | |
| 142 | implements(IExternalBugTracker) |
| 143 | |
| 144 | - batch_size = 100 |
| 145 | + batch_size = None |
| 146 | batch_query_threshold = config.checkwatches.batch_query_threshold |
| 147 | comment_template = 'default_remotecomment_template.txt' |
| 148 | |
| 149 | |
| 150 | === modified file 'lib/lp/bugs/externalbugtracker/debbugs.py' |
| 151 | --- lib/lp/bugs/externalbugtracker/debbugs.py 2010-03-31 20:09:55 +0000 |
| 152 | +++ lib/lp/bugs/externalbugtracker/debbugs.py 2010-04-08 15:38:28 +0000 |
| 153 | @@ -22,18 +22,19 @@ |
| 154 | |
| 155 | from canonical.config import config |
| 156 | from canonical.database.sqlbase import commit |
| 157 | +from canonical.launchpad.interfaces.message import IMessageSet |
| 158 | +from canonical.launchpad.mail import simple_sendmail |
| 159 | +from canonical.launchpad.webapp import urlsplit |
| 160 | + |
| 161 | from lp.bugs.externalbugtracker import ( |
| 162 | - BugNotFound, BugTrackerConnectError, ExternalBugTracker, |
| 163 | - InvalidBugId, UnknownRemoteStatusError) |
| 164 | -from canonical.launchpad.interfaces.message import IMessageSet |
| 165 | + BATCH_SIZE_UNLIMITED, BugNotFound, BugTrackerConnectError, |
| 166 | + ExternalBugTracker, InvalidBugId, UnknownRemoteStatusError) |
| 167 | +from lp.bugs.externalbugtracker.isolation import ensure_no_transaction |
| 168 | from lp.bugs.interfaces.bugtask import BugTaskImportance, BugTaskStatus |
| 169 | from lp.bugs.interfaces.externalbugtracker import ( |
| 170 | ISupportsBugImport, ISupportsCommentImport, ISupportsCommentPushing, |
| 171 | UNKNOWN_REMOTE_IMPORTANCE) |
| 172 | -from canonical.launchpad.mail import simple_sendmail |
| 173 | from lp.bugs.scripts import debbugs |
| 174 | -from canonical.launchpad.webapp import urlsplit |
| 175 | -from lp.bugs.externalbugtracker.isolation import ensure_no_transaction |
| 176 | |
| 177 | |
| 178 | debbugsstatusmap = {'open': BugTaskStatus.NEW, |
| 179 | @@ -59,7 +60,7 @@ |
| 180 | # Because we keep a local copy of debbugs, we remove the batch_size |
| 181 | # limit so that all debbugs watches that need checking will be |
| 182 | # checked each time checkwatches runs. |
| 183 | - batch_size = None |
| 184 | + batch_size = BATCH_SIZE_UNLIMITED |
| 185 | |
| 186 | def __init__(self, baseurl, db_location=None): |
| 187 | super(DebBugs, self).__init__(baseurl) |
| 188 | |
| 189 | === added directory 'lib/lp/bugs/scripts/checkwatches/tests' |
| 190 | === added file 'lib/lp/bugs/scripts/checkwatches/tests/__init__.py' |
| 191 | === added file 'lib/lp/bugs/scripts/checkwatches/tests/test_scheduler.py' |
| 192 | --- lib/lp/bugs/scripts/checkwatches/tests/test_scheduler.py 1970-01-01 00:00:00 +0000 |
| 193 | +++ lib/lp/bugs/scripts/checkwatches/tests/test_scheduler.py 2010-04-08 15:38:28 +0000 |
| 194 | @@ -0,0 +1,110 @@ |
| 195 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
| 196 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
| 197 | + |
| 198 | +"""XXX: Module docstring goes here.""" |
| 199 | + |
| 200 | +__metaclass__ = type |
| 201 | + |
| 202 | +import transaction |
| 203 | +import unittest |
| 204 | + |
| 205 | +from datetime import datetime, timedelta |
| 206 | +from pytz import utc |
| 207 | + |
| 208 | +from zope.component import getUtility |
| 209 | + |
| 210 | +from canonical.launchpad.scripts.logger import QuietFakeLogger |
| 211 | +from canonical.testing import DatabaseFunctionalLayer |
| 212 | + |
| 213 | +from lp.bugs.interfaces.bugwatch import ( |
| 214 | + BugWatchActivityStatus, IBugWatchSet) |
| 215 | +from lp.bugs.scripts.checkwatches.scheduler import BugWatchScheduler |
| 216 | + |
| 217 | +from lp.testing import TestCaseWithFactory |
| 218 | + |
| 219 | + |
| 220 | +class TestBugWatchScheduler(TestCaseWithFactory): |
| 221 | + """Tests for the BugWatchScheduler, which runs as part of garbo.""" |
| 222 | + |
| 223 | + layer = DatabaseFunctionalLayer |
| 224 | + |
| 225 | + def setUp(self): |
| 226 | + super(TestBugWatchScheduler, self).setUp('foo.bar@canonical.com') |
| 227 | + # We'll make sure that all the other bug watches look like |
| 228 | + # they've been scheduled so that only our watch gets scheduled. |
| 229 | + for watch in getUtility(IBugWatchSet).search(): |
| 230 | + watch.next_check = datetime.now(utc) |
| 231 | + self.bug_watch = self.factory.makeBugWatch() |
| 232 | + self.scheduler = BugWatchScheduler(QuietFakeLogger()) |
| 233 | + transaction.commit() |
| 234 | + |
| 235 | + def test_scheduler_schedules_unchecked_watches(self): |
| 236 | + # The BugWatchScheduler will schedule a BugWatch that has never |
| 237 | + # been checked to be checked immediately. |
| 238 | + self.bug_watch.next_check = None |
| 239 | + self.scheduler(1) |
| 240 | + |
| 241 | + self.assertNotEqual(None, self.bug_watch.next_check) |
| 242 | + self.assertTrue( |
| 243 | + self.bug_watch.next_check <= datetime.now(utc)) |
| 244 | + |
| 245 | + def test_scheduler_schedules_working_watches(self): |
| 246 | + # If a watch has been checked and has never failed its next |
| 247 | + # check will be scheduled for 24 hours after its last check. |
| 248 | + now = datetime.now(utc) |
| 249 | + # Add some succesful activity to ensure that successful activity |
| 250 | + # is handled correctly. |
| 251 | + self.bug_watch.addActivity() |
| 252 | + self.bug_watch.lastchecked = now |
| 253 | + self.bug_watch.next_check = None |
| 254 | + transaction.commit() |
| 255 | + self.scheduler(1) |
| 256 | + |
| 257 | + self.assertEqual( |
| 258 | + now + timedelta(hours=24), self.bug_watch.next_check) |
| 259 | + |
| 260 | + def test_scheduler_schedules_failing_watches(self): |
| 261 | + # If a watch has failed once, it will be scheduled more than 24 |
| 262 | + # hours after its last check. |
| 263 | + now = datetime.now(utc) |
| 264 | + self.bug_watch.lastchecked = now |
| 265 | + |
| 266 | + # The delay depends on the number of failures that the watch has |
| 267 | + # had. |
| 268 | + for failure_count in range(1, 6): |
| 269 | + self.bug_watch.next_check = None |
| 270 | + self.bug_watch.addActivity( |
| 271 | + result=BugWatchActivityStatus.BUG_NOT_FOUND) |
| 272 | + transaction.commit() |
| 273 | + self.scheduler(1) |
| 274 | + |
| 275 | + coefficient = self.scheduler.delay_coefficient * failure_count |
| 276 | + self.assertEqual( |
| 277 | + now + timedelta(days=1 + coefficient), |
| 278 | + self.bug_watch.next_check) |
| 279 | + |
| 280 | + # The scheduler only looks at the last 5 activity items, so even |
| 281 | + # if there have been more failures the maximum delay will be 7 |
| 282 | + # days. |
| 283 | + for count in range(10): |
| 284 | + self.bug_watch.addActivity( |
| 285 | + result=BugWatchActivityStatus.BUG_NOT_FOUND) |
| 286 | + self.bug_watch.next_check = None |
| 287 | + transaction.commit() |
| 288 | + self.scheduler(1) |
| 289 | + self.assertEqual( |
| 290 | + now + timedelta(days=7), self.bug_watch.next_check) |
| 291 | + |
| 292 | + def test_scheduler_doesnt_schedule_scheduled_watches(self): |
| 293 | + # The scheduler will ignore watches whose next_check has been |
| 294 | + # set. |
| 295 | + next_check_date = datetime.now(utc) + timedelta(days=1) |
| 296 | + self.bug_watch.next_check = next_check_date |
| 297 | + transaction.commit() |
| 298 | + self.scheduler(1) |
| 299 | + |
| 300 | + self.assertEqual(next_check_date, self.bug_watch.next_check) |
| 301 | + |
| 302 | + |
| 303 | +def test_suite(): |
| 304 | + return unittest.TestLoader().loadTestsFromName(__name__) |
| 305 | |
| 306 | === renamed file 'lib/lp/bugs/scripts/tests/test_checkwatches.py' => 'lib/lp/bugs/scripts/checkwatches/tests/test_updater.py' |
| 307 | --- lib/lp/bugs/scripts/tests/test_checkwatches.py 2010-03-25 21:59:48 +0000 |
| 308 | +++ lib/lp/bugs/scripts/checkwatches/tests/test_updater.py 2010-04-08 15:38:28 +0000 |
| 309 | @@ -167,6 +167,21 @@ |
| 310 | # stop SQL statemnet logging. |
| 311 | clear_request_started() |
| 312 | |
| 313 | + def test_suggest_batch_size(self): |
| 314 | + class RemoteSystem: pass |
| 315 | + remote_system = RemoteSystem() |
| 316 | + # When the batch_size is None, suggest_batch_size() will set |
| 317 | + # it accordingly. |
| 318 | + remote_system.batch_size = None |
| 319 | + checkwatches.updater.suggest_batch_size(remote_system, 1) |
| 320 | + self.failUnlessEqual(100, remote_system.batch_size) |
| 321 | + remote_system.batch_size = None |
| 322 | + checkwatches.updater.suggest_batch_size(remote_system, 12350) |
| 323 | + self.failUnlessEqual(247, remote_system.batch_size) |
| 324 | + # If the batch_size is already set, it will not be changed. |
| 325 | + checkwatches.updater.suggest_batch_size(remote_system, 99999) |
| 326 | + self.failUnlessEqual(247, remote_system.batch_size) |
| 327 | + |
| 328 | |
| 329 | class TestUpdateBugsWithLinkedQuestions(unittest.TestCase): |
| 330 | """Tests for updating bugs with linked questions.""" |
| 331 | |
| 332 | === modified file 'lib/lp/bugs/scripts/checkwatches/updater.py' |
| 333 | --- lib/lp/bugs/scripts/checkwatches/updater.py 2010-03-26 21:03:30 +0000 |
| 334 | +++ lib/lp/bugs/scripts/checkwatches/updater.py 2010-04-08 15:38:28 +0000 |
| 335 | @@ -61,16 +61,15 @@ |
| 336 | |
| 337 | from lp.bugs import externalbugtracker |
| 338 | from lp.bugs.externalbugtracker import ( |
| 339 | - BugNotFound, BugTrackerConnectError, BugWatchUpdateError, |
| 340 | - BugWatchUpdateWarning, InvalidBugId, PrivateRemoteBug, |
| 341 | - UnknownBugTrackerTypeError, UnknownRemoteStatusError, UnparseableBugData, |
| 342 | - UnparseableBugTrackerVersion, UnsupportedBugTrackerVersion) |
| 343 | -from lp.bugs.externalbugtracker.bugzilla import ( |
| 344 | - BugzillaAPI) |
| 345 | + BATCH_SIZE_UNLIMITED, BugNotFound, BugTrackerConnectError, |
| 346 | + BugWatchUpdateError, BugWatchUpdateWarning, InvalidBugId, |
| 347 | + PrivateRemoteBug, UnknownBugTrackerTypeError, UnknownRemoteStatusError, |
| 348 | + UnparseableBugData, UnparseableBugTrackerVersion, |
| 349 | + UnsupportedBugTrackerVersion) |
| 350 | +from lp.bugs.externalbugtracker.bugzilla import BugzillaAPI |
| 351 | from lp.bugs.externalbugtracker.isolation import check_no_transaction |
| 352 | from lp.bugs.interfaces.bug import IBugSet |
| 353 | -from lp.bugs.interfaces.externalbugtracker import ( |
| 354 | - ISupportsBackLinking) |
| 355 | +from lp.bugs.interfaces.externalbugtracker import ISupportsBackLinking |
| 356 | from lp.services.limitedlist import LimitedList |
| 357 | from lp.services.scripts.base import LaunchpadCronScript |
| 358 | |
| 359 | @@ -78,6 +77,11 @@ |
| 360 | SYNCABLE_GNOME_PRODUCTS = [] |
| 361 | MAX_SQL_STATEMENTS_LOGGED = 10000 |
| 362 | |
| 363 | +# The minimum batch size to suggest to an IExternalBugTracker. |
| 364 | +SUGGESTED_BATCH_SIZE_MIN = 100 |
| 365 | +# The proportion of all watches to suggest as a batch size. |
| 366 | +SUGGESTED_BATCH_SIZE_PROPORTION = 0.02 |
| 367 | + |
| 368 | |
| 369 | class TooMuchTimeSkew(BugWatchUpdateError): |
| 370 | """Time difference between ourselves and the remote server is too much.""" |
| 371 | @@ -242,6 +246,22 @@ |
| 372 | yield item |
| 373 | |
| 374 | |
| 375 | +def suggest_batch_size(remote_system, num_watches): |
| 376 | + """Suggest a value for batch_size if it's not set. |
| 377 | + |
| 378 | + Given the number of bug watches for a `remote_system`, this sets a |
| 379 | + suggested batch size on it. If `remote_system` already has a batch |
| 380 | + size set, this does not override it. |
| 381 | + |
| 382 | + :param remote_system: An `ExternalBugTracker`. |
| 383 | + :param num_watches: The number of watches for `remote_system`. |
| 384 | + """ |
| 385 | + if remote_system.batch_size is None: |
| 386 | + remote_system.batch_size = max( |
| 387 | + SUGGESTED_BATCH_SIZE_MIN, |
| 388 | + int(SUGGESTED_BATCH_SIZE_PROPORTION * num_watches)) |
| 389 | + |
| 390 | + |
| 391 | class BugWatchUpdater(object): |
| 392 | """Takes responsibility for updating remote bug watches.""" |
| 393 | |
| 394 | @@ -381,7 +401,12 @@ |
| 395 | `BaseScheduler`. If no scheduler is given, `SerialScheduler` |
| 396 | will be used, which simply runs the jobs in order. |
| 397 | """ |
| 398 | - self.log.debug("Using a global batch size of %s" % batch_size) |
| 399 | + if batch_size is None: |
| 400 | + self.log.debug("No global batch size specified.") |
| 401 | + elif batch_size == BATCH_SIZE_UNLIMITED: |
| 402 | + self.log.debug("Using an unlimited global batch size.") |
| 403 | + else: |
| 404 | + self.log.debug("Using a global batch size of %s" % batch_size) |
| 405 | |
| 406 | # Default to using the very simple SerialScheduler. |
| 407 | if scheduler is None: |
| 408 | @@ -497,6 +522,7 @@ |
| 409 | def _getExternalBugTrackersAndWatches(self, bug_tracker, bug_watches): |
| 410 | """Return an `ExternalBugTracker` instance for `bug_tracker`.""" |
| 411 | with self.transaction: |
| 412 | + num_watches = bug_tracker.watches.count() |
| 413 | remotesystem = ( |
| 414 | externalbugtracker.get_external_bugtracker(bug_tracker)) |
| 415 | # We special-case the Gnome Bugzilla. |
| 416 | @@ -506,6 +532,9 @@ |
| 417 | # Probe the remote system for additional capabilities. |
| 418 | remotesystem_to_use = remotesystem.getExternalBugTrackerToUse() |
| 419 | |
| 420 | + # Try to hint at how many bug watches to check each time. |
| 421 | + suggest_batch_size(remotesystem_to_use, num_watches) |
| 422 | + |
| 423 | if (is_gnome_bugzilla and |
| 424 | isinstance(remotesystem_to_use, BugzillaAPI) and |
| 425 | len(self._syncable_gnome_products) > 0): |
| 426 | @@ -656,11 +685,6 @@ |
| 427 | # by the ExternalBugTracker. |
| 428 | batch_size = remotesystem.batch_size |
| 429 | |
| 430 | - if batch_size == 0: |
| 431 | - # A batch_size of 0 means that there's no batch size limit |
| 432 | - # for this bug tracker. |
| 433 | - batch_size = None |
| 434 | - |
| 435 | with self.transaction: |
| 436 | old_bug_watches = set( |
| 437 | bug_watch for bug_watch in bug_watches |
| 438 | @@ -693,7 +717,7 @@ |
| 439 | # are actually some bugs that we're interested in so as to |
| 440 | # avoid unnecessary network traffic. |
| 441 | if server_time is not None and len(remote_old_ids) > 0: |
| 442 | - if batch_size is None: |
| 443 | + if batch_size == BATCH_SIZE_UNLIMITED: |
| 444 | remote_old_ids_to_check = ( |
| 445 | remotesystem.getModifiedRemoteBugs( |
| 446 | remote_old_ids, oldest_lastchecked)) |
| 447 | @@ -721,7 +745,7 @@ |
| 448 | remote_ids_to_check = chain( |
| 449 | remote_ids_with_comments, remote_new_ids, remote_old_ids_to_check) |
| 450 | |
| 451 | - if batch_size is not None: |
| 452 | + if batch_size != BATCH_SIZE_UNLIMITED: |
| 453 | # Some remote bug IDs may appear in more than one list so |
| 454 | # we must filter the list before slicing. |
| 455 | remote_ids_to_check = islice( |
| 456 | |
| 457 | === modified file 'lib/lp/bugs/tests/externalbugtracker.py' |
| 458 | --- lib/lp/bugs/tests/externalbugtracker.py 2009-12-23 12:14:59 +0000 |
| 459 | +++ lib/lp/bugs/tests/externalbugtracker.py 2010-04-08 15:38:28 +0000 |
| 460 | @@ -24,8 +24,8 @@ |
| 461 | from canonical.config import config |
| 462 | from canonical.database.sqlbase import commit, ZopelessTransactionManager |
| 463 | from lp.bugs.externalbugtracker import ( |
| 464 | - BugNotFound, BugTrackerConnectError, Bugzilla, DebBugs, |
| 465 | - ExternalBugTracker, Mantis, RequestTracker, Roundup, SourceForge, |
| 466 | + BATCH_SIZE_UNLIMITED, BugNotFound, BugTrackerConnectError, Bugzilla, |
| 467 | + DebBugs, ExternalBugTracker, Mantis, RequestTracker, Roundup, SourceForge, |
| 468 | Trac) |
| 469 | from lp.bugs.externalbugtracker.trac import ( |
| 470 | FAULT_TICKET_NOT_FOUND, LP_PLUGIN_BUG_IDS_ONLY, LP_PLUGIN_FULL, |
| 471 | @@ -152,6 +152,8 @@ |
| 472 | implementation, though it doesn't actually do anything. |
| 473 | """ |
| 474 | |
| 475 | + batch_size = BATCH_SIZE_UNLIMITED |
| 476 | + |
| 477 | def __init__(self, baseurl='http://example.com/'): |
| 478 | super(TestExternalBugTracker, self).__init__(baseurl) |
| 479 | |
| 480 | |
| 481 | === modified file 'lib/lp/bugs/tests/test_bugwatch.py' |
| 482 | --- lib/lp/bugs/tests/test_bugwatch.py 2010-04-07 12:46:15 +0000 |
| 483 | +++ lib/lp/bugs/tests/test_bugwatch.py 2010-04-08 15:38:28 +0000 |
| 484 | @@ -8,9 +8,6 @@ |
| 485 | import transaction |
| 486 | import unittest |
| 487 | |
| 488 | -from datetime import datetime, timedelta |
| 489 | -from pytz import utc |
| 490 | - |
| 491 | from urlparse import urlunsplit |
| 492 | |
| 493 | from zope.component import getUtility |
| 494 | @@ -24,10 +21,8 @@ |
| 495 | |
| 496 | from lp.bugs.interfaces.bugtracker import BugTrackerType, IBugTrackerSet |
| 497 | from lp.bugs.interfaces.bugwatch import ( |
| 498 | - BugWatchActivityStatus, IBugWatchSet, NoBugTrackerFound, |
| 499 | - UnrecognizedBugTrackerURL) |
| 500 | -from lp.bugs.scripts.checkwatches.scheduler import ( |
| 501 | - BugWatchScheduler, MAX_SAMPLE_SIZE) |
| 502 | + IBugWatchSet, NoBugTrackerFound, UnrecognizedBugTrackerURL) |
| 503 | +from lp.bugs.scripts.checkwatches.scheduler import MAX_SAMPLE_SIZE |
| 504 | from lp.registry.interfaces.person import IPersonSet |
| 505 | |
| 506 | from lp.testing import TestCaseWithFactory |
| 507 | @@ -466,88 +461,5 @@ |
| 508 | self.failUnless("Activity %s" % i in messages) |
| 509 | |
| 510 | |
| 511 | -class TestBugWatchScheduler(TestCaseWithFactory): |
| 512 | - """Tests for the BugWatchScheduler, which runs as part of garbo.""" |
| 513 | - |
| 514 | - layer = DatabaseFunctionalLayer |
| 515 | - |
| 516 | - def setUp(self): |
| 517 | - super(TestBugWatchScheduler, self).setUp('foo.bar@canonical.com') |
| 518 | - # We'll make sure that all the other bug watches look like |
| 519 | - # they've been scheduled so that only our watch gets scheduled. |
| 520 | - for watch in getUtility(IBugWatchSet).search(): |
| 521 | - watch.next_check = datetime.now(utc) |
| 522 | - self.bug_watch = self.factory.makeBugWatch() |
| 523 | - self.scheduler = BugWatchScheduler(QuietFakeLogger()) |
| 524 | - transaction.commit() |
| 525 | - |
| 526 | - def test_scheduler_schedules_unchecked_watches(self): |
| 527 | - # The BugWatchScheduler will schedule a BugWatch that has never |
| 528 | - # been checked to be checked immediately. |
| 529 | - self.bug_watch.next_check = None |
| 530 | - self.scheduler(1) |
| 531 | - |
| 532 | - self.assertNotEqual(None, self.bug_watch.next_check) |
| 533 | - self.assertTrue( |
| 534 | - self.bug_watch.next_check <= datetime.now(utc)) |
| 535 | - |
| 536 | - def test_scheduler_schedules_working_watches(self): |
| 537 | - # If a watch has been checked and has never failed its next |
| 538 | - # check will be scheduled for 24 hours after its last check. |
| 539 | - now = datetime.now(utc) |
| 540 | - # Add some succesful activity to ensure that successful activity |
| 541 | - # is handled correctly. |
| 542 | - self.bug_watch.addActivity() |
| 543 | - self.bug_watch.lastchecked = now |
| 544 | - self.bug_watch.next_check = None |
| 545 | - transaction.commit() |
| 546 | - self.scheduler(1) |
| 547 | - |
| 548 | - self.assertEqual( |
| 549 | - now + timedelta(hours=24), self.bug_watch.next_check) |
| 550 | - |
| 551 | - def test_scheduler_schedules_failing_watches(self): |
| 552 | - # If a watch has failed once, it will be scheduled more than 24 |
| 553 | - # hours after its last check. |
| 554 | - now = datetime.now(utc) |
| 555 | - self.bug_watch.lastchecked = now |
| 556 | - |
| 557 | - # The delay depends on the number of failures that the watch has |
| 558 | - # had. |
| 559 | - for failure_count in range(1, 6): |
| 560 | - self.bug_watch.next_check = None |
| 561 | - self.bug_watch.addActivity( |
| 562 | - result=BugWatchActivityStatus.BUG_NOT_FOUND) |
| 563 | - transaction.commit() |
| 564 | - self.scheduler(1) |
| 565 | - |
| 566 | - coefficient = self.scheduler.delay_coefficient * failure_count |
| 567 | - self.assertEqual( |
| 568 | - now + timedelta(days=1 + coefficient), |
| 569 | - self.bug_watch.next_check) |
| 570 | - |
| 571 | - # The scheduler only looks at the last 5 activity items, so even |
| 572 | - # if there have been more failures the maximum delay will be 7 |
| 573 | - # days. |
| 574 | - for count in range(10): |
| 575 | - self.bug_watch.addActivity( |
| 576 | - result=BugWatchActivityStatus.BUG_NOT_FOUND) |
| 577 | - self.bug_watch.next_check = None |
| 578 | - transaction.commit() |
| 579 | - self.scheduler(1) |
| 580 | - self.assertEqual( |
| 581 | - now + timedelta(days=7), self.bug_watch.next_check) |
| 582 | - |
| 583 | - def test_scheduler_doesnt_schedule_scheduled_watches(self): |
| 584 | - # The scheduler will ignore watches whose next_check has been |
| 585 | - # set. |
| 586 | - next_check_date = datetime.now(utc) + timedelta(days=1) |
| 587 | - self.bug_watch.next_check = next_check_date |
| 588 | - transaction.commit() |
| 589 | - self.scheduler(1) |
| 590 | - |
| 591 | - self.assertEqual(next_check_date, self.bug_watch.next_check) |
| 592 | - |
| 593 | - |
| 594 | def test_suite(): |
| 595 | return unittest.TestLoader().loadTestsFromName(__name__) |

Hi Gavin,
This is a nice branch. I just have two comments besides the lint errors.
merge-conditional
-Edwin
== Pyflakes notices ==
lib/lp/ bugs/externalbu gtracker/ debbugs. py
124: local variable 'severity' is assigned to but never used
lib/lp/ bugs/scripts/ checkwatches/ tests/test_ updater. py
157: local variable 'bug' is assigned to but never used
>=== modified file 'lib/lp/ bugs/externalbu gtracker/ debbugs. py' bugs/externalbu gtracker/ debbugs. py 2010-03-16 16:52:42 +0000 bugs/externalbu gtracker/ debbugs. py 2010-03-31 15:33:31 +0000
>--- lib/lp/
>+++ lib/lp/
>@@ -61,7 +61,7 @@
> # Because we keep a local copy of debbugs, we remove the batch_size
> # limit so that all debbugs watches that need checking will be
> # checked each time checkwatches runs.
>- batch_size = None
>+ batch_size = 0
Zero is a magic number. It would be good to define it as a constant Check() where you change
as opposed to relying on comments to explain what it means. You might
want to define the constant as a unique object such as:
UNLIMITED = object()
so that there is never the possiblity of someone using zero and getting
the opposite results than they expect. That would allow you to get
rid of this if-statement in _getRemoteIdsTo
the meaning of the magic values.
if batch_size == 0:
batch_size = None
> _init__ (baseurl) bugs/scripts/ checkwatches/ updater. py' bugs/scripts/ checkwatches/ updater. py 2010-03-26 21:03:30 +0000 bugs/scripts/ checkwatches/ updater. py 2010-03-31 15:33:31 +0000 batch_size( remote_ system, num_watches): cker`. system. batch_size is None: system. batch_size = max( BATCH_SIZE_ MIN, int( BATCH_SIZE_ PROPORTION * num_watches))
> def __init__(self, baseurl, db_location=None):
> super(DebBugs, self)._
>
>=== modified file 'lib/lp/
>--- lib/lp/
>+++ lib/lp/
>@@ -242,6 +247,22 @@
> yield item
>
>
>+def suggest_
>+ """Suggest a value for batch_size if it's not set.
>+
>+ Givend the number of bug watches for an `remote_system`, this sets
>+ a suggested batch size on it. If `remote_system` already has a
>+ batch size set, this does not override it.
>+
>+ :param remote_system: An `ExternalBugTra
>+ :param num_watches: The number of watches for `remote_system`.
>+ """
>+ if remote_
>+ remote_
>+ SUGGESTED_
>+ SUGGESTED_
I think it would be easier to read with this wrapping.
remote_ system. batch_size = max(
SUGGESTED_ BATCH_SIZE_ MIN,
int( SUGGESTED_ BATCH_SIZE_ PROPORTION * num_watches))
>+ (object) :
> class BugWatchUpdater
> """Takes responsibility for updating remote bug watches."""
>