Merge lp:~adeuring/launchpad/bug-494075 into lp:launchpad/db-devel
- bug-494075
- Merge into db-devel
Proposed by
Abel Deuring
Status: | Merged |
---|---|
Merged at revision: | not available |
Proposed branch: | lp:~adeuring/launchpad/bug-494075 |
Merge into: | lp:launchpad/db-devel |
Diff against target: |
1374 lines (+784/-245) 17 files modified
lib/canonical/launchpad/webapp/adapter.py (+13/-1) lib/canonical/launchpad/webapp/ftests/test_adapter.txt (+8/-2) lib/lp/buildmaster/buildergroup.py (+8/-3) lib/lp/buildmaster/interfaces/buildfarmjobbehavior.py (+61/-0) lib/lp/buildmaster/model/buildfarmjobbehavior.py (+71/-0) lib/lp/buildmaster/tests/buildfarmjobbehavior.txt (+134/-0) lib/lp/soyuz/browser/tests/builder-views.txt (+1/-0) lib/lp/soyuz/configure.zcml (+7/-0) lib/lp/soyuz/doc/buildd-slavescanner.txt (+48/-12) lib/lp/soyuz/doc/builder.txt (+12/-0) lib/lp/soyuz/doc/buildqueue.txt (+10/-0) lib/lp/soyuz/interfaces/builder.py (+10/-25) lib/lp/soyuz/interfaces/buildqueue.py (+5/-1) lib/lp/soyuz/model/binarypackagebuildbehavior.py (+271/-0) lib/lp/soyuz/model/builder.py (+64/-201) lib/lp/soyuz/model/buildqueue.py (+7/-0) lib/lp/soyuz/tests/test_builder.py (+54/-0) |
To merge this branch: | bzr merge lp:~adeuring/launchpad/bug-494075 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Brad Crittenden (community) | code | Approve | |
Review via email: mp+15823@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Abel Deuring (adeuring) wrote : | # |
Revision history for this message
Brad Crittenden (bac) wrote : | # |
Hi Abel,
Thanks for this fix. Anything to make chasing down OOPs easier is great.
In your actual diff at http://
""A variant o TimeoutError hat reports the original PostgreSQL error."""
Looks like you were thinking faster than your fingers would go.
Other than that it's fine.
review:
Approve
(code)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/canonical/launchpad/webapp/adapter.py' |
2 | --- lib/canonical/launchpad/webapp/adapter.py 2009-08-06 15:45:18 +0000 |
3 | +++ lib/canonical/launchpad/webapp/adapter.py 2009-12-08 20:16:13 +0000 |
4 | @@ -62,6 +62,18 @@ |
5 | classImplements(TimeoutError, IRequestExpired) |
6 | |
7 | |
8 | +class LaunchpadTimeoutError(TimeoutError): |
9 | + """A variant of TimeoutError that reports the original PostgreSQL error. |
10 | + """ |
11 | + |
12 | + def __init__(self, statement, params, original_error): |
13 | + super(LaunchpadTimeoutError, self).__init__(statement, params) |
14 | + self.original_error = original_error |
15 | + |
16 | + def __str__(self): |
17 | + return ('Statement: %r\nParameters:%r\nOriginal error: %r' |
18 | + % (self.statement, self.params, self.original_error)) |
19 | + |
20 | def _get_dirty_commit_flags(): |
21 | """Return the current dirty commit status""" |
22 | from canonical.ftests.pgsql import ConnectionWrapper |
23 | @@ -420,7 +432,7 @@ |
24 | return |
25 | if isinstance(error, QueryCanceledError): |
26 | OpStats.stats['timeouts'] += 1 |
27 | - raise TimeoutError(statement, params) |
28 | + raise LaunchpadTimeoutError(statement, params, error) |
29 | |
30 | def get_remaining_time(self): |
31 | """See `TimeoutTracer`""" |
32 | |
33 | === modified file 'lib/canonical/launchpad/webapp/ftests/test_adapter.txt' |
34 | --- lib/canonical/launchpad/webapp/ftests/test_adapter.txt 2009-08-06 15:45:18 +0000 |
35 | +++ lib/canonical/launchpad/webapp/ftests/test_adapter.txt 2009-12-08 20:16:13 +0000 |
36 | @@ -115,7 +115,10 @@ |
37 | >>> store.execute('SELECT pg_sleep(0.200)', noresult=True) |
38 | Traceback (most recent call last): |
39 | ... |
40 | - TimeoutError: ...SELECT pg_sleep(0.200)... |
41 | + LaunchpadTimeoutError: Statement: 'SELECT pg_sleep(0.200)' |
42 | + Parameters:() |
43 | + Original error: QueryCanceledError('canceling statement due to |
44 | + statement timeout\n',) |
45 | |
46 | Even though the statement timed out, it is recorded in the statement log: |
47 | |
48 | @@ -175,7 +178,10 @@ |
49 | >>> store.execute('SELECT pg_sleep(0.2)', noresult=True) |
50 | Traceback (most recent call last): |
51 | ... |
52 | - TimeoutError: ...SELECT pg_sleep(0.2)... |
53 | + LaunchpadTimeoutError: Statement: 'SELECT pg_sleep(0.2)' |
54 | + Parameters:() |
55 | + Original error: QueryCanceledError('canceling statement due to |
56 | + statement timeout\n',) |
57 | >>> clear_request_started() |
58 | |
59 | |
60 | |
61 | === modified file 'lib/lp/buildmaster/buildergroup.py' |
62 | --- lib/lp/buildmaster/buildergroup.py 2009-11-13 19:34:17 +0000 |
63 | +++ lib/lp/buildmaster/buildergroup.py 2009-12-08 20:16:13 +0000 |
64 | @@ -171,8 +171,8 @@ |
65 | Perform the required actions for each state. |
66 | """ |
67 | try: |
68 | - (builder_status, build_id, build_status, logtail, filemap, |
69 | - dependencies) = queueItem.builder.slaveStatus() |
70 | + slave_status = queueItem.builder.slaveStatus() |
71 | + |
72 | except (xmlrpclib.Fault, socket.error), info: |
73 | # XXX cprov 2005-06-29: |
74 | # Hmm, a problem with the xmlrpc interface, |
75 | @@ -192,6 +192,7 @@ |
76 | 'BuilderStatus.WAITING': self.updateBuild_WAITING, |
77 | } |
78 | |
79 | + builder_status = slave_status['builder_status'] |
80 | if builder_status not in builder_status_handlers: |
81 | self.logger.critical( |
82 | "Builder on %s returned unknown status %s, failing it" |
83 | @@ -209,7 +210,11 @@ |
84 | # from the IBuilder content class, it arrives protected by a Zope |
85 | # Security Proxy, which is not declared, thus empty. Before passing |
86 | # it to the status handlers we will simply remove the proxy. |
87 | - logtail = removeSecurityProxy(logtail) |
88 | + logtail = removeSecurityProxy(slave_status.get('logtail')) |
89 | + build_id = slave_status.get('build_id') |
90 | + build_status = slave_status.get('build_status') |
91 | + filemap = slave_status.get('filemap') |
92 | + dependencies = slave_status.get('dependencies') |
93 | |
94 | method = builder_status_handlers[builder_status] |
95 | try: |
96 | |
97 | === added file 'lib/lp/buildmaster/interfaces/buildfarmjobbehavior.py' |
98 | --- lib/lp/buildmaster/interfaces/buildfarmjobbehavior.py 1970-01-01 00:00:00 +0000 |
99 | +++ lib/lp/buildmaster/interfaces/buildfarmjobbehavior.py 2009-12-08 20:16:13 +0000 |
100 | @@ -0,0 +1,61 @@ |
101 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
102 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
103 | + |
104 | +# pylint: disable-msg=E0211,E0213 |
105 | + |
106 | +"""Interface for build farm job behaviors.""" |
107 | + |
108 | +__metaclass__ = type |
109 | + |
110 | +__all__ = [ |
111 | + 'IBuildFarmJobBehavior', |
112 | + ] |
113 | + |
114 | +from zope.interface import Attribute, Interface |
115 | + |
116 | + |
117 | +class BuildBehaviorMismatch(Exception): |
118 | + """ |
119 | + A general exception that can be raised when the builder's current behavior |
120 | + does not match the expected behavior. |
121 | + """ |
122 | + |
123 | + |
124 | +class IBuildFarmJobBehavior(Interface): |
125 | + |
126 | + status = Attribute( |
127 | + "Generated status information for this particular job.") |
128 | + |
129 | + def setBuilder(builder): |
130 | + """Sets the associated builder reference for this instance.""" |
131 | + |
132 | + def logStartBuild(build_queue_item, logger): |
133 | + """Log the start of a specific build queue item. |
134 | + |
135 | + The form of the log message will vary depending on the type of build. |
136 | + :param build_queue_item: A BuildQueueItem to build. |
137 | + :param logger: A logger to be used to log diagnostic information. |
138 | + """ |
139 | + |
140 | + def dispatchBuildToSlave(build_queue_item, logger): |
141 | + """Dispatch a specific build to the slave. |
142 | + |
143 | + :param build_queue_item: The `BuildQueueItem` that will be built. |
144 | + :logger: A logger to be used to log diagnostic information. |
145 | + """ |
146 | + |
147 | + def verifyBuildRequest(build_queue_item, logger): |
148 | + """Carry out any pre-build checks. |
149 | + |
150 | + :param build_queue_item: The `BuildQueueItem` that is to be built. |
151 | + :logger: A logger to be used to log diagnostic information. |
152 | + """ |
153 | + |
154 | + def slaveStatus(self, raw_slave_status): |
155 | + """Return a dict of custom slave status values for this behavior. |
156 | + |
157 | + :param raw_slave_status: The value returned by the build slave's |
158 | + status() method. |
159 | + :return: a dict of extra key/values to be included in the result |
160 | + of IBuilder.slaveStatus(). |
161 | + """ |
162 | |
163 | === added file 'lib/lp/buildmaster/model/buildfarmjobbehavior.py' |
164 | --- lib/lp/buildmaster/model/buildfarmjobbehavior.py 1970-01-01 00:00:00 +0000 |
165 | +++ lib/lp/buildmaster/model/buildfarmjobbehavior.py 2009-12-08 20:16:13 +0000 |
166 | @@ -0,0 +1,71 @@ |
167 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
168 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
169 | + |
170 | +# pylint: disable-msg=E0211,E0213 |
171 | + |
172 | +"""Base and idle BuildFarmJobBehavior classes.""" |
173 | + |
174 | +__metaclass__ = type |
175 | + |
176 | +__all__ = [ |
177 | + 'BuildFarmJobBehaviorBase', |
178 | + 'IdleBuildBehavior' |
179 | + ] |
180 | + |
181 | +from zope.interface import implements |
182 | + |
183 | +from lp.buildmaster.interfaces.buildfarmjobbehavior import ( |
184 | + BuildBehaviorMismatch, IBuildFarmJobBehavior) |
185 | + |
186 | + |
187 | +class BuildFarmJobBehaviorBase: |
188 | + """Ensures that all behaviors inherit the same initialisation. |
189 | + |
190 | + All build-farm job behaviors should inherit from this. |
191 | + """ |
192 | + |
193 | + def __init__(self, buildfarmjob): |
194 | + """Store a reference to the job_type with which we were created.""" |
195 | + self.buildfarmjob = buildfarmjob |
196 | + self._builder = None |
197 | + |
198 | + def setBuilder(self, builder): |
199 | + """The builder should be set once and not changed.""" |
200 | + self._builder = builder |
201 | + |
202 | + def verifyBuildRequest(self, build_queue_item, logger): |
203 | + """The default behavior is a no-op.""" |
204 | + pass |
205 | + |
206 | + def slaveStatus(self, raw_slave_status): |
207 | + """See `IBuildFarmJobBehavior`. |
208 | + |
209 | + The default behavior is that we don't add any extra values.""" |
210 | + return {} |
211 | + |
212 | + |
213 | +class IdleBuildBehavior(BuildFarmJobBehaviorBase): |
214 | + |
215 | + implements(IBuildFarmJobBehavior) |
216 | + |
217 | + def __init__(self): |
218 | + """The idle behavior is special in that a buildfarmjob is not |
219 | + specified during initialisation as it is not the result of an |
220 | + adaption. |
221 | + """ |
222 | + super(IdleBuildBehavior, self).__init__(None) |
223 | + |
224 | + def logStartBuild(self, build_queue_item, logger): |
225 | + """See `IBuildFarmJobBehavior`.""" |
226 | + raise BuildBehaviorMismatch( |
227 | + "Builder was idle when asked to log the start of a build.") |
228 | + |
229 | + def dispatchBuildToSlave(self, build_queue_item, logger): |
230 | + """See `IBuildFarmJobBehavior`.""" |
231 | + raise BuildBehaviorMismatch( |
232 | + "Builder was idle when asked to dispatch a build to the slave.") |
233 | + |
234 | + @property |
235 | + def status(self): |
236 | + """See `IBuildFarmJobBehavior`.""" |
237 | + return "Idle" |
238 | |
239 | === added file 'lib/lp/buildmaster/tests/buildfarmjobbehavior.txt' |
240 | --- lib/lp/buildmaster/tests/buildfarmjobbehavior.txt 1970-01-01 00:00:00 +0000 |
241 | +++ lib/lp/buildmaster/tests/buildfarmjobbehavior.txt 2009-12-08 20:16:13 +0000 |
242 | @@ -0,0 +1,134 @@ |
243 | +BuildFarmJobBehavior |
244 | +==================== |
245 | + |
246 | +The Launchpad build farm was originally designed for building binary |
247 | +packages from source packages, but was subsequently generalised to support |
248 | +other types of build farm jobs. |
249 | + |
250 | +The `BuildFarmJobBehavior` class encapsulates job-type-specific behavior |
251 | +with a standard interface to which our generic IBuilder class delegates. |
252 | +The result is that neither our generic IBuilder class or any call-sites |
253 | +(such as the build master) need any knowledge of different job types or |
254 | +how to handle them. |
255 | + |
256 | + |
257 | +Creating a new behavior |
258 | +----------------------- |
259 | + |
260 | +A new behavior should implement the `IBuildFarmJobBehavior` interface |
261 | +and extend BuildFarmJobBehaviorBase. A new behavior will only be required |
262 | +to define one method - dispatchBuildToSlave() - to correctly implement |
263 | +the interface, but will usually want to customise the other properties and |
264 | +methods as well. |
265 | + |
266 | + >>> from lp.buildmaster.interfaces.buildfarmjobbehavior import ( |
267 | + ... IBuildFarmJobBehavior) |
268 | + >>> from lp.buildmaster.model.buildfarmjobbehavior import ( |
269 | + ... BuildFarmJobBehaviorBase) |
270 | + >>> from zope.interface import implements |
271 | + |
272 | + >>> class MyNewBuildBehavior(BuildFarmJobBehaviorBase): |
273 | + ... """A custom build behavior for building blah.""" |
274 | + ... implements(IBuildFarmJobBehavior) |
275 | + ... |
276 | + ... def dispatchBuildToSlave(self, build_queue_item, logger): |
277 | + ... print "Did something special to dispatch MySpecialBuild." |
278 | + ... |
279 | + ... @property |
280 | + ... def status(self): |
281 | + ... return "Currently building a MyNewBuild object." |
282 | + |
283 | +For this documentation, we'll also need a dummy new build farm job. |
284 | + |
285 | + >>> from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJob |
286 | + >>> class IMyNewBuildFarmJob(IBuildFarmJob): |
287 | + ... "Normally defines job-type specific database fields.""" |
288 | + >>> class MyNewBuildFarmJob(object): |
289 | + ... implements(IMyNewBuildFarmJob) |
290 | + |
291 | +Custom behaviors are not normally instantiated directly, instead an adapter is |
292 | +specified for the specific IBuildFarmJob. Normaly we'd add some ZCML to |
293 | +adapt our specific build farm job to its behavior like: |
294 | + |
295 | + <!-- MyNewBuildBehavior --> |
296 | + <adapter |
297 | + for="lp.myapp.interfaces.mynewbuildfarmjob.IMyNewBuildFarmJob" |
298 | + provides="lp.buildmaster.interfaces.buildfarmjobbehavior.IBuildFarmJobBehavior" |
299 | + factory="lp.myapp.model.mynewbuildbehavior.MyNewBuildBehavior" |
300 | + permission="zope.Public" /> |
301 | + |
302 | +But for the sake of this documentation we'll add the adapter manually. |
303 | + |
304 | + >>> from zope.app.testing import ztapi |
305 | + >>> ztapi.provideAdapter( |
306 | + ... MyNewBuildFarmJob, IBuildFarmJobBehavior, MyNewBuildBehavior) |
307 | + |
308 | +This will then allow the builder to request and set the required behavior from |
309 | +the current job. Bob the builder currently has a binary package job and so |
310 | +finds itself with a binary package build behavior which defines the status |
311 | +attribute with some binary-build specific information. |
312 | + |
313 | + >>> from lp.soyuz.model.builder import Builder |
314 | + >>> from canonical.launchpad.webapp.interfaces import ( |
315 | + ... IStoreSelector, MAIN_STORE, DEFAULT_FLAVOR) |
316 | + >>> store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR) |
317 | + >>> bob = store.find(Builder, Builder.name == 'bob').one() |
318 | + >>> print bob.status |
319 | + Building i386 build of mozilla-firefox 0.9 in ubuntu hoary RELEASE |
320 | + |
321 | +XXX Michael Nelson 2009-12-04 bug 484819. At the time of writing the |
322 | +BuildQueue.specific_job method has not been updated to support different |
323 | +job types, so we will set the behavior explicitly here. |
324 | + |
325 | + >>> bob.current_build_behavior = IBuildFarmJobBehavior( |
326 | + ... MyNewBuildFarmJob()) |
327 | + |
328 | +Once the builder has the relevant behavior, it is able to provide both general |
329 | +builder functionality of its own accord, while delegating any build-type |
330 | +specific functionality to the behavior. For example, if the builder is not |
331 | +disabled, the builder delegates the status property to the behavior. |
332 | + |
333 | + >>> print bob.status |
334 | + Currently building a MyNewBuild object. |
335 | + |
336 | +On the other hand, if the builder is disabled, the builder itself determines |
337 | +the status. |
338 | + |
339 | + >>> bob.builderok = False |
340 | + >>> print bob.status |
341 | + Disabled |
342 | + >>> bob.builderok = True |
343 | + |
344 | +The IBuildFarmJobBehavior interface currently provides customisation points |
345 | +throughout the build life-cycle, from logging the start of a build, verifying |
346 | +that the provided queue item is ready to be built, dispatching the build etc., |
347 | +and allows further customisation to be added easily. |
348 | + |
349 | +Please refer to the IBuildFarmJobBehavior interface to see the currently |
350 | +provided build-type specific customisation points. |
351 | + |
352 | + |
353 | +The IdleBuildBehavior |
354 | +--------------------- |
355 | + |
356 | +If a Builder does not have a currentjob (and therefore an appropriate |
357 | +build behavior) it will automatically get the special IdleBuildBehavior |
358 | +to ensure that it fulfills its contract to implement IBuildFarmJobBehavior. |
359 | + |
360 | +First, we'll reset the current build behavior and destroy the current job. |
361 | + |
362 | + >>> bob.current_build_behavior = None |
363 | + >>> bob.currentjob.destroySelf() |
364 | + |
365 | + >>> print bob.status |
366 | + Idle |
367 | + |
368 | +Attempting to use any other build-related functionality when a builder is |
369 | +idle, such as making a call to log the start of a build, will raise an |
370 | +appropriate exception. |
371 | + |
372 | + >>> bob.logStartBuild(None, None) |
373 | + Traceback (most recent call last): |
374 | + ... |
375 | + BuildBehaviorMismatch: Builder was idle when asked to log the start of a |
376 | + build. |
377 | |
378 | === modified file 'lib/lp/soyuz/browser/tests/builder-views.txt' |
379 | --- lib/lp/soyuz/browser/tests/builder-views.txt 2009-11-24 07:02:21 +0000 |
380 | +++ lib/lp/soyuz/browser/tests/builder-views.txt 2009-12-08 20:16:13 +0000 |
381 | @@ -228,6 +228,7 @@ |
382 | >>> login("foo.bar@canonical.com") |
383 | >>> private_job = store.get(BuildQueue, private_job_id) |
384 | >>> Store.of(private_job).remove(private_job) |
385 | + >>> frog.current_build_behavior = None |
386 | |
387 | >>> login('no-priv@canonical.com') |
388 | >>> nopriv_view = getMultiAdapter((frog, empty_request), name="+index") |
389 | |
390 | === modified file 'lib/lp/soyuz/configure.zcml' |
391 | --- lib/lp/soyuz/configure.zcml 2009-11-16 22:06:14 +0000 |
392 | +++ lib/lp/soyuz/configure.zcml 2009-12-08 20:16:13 +0000 |
393 | @@ -892,4 +892,11 @@ |
394 | interface="lp.soyuz.interfaces.buildpackagejob.IBuildPackageJob"/> |
395 | </class> |
396 | |
397 | + <!-- BinaryPackageBuildBehavior --> |
398 | + <adapter |
399 | + for="lp.soyuz.interfaces.buildpackagejob.IBuildPackageJob" |
400 | + provides="lp.buildmaster.interfaces.buildfarmjobbehavior.IBuildFarmJobBehavior" |
401 | + factory="lp.soyuz.model.binarypackagebuildbehavior.BinaryPackageBuildBehavior" |
402 | + permission="zope.Public" /> |
403 | + |
404 | </configure> |
405 | |
406 | === modified file 'lib/lp/soyuz/doc/buildd-slavescanner.txt' |
407 | --- lib/lp/soyuz/doc/buildd-slavescanner.txt 2009-11-13 19:34:17 +0000 |
408 | +++ lib/lp/soyuz/doc/buildd-slavescanner.txt 2009-12-08 20:16:13 +0000 |
409 | @@ -1494,8 +1494,8 @@ |
410 | |
411 | == Builder Status Handler == |
412 | |
413 | -IBuilder.slaveStatus should return a size homogeneous tuple despite of |
414 | -the current slave state. This tuple should contain, in this order: |
415 | +IBuilder.slaveStatus should return a dict containing the following |
416 | +items: |
417 | |
418 | * slave status string: 'BuilderStatus.IDLE' |
419 | * job identifier string: '1-1' |
420 | @@ -1504,24 +1504,60 @@ |
421 | * result file list: {'foo.deb', 'foo.changes'} or None |
422 | * dependencies string: 'bar baz zaz' or None |
423 | |
424 | + # Define a helper to print the slave status dict. |
425 | + >>> from collections import defaultdict |
426 | + >>> def printSlaveStatus(status_dict): |
427 | + ... status_dict = defaultdict(lambda:None, status_dict) |
428 | + ... print ( |
429 | + ... "builder_status: %(builder_status)s\n" |
430 | + ... "build_id: %(build_id)s\n" |
431 | + ... "build_status: %(build_status)s\n" |
432 | + ... "logtail: %(logtail)r\n" |
433 | + ... "filemap: %(filemap)s\n" |
434 | + ... "dependencies: %(dependencies)s\n" % status_dict) |
435 | |
436 | >>> a_builder.setSlaveForTesting(OkSlave()) |
437 | - >>> a_builder.slaveStatus() |
438 | - ('BuilderStatus.IDLE', '', None, None, None, None) |
439 | + >>> printSlaveStatus(a_builder.slaveStatus()) |
440 | + builder_status: BuilderStatus.IDLE |
441 | + build_id: |
442 | + build_status: None |
443 | + logtail: None |
444 | + filemap: None |
445 | + dependencies: None |
446 | |
447 | >>> a_builder.setSlaveForTesting(BuildingSlave()) |
448 | - >>> a_builder.slaveStatus() |
449 | - ('BuilderStatus.BUILDING', '1-1', None, <xmlrpclib.Binary ...>, None, None) |
450 | + >>> printSlaveStatus(a_builder.slaveStatus()) |
451 | + builder_status: BuilderStatus.BUILDING |
452 | + build_id: 1-1 |
453 | + build_status: None |
454 | + logtail: <xmlrpclib.Binary ...> |
455 | + filemap: None |
456 | + dependencies: None |
457 | |
458 | >>> a_builder.setSlaveForTesting(WaitingSlave(state='BuildStatus.OK')) |
459 | - >>> a_builder.slaveStatus() |
460 | - ('BuilderStatus.WAITING', '1-1', 'BuildStatus.OK', None, {}, None) |
461 | + >>> printSlaveStatus(a_builder.slaveStatus()) |
462 | + builder_status: BuilderStatus.WAITING |
463 | + build_id: 1-1 |
464 | + build_status: BuildStatus.OK |
465 | + logtail: None |
466 | + filemap: {} |
467 | + dependencies: None |
468 | |
469 | >>> a_builder.setSlaveForTesting(AbortingSlave()) |
470 | - >>> a_builder.slaveStatus() |
471 | - ('BuilderStatus.ABORTING', '1-1', None, None, None, None) |
472 | + >>> printSlaveStatus(a_builder.slaveStatus()) |
473 | + builder_status: BuilderStatus.ABORTING |
474 | + build_id: 1-1 |
475 | + build_status: None |
476 | + logtail: None |
477 | + filemap: None |
478 | + dependencies: None |
479 | |
480 | >>> a_builder.setSlaveForTesting(AbortedSlave()) |
481 | - >>> a_builder.slaveStatus() |
482 | - ('BuilderStatus.ABORTED', '1-1', None, None, None, None) |
483 | + >>> printSlaveStatus(a_builder.slaveStatus()) |
484 | + builder_status: BuilderStatus.ABORTED |
485 | + build_id: 1-1 |
486 | + build_status: None |
487 | + logtail: None |
488 | + filemap: None |
489 | + dependencies: None |
490 | |
491 | |
492 | === modified file 'lib/lp/soyuz/doc/builder.txt' |
493 | --- lib/lp/soyuz/doc/builder.txt 2009-11-12 12:52:23 +0000 |
494 | +++ lib/lp/soyuz/doc/builder.txt 2009-12-08 20:16:13 +0000 |
495 | @@ -40,6 +40,18 @@ |
496 | >>> builder.builderok = True |
497 | >>> builder.failnotes = None |
498 | |
499 | +A builder provides a current_build_behavior attribute to which all the |
500 | +build-type specific behavior for the current build is delegated. For |
501 | +our sample data, the behavior is specifically a behavior for dealing |
502 | +with binary packages |
503 | + |
504 | + >>> from zope.security.proxy import isinstance |
505 | + >>> from lp.soyuz.model.binarypackagebuildbehavior import ( |
506 | + ... BinaryPackageBuildBehavior) |
507 | + >>> isinstance( |
508 | + ... builder.current_build_behavior, BinaryPackageBuildBehavior) |
509 | + True |
510 | + |
511 | In case of copy archives the status string will show both the copy |
512 | archive owner as well as the copy archive name. |
513 | |
514 | |
515 | === modified file 'lib/lp/soyuz/doc/buildqueue.txt' |
516 | --- lib/lp/soyuz/doc/buildqueue.txt 2009-11-13 19:34:17 +0000 |
517 | +++ lib/lp/soyuz/doc/buildqueue.txt 2009-12-08 20:16:13 +0000 |
518 | @@ -98,6 +98,16 @@ |
519 | (True, 1000) |
520 | |
521 | |
522 | +The BuildQueue item is responsible for providing the required build behavior |
523 | +for the item. |
524 | + |
525 | + >>> from zope.security.proxy import isinstance |
526 | + >>> from lp.soyuz.model.binarypackagebuildbehavior import ( |
527 | + ... BinaryPackageBuildBehavior) |
528 | + >>> isinstance(bq.required_build_behavior, BinaryPackageBuildBehavior) |
529 | + True |
530 | + |
531 | + |
532 | == Dispatching and Reseting jobs == |
533 | |
534 | The sampledata contains an active job, being built by the 'bob' |
535 | |
536 | === modified file 'lib/lp/soyuz/interfaces/builder.py' |
537 | --- lib/lp/soyuz/interfaces/builder.py 2009-11-20 18:06:28 +0000 |
538 | +++ lib/lp/soyuz/interfaces/builder.py 2009-12-08 20:16:13 +0000 |
539 | @@ -19,11 +19,13 @@ |
540 | ] |
541 | |
542 | from zope.interface import Interface, Attribute |
543 | -from zope.schema import Choice, TextLine, Text, Bool |
544 | +from zope.schema import Bool, Choice, Field, Text, TextLine |
545 | |
546 | from canonical.launchpad import _ |
547 | from canonical.launchpad.fields import Title, Description |
548 | from lp.registry.interfaces.role import IHasOwner |
549 | +from lp.buildmaster.interfaces.buildfarmjobbehavior import ( |
550 | + IBuildFarmJobBehavior) |
551 | from canonical.launchpad.validators.name import name_validator |
552 | from canonical.launchpad.validators.url import builder_url_validator |
553 | |
554 | @@ -57,7 +59,7 @@ |
555 | """The build slave has suffered an error and cannot be used.""" |
556 | |
557 | |
558 | -class IBuilder(IHasOwner): |
559 | +class IBuilder(IHasOwner, IBuildFarmJobBehavior): |
560 | """Build-slave information and state. |
561 | |
562 | Builder instance represents a single builder slave machine within the |
563 | @@ -138,6 +140,10 @@ |
564 | "new jobs. "), |
565 | required=False) |
566 | |
567 | + current_build_behavior = Field( |
568 | + title=u"The current behavior of the builder for the current job.", |
569 | + required=False) |
570 | + |
571 | def cacheFileOnSlave(logger, libraryfilealias): |
572 | """Ask the slave to cache a librarian file to its local disk. |
573 | |
574 | @@ -148,19 +154,6 @@ |
575 | file. |
576 | """ |
577 | |
578 | - def cachePrivateSourceOnSlave(logger, build_queue_item): |
579 | - """Ask the slave to download source files for a private build. |
580 | - |
581 | - The slave will cache the files for the source in build_queue_item |
582 | - to its local disk in preparation for a private build. Private builds |
583 | - will always take the source files from the archive rather than the |
584 | - librarian since the archive has more granular access to each |
585 | - archive's files. |
586 | - |
587 | - :param logger: A logger used for providing debug information. |
588 | - :param build_queue_item: The `IBuildQueue` being built. |
589 | - """ |
590 | - |
591 | def checkCanBuildForDistroArchSeries(distro_arch_series): |
592 | """Check that the slave can compile for the given distro_arch_release. |
593 | |
594 | @@ -215,16 +208,8 @@ |
595 | def slaveStatus(): |
596 | """Get the slave status for this builder. |
597 | |
598 | - * builder_status => string |
599 | - * build_id => string |
600 | - * build_status => string or None |
601 | - * logtail => string or None |
602 | - * filename => dictionary or None |
603 | - * dependencies => string or None |
604 | - |
605 | - :return: a tuple containing ( |
606 | - builder_status, build_id, build_status, logtail, filemap, |
607 | - dependencies) |
608 | + :return: a dict containing at least builder_status, but potentially |
609 | + other values included by the current build behavior. |
610 | """ |
611 | |
612 | def slaveStatusSentence(): |
613 | |
614 | === modified file 'lib/lp/soyuz/interfaces/buildqueue.py' |
615 | --- lib/lp/soyuz/interfaces/buildqueue.py 2009-11-20 18:06:28 +0000 |
616 | +++ lib/lp/soyuz/interfaces/buildqueue.py 2009-12-08 20:16:13 +0000 |
617 | @@ -13,7 +13,7 @@ |
618 | ] |
619 | |
620 | from zope.interface import Interface, Attribute |
621 | -from zope.schema import Choice, Datetime, Timedelta |
622 | +from zope.schema import Choice, Datetime, Field, Timedelta |
623 | |
624 | from lazr.restful.fields import Reference |
625 | |
626 | @@ -51,6 +51,10 @@ |
627 | title=_('Job type'), required=True, vocabulary=BuildFarmJobType, |
628 | description=_("The type of this job.")) |
629 | |
630 | + required_build_behavior = Field( |
631 | + title=_('The builder behavior required to run this job.'), |
632 | + required=False, readonly=True) |
633 | + |
634 | estimated_duration = Timedelta( |
635 | title=_("Estimated Job Duration"), required=True, |
636 | description=_("Estimated job duration interval.")) |
637 | |
638 | === added file 'lib/lp/soyuz/model/binarypackagebuildbehavior.py' |
639 | --- lib/lp/soyuz/model/binarypackagebuildbehavior.py 1970-01-01 00:00:00 +0000 |
640 | +++ lib/lp/soyuz/model/binarypackagebuildbehavior.py 2009-12-08 20:16:13 +0000 |
641 | @@ -0,0 +1,271 @@ |
642 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
643 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
644 | + |
645 | +# pylint: disable-msg=E0211,E0213 |
646 | + |
647 | +"""Builder behavior for binary package builds.""" |
648 | + |
649 | +__metaclass__ = type |
650 | + |
651 | +__all__ = [ |
652 | + 'BinaryPackageBuildBehavior', |
653 | + ] |
654 | + |
655 | +import socket |
656 | +import xmlrpclib |
657 | + |
658 | +from canonical.launchpad.webapp import urlappend |
659 | +from lp.buildmaster.interfaces.buildfarmjobbehavior import ( |
660 | + IBuildFarmJobBehavior) |
661 | +from lp.buildmaster.model.buildfarmjobbehavior import ( |
662 | + BuildFarmJobBehaviorBase) |
663 | +from lp.registry.interfaces.pocket import PackagePublishingPocket |
664 | +from lp.soyuz.adapters.archivedependencies import ( |
665 | + get_primary_current_component, get_sources_list_for_building) |
666 | +from lp.soyuz.interfaces.archive import ArchivePurpose |
667 | +from lp.soyuz.interfaces.build import IBuildSet |
668 | +from lp.soyuz.interfaces.builder import BuildSlaveFailure, CannotBuild |
669 | + |
670 | +from zope.component import getUtility |
671 | +from zope.interface import implements |
672 | + |
673 | + |
674 | +class BinaryPackageBuildBehavior(BuildFarmJobBehaviorBase): |
675 | + """Define the behavior of binary package builds.""" |
676 | + |
677 | + implements(IBuildFarmJobBehavior) |
678 | + |
679 | + def logStartBuild(self, build_queue_item, logger): |
680 | + """See `IBuildFarmJobBehavior`.""" |
681 | + build = getUtility(IBuildSet).getByQueueEntry(build_queue_item) |
682 | + spr = build.sourcepackagerelease |
683 | + |
684 | + logger.info("startBuild(%s, %s, %s, %s)", self._builder.url, |
685 | + spr.name, spr.version, build.pocket.title) |
686 | + |
687 | + @property |
688 | + def status(self): |
689 | + """See `IBuildFarmJobBehavior`.""" |
690 | + build = getUtility(IBuildSet).getByQueueEntry( |
691 | + self._builder.currentjob) |
692 | + msg = 'Building %s' % build.title |
693 | + archive = build.archive |
694 | + if not archive.owner.private and (archive.is_ppa or archive.is_copy): |
695 | + return '%s [%s/%s]' % (msg, archive.owner.name, archive.name) |
696 | + else: |
697 | + return msg |
698 | + |
699 | + def dispatchBuildToSlave(self, build_queue_item, logger): |
700 | + """See `IBuildFarmJobBehavior`.""" |
701 | + |
702 | + # Start the binary package build on the slave builder. First |
703 | + # we send the chroot. |
704 | + build = getUtility(IBuildSet).getByQueueEntry(build_queue_item) |
705 | + chroot = build.distroarchseries.getChroot() |
706 | + self._builder.cacheFileOnSlave(logger, chroot) |
707 | + |
708 | + # Build filemap structure with the files required in this build |
709 | + # and send them to the slave. |
710 | + # If the build is private we tell the slave to get the files from the |
711 | + # archive instead of the librarian because the slaves cannot |
712 | + # access the restricted librarian. |
713 | + private = build.archive.private |
714 | + if private: |
715 | + self._cachePrivateSourceOnSlave(build_queue_item, logger) |
716 | + filemap = {} |
717 | + for source_file in build.sourcepackagerelease.files: |
718 | + lfa = source_file.libraryfile |
719 | + filemap[lfa.filename] = lfa.content.sha1 |
720 | + if not private: |
721 | + self._builder.cacheFileOnSlave( |
722 | + logger, source_file.libraryfile) |
723 | + |
724 | + # Generate a string which can be used to cross-check when obtaining |
725 | + # results so we know we are referring to the right database object in |
726 | + # subsequent runs. |
727 | + buildid = "%s-%s" % (build.id, build_queue_item.id) |
728 | + chroot_sha1 = chroot.content.sha1 |
729 | + logger.debug( |
730 | + "Initiating build %s on %s" % (buildid, self._builder.url)) |
731 | + |
732 | + try: |
733 | + args = self._extraBuildArgs(build) |
734 | + status, info = self._builder.slave.build( |
735 | + buildid, "debian", chroot_sha1, filemap, args) |
736 | + message = """%s (%s): |
737 | + ***** RESULT ***** |
738 | + %s |
739 | + %s |
740 | + %s: %s |
741 | + ****************** |
742 | + """ % ( |
743 | + self._builder.name, |
744 | + self._builder.url, |
745 | + filemap, |
746 | + args, |
747 | + status, |
748 | + info, |
749 | + ) |
750 | + logger.info(message) |
751 | + except xmlrpclib.Fault, info: |
752 | + # Mark builder as 'failed'. |
753 | + logger.debug( |
754 | + "Disabling builder: %s" % self._builder.url, exc_info=1) |
755 | + self._builder.failbuilder( |
756 | + "Exception (%s) when setting up to new job" % info) |
757 | + raise BuildSlaveFailure |
758 | + except socket.error, info: |
759 | + error_message = "Exception (%s) when setting up new job" % info |
760 | + self._builder.handleTimeout(logger, error_message) |
761 | + raise BuildSlaveFailure |
762 | + |
763 | + def verifyBuildRequest(self, build_queue_item, logger): |
764 | + """Assert some pre-build checks. |
765 | + |
766 | + The build request is checked: |
767 | + * Virtualized builds can't build on a non-virtual builder |
768 | + * Ensure that we have a chroot |
769 | + * Ensure that the build pocket allows builds for the current |
770 | + distroseries state. |
771 | + """ |
772 | + build = getUtility(IBuildSet).getByQueueEntry(build_queue_item) |
773 | + assert not (not self._builder.virtualized and build.is_virtualized), ( |
774 | + "Attempt to build non-virtual item on a virtual builder.") |
775 | + |
776 | + # Assert that we are not silently building SECURITY jobs. |
777 | + # See findBuildCandidates. Once we start building SECURITY |
778 | + # correctly from EMBARGOED archive this assertion can be removed. |
779 | + # XXX Julian 2007-12-18 spec=security-in-soyuz: This is being |
780 | + # addressed in the work on the blueprint: |
781 | + # https://blueprints.launchpad.net/soyuz/+spec/security-in-soyuz |
782 | + target_pocket = build.pocket |
783 | + assert target_pocket != PackagePublishingPocket.SECURITY, ( |
784 | + "Soyuz is not yet capable of building SECURITY uploads.") |
785 | + |
786 | + # Ensure build has the needed chroot |
787 | + chroot = build.distroarchseries.getChroot() |
788 | + if chroot is None: |
789 | + raise CannotBuild( |
790 | + "Missing CHROOT for %s/%s/%s" % ( |
791 | + build.distroseries.distribution.name, |
792 | + build.distroseries.name, |
793 | + build.distroarchseries.architecturetag) |
794 | + ) |
795 | + |
796 | + # The main distribution has policies to prevent uploads to some |
797 | + # pockets (e.g. security) during different parts of the distribution |
798 | + # series lifecycle. These do not apply to PPA builds nor any archive |
799 | + # that allows release pocket updates. |
800 | + if (build.archive.purpose != ArchivePurpose.PPA and |
801 | + not build.archive.allowUpdatesToReleasePocket()): |
802 | + # XXX Robert Collins 2007-05-26: not an explicit CannotBuild |
803 | + # exception yet because the callers have not been audited |
804 | + assert build.distroseries.canUploadToPocket(build.pocket), ( |
805 | + "%s (%s) can not be built for pocket %s: invalid pocket due " |
806 | + "to the series status of %s." |
807 | + % (build.title, build.id, build.pocket.name, |
808 | + build.distroseries.name)) |
809 | + |
810 | + def slaveStatus(self, raw_slave_status): |
811 | + """Parse and return the binary build specific status info. |
812 | + |
813 | + This includes: |
814 | + * build_id => string |
815 | + * build_status => string or None |
816 | + * logtail => string or None |
817 | + * filename => dictionary or None |
818 | + * dependencies => string or None |
819 | + """ |
820 | + builder_status = raw_slave_status[0] |
821 | + extra_info = {} |
822 | + if builder_status == 'BuilderStatus.WAITING': |
823 | + extra_info['build_status'] = raw_slave_status[1] |
824 | + extra_info['build_id'] = raw_slave_status[2] |
825 | + build_status_with_files = [ |
826 | + 'BuildStatus.OK', |
827 | + 'BuildStatus.PACKAGEFAIL', |
828 | + 'BuildStatus.DEPFAIL', |
829 | + ] |
830 | + if extra_info['build_status'] in build_status_with_files: |
831 | + extra_info['filemap'] = raw_slave_status[3] |
832 | + extra_info['dependencies'] = raw_slave_status[4] |
833 | + else: |
834 | + extra_info['build_id'] = raw_slave_status[1] |
835 | + if builder_status == 'BuilderStatus.BUILDING': |
836 | + extra_info['logtail'] = raw_slave_status[2] |
837 | + |
838 | + return extra_info |
839 | + |
840 | + def _cachePrivateSourceOnSlave(self, build_queue_item, logger): |
841 | + """Ask the slave to download source files for a private build. |
842 | + |
843 | + The slave will cache the files for the source in build_queue_item |
844 | + to its local disk in preparation for a private build. Private builds |
845 | + will always take the source files from the archive rather than the |
846 | + librarian since the archive has more granular access to each |
847 | + archive's files. |
848 | + |
849 | + :param build_queue_item: The `IBuildQueue` being built. |
850 | + :param logger: A logger used for providing debug information. |
851 | + """ |
852 | + # The URL to the file in the archive consists of these parts: |
853 | + # archive_url / makePoolPath() / filename |
854 | + # Once this is constructed we add the http basic auth info. |
855 | + |
856 | + # Avoid circular imports. |
857 | + from lp.soyuz.model.publishing import makePoolPath |
858 | + |
859 | + build = getUtility(IBuildSet).getByQueueEntry(build_queue_item) |
860 | + archive = build.archive |
861 | + archive_url = archive.archive_url |
862 | + component_name = build.current_component.name |
863 | + for source_file in build.sourcepackagerelease.files: |
864 | + file_name = source_file.libraryfile.filename |
865 | + sha1 = source_file.libraryfile.content.sha1 |
866 | + source_name = build.sourcepackagerelease.sourcepackagename.name |
867 | + poolpath = makePoolPath(source_name, component_name) |
868 | + url = urlappend(archive_url, poolpath) |
869 | + url = urlappend(url, file_name) |
870 | + logger.debug("Asking builder on %s to ensure it has file %s " |
871 | + "(%s, %s)" % ( |
872 | + self._builder.url, file_name, url, sha1)) |
873 | + self._builder._sendFileToSlave( |
874 | + url, sha1, "buildd", archive.buildd_secret) |
875 | + |
876 | + def _extraBuildArgs(self, build): |
877 | + """ |
878 | + Return the extra arguments required by the slave for the given build. |
879 | + """ |
880 | + # Build extra arguments. |
881 | + args = {} |
882 | + # turn 'arch_indep' ON only if build is archindep or if |
883 | + # the specific architecture is the nominatedarchindep for |
884 | + # this distroseries (in case it requires any archindep source) |
885 | + args['arch_indep'] = build.distroarchseries.isNominatedArchIndep |
886 | + |
887 | + suite = build.distroarchseries.distroseries.name |
888 | + if build.pocket != PackagePublishingPocket.RELEASE: |
889 | + suite += "-%s" % (build.pocket.name.lower()) |
890 | + args['suite'] = suite |
891 | + |
892 | + archive_purpose = build.archive.purpose |
893 | + if (archive_purpose == ArchivePurpose.PPA and |
894 | + not build.archive.require_virtualized): |
895 | + # If we're building a non-virtual PPA, override the purpose |
896 | + # to PRIMARY and use the primary component override. |
897 | + # This ensures that the package mangling tools will run over |
898 | + # the built packages. |
899 | + args['archive_purpose'] = ArchivePurpose.PRIMARY.name |
900 | + args["ogrecomponent"] = ( |
901 | + get_primary_current_component(build)) |
902 | + else: |
903 | + args['archive_purpose'] = archive_purpose.name |
904 | + args["ogrecomponent"] = ( |
905 | + build.current_component.name) |
906 | + |
907 | + args['archives'] = get_sources_list_for_building(build) |
908 | + |
909 | + # Let the build slave know whether this is a build in a private |
910 | + # archive. |
911 | + args['archive_private'] = build.archive.private |
912 | + return args |
913 | |
914 | === modified file 'lib/lp/soyuz/model/builder.py' |
915 | --- lib/lp/soyuz/model/builder.py 2009-11-20 18:06:28 +0000 |
916 | +++ lib/lp/soyuz/model/builder.py 2009-12-08 20:16:13 +0000 |
917 | @@ -20,6 +20,8 @@ |
918 | import urllib2 |
919 | import xmlrpclib |
920 | |
921 | +from lazr.delegates import delegates |
922 | + |
923 | from zope.interface import implements |
924 | from zope.component import getUtility |
925 | |
926 | @@ -31,10 +33,11 @@ |
927 | from canonical.cachedproperty import cachedproperty |
928 | from canonical.config import config |
929 | from canonical.buildd.slave import BuilderStatus |
930 | +from lp.buildmaster.interfaces.buildfarmjobbehavior import ( |
931 | + BuildBehaviorMismatch, IBuildFarmJobBehavior) |
932 | from lp.buildmaster.master import BuilddMaster |
933 | +from lp.buildmaster.model.buildfarmjobbehavior import IdleBuildBehavior |
934 | from canonical.database.sqlbase import SQLBase, sqlvalues |
935 | -from lp.soyuz.adapters.archivedependencies import ( |
936 | - get_primary_current_component, get_sources_list_for_building) |
937 | from lp.soyuz.model.buildqueue import BuildQueue |
938 | from lp.registry.interfaces.person import validate_public_person |
939 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
940 | @@ -54,6 +57,7 @@ |
941 | PackagePublishingStatus) |
942 | from lp.soyuz.model.buildpackagejob import BuildPackageJob |
943 | from canonical.launchpad.webapp import urlappend |
944 | +from canonical.lazr.utils import safe_hasattr |
945 | from canonical.librarian.utils import copy_and_close |
946 | |
947 | |
948 | @@ -111,9 +115,11 @@ |
949 | |
950 | return (stdout, stderr, resume_process.returncode) |
951 | |
952 | + |
953 | class Builder(SQLBase): |
954 | |
955 | implements(IBuilder, IHasBuildRecords) |
956 | + delegates(IBuildFarmJobBehavior, context="current_build_behavior") |
957 | _table = 'Builder' |
958 | |
959 | _defaultOrder = ['id'] |
960 | @@ -135,6 +141,44 @@ |
961 | vm_host = StringCol(dbName='vm_host') |
962 | active = BoolCol(dbName='active', notNull=True, default=True) |
963 | |
964 | + def _getCurrentBuildBehavior(self): |
965 | + """Return the current build behavior.""" |
966 | + if not safe_hasattr(self, '_current_build_behavior'): |
967 | + self._current_build_behavior = None |
968 | + |
969 | + if (self._current_build_behavior is None or |
970 | + isinstance(self._current_build_behavior, IdleBuildBehavior)): |
971 | + # If we don't currently have a current build behavior set, |
972 | + # or we are currently idle, then... |
973 | + currentjob = self.currentjob |
974 | + if currentjob is not None: |
975 | + # ...we'll set it based on our current job. |
976 | + self._current_build_behavior = ( |
977 | + currentjob.required_build_behavior) |
978 | + self._current_build_behavior.setBuilder(self) |
979 | + return self._current_build_behavior |
980 | + elif self._current_build_behavior is None: |
981 | + # If we don't have a current job or an idle behavior |
982 | + # already set, then we just set the idle behavior |
983 | + # before returning. |
984 | + self._current_build_behavior = IdleBuildBehavior() |
985 | + return self._current_build_behavior |
986 | + |
987 | + else: |
988 | + # We did have a current non-idle build behavior set, so |
989 | + # we just return it. |
990 | + return self._current_build_behavior |
991 | + |
992 | + |
993 | + def _setCurrentBuildBehavior(self, new_behavior): |
994 | + """Set the current build behavior.""" |
995 | + self._current_build_behavior = new_behavior |
996 | + if self._current_build_behavior is not None: |
997 | + self._current_build_behavior.setBuilder(self) |
998 | + |
999 | + current_build_behavior = property( |
1000 | + _getCurrentBuildBehavior, _setCurrentBuildBehavior) |
1001 | + |
1002 | def cacheFileOnSlave(self, logger, libraryfilealias): |
1003 | """See `IBuilder`.""" |
1004 | url = libraryfilealias.http_url |
1005 | @@ -143,30 +187,6 @@ |
1006 | url, libraryfilealias.content.sha1)) |
1007 | self._sendFileToSlave(url, libraryfilealias.content.sha1) |
1008 | |
1009 | - def cachePrivateSourceOnSlave(self, logger, build_queue_item): |
1010 | - """See `IBuilder`.""" |
1011 | - # The URL to the file in the archive consists of these parts: |
1012 | - # archive_url / makePoolPath() / filename |
1013 | - # Once this is constructed we add the http basic auth info. |
1014 | - |
1015 | - # Avoid circular imports. |
1016 | - from lp.soyuz.model.publishing import makePoolPath |
1017 | - |
1018 | - build = getUtility(IBuildSet).getByQueueEntry(build_queue_item) |
1019 | - archive = build.archive |
1020 | - archive_url = archive.archive_url |
1021 | - component_name = build.current_component.name |
1022 | - for source_file in build.sourcepackagerelease.files: |
1023 | - file_name = source_file.libraryfile.filename |
1024 | - sha1 = source_file.libraryfile.content.sha1 |
1025 | - source_name = build.sourcepackagerelease.sourcepackagename.name |
1026 | - poolpath = makePoolPath(source_name, component_name) |
1027 | - url = urlappend(archive_url, poolpath) |
1028 | - url = urlappend(url, file_name) |
1029 | - logger.debug("Asking builder on %s to ensure it has file %s " |
1030 | - "(%s, %s)" % (self.url, file_name, url, sha1)) |
1031 | - self._sendFileToSlave(url, sha1, "buildd", archive.buildd_secret) |
1032 | - |
1033 | def _sendFileToSlave(self, url, sha1, username="", password=""): |
1034 | """Helper to send the file at 'url' with 'sha1' to this builder.""" |
1035 | if not self.builderok: |
1036 | @@ -256,156 +276,22 @@ |
1037 | """See IBuilder.""" |
1038 | self.slave = proxy |
1039 | |
1040 | - def _verifyBuildRequest(self, build_queue_item, logger): |
1041 | - """Assert some pre-build checks. |
1042 | - |
1043 | - The build request is checked: |
1044 | - * Virtualized builds can't build on a non-virtual builder |
1045 | - * Ensure that we have a chroot |
1046 | - * Ensure that the build pocket allows builds for the current |
1047 | - distroseries state. |
1048 | - """ |
1049 | - build = getUtility(IBuildSet).getByQueueEntry(build_queue_item) |
1050 | - assert not (not self.virtualized and build.is_virtualized), ( |
1051 | - "Attempt to build non-virtual item on a virtual builder.") |
1052 | - |
1053 | - # Assert that we are not silently building SECURITY jobs. |
1054 | - # See findBuildCandidates. Once we start building SECURITY |
1055 | - # correctly from EMBARGOED archive this assertion can be removed. |
1056 | - # XXX Julian 2007-12-18 spec=security-in-soyuz: This is being |
1057 | - # addressed in the work on the blueprint: |
1058 | - # https://blueprints.launchpad.net/soyuz/+spec/security-in-soyuz |
1059 | - target_pocket = build.pocket |
1060 | - assert target_pocket != PackagePublishingPocket.SECURITY, ( |
1061 | - "Soyuz is not yet capable of building SECURITY uploads.") |
1062 | - |
1063 | - # Ensure build has the needed chroot |
1064 | - build = getUtility(IBuildSet).getByQueueEntry(build_queue_item) |
1065 | - chroot = build.distroarchseries.getChroot() |
1066 | - if chroot is None: |
1067 | - raise CannotBuild( |
1068 | - "Missing CHROOT for %s/%s/%s" % ( |
1069 | - build.distroseries.distribution.name, |
1070 | - build.distroseries.name, |
1071 | - build.distroarchseries.architecturetag) |
1072 | - ) |
1073 | - |
1074 | - # The main distribution has policies to prevent uploads to some |
1075 | - # pockets (e.g. security) during different parts of the distribution |
1076 | - # series lifecycle. These do not apply to PPA builds nor any archive |
1077 | - # that allows release pocket updates. |
1078 | - if (build.archive.purpose != ArchivePurpose.PPA and |
1079 | - not build.archive.allowUpdatesToReleasePocket()): |
1080 | - # XXX Robert Collins 2007-05-26: not an explicit CannotBuild |
1081 | - # exception yet because the callers have not been audited |
1082 | - assert build.distroseries.canUploadToPocket(build.pocket), ( |
1083 | - "%s (%s) can not be built for pocket %s: invalid pocket due " |
1084 | - "to the series status of %s." |
1085 | - % (build.title, build.id, build.pocket.name, |
1086 | - build.distroseries.name)) |
1087 | - |
1088 | - def _dispatchBuildToSlave(self, build_queue_item, args, buildid, logger): |
1089 | - """Start the build on the slave builder.""" |
1090 | - # Send chroot. |
1091 | - build = getUtility(IBuildSet).getByQueueEntry(build_queue_item) |
1092 | - chroot = build.distroarchseries.getChroot() |
1093 | - self.cacheFileOnSlave(logger, chroot) |
1094 | - |
1095 | - # Build filemap structure with the files required in this build |
1096 | - # and send them to the slave. |
1097 | - # If the build is private we tell the slave to get the files from the |
1098 | - # archive instead of the librarian because the slaves cannot |
1099 | - # access the restricted librarian. |
1100 | - private = build.archive.private |
1101 | - if private: |
1102 | - self.cachePrivateSourceOnSlave(logger, build_queue_item) |
1103 | - filemap = {} |
1104 | - for source_file in build.sourcepackagerelease.files: |
1105 | - lfa = source_file.libraryfile |
1106 | - filemap[lfa.filename] = lfa.content.sha1 |
1107 | - if not private: |
1108 | - self.cacheFileOnSlave(logger, source_file.libraryfile) |
1109 | - |
1110 | - chroot_sha1 = chroot.content.sha1 |
1111 | - try: |
1112 | - status, info = self.slave.build( |
1113 | - buildid, "debian", chroot_sha1, filemap, args) |
1114 | - message = """%s (%s): |
1115 | - ***** RESULT ***** |
1116 | - %s |
1117 | - %s |
1118 | - %s: %s |
1119 | - ****************** |
1120 | - """ % (self.name, self.url, filemap, args, status, info) |
1121 | - logger.info(message) |
1122 | - except xmlrpclib.Fault, info: |
1123 | - # Mark builder as 'failed'. |
1124 | - logger.debug("Disabling builder: %s" % self.url, exc_info=1) |
1125 | - self.failbuilder( |
1126 | - "Exception (%s) when setting up to new job" % info) |
1127 | - raise BuildSlaveFailure |
1128 | - except socket.error, info: |
1129 | - error_message = "Exception (%s) when setting up new job" % info |
1130 | - self.handleTimeout(logger, error_message) |
1131 | - raise BuildSlaveFailure |
1132 | - |
1133 | def startBuild(self, build_queue_item, logger): |
1134 | """See IBuilder.""" |
1135 | - build = getUtility(IBuildSet).getByQueueEntry(build_queue_item) |
1136 | - spr = build.sourcepackagerelease |
1137 | - logger.info("startBuild(%s, %s, %s, %s)", self.url, |
1138 | - spr.name, spr.version, build.pocket.title) |
1139 | + # Set the build behavior depending on the provided build queue item. |
1140 | + self.current_build_behavior = build_queue_item.required_build_behavior |
1141 | + self.logStartBuild(build_queue_item, logger) |
1142 | |
1143 | # Make sure the request is valid; an exception is raised if it's not. |
1144 | - self._verifyBuildRequest(build_queue_item, logger) |
1145 | + self.verifyBuildRequest(build_queue_item, logger) |
1146 | |
1147 | # If we are building a virtual build, resume the virtual machine. |
1148 | if self.virtualized: |
1149 | self.resumeSlaveHost() |
1150 | |
1151 | - # Build extra arguments. |
1152 | - args = {} |
1153 | - # turn 'arch_indep' ON only if build is archindep or if |
1154 | - # the specific architecture is the nominatedarchindep for |
1155 | - # this distroseries (in case it requires any archindep source) |
1156 | - build = getUtility(IBuildSet).getByQueueEntry(build_queue_item) |
1157 | - args['arch_indep'] = build.distroarchseries.isNominatedArchIndep |
1158 | - |
1159 | - suite = build.distroarchseries.distroseries.name |
1160 | - if build.pocket != PackagePublishingPocket.RELEASE: |
1161 | - suite += "-%s" % (build.pocket.name.lower()) |
1162 | - args['suite'] = suite |
1163 | - |
1164 | - archive_purpose = build.archive.purpose |
1165 | - if (archive_purpose == ArchivePurpose.PPA and |
1166 | - not build.archive.require_virtualized): |
1167 | - # If we're building a non-virtual PPA, override the purpose |
1168 | - # to PRIMARY and use the primary component override. |
1169 | - # This ensures that the package mangling tools will run over |
1170 | - # the built packages. |
1171 | - args['archive_purpose'] = ArchivePurpose.PRIMARY.name |
1172 | - args["ogrecomponent"] = ( |
1173 | - get_primary_current_component(build)) |
1174 | - else: |
1175 | - args['archive_purpose'] = archive_purpose.name |
1176 | - args["ogrecomponent"] = ( |
1177 | - build.current_component.name) |
1178 | - |
1179 | - args['archives'] = get_sources_list_for_building(build) |
1180 | - |
1181 | - # Let the build slave know whether this is a build in a private |
1182 | - # archive. |
1183 | - args['archive_private'] = build.archive.private |
1184 | - |
1185 | - # Generate a string which can be used to cross-check when obtaining |
1186 | - # results so we know we are referring to the right database object in |
1187 | - # subsequent runs. |
1188 | - buildid = "%s-%s" % (build.id, build_queue_item.id) |
1189 | - logger.debug("Initiating build %s on %s" % (buildid, self.url)) |
1190 | - |
1191 | # Do it. |
1192 | build_queue_item.markAsBuilding(self) |
1193 | - self._dispatchBuildToSlave(build_queue_item, args, buildid, logger) |
1194 | + self.dispatchBuildToSlave(build_queue_item, logger) |
1195 | |
1196 | # XXX cprov 2009-06-24: This code does not belong to the content |
1197 | # class domain. Here we cannot make sensible decisions about what |
1198 | @@ -418,25 +304,20 @@ |
1199 | if self.failnotes is not None: |
1200 | return self.failnotes |
1201 | return 'Disabled' |
1202 | - # Cache the 'currentjob', so we don't have to hit the database |
1203 | - # more than once. |
1204 | - currentjob = self.currentjob |
1205 | - if currentjob is None: |
1206 | - return 'Idle' |
1207 | |
1208 | - build = getUtility(IBuildSet).getByQueueEntry(currentjob) |
1209 | - msg = 'Building %s' % build.title |
1210 | - archive = build.archive |
1211 | - if not archive.owner.private and (archive.is_ppa or archive.is_copy): |
1212 | - return '%s [%s/%s]' % (msg, archive.owner.name, archive.name) |
1213 | - else: |
1214 | - return msg |
1215 | + # If the builder is OK then we delegate the status |
1216 | + # to our current behavior. |
1217 | + return self.current_build_behavior.status |
1218 | |
1219 | def failbuilder(self, reason): |
1220 | """See IBuilder""" |
1221 | self.builderok = False |
1222 | self.failnotes = reason |
1223 | |
1224 | + # XXX Michael Nelson 20091202 bug=491330. The current UI assumes |
1225 | + # that the builder history will display binary build records, as |
1226 | + # returned by getBuildRecords() below. See the bug for a discussion |
1227 | + # of the options. |
1228 | def getBuildRecords(self, build_state=None, name=None, arch_tag=None, |
1229 | user=None): |
1230 | """See IHasBuildRecords.""" |
1231 | @@ -447,29 +328,11 @@ |
1232 | """See IBuilder.""" |
1233 | builder_version, builder_arch, mechanisms = self.slave.info() |
1234 | status_sentence = self.slave.status() |
1235 | - builder_status = status_sentence[0] |
1236 | - |
1237 | - if builder_status == 'BuilderStatus.WAITING': |
1238 | - (build_status, build_id) = status_sentence[1:3] |
1239 | - build_status_with_files = [ |
1240 | - 'BuildStatus.OK', |
1241 | - 'BuildStatus.PACKAGEFAIL', |
1242 | - 'BuildStatus.DEPFAIL', |
1243 | - ] |
1244 | - if build_status in build_status_with_files: |
1245 | - (filemap, dependencies) = status_sentence[3:] |
1246 | - else: |
1247 | - filemap = dependencies = None |
1248 | - logtail = None |
1249 | - elif builder_status == 'BuilderStatus.BUILDING': |
1250 | - (build_id, logtail) = status_sentence[1:] |
1251 | - build_status = filemap = dependencies = None |
1252 | - else: |
1253 | - build_id = status_sentence[1] |
1254 | - build_status = logtail = filemap = dependencies = None |
1255 | - |
1256 | - return (builder_status, build_id, build_status, logtail, filemap, |
1257 | - dependencies) |
1258 | + |
1259 | + status = {'builder_status': status_sentence[0]} |
1260 | + status.update( |
1261 | + self.current_build_behavior.slaveStatus(status_sentence)) |
1262 | + return status |
1263 | |
1264 | def slaveStatusSentence(self): |
1265 | """See IBuilder.""" |
1266 | @@ -671,7 +534,7 @@ |
1267 | logger = self._getSlaveScannerLogger() |
1268 | try: |
1269 | self.startBuild(candidate, logger) |
1270 | - except (BuildSlaveFailure, CannotBuild), err: |
1271 | + except (BuildSlaveFailure, CannotBuild, BuildBehaviorMismatch), err: |
1272 | logger.warn('Could not build: %s' % err) |
1273 | |
1274 | def handleTimeout(self, logger, error_message): |
1275 | |
1276 | === modified file 'lib/lp/soyuz/model/buildqueue.py' |
1277 | --- lib/lp/soyuz/model/buildqueue.py 2009-11-20 18:06:28 +0000 |
1278 | +++ lib/lp/soyuz/model/buildqueue.py 2009-12-08 20:16:13 +0000 |
1279 | @@ -24,6 +24,8 @@ |
1280 | from canonical.database.sqlbase import SQLBase, sqlvalues |
1281 | from canonical.launchpad.webapp.interfaces import NotFoundError |
1282 | from lp.buildmaster.interfaces.buildfarmjob import BuildFarmJobType |
1283 | +from lp.buildmaster.interfaces.buildfarmjobbehavior import ( |
1284 | + IBuildFarmJobBehavior) |
1285 | from lp.services.job.interfaces.job import JobStatus |
1286 | from lp.services.job.model.job import Job |
1287 | from lp.soyuz.interfaces.build import BuildStatus, IBuildSet |
1288 | @@ -49,6 +51,11 @@ |
1289 | estimated_duration = IntervalCol() |
1290 | |
1291 | @property |
1292 | + def required_build_behavior(self): |
1293 | + """See `IBuildQueue`.""" |
1294 | + return IBuildFarmJobBehavior(self.specific_job) |
1295 | + |
1296 | + @property |
1297 | def specific_job(self): |
1298 | """See `IBuildQueue`.""" |
1299 | store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR) |
1300 | |
1301 | === modified file 'lib/lp/soyuz/tests/test_builder.py' |
1302 | --- lib/lp/soyuz/tests/test_builder.py 2009-11-13 16:37:05 +0000 |
1303 | +++ lib/lp/soyuz/tests/test_builder.py 2009-12-08 20:16:13 +0000 |
1304 | @@ -8,10 +8,15 @@ |
1305 | from zope.component import getUtility |
1306 | |
1307 | from canonical.testing import LaunchpadZopelessLayer |
1308 | +from lp.buildmaster.interfaces.buildfarmjobbehavior import ( |
1309 | + BuildBehaviorMismatch, IBuildFarmJobBehavior) |
1310 | +from lp.buildmaster.model.buildfarmjobbehavior import IdleBuildBehavior |
1311 | from lp.soyuz.interfaces.archive import ArchivePurpose |
1312 | from lp.soyuz.interfaces.build import BuildStatus, IBuildSet |
1313 | from lp.soyuz.interfaces.builder import IBuilderSet |
1314 | from lp.soyuz.interfaces.publishing import PackagePublishingStatus |
1315 | +from lp.soyuz.model.binarypackagebuildbehavior import ( |
1316 | + BinaryPackageBuildBehavior) |
1317 | from lp.soyuz.tests.test_publishing import SoyuzTestPublisher |
1318 | from lp.testing import TestCaseWithFactory |
1319 | |
1320 | @@ -214,5 +219,54 @@ |
1321 | self.failUnlessEqual('primary', build.archive.name) |
1322 | self.failUnlessEqual('firefox', build.sourcepackagerelease.name) |
1323 | |
1324 | + |
1325 | +class TestCurrentBuildBehavior(TestCaseWithFactory): |
1326 | + """This test ensures the get/set behavior of IBuilder's |
1327 | + current_build_behavior property. |
1328 | + """ |
1329 | + |
1330 | + layer = LaunchpadZopelessLayer |
1331 | + |
1332 | + def setUp(self): |
1333 | + """Create a new builder ready for testing.""" |
1334 | + super(TestCurrentBuildBehavior, self).setUp() |
1335 | + self.builder = self.factory.makeBuilder(name='builder') |
1336 | + |
1337 | + # Have a publisher and a ppa handy for some of the tests below. |
1338 | + self.publisher = SoyuzTestPublisher() |
1339 | + self.publisher.prepareBreezyAutotest() |
1340 | + self.ppa_joe = self.factory.makeArchive(name="joesppa") |
1341 | + |
1342 | + self.build = self.publisher.getPubSource( |
1343 | + sourcename="gedit", status=PackagePublishingStatus.PUBLISHED, |
1344 | + archive=self.ppa_joe).createMissingBuilds()[0] |
1345 | + |
1346 | + self.buildfarmjob = self.build.buildqueue_record.specific_job |
1347 | + |
1348 | + def test_idle_behavior_when_no_current_build(self): |
1349 | + """We return an idle behavior when there is no behavior specified |
1350 | + nor a current build. |
1351 | + """ |
1352 | + self.assertIsInstance( |
1353 | + self.builder.current_build_behavior, IdleBuildBehavior) |
1354 | + |
1355 | + def test_set_behavior_sets_builder(self): |
1356 | + """Setting a builder's behavior also associates the behavior with the |
1357 | + builder.""" |
1358 | + behavior = IBuildFarmJobBehavior(self.buildfarmjob) |
1359 | + self.builder.current_build_behavior = behavior |
1360 | + |
1361 | + self.assertEqual(behavior, self.builder.current_build_behavior) |
1362 | + self.assertEqual(behavior._builder, self.builder) |
1363 | + |
1364 | + def test_current_job_behavior(self): |
1365 | + """The current behavior is set automatically from the current job.""" |
1366 | + # Set the builder attribute on the buildqueue record so that our |
1367 | + # builder will think it has a current build. |
1368 | + self.build.buildqueue_record.builder = self.builder |
1369 | + |
1370 | + self.assertIsInstance( |
1371 | + self.builder.current_build_behavior, BinaryPackageBuildBehavior) |
1372 | + |
1373 | def test_suite(): |
1374 | return unittest.TestLoader().loadTestsFromName(__name__) |
When psycopg QueryCancelError occurs, we see only the SQL statement leading to the timeout and its parameters, but not the details of the QueryCancelError. This makes it hard to understand what heppend for example in OOPS-1438B2246.
This branch adds the original QueryCancelError message to the data reported for TimeoutErrors.
test: ./bin/test -vv -t test_adapter.txt
= Launchpad lint =
Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.
Linting changed files: /launchpad/ webapp/ adapter. py /launchpad/ webapp/ ftests/ test_adapter. txt ~/canonical/ lp-branches/bug-494075$ make lint
lib/canonical
lib/canonical
abel@klato4:
= Launchpad lint =
Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.
Linting changed files: /launchpad/ webapp/ adapter. py /launchpad/ webapp/ ftests/ test_adapter. txt
lib/canonical
lib/canonical