Merge lp:~al-maisan/launchpad/merge-conflict into lp:launchpad/db-devel
- merge-conflict
- Merge into db-devel
Proposed by
Muharem Hrnjadovic
Status: | Merged |
---|---|
Merged at revision: | not available |
Proposed branch: | lp:~al-maisan/launchpad/merge-conflict |
Merge into: | lp:launchpad/db-devel |
Diff against target: |
664 lines (+301/-45) 15 files modified
cronscripts/code-import-dispatcher.py (+1/-1) lib/canonical/launchpad/doc/db-policy.txt (+126/-0) lib/canonical/launchpad/doc/storm.txt (+5/-0) lib/canonical/launchpad/ftests/test_system_documentation.py (+4/-0) lib/canonical/launchpad/webapp/dbpolicy.py (+11/-0) lib/canonical/launchpad/webapp/interfaces.py (+14/-0) lib/lp/code/mail/codereviewcomment.py (+1/-1) lib/lp/code/mail/tests/test_codereviewcomment.py (+8/-0) lib/lp/codehosting/codeimport/dispatcher.py (+28/-3) lib/lp/codehosting/codeimport/tests/test_dispatcher.py (+34/-9) lib/lp/codehosting/scanner/email.py (+10/-5) lib/lp/codehosting/scanner/tests/test_email.py (+15/-4) lib/lp/scripts/utilities/importfascist.py (+24/-21) lib/lp/soyuz/model/buildqueue.py (+3/-0) lib/lp/soyuz/tests/test_buildqueue.py (+17/-1) |
To merge this branch: | bzr merge lp:~al-maisan/launchpad/merge-conflict |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Graham Binns (community) | code | Approve | |
Review via email: mp+19981@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Muharem Hrnjadovic (al-maisan) wrote : | # |
Revision history for this message
Graham Binns (gmb) : | # |
review:
Approve
(code)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'cronscripts/code-import-dispatcher.py' | |||
2 | --- cronscripts/code-import-dispatcher.py 2010-02-22 01:36:30 +0000 | |||
3 | +++ cronscripts/code-import-dispatcher.py 2010-02-23 16:20:35 +0000 | |||
4 | @@ -37,7 +37,7 @@ | |||
5 | 37 | globalErrorUtility.configure('codeimportdispatcher') | 37 | globalErrorUtility.configure('codeimportdispatcher') |
6 | 38 | 38 | ||
7 | 39 | dispatcher = CodeImportDispatcher(self.logger, self.options.max_jobs) | 39 | dispatcher = CodeImportDispatcher(self.logger, self.options.max_jobs) |
9 | 40 | dispatcher.findAndDispatchJob( | 40 | dispatcher.findAndDispatchJobs( |
10 | 41 | ServerProxy(config.codeimportdispatcher.codeimportscheduler_url)) | 41 | ServerProxy(config.codeimportdispatcher.codeimportscheduler_url)) |
11 | 42 | 42 | ||
12 | 43 | 43 | ||
13 | 44 | 44 | ||
14 | === added file 'lib/canonical/launchpad/doc/db-policy.txt' | |||
15 | --- lib/canonical/launchpad/doc/db-policy.txt 1970-01-01 00:00:00 +0000 | |||
16 | +++ lib/canonical/launchpad/doc/db-policy.txt 2010-02-23 16:20:35 +0000 | |||
17 | @@ -0,0 +1,126 @@ | |||
18 | 1 | Storm Stores & Database Policies | ||
19 | 2 | ================================ | ||
20 | 3 | |||
21 | 4 | Launchpad has multiple master and slave databases. Changes to data are | ||
22 | 5 | made on the master and replicated asynchronously to the slave | ||
23 | 6 | databases. Slave databases will usually lag a few seconds behind their | ||
24 | 7 | master. Under high load they may lag a few minutes behind, during | ||
25 | 8 | maintenance they may lag a few hours behind and if things explode | ||
26 | 9 | while admins are on holiday they may lag days behind. | ||
27 | 10 | |||
28 | 11 | If know your code needs to change data, or must have the latest posible | ||
29 | 12 | information, you retrieve objects from the master databases that stores | ||
30 | 13 | the data for your database class. | ||
31 | 14 | |||
32 | 15 | >>> from canonical.launchpad.interfaces.lpstorm import IMasterStore | ||
33 | 16 | >>> from lp.registry.model.person import Person | ||
34 | 17 | >>> import transaction | ||
35 | 18 | |||
36 | 19 | >>> writable_janitor = IMasterStore(Person).find( | ||
37 | 20 | ... Person, Person.name == 'janitor').one() | ||
38 | 21 | |||
39 | 22 | >>> writable_janitor.displayname = 'Jack the Janitor' | ||
40 | 23 | >>> transaction.commit() | ||
41 | 24 | |||
42 | 25 | Sometimes though we know we will not make changes and don't care much | ||
43 | 26 | if the information is a little out of date. In these cases you should | ||
44 | 27 | explicitly retrieve objects from a slave. | ||
45 | 28 | |||
46 | 29 | The more agressively we retrieve objects from slave databases instead | ||
47 | 30 | of the master, the better the overall performance of Launchpad will be. | ||
48 | 31 | We can distribute this load over many slave databases but are limited to | ||
49 | 32 | a single master. | ||
50 | 33 | |||
51 | 34 | >>> from canonical.launchpad.interfaces.lpstorm import ISlaveStore | ||
52 | 35 | >>> ro_janitor = ISlaveStore(Person).find( | ||
53 | 36 | ... Person, Person.name == 'janitor').one() | ||
54 | 37 | >>> ro_janitor is writable_janitor | ||
55 | 38 | False | ||
56 | 39 | |||
57 | 40 | >>> ro_janitor.displayname = 'Janice the Janitor' | ||
58 | 41 | >>> transaction.commit() | ||
59 | 42 | Traceback (most recent call last): | ||
60 | 43 | ... | ||
61 | 44 | InternalError: transaction is read-only | ||
62 | 45 | |||
63 | 46 | >>> transaction.abort() | ||
64 | 47 | |||
65 | 48 | Much of our code does not know if the objects being retrieved need to be | ||
66 | 49 | updatable to or have to be absolutely up to date. In this case, we | ||
67 | 50 | retrieve objects from the default store. What object being returned | ||
68 | 51 | depends on the currently installed database policy. | ||
69 | 52 | |||
70 | 53 | >>> from canonical.launchpad.interfaces.lpstorm import IStore | ||
71 | 54 | >>> default_janitor = IStore(Person).find( | ||
72 | 55 | ... Person, Person.name == 'janitor').one() | ||
73 | 56 | >>> default_janitor is writable_janitor | ||
74 | 57 | True | ||
75 | 58 | |||
76 | 59 | As you can see, the default database policy retrieves objects from | ||
77 | 60 | the master database. This allows our code written before database | ||
78 | 61 | replication was implemented to keep working. | ||
79 | 62 | |||
80 | 63 | To alter this behavior, you can install a different database policy. | ||
81 | 64 | |||
82 | 65 | >>> from canonical.launchpad.webapp.dbpolicy import SlaveDatabasePolicy | ||
83 | 66 | >>> with SlaveDatabasePolicy(): | ||
84 | 67 | ... default_janitor = IStore(Person).find( | ||
85 | 68 | ... Person, Person.name == 'janitor').one() | ||
86 | 69 | >>> default_janitor is writable_janitor | ||
87 | 70 | False | ||
88 | 71 | |||
89 | 72 | The database policy can also affect what happens when objects are | ||
90 | 73 | explicitly retrieved from a slave or master database. For example, | ||
91 | 74 | if we have code that needs to run during database maintenance or | ||
92 | 75 | code we want to prove only accesses slave database resources, we can | ||
93 | 76 | raise an exception if an attempt is made to access master database | ||
94 | 77 | resources. | ||
95 | 78 | |||
96 | 79 | >>> from canonical.launchpad.webapp.dbpolicy import ( | ||
97 | 80 | ... SlaveOnlyDatabasePolicy) | ||
98 | 81 | >>> with SlaveOnlyDatabasePolicy(): | ||
99 | 82 | ... whoops = IMasterStore(Person).find( | ||
100 | 83 | ... Person, Person.name == 'janitor').one() | ||
101 | 84 | Traceback (most recent call last): | ||
102 | 85 | ... | ||
103 | 86 | DisallowedStore: master | ||
104 | 87 | |||
105 | 88 | We can even ensure no database activity occurs at all, for instance | ||
106 | 89 | if we need to guarantee a potentially long running call doesn't access | ||
107 | 90 | the database at all starting a new and potentially long running | ||
108 | 91 | database transaction. | ||
109 | 92 | |||
110 | 93 | >>> from canonical.launchpad.webapp.dbpolicy import DatabaseBlockedPolicy | ||
111 | 94 | >>> with DatabaseBlockedPolicy(): | ||
112 | 95 | ... whoops = IStore(Person).find( | ||
113 | 96 | ... Person, Person.name == 'janitor').one() | ||
114 | 97 | Traceback (most recent call last): | ||
115 | 98 | ... | ||
116 | 99 | DisallowedStore: ('main', 'default') | ||
117 | 100 | |||
118 | 101 | Database policies can also be installed and uninstalled using the | ||
119 | 102 | IStoreSelector utility for cases where the 'with' syntax cannot | ||
120 | 103 | be used. | ||
121 | 104 | |||
122 | 105 | >>> from canonical.launchpad.webapp.interfaces import IStoreSelector | ||
123 | 106 | >>> getUtility(IStoreSelector).push(SlaveDatabasePolicy()) | ||
124 | 107 | >>> try: | ||
125 | 108 | ... default_janitor = IStore(Person).find( | ||
126 | 109 | ... Person, Person.name == 'janitor').one() | ||
127 | 110 | ... finally: | ||
128 | 111 | ... db_policy = getUtility(IStoreSelector).pop() | ||
129 | 112 | >>> default_janitor is ro_janitor | ||
130 | 113 | True | ||
131 | 114 | |||
132 | 115 | Casting | ||
133 | 116 | ------- | ||
134 | 117 | |||
135 | 118 | If you need to change an object you have a read only copy of, or are | ||
136 | 119 | unsure if the object is writable or not, you can easily cast it | ||
137 | 120 | to a writable copy. This is a noop if the object is already writable | ||
138 | 121 | so is good defensive programming. | ||
139 | 122 | |||
140 | 123 | >>> from canonical.launchpad.interfaces.lpstorm import IMasterObject | ||
141 | 124 | >>> IMasterObject(ro_janitor) is writable_janitor | ||
142 | 125 | True | ||
143 | 126 | |||
144 | 0 | 127 | ||
145 | === modified file 'lib/canonical/launchpad/doc/storm.txt' | |||
146 | --- lib/canonical/launchpad/doc/storm.txt 2009-08-21 17:43:28 +0000 | |||
147 | +++ lib/canonical/launchpad/doc/storm.txt 2010-02-23 16:20:35 +0000 | |||
148 | @@ -1,3 +1,8 @@ | |||
149 | 1 | Note: A more readable version of this is in db-policy.txt. Most of this | ||
150 | 2 | doctest will disappear soon when the auth replication set is collapsed | ||
151 | 3 | back into the main replication set as part of login server seperation. | ||
152 | 4 | -- StuartBishop 20100222 | ||
153 | 5 | |||
154 | 1 | In addition to what Storm provides, we also have some Launchpad | 6 | In addition to what Storm provides, we also have some Launchpad |
155 | 2 | specific Storm tools to cope with our master and slave store arrangement. | 7 | specific Storm tools to cope with our master and slave store arrangement. |
156 | 3 | 8 | ||
157 | 4 | 9 | ||
158 | === modified file 'lib/canonical/launchpad/ftests/test_system_documentation.py' | |||
159 | --- lib/canonical/launchpad/ftests/test_system_documentation.py 2010-02-10 23:14:56 +0000 | |||
160 | +++ lib/canonical/launchpad/ftests/test_system_documentation.py 2010-02-23 16:20:35 +0000 | |||
161 | @@ -7,6 +7,8 @@ | |||
162 | 7 | """ | 7 | """ |
163 | 8 | # pylint: disable-msg=C0103 | 8 | # pylint: disable-msg=C0103 |
164 | 9 | 9 | ||
165 | 10 | from __future__ import with_statement | ||
166 | 11 | |||
167 | 10 | import logging | 12 | import logging |
168 | 11 | import os | 13 | import os |
169 | 12 | import unittest | 14 | import unittest |
170 | @@ -391,6 +393,8 @@ | |||
171 | 391 | one_test = LayeredDocFileSuite( | 393 | one_test = LayeredDocFileSuite( |
172 | 392 | path, setUp=setUp, tearDown=tearDown, | 394 | path, setUp=setUp, tearDown=tearDown, |
173 | 393 | layer=LaunchpadFunctionalLayer, | 395 | layer=LaunchpadFunctionalLayer, |
174 | 396 | # 'icky way of running doctests with __future__ imports | ||
175 | 397 | globs={'with_statement': with_statement}, | ||
176 | 394 | stdout_logging_level=logging.WARNING | 398 | stdout_logging_level=logging.WARNING |
177 | 395 | ) | 399 | ) |
178 | 396 | suite.addTest(one_test) | 400 | suite.addTest(one_test) |
179 | 397 | 401 | ||
180 | === modified file 'lib/canonical/launchpad/webapp/dbpolicy.py' | |||
181 | --- lib/canonical/launchpad/webapp/dbpolicy.py 2010-02-05 12:17:56 +0000 | |||
182 | +++ lib/canonical/launchpad/webapp/dbpolicy.py 2010-02-23 16:20:35 +0000 | |||
183 | @@ -111,6 +111,17 @@ | |||
184 | 111 | """See `IDatabasePolicy`.""" | 111 | """See `IDatabasePolicy`.""" |
185 | 112 | pass | 112 | pass |
186 | 113 | 113 | ||
187 | 114 | def __enter__(self): | ||
188 | 115 | """See `IDatabasePolicy`.""" | ||
189 | 116 | getUtility(IStoreSelector).push(self) | ||
190 | 117 | |||
191 | 118 | def __exit__(self, exc_type, exc_value, traceback): | ||
192 | 119 | """See `IDatabasePolicy`.""" | ||
193 | 120 | policy = getUtility(IStoreSelector).pop() | ||
194 | 121 | assert policy is self, ( | ||
195 | 122 | "Unexpected database policy %s returned by store selector" | ||
196 | 123 | % repr(policy)) | ||
197 | 124 | |||
198 | 114 | 125 | ||
199 | 115 | class DatabaseBlockedPolicy(BaseDatabasePolicy): | 126 | class DatabaseBlockedPolicy(BaseDatabasePolicy): |
200 | 116 | """`IDatabasePolicy` that blocks all access to the database.""" | 127 | """`IDatabasePolicy` that blocks all access to the database.""" |
201 | 117 | 128 | ||
202 | === modified file 'lib/canonical/launchpad/webapp/interfaces.py' | |||
203 | --- lib/canonical/launchpad/webapp/interfaces.py 2010-02-17 11:13:06 +0000 | |||
204 | +++ lib/canonical/launchpad/webapp/interfaces.py 2010-02-23 16:20:35 +0000 | |||
205 | @@ -756,6 +756,20 @@ | |||
206 | 756 | The publisher adapts the request to `IDatabasePolicy` to | 756 | The publisher adapts the request to `IDatabasePolicy` to |
207 | 757 | instantiate the policy for the current request. | 757 | instantiate the policy for the current request. |
208 | 758 | """ | 758 | """ |
209 | 759 | def __enter__(): | ||
210 | 760 | """Standard Python context manager interface. | ||
211 | 761 | |||
212 | 762 | The IDatabasePolicy will install itself using the IStoreSelector | ||
213 | 763 | utility. | ||
214 | 764 | """ | ||
215 | 765 | |||
216 | 766 | def __exit__(exc_type, exc_value, traceback): | ||
217 | 767 | """Standard Python context manager interface. | ||
218 | 768 | |||
219 | 769 | The IDatabasePolicy will uninstall itself using the IStoreSelector | ||
220 | 770 | utility. | ||
221 | 771 | """ | ||
222 | 772 | |||
223 | 759 | def getStore(name, flavor): | 773 | def getStore(name, flavor): |
224 | 760 | """Retrieve a Store. | 774 | """Retrieve a Store. |
225 | 761 | 775 | ||
226 | 762 | 776 | ||
227 | === modified file 'lib/lp/code/mail/codereviewcomment.py' | |||
228 | --- lib/lp/code/mail/codereviewcomment.py 2009-10-29 23:51:35 +0000 | |||
229 | +++ lib/lp/code/mail/codereviewcomment.py 2010-02-23 16:20:35 +0000 | |||
230 | @@ -133,7 +133,7 @@ | |||
231 | 133 | def _addAttachments(self, ctrl, email): | 133 | def _addAttachments(self, ctrl, email): |
232 | 134 | """Add the attachments from the original message.""" | 134 | """Add the attachments from the original message.""" |
233 | 135 | # Only reattach the display_aliases. | 135 | # Only reattach the display_aliases. |
235 | 136 | for content, content_type, filename in self.attachments: | 136 | for content, filename, content_type in self.attachments: |
236 | 137 | # Append directly to the controller's list. | 137 | # Append directly to the controller's list. |
237 | 138 | ctrl.addAttachment( | 138 | ctrl.addAttachment( |
238 | 139 | content, content_type=content_type, filename=filename) | 139 | content, content_type=content_type, filename=filename) |
239 | 140 | 140 | ||
240 | === modified file 'lib/lp/code/mail/tests/test_codereviewcomment.py' | |||
241 | --- lib/lp/code/mail/tests/test_codereviewcomment.py 2009-11-01 23:13:29 +0000 | |||
242 | +++ lib/lp/code/mail/tests/test_codereviewcomment.py 2010-02-23 16:20:35 +0000 | |||
243 | @@ -215,6 +215,14 @@ | |||
244 | 215 | [outgoing_attachment] = mailer.attachments | 215 | [outgoing_attachment] = mailer.attachments |
245 | 216 | self.assertEqual('inc.diff', outgoing_attachment[1]) | 216 | self.assertEqual('inc.diff', outgoing_attachment[1]) |
246 | 217 | self.assertEqual('text/x-diff', outgoing_attachment[2]) | 217 | self.assertEqual('text/x-diff', outgoing_attachment[2]) |
247 | 218 | # The attachments are attached to the outgoing message. | ||
248 | 219 | person = bmp.target_branch.owner | ||
249 | 220 | message = mailer.generateEmail( | ||
250 | 221 | person.preferredemail.email, person).makeMessage() | ||
251 | 222 | self.assertTrue(message.is_multipart()) | ||
252 | 223 | attachment = message.get_payload()[1] | ||
253 | 224 | self.assertEqual('inc.diff', attachment.get_filename()) | ||
254 | 225 | self.assertEqual('text/x-diff', attachment['content-type']) | ||
255 | 218 | 226 | ||
256 | 219 | def makeCommentAndParticipants(self): | 227 | def makeCommentAndParticipants(self): |
257 | 220 | """Create a merge proposal and comment. | 228 | """Create a merge proposal and comment. |
258 | 221 | 229 | ||
259 | === modified file 'lib/lp/codehosting/codeimport/dispatcher.py' | |||
260 | --- lib/lp/codehosting/codeimport/dispatcher.py 2010-02-22 01:36:30 +0000 | |||
261 | +++ lib/lp/codehosting/codeimport/dispatcher.py 2010-02-23 16:20:35 +0000 | |||
262 | @@ -16,6 +16,7 @@ | |||
263 | 16 | import os | 16 | import os |
264 | 17 | import socket | 17 | import socket |
265 | 18 | import subprocess | 18 | import subprocess |
266 | 19 | import time | ||
267 | 19 | 20 | ||
268 | 20 | from canonical.config import config | 21 | from canonical.config import config |
269 | 21 | 22 | ||
270 | @@ -32,13 +33,14 @@ | |||
271 | 32 | worker_script = os.path.join( | 33 | worker_script = os.path.join( |
272 | 33 | config.root, 'scripts', 'code-import-worker-db.py') | 34 | config.root, 'scripts', 'code-import-worker-db.py') |
273 | 34 | 35 | ||
275 | 35 | def __init__(self, logger, worker_limit): | 36 | def __init__(self, logger, worker_limit, _sleep=time.sleep): |
276 | 36 | """Initialize an instance. | 37 | """Initialize an instance. |
277 | 37 | 38 | ||
278 | 38 | :param logger: A `Logger` object. | 39 | :param logger: A `Logger` object. |
279 | 39 | """ | 40 | """ |
280 | 40 | self.logger = logger | 41 | self.logger = logger |
281 | 41 | self.worker_limit = worker_limit | 42 | self.worker_limit = worker_limit |
282 | 43 | self._sleep = _sleep | ||
283 | 42 | 44 | ||
284 | 43 | def getHostname(self): | 45 | def getHostname(self): |
285 | 44 | """Return the hostname of this machine. | 46 | """Return the hostname of this machine. |
286 | @@ -65,15 +67,38 @@ | |||
287 | 65 | 67 | ||
288 | 66 | 68 | ||
289 | 67 | def findAndDispatchJob(self, scheduler_client): | 69 | def findAndDispatchJob(self, scheduler_client): |
291 | 68 | """Check for and dispatch a job if necessary.""" | 70 | """Check for and dispatch a job if necessary. |
292 | 71 | |||
293 | 72 | :return: A boolean, true if a job was found and dispatched. | ||
294 | 73 | """ | ||
295 | 69 | 74 | ||
296 | 70 | job_id = scheduler_client.getJobForMachine( | 75 | job_id = scheduler_client.getJobForMachine( |
297 | 71 | self.getHostname(), self.worker_limit) | 76 | self.getHostname(), self.worker_limit) |
298 | 72 | 77 | ||
299 | 73 | if job_id == 0: | 78 | if job_id == 0: |
300 | 74 | self.logger.info("No jobs pending.") | 79 | self.logger.info("No jobs pending.") |
302 | 75 | return | 80 | return False |
303 | 76 | 81 | ||
304 | 77 | self.logger.info("Dispatching job %d." % job_id) | 82 | self.logger.info("Dispatching job %d." % job_id) |
305 | 78 | 83 | ||
306 | 79 | self.dispatchJob(job_id) | 84 | self.dispatchJob(job_id) |
307 | 85 | return True | ||
308 | 86 | |||
309 | 87 | def _getSleepInterval(self): | ||
310 | 88 | """How long to sleep for until asking for a new job. | ||
311 | 89 | |||
312 | 90 | The basic idea is to wait longer if the machine is more heavily | ||
313 | 91 | loaded, so that less loaded slaves get a chance to grab some jobs. | ||
314 | 92 | |||
315 | 93 | We assume worker_limit will be roughly the number of CPUs in the | ||
316 | 94 | machine, so load/worker_limit is roughly how loaded the machine is. | ||
317 | 95 | """ | ||
318 | 96 | return 5*os.getloadavg()[0]/self.worker_limit | ||
319 | 97 | |||
320 | 98 | def findAndDispatchJobs(self, scheduler_client): | ||
321 | 99 | """Call findAndDispatchJob until no job is found.""" | ||
322 | 100 | while True: | ||
323 | 101 | found = self.findAndDispatchJob(scheduler_client) | ||
324 | 102 | if not found: | ||
325 | 103 | break | ||
326 | 104 | self._sleep(self._getSleepInterval()) | ||
327 | 80 | 105 | ||
328 | === modified file 'lib/lp/codehosting/codeimport/tests/test_dispatcher.py' | |||
329 | --- lib/lp/codehosting/codeimport/tests/test_dispatcher.py 2010-02-22 02:06:57 +0000 | |||
330 | +++ lib/lp/codehosting/codeimport/tests/test_dispatcher.py 2010-02-23 16:20:35 +0000 | |||
331 | @@ -24,11 +24,11 @@ | |||
332 | 24 | class StubSchedulerClient: | 24 | class StubSchedulerClient: |
333 | 25 | """A scheduler client that returns a pre-arranged answer.""" | 25 | """A scheduler client that returns a pre-arranged answer.""" |
334 | 26 | 26 | ||
337 | 27 | def __init__(self, id_to_return): | 27 | def __init__(self, ids_to_return): |
338 | 28 | self.id_to_return = id_to_return | 28 | self.ids_to_return = ids_to_return |
339 | 29 | 29 | ||
340 | 30 | def getJobForMachine(self, machine, limit): | 30 | def getJobForMachine(self, machine, limit): |
342 | 31 | return self.id_to_return | 31 | return self.ids_to_return.pop(0) |
343 | 32 | 32 | ||
344 | 33 | 33 | ||
345 | 34 | class MockSchedulerClient: | 34 | class MockSchedulerClient: |
346 | @@ -51,9 +51,10 @@ | |||
347 | 51 | TestCase.setUp(self) | 51 | TestCase.setUp(self) |
348 | 52 | self.pushConfig('codeimportdispatcher', forced_hostname='none') | 52 | self.pushConfig('codeimportdispatcher', forced_hostname='none') |
349 | 53 | 53 | ||
351 | 54 | def makeDispatcher(self, worker_limit=10): | 54 | def makeDispatcher(self, worker_limit=10, _sleep=lambda delay: None): |
352 | 55 | """Make a `CodeImportDispatcher`.""" | 55 | """Make a `CodeImportDispatcher`.""" |
354 | 56 | return CodeImportDispatcher(QuietFakeLogger(), worker_limit) | 56 | return CodeImportDispatcher( |
355 | 57 | QuietFakeLogger(), worker_limit, _sleep=_sleep) | ||
356 | 57 | 58 | ||
357 | 58 | def test_getHostname(self): | 59 | def test_getHostname(self): |
358 | 59 | # By default, getHostname return the same as socket.gethostname() | 60 | # By default, getHostname return the same as socket.gethostname() |
359 | @@ -111,16 +112,16 @@ | |||
360 | 111 | calls = [] | 112 | calls = [] |
361 | 112 | dispatcher = self.makeDispatcher() | 113 | dispatcher = self.makeDispatcher() |
362 | 113 | dispatcher.dispatchJob = lambda job_id: calls.append(job_id) | 114 | dispatcher.dispatchJob = lambda job_id: calls.append(job_id) |
365 | 114 | dispatcher.findAndDispatchJob(StubSchedulerClient(10)) | 115 | found = dispatcher.findAndDispatchJob(StubSchedulerClient([10])) |
366 | 115 | self.assertEqual([10], calls) | 116 | self.assertEqual(([10], True), (calls, found)) |
367 | 116 | 117 | ||
368 | 117 | def test_findAndDispatchJob_noJobWaiting(self): | 118 | def test_findAndDispatchJob_noJobWaiting(self): |
369 | 118 | # If there is no job to dispatch, then we just exit quietly. | 119 | # If there is no job to dispatch, then we just exit quietly. |
370 | 119 | calls = [] | 120 | calls = [] |
371 | 120 | dispatcher = self.makeDispatcher() | 121 | dispatcher = self.makeDispatcher() |
372 | 121 | dispatcher.dispatchJob = lambda job_id: calls.append(job_id) | 122 | dispatcher.dispatchJob = lambda job_id: calls.append(job_id) |
375 | 122 | dispatcher.findAndDispatchJob(StubSchedulerClient(0)) | 123 | found = dispatcher.findAndDispatchJob(StubSchedulerClient([0])) |
376 | 123 | self.assertEqual([], calls) | 124 | self.assertEqual(([], False), (calls, found)) |
377 | 124 | 125 | ||
378 | 125 | def test_findAndDispatchJob_calls_getJobForMachine_with_limit(self): | 126 | def test_findAndDispatchJob_calls_getJobForMachine_with_limit(self): |
379 | 126 | # findAndDispatchJob calls getJobForMachine on the scheduler client | 127 | # findAndDispatchJob calls getJobForMachine on the scheduler client |
380 | @@ -133,5 +134,29 @@ | |||
381 | 133 | [(dispatcher.getHostname(), worker_limit)], | 134 | [(dispatcher.getHostname(), worker_limit)], |
382 | 134 | scheduler_client.calls) | 135 | scheduler_client.calls) |
383 | 135 | 136 | ||
384 | 137 | def test_findAndDispatchJobs(self): | ||
385 | 138 | # findAndDispatchJobs calls getJobForMachine on the scheduler_client, | ||
386 | 139 | # dispatching jobs, until it indicates that there are no more jobs to | ||
387 | 140 | # dispatch. | ||
388 | 141 | calls = [] | ||
389 | 142 | dispatcher = self.makeDispatcher() | ||
390 | 143 | dispatcher.dispatchJob = lambda job_id: calls.append(job_id) | ||
391 | 144 | dispatcher.findAndDispatchJobs(StubSchedulerClient([10, 9, 0])) | ||
392 | 145 | self.assertEqual([10, 9], calls) | ||
393 | 146 | |||
394 | 147 | def test_findAndDispatchJobs_sleeps(self): | ||
395 | 148 | # After finding a job, findAndDispatchJobs sleeps for an interval as | ||
396 | 149 | # returned by _getSleepInterval. | ||
397 | 150 | sleep_calls = [] | ||
398 | 151 | interval = self.factory.getUniqueInteger() | ||
399 | 152 | def _sleep(delay): | ||
400 | 153 | sleep_calls.append(delay) | ||
401 | 154 | dispatcher = self.makeDispatcher(_sleep=_sleep) | ||
402 | 155 | dispatcher.dispatchJob = lambda job_id: None | ||
403 | 156 | dispatcher._getSleepInterval = lambda : interval | ||
404 | 157 | dispatcher.findAndDispatchJobs(StubSchedulerClient([10, 0])) | ||
405 | 158 | self.assertEqual([interval], sleep_calls) | ||
406 | 159 | |||
407 | 160 | |||
408 | 136 | def test_suite(): | 161 | def test_suite(): |
409 | 137 | return TestLoader().loadTestsFromName(__name__) | 162 | return TestLoader().loadTestsFromName(__name__) |
410 | 138 | 163 | ||
411 | === modified file 'lib/lp/codehosting/scanner/email.py' | |||
412 | --- lib/lp/codehosting/scanner/email.py 2010-01-06 12:15:42 +0000 | |||
413 | +++ lib/lp/codehosting/scanner/email.py 2010-02-23 16:20:35 +0000 | |||
414 | @@ -38,15 +38,18 @@ | |||
415 | 38 | if number_removed == 0: | 38 | if number_removed == 0: |
416 | 39 | return | 39 | return |
417 | 40 | if number_removed == 1: | 40 | if number_removed == 1: |
419 | 41 | contents = '1 revision was removed from the branch.' | 41 | count = '1 revision' |
420 | 42 | contents = '%s was removed from the branch.' % count | ||
421 | 42 | else: | 43 | else: |
424 | 43 | contents = ('%d revisions were removed from the branch.' | 44 | count = '%d revisions' % number_removed |
425 | 44 | % number_removed) | 45 | contents = '%s were removed from the branch.' % count |
426 | 45 | # No diff is associated with the removed email. | 46 | # No diff is associated with the removed email. |
427 | 47 | subject = "[Branch %s] %s removed" % ( | ||
428 | 48 | revisions_removed.db_branch.unique_name, count) | ||
429 | 46 | getUtility(IRevisionMailJobSource).create( | 49 | getUtility(IRevisionMailJobSource).create( |
430 | 47 | revisions_removed.db_branch, revno='removed', | 50 | revisions_removed.db_branch, revno='removed', |
431 | 48 | from_address=config.canonical.noreply_from_address, | 51 | from_address=config.canonical.noreply_from_address, |
433 | 49 | body=contents, perform_diff=False, subject=None) | 52 | body=contents, perform_diff=False, subject=subject) |
434 | 50 | 53 | ||
435 | 51 | 54 | ||
436 | 52 | @adapter(events.TipChanged) | 55 | @adapter(events.TipChanged) |
437 | @@ -62,9 +65,11 @@ | |||
438 | 62 | message = ('First scan of the branch detected %s' | 65 | message = ('First scan of the branch detected %s' |
439 | 63 | ' in the revision history of the branch.' % | 66 | ' in the revision history of the branch.' % |
440 | 64 | revisions) | 67 | revisions) |
441 | 68 | subject = "[Branch %s] %s" % ( | ||
442 | 69 | tip_changed.db_branch.unique_name, revisions) | ||
443 | 65 | getUtility(IRevisionMailJobSource).create( | 70 | getUtility(IRevisionMailJobSource).create( |
444 | 66 | tip_changed.db_branch, 'initial', | 71 | tip_changed.db_branch, 'initial', |
446 | 67 | config.canonical.noreply_from_address, message, False, None) | 72 | config.canonical.noreply_from_address, message, False, subject) |
447 | 68 | else: | 73 | else: |
448 | 69 | getUtility(IRevisionsAddedJobSource).create( | 74 | getUtility(IRevisionsAddedJobSource).create( |
449 | 70 | tip_changed.db_branch, tip_changed.db_branch.last_scanned_id, | 75 | tip_changed.db_branch, tip_changed.db_branch.last_scanned_id, |
450 | 71 | 76 | ||
451 | === modified file 'lib/lp/codehosting/scanner/tests/test_email.py' | |||
452 | --- lib/lp/codehosting/scanner/tests/test_email.py 2009-07-17 00:26:05 +0000 | |||
453 | +++ lib/lp/codehosting/scanner/tests/test_email.py 2010-02-23 16:20:35 +0000 | |||
454 | @@ -63,8 +63,12 @@ | |||
455 | 63 | self.assertEqual(len(stub.test_emails), 1) | 63 | self.assertEqual(len(stub.test_emails), 1) |
456 | 64 | [initial_email] = stub.test_emails | 64 | [initial_email] = stub.test_emails |
457 | 65 | expected = 'First scan of the branch detected 0 revisions' | 65 | expected = 'First scan of the branch detected 0 revisions' |
459 | 66 | email_body = email.message_from_string(initial_email[2]).get_payload() | 66 | message = email.message_from_string(initial_email[2]) |
460 | 67 | email_body = message.get_payload() | ||
461 | 67 | self.assertTextIn(expected, email_body) | 68 | self.assertTextIn(expected, email_body) |
462 | 69 | self.assertEqual( | ||
463 | 70 | '[Branch %s] 0 revisions' % self.db_branch.unique_name, | ||
464 | 71 | message['Subject']) | ||
465 | 68 | 72 | ||
466 | 69 | def test_import_revision(self): | 73 | def test_import_revision(self): |
467 | 70 | self.commitRevision() | 74 | self.commitRevision() |
468 | @@ -74,8 +78,12 @@ | |||
469 | 74 | [initial_email] = stub.test_emails | 78 | [initial_email] = stub.test_emails |
470 | 75 | expected = ('First scan of the branch detected 1 revision' | 79 | expected = ('First scan of the branch detected 1 revision' |
471 | 76 | ' in the revision history of the=\n branch.') | 80 | ' in the revision history of the=\n branch.') |
473 | 77 | email_body = email.message_from_string(initial_email[2]).get_payload() | 81 | message = email.message_from_string(initial_email[2]) |
474 | 82 | email_body = message.get_payload() | ||
475 | 78 | self.assertTextIn(expected, email_body) | 83 | self.assertTextIn(expected, email_body) |
476 | 84 | self.assertEqual( | ||
477 | 85 | '[Branch %s] 1 revision' % self.db_branch.unique_name, | ||
478 | 86 | message['Subject']) | ||
479 | 79 | 87 | ||
480 | 80 | def test_import_uncommit(self): | 88 | def test_import_uncommit(self): |
481 | 81 | self.commitRevision() | 89 | self.commitRevision() |
482 | @@ -88,9 +96,12 @@ | |||
483 | 88 | self.assertEqual(len(stub.test_emails), 1) | 96 | self.assertEqual(len(stub.test_emails), 1) |
484 | 89 | [uncommit_email] = stub.test_emails | 97 | [uncommit_email] = stub.test_emails |
485 | 90 | expected = '1 revision was removed from the branch.' | 98 | expected = '1 revision was removed from the branch.' |
488 | 91 | email_body = email.message_from_string( | 99 | message = email.message_from_string(uncommit_email[2]) |
489 | 92 | uncommit_email[2]).get_payload() | 100 | email_body = message.get_payload() |
490 | 93 | self.assertTextIn(expected, email_body) | 101 | self.assertTextIn(expected, email_body) |
491 | 102 | self.assertEqual( | ||
492 | 103 | '[Branch %s] 1 revision removed' % self.db_branch.unique_name, | ||
493 | 104 | message['Subject']) | ||
494 | 94 | 105 | ||
495 | 95 | def test_import_recommit(self): | 106 | def test_import_recommit(self): |
496 | 96 | # When scanning the uncommit and new commit there should be an email | 107 | # When scanning the uncommit and new commit there should be an email |
497 | 97 | 108 | ||
498 | === modified file 'lib/lp/scripts/utilities/importfascist.py' | |||
499 | --- lib/lp/scripts/utilities/importfascist.py 2010-02-04 03:07:25 +0000 | |||
500 | +++ lib/lp/scripts/utilities/importfascist.py 2010-02-23 16:20:35 +0000 | |||
501 | @@ -173,12 +173,15 @@ | |||
502 | 173 | % self.import_into) | 173 | % self.import_into) |
503 | 174 | 174 | ||
504 | 175 | 175 | ||
505 | 176 | # The names of the arguments form part of the interface of __import__(...), and | ||
506 | 177 | # must not be changed, as code may choose to invoke __import__ using keyword | ||
507 | 178 | # arguments - e.g. the encodings module in Python 2.6. | ||
508 | 176 | # pylint: disable-msg=W0102,W0602 | 179 | # pylint: disable-msg=W0102,W0602 |
510 | 177 | def import_fascist(module_name, globals={}, locals={}, from_list=[], level=-1): | 180 | def import_fascist(name, globals={}, locals={}, fromlist=[], level=-1): |
511 | 178 | global naughty_imports | 181 | global naughty_imports |
512 | 179 | 182 | ||
513 | 180 | try: | 183 | try: |
515 | 181 | module = original_import(module_name, globals, locals, from_list, level) | 184 | module = original_import(name, globals, locals, fromlist, level) |
516 | 182 | except ImportError: | 185 | except ImportError: |
517 | 183 | # XXX sinzui 2008-04-17 bug=277274: | 186 | # XXX sinzui 2008-04-17 bug=277274: |
518 | 184 | # import_fascist screws zope configuration module which introspects | 187 | # import_fascist screws zope configuration module which introspects |
519 | @@ -188,18 +191,18 @@ | |||
520 | 188 | # time doesn't exist and dies a horrible death because of the import | 191 | # time doesn't exist and dies a horrible death because of the import |
521 | 189 | # fascist. That's the long explanation for why we special case this | 192 | # fascist. That's the long explanation for why we special case this |
522 | 190 | # module. | 193 | # module. |
526 | 191 | if module_name.startswith('zope.app.layers.'): | 194 | if name.startswith('zope.app.layers.'): |
527 | 192 | module_name = module_name[16:] | 195 | name = name[16:] |
528 | 193 | module = original_import(module_name, globals, locals, from_list, level) | 196 | module = original_import(name, globals, locals, fromlist, level) |
529 | 194 | else: | 197 | else: |
530 | 195 | raise | 198 | raise |
531 | 196 | # Python's re module imports some odd stuff every time certain regexes | 199 | # Python's re module imports some odd stuff every time certain regexes |
532 | 197 | # are used. Let's optimize this. | 200 | # are used. Let's optimize this. |
534 | 198 | if module_name == 'sre': | 201 | if name == 'sre': |
535 | 199 | return module | 202 | return module |
536 | 200 | 203 | ||
537 | 201 | # Mailman 2.1 code base is originally circa 1998, so yeah, no __all__'s. | 204 | # Mailman 2.1 code base is originally circa 1998, so yeah, no __all__'s. |
539 | 202 | if module_name.startswith('Mailman'): | 205 | if name.startswith('Mailman'): |
540 | 203 | return module | 206 | return module |
541 | 204 | 207 | ||
542 | 205 | # Some uses of __import__ pass None for globals, so handle that. | 208 | # Some uses of __import__ pass None for globals, so handle that. |
543 | @@ -215,14 +218,14 @@ | |||
544 | 215 | 218 | ||
545 | 216 | # Check the "NotFoundError" policy. | 219 | # Check the "NotFoundError" policy. |
546 | 217 | if (import_into.startswith('canonical.launchpad.database') and | 220 | if (import_into.startswith('canonical.launchpad.database') and |
549 | 218 | module_name == 'zope.exceptions'): | 221 | name == 'zope.exceptions'): |
550 | 219 | if from_list and 'NotFoundError' in from_list: | 222 | if fromlist and 'NotFoundError' in fromlist: |
551 | 220 | raise NotFoundPolicyViolation(import_into) | 223 | raise NotFoundPolicyViolation(import_into) |
552 | 221 | 224 | ||
553 | 222 | # Check the database import policy. | 225 | # Check the database import policy. |
555 | 223 | if (module_name.startswith(database_root) and | 226 | if (name.startswith(database_root) and |
556 | 224 | not database_import_allowed_into(import_into)): | 227 | not database_import_allowed_into(import_into)): |
558 | 225 | error = DatabaseImportPolicyViolation(import_into, module_name) | 228 | error = DatabaseImportPolicyViolation(import_into, name) |
559 | 226 | naughty_imports.add(error) | 229 | naughty_imports.add(error) |
560 | 227 | # Raise an error except in the case of browser.traversers. | 230 | # Raise an error except in the case of browser.traversers. |
561 | 228 | # This exception to raising an error is only temporary, until | 231 | # This exception to raising an error is only temporary, until |
562 | @@ -231,28 +234,28 @@ | |||
563 | 231 | raise error | 234 | raise error |
564 | 232 | 235 | ||
565 | 233 | # Check the import from __all__ policy. | 236 | # Check the import from __all__ policy. |
567 | 234 | if from_list is not None and ( | 237 | if fromlist is not None and ( |
568 | 235 | import_into.startswith('canonical') or import_into.startswith('lp')): | 238 | import_into.startswith('canonical') or import_into.startswith('lp')): |
569 | 236 | # We only want to warn about "from foo import bar" violations in our | 239 | # We only want to warn about "from foo import bar" violations in our |
570 | 237 | # own code. | 240 | # own code. |
572 | 238 | from_list = list(from_list) | 241 | fromlist = list(fromlist) |
573 | 239 | module_all = getattr(module, '__all__', None) | 242 | module_all = getattr(module, '__all__', None) |
574 | 240 | if module_all is None: | 243 | if module_all is None: |
576 | 241 | if from_list == ['*']: | 244 | if fromlist == ['*']: |
577 | 242 | # "from foo import *" is naughty if foo has no __all__ | 245 | # "from foo import *" is naughty if foo has no __all__ |
579 | 243 | error = FromStarPolicyViolation(import_into, module_name) | 246 | error = FromStarPolicyViolation(import_into, name) |
580 | 244 | naughty_imports.add(error) | 247 | naughty_imports.add(error) |
581 | 245 | raise error | 248 | raise error |
582 | 246 | else: | 249 | else: |
584 | 247 | if from_list == ['*']: | 250 | if fromlist == ['*']: |
585 | 248 | # "from foo import *" is allowed if foo has an __all__ | 251 | # "from foo import *" is allowed if foo has an __all__ |
586 | 249 | return module | 252 | return module |
587 | 250 | if is_test_module(import_into): | 253 | if is_test_module(import_into): |
588 | 251 | # We don't bother checking imports into test modules. | 254 | # We don't bother checking imports into test modules. |
589 | 252 | return module | 255 | return module |
593 | 253 | allowed_from_list = valid_imports_not_in_all.get( | 256 | allowed_fromlist = valid_imports_not_in_all.get( |
594 | 254 | module_name, set()) | 257 | name, set()) |
595 | 255 | for attrname in from_list: | 258 | for attrname in fromlist: |
596 | 256 | # Check that each thing we are importing into the module is | 259 | # Check that each thing we are importing into the module is |
597 | 257 | # either in __all__, is a module itself, or is a specific | 260 | # either in __all__, is a module itself, or is a specific |
598 | 258 | # exception. | 261 | # exception. |
599 | @@ -264,13 +267,13 @@ | |||
600 | 264 | # You can import modules even when they aren't declared in | 267 | # You can import modules even when they aren't declared in |
601 | 265 | # __all__. | 268 | # __all__. |
602 | 266 | continue | 269 | continue |
604 | 267 | if attrname in allowed_from_list: | 270 | if attrname in allowed_fromlist: |
605 | 268 | # Some things can be imported even if they aren't in | 271 | # Some things can be imported even if they aren't in |
606 | 269 | # __all__. | 272 | # __all__. |
607 | 270 | continue | 273 | continue |
608 | 271 | if attrname not in module_all: | 274 | if attrname not in module_all: |
609 | 272 | error = NotInModuleAllPolicyViolation( | 275 | error = NotInModuleAllPolicyViolation( |
611 | 273 | import_into, module_name, attrname) | 276 | import_into, name, attrname) |
612 | 274 | naughty_imports.add(error) | 277 | naughty_imports.add(error) |
613 | 275 | return module | 278 | return module |
614 | 276 | 279 | ||
615 | 277 | 280 | ||
616 | === modified file 'lib/lp/soyuz/model/buildqueue.py' | |||
617 | --- lib/lp/soyuz/model/buildqueue.py 2010-02-02 14:01:14 +0000 | |||
618 | +++ lib/lp/soyuz/model/buildqueue.py 2010-02-23 16:20:35 +0000 | |||
619 | @@ -506,6 +506,9 @@ | |||
620 | 506 | result_set = store.find( | 506 | result_set = store.find( |
621 | 507 | BuildQueue, | 507 | BuildQueue, |
622 | 508 | BuildQueue.job == Job.id, | 508 | BuildQueue.job == Job.id, |
623 | 509 | # XXX Michael Nelson 2010-02-22 bug=499421 | ||
624 | 510 | # Avoid corrupt build jobs where the builder is None. | ||
625 | 511 | BuildQueue.builder != None, | ||
626 | 509 | # status is a property. Let's use _status. | 512 | # status is a property. Let's use _status. |
627 | 510 | Job._status == JobStatus.RUNNING, | 513 | Job._status == JobStatus.RUNNING, |
628 | 511 | Job.date_started != None) | 514 | Job.date_started != None) |
629 | 512 | 515 | ||
630 | === modified file 'lib/lp/soyuz/tests/test_buildqueue.py' | |||
631 | --- lib/lp/soyuz/tests/test_buildqueue.py 2010-02-22 22:50:46 +0000 | |||
632 | +++ lib/lp/soyuz/tests/test_buildqueue.py 2010-02-23 16:20:35 +0000 | |||
633 | @@ -177,6 +177,19 @@ | |||
634 | 177 | buildqueue = BuildQueue(job=job.id) | 177 | buildqueue = BuildQueue(job=job.id) |
635 | 178 | self.assertEquals(buildqueue, self.buildqueueset.getByJob(job)) | 178 | self.assertEquals(buildqueue, self.buildqueueset.getByJob(job)) |
636 | 179 | 179 | ||
637 | 180 | def test_getActiveBuildJobs_no_builder_bug499421(self): | ||
638 | 181 | # An active build queue item that does not have a builder will | ||
639 | 182 | # not be included in the results and so will not block the | ||
640 | 183 | # buildd-manager. | ||
641 | 184 | active_jobs = self.buildqueueset.getActiveBuildJobs() | ||
642 | 185 | self.assertEqual(1, active_jobs.count()) | ||
643 | 186 | active_job = active_jobs[0] | ||
644 | 187 | active_job.builder = None | ||
645 | 188 | self.assertTrue( | ||
646 | 189 | self.buildqueueset.getActiveBuildJobs().is_empty(), | ||
647 | 190 | "An active build job must have a builder.") | ||
648 | 191 | |||
649 | 192 | |||
650 | 180 | 193 | ||
651 | 181 | class TestBuildQueueBase(TestCaseWithFactory): | 194 | class TestBuildQueueBase(TestCaseWithFactory): |
652 | 182 | """Setup the test publisher and some builders.""" | 195 | """Setup the test publisher and some builders.""" |
653 | @@ -248,7 +261,10 @@ | |||
654 | 248 | 261 | ||
655 | 249 | # hppa native | 262 | # hppa native |
656 | 250 | self.builders[(self.hppa_proc.id, False)] = [ | 263 | self.builders[(self.hppa_proc.id, False)] = [ |
658 | 251 | self.h5, self.h6, self.h7] | 264 | self.h5, |
659 | 265 | self.h6, | ||
660 | 266 | self.h7, | ||
661 | 267 | ] | ||
662 | 252 | # hppa virtual | 268 | # hppa virtual |
663 | 253 | self.builders[(self.hppa_proc.id, True)] = [ | 269 | self.builders[(self.hppa_proc.id, True)] = [ |
664 | 254 | self.h1, self.h2, self.h3, self.h4] | 270 | self.h1, self.h2, self.h3, self.h4] |
Fixes the merge conflict between stable and db-devel.