OLD | NEW |
| (Empty) |
1 | |
2 import random, weakref | |
3 from zope.interface import implements | |
4 from twisted.python import log, components | |
5 from twisted.python.failure import Failure | |
6 from twisted.spread import pb | |
7 from twisted.internet import reactor, defer | |
8 | |
9 from buildbot import interfaces | |
10 from buildbot.status.progress import Expectations | |
11 from buildbot.util import now | |
12 from buildbot.process import base | |
13 from buildbot.process.properties import Properties | |
14 | |
15 (ATTACHING, # slave attached, still checking hostinfo/etc | |
16 IDLE, # idle, available for use | |
17 PINGING, # build about to start, making sure it is still alive | |
18 BUILDING, # build is running | |
19 LATENT, # latent slave is not substantiated; similar to idle | |
20 SUBSTANTIATING, | |
21 ) = range(6) | |
22 | |
23 | |
24 class AbstractSlaveBuilder(pb.Referenceable): | |
25 """I am the master-side representative for one of the | |
26 L{buildbot.slave.bot.SlaveBuilder} objects that lives in a remote | |
27 buildbot. When a remote builder connects, I query it for command versions | |
28 and then make it available to any Builds that are ready to run. """ | |
29 | |
30 def __init__(self): | |
31 self.ping_watchers = [] | |
32 self.state = None # set in subclass | |
33 self.remote = None | |
34 self.slave = None | |
35 self.builder_name = None | |
36 | |
37 def __repr__(self): | |
38 r = ["<", self.__class__.__name__] | |
39 if self.builder_name: | |
40 r.extend([" builder=", self.builder_name]) | |
41 if self.slave: | |
42 r.extend([" slave=", self.slave.slavename]) | |
43 r.append(">") | |
44 return ''.join(r) | |
45 | |
46 def setBuilder(self, b): | |
47 self.builder = b | |
48 self.builder_name = b.name | |
49 | |
50 def getSlaveCommandVersion(self, command, oldversion=None): | |
51 if self.remoteCommands is None: | |
52 # the slave is 0.5.0 or earlier | |
53 return oldversion | |
54 return self.remoteCommands.get(command) | |
55 | |
56 def isAvailable(self): | |
57 # if this SlaveBuilder is busy, then it's definitely not available | |
58 if self.isBusy(): | |
59 return False | |
60 | |
61 # otherwise, check in with the BuildSlave | |
62 if self.slave: | |
63 return self.slave.canStartBuild() | |
64 | |
65 # no slave? not very available. | |
66 return False | |
67 | |
68 def isBusy(self): | |
69 return self.state not in (IDLE, LATENT) | |
70 | |
71 def buildStarted(self): | |
72 self.state = BUILDING | |
73 | |
74 def buildFinished(self): | |
75 self.state = IDLE | |
76 reactor.callLater(0, self.builder.botmaster.maybeStartAllBuilds) | |
77 | |
78 def attached(self, slave, remote, commands): | |
79 """ | |
80 @type slave: L{buildbot.buildslave.BuildSlave} | |
81 @param slave: the BuildSlave that represents the buildslave as a | |
82 whole | |
83 @type remote: L{twisted.spread.pb.RemoteReference} | |
84 @param remote: a reference to the L{buildbot.slave.bot.SlaveBuilder} | |
85 @type commands: dict: string -> string, or None | |
86 @param commands: provides the slave's version of each RemoteCommand | |
87 """ | |
88 self.state = ATTACHING | |
89 self.remote = remote | |
90 self.remoteCommands = commands # maps command name to version | |
91 if self.slave is None: | |
92 self.slave = slave | |
93 self.slave.addSlaveBuilder(self) | |
94 else: | |
95 assert self.slave == slave | |
96 log.msg("Buildslave %s attached to %s" % (slave.slavename, | |
97 self.builder_name)) | |
98 d = self.remote.callRemote("setMaster", self) | |
99 d.addErrback(self._attachFailure, "Builder.setMaster") | |
100 d.addCallback(self._attached2) | |
101 return d | |
102 | |
103 def _attached2(self, res): | |
104 d = self.remote.callRemote("print", "attached") | |
105 d.addErrback(self._attachFailure, "Builder.print 'attached'") | |
106 d.addCallback(self._attached3) | |
107 return d | |
108 | |
109 def _attached3(self, res): | |
110 # now we say they're really attached | |
111 self.state = IDLE | |
112 return self | |
113 | |
114 def _attachFailure(self, why, where): | |
115 assert isinstance(where, str) | |
116 log.msg(where) | |
117 log.err(why) | |
118 return why | |
119 | |
120 def prepare(self, builder_status): | |
121 return defer.succeed(None) | |
122 | |
123 def ping(self, status=None): | |
124 """Ping the slave to make sure it is still there. Returns a Deferred | |
125 that fires with True if it is. | |
126 | |
127 @param status: if you point this at a BuilderStatus, a 'pinging' | |
128 event will be pushed. | |
129 """ | |
130 oldstate = self.state | |
131 self.state = PINGING | |
132 newping = not self.ping_watchers | |
133 d = defer.Deferred() | |
134 self.ping_watchers.append(d) | |
135 if newping: | |
136 if status: | |
137 event = status.addEvent(["pinging"]) | |
138 d2 = defer.Deferred() | |
139 d2.addCallback(self._pong_status, event) | |
140 self.ping_watchers.insert(0, d2) | |
141 # I think it will make the tests run smoother if the status | |
142 # is updated before the ping completes | |
143 Ping().ping(self.remote).addCallback(self._pong) | |
144 | |
145 def reset_state(res): | |
146 if self.state == PINGING: | |
147 self.state = oldstate | |
148 return res | |
149 d.addCallback(reset_state) | |
150 return d | |
151 | |
152 def _pong(self, res): | |
153 watchers, self.ping_watchers = self.ping_watchers, [] | |
154 for d in watchers: | |
155 d.callback(res) | |
156 | |
157 def _pong_status(self, res, event): | |
158 if res: | |
159 event.text = ["ping", "success"] | |
160 else: | |
161 event.text = ["ping", "failed"] | |
162 event.finish() | |
163 | |
164 def detached(self): | |
165 log.msg("Buildslave %s detached from %s" % (self.slave.slavename, | |
166 self.builder_name)) | |
167 if self.slave: | |
168 self.slave.removeSlaveBuilder(self) | |
169 self.slave = None | |
170 self.remote = None | |
171 self.remoteCommands = None | |
172 | |
173 | |
174 class Ping: | |
175 running = False | |
176 | |
177 def ping(self, remote): | |
178 assert not self.running | |
179 self.running = True | |
180 log.msg("sending ping") | |
181 self.d = defer.Deferred() | |
182 # TODO: add a distinct 'ping' command on the slave.. using 'print' | |
183 # for this purpose is kind of silly. | |
184 remote.callRemote("print", "ping").addCallbacks(self._pong, | |
185 self._ping_failed, | |
186 errbackArgs=(remote,)) | |
187 return self.d | |
188 | |
189 def _pong(self, res): | |
190 log.msg("ping finished: success") | |
191 self.d.callback(True) | |
192 | |
193 def _ping_failed(self, res, remote): | |
194 log.msg("ping finished: failure") | |
195 # the slave has some sort of internal error, disconnect them. If we | |
196 # don't, we'll requeue a build and ping them again right away, | |
197 # creating a nasty loop. | |
198 remote.broker.transport.loseConnection() | |
199 # TODO: except, if they actually did manage to get this far, they'll | |
200 # probably reconnect right away, and we'll do this game again. Maybe | |
201 # it would be better to leave them in the PINGING state. | |
202 self.d.callback(False) | |
203 | |
204 | |
205 class SlaveBuilder(AbstractSlaveBuilder): | |
206 | |
207 def __init__(self): | |
208 AbstractSlaveBuilder.__init__(self) | |
209 self.state = ATTACHING | |
210 | |
211 def detached(self): | |
212 AbstractSlaveBuilder.detached(self) | |
213 if self.slave: | |
214 self.slave.removeSlaveBuilder(self) | |
215 self.slave = None | |
216 self.state = ATTACHING | |
217 | |
218 def buildFinished(self): | |
219 # Call the slave's buildFinished if we can; the slave may be waiting | |
220 # to do a graceful shutdown and needs to know when it's idle. | |
221 # After, we check to see if we can start other builds. | |
222 self.state = IDLE | |
223 if self.slave: | |
224 d = self.slave.buildFinished(self) | |
225 d.addCallback(lambda x: reactor.callLater(0, self.builder.botmaster.
maybeStartAllBuilds)) | |
226 else: | |
227 reactor.callLater(0, self.builder.botmaster.maybeStartAllBuilds) | |
228 | |
229 | |
230 class LatentSlaveBuilder(AbstractSlaveBuilder): | |
231 def __init__(self, slave, builder): | |
232 AbstractSlaveBuilder.__init__(self) | |
233 self.slave = slave | |
234 self.state = LATENT | |
235 self.setBuilder(builder) | |
236 self.slave.addSlaveBuilder(self) | |
237 log.msg("Latent buildslave %s attached to %s" % (slave.slavename, | |
238 self.builder_name)) | |
239 | |
240 def prepare(self, builder_status): | |
241 log.msg("substantiating slave %s" % (self,)) | |
242 d = self.substantiate() | |
243 def substantiation_failed(f): | |
244 builder_status.addPointEvent(['removing', 'latent', | |
245 self.slave.slavename]) | |
246 self.slave.disconnect() | |
247 # TODO: should failover to a new Build | |
248 return f | |
249 d.addErrback(substantiation_failed) | |
250 return d | |
251 | |
252 def substantiate(self): | |
253 self.state = SUBSTANTIATING | |
254 d = self.slave.substantiate(self) | |
255 if not self.slave.substantiated: | |
256 event = self.builder.builder_status.addEvent( | |
257 ["substantiating"]) | |
258 def substantiated(res): | |
259 msg = ["substantiate", "success"] | |
260 if isinstance(res, basestring): | |
261 msg.append(res) | |
262 elif isinstance(res, (tuple, list)): | |
263 msg.extend(res) | |
264 event.text = msg | |
265 event.finish() | |
266 return res | |
267 def substantiation_failed(res): | |
268 event.text = ["substantiate", "failed"] | |
269 # TODO add log of traceback to event | |
270 event.finish() | |
271 return res | |
272 d.addCallbacks(substantiated, substantiation_failed) | |
273 return d | |
274 | |
275 def detached(self): | |
276 AbstractSlaveBuilder.detached(self) | |
277 self.state = LATENT | |
278 | |
279 def buildStarted(self): | |
280 AbstractSlaveBuilder.buildStarted(self) | |
281 self.slave.buildStarted(self) | |
282 | |
283 def buildFinished(self): | |
284 AbstractSlaveBuilder.buildFinished(self) | |
285 self.slave.buildFinished(self) | |
286 | |
287 def _attachFailure(self, why, where): | |
288 self.state = LATENT | |
289 return AbstractSlaveBuilder._attachFailure(self, why, where) | |
290 | |
291 def ping(self, status=None): | |
292 if not self.slave.substantiated: | |
293 if status: | |
294 status.addEvent(["ping", "latent"]).finish() | |
295 return defer.succeed(True) | |
296 return AbstractSlaveBuilder.ping(self, status) | |
297 | |
298 | |
299 class Builder(pb.Referenceable): | |
300 """I manage all Builds of a given type. | |
301 | |
302 Each Builder is created by an entry in the config file (the c['builders'] | |
303 list), with a number of parameters. | |
304 | |
305 One of these parameters is the L{buildbot.process.factory.BuildFactory} | |
306 object that is associated with this Builder. The factory is responsible | |
307 for creating new L{Build<buildbot.process.base.Build>} objects. Each | |
308 Build object defines when and how the build is performed, so a new | |
309 Factory or Builder should be defined to control this behavior. | |
310 | |
311 The Builder holds on to a number of L{base.BuildRequest} objects in a | |
312 list named C{.buildable}. Incoming BuildRequest objects will be added to | |
313 this list, or (if possible) merged into an existing request. When a slave | |
314 becomes available, I will use my C{BuildFactory} to turn the request into | |
315 a new C{Build} object. The C{BuildRequest} is forgotten, the C{Build} | |
316 goes into C{.building} while it runs. Once the build finishes, I will | |
317 discard it. | |
318 | |
319 I maintain a list of available SlaveBuilders, one for each connected | |
320 slave that the C{slavenames} parameter says we can use. Some of these | |
321 will be idle, some of them will be busy running builds for me. If there | |
322 are multiple slaves, I can run multiple builds at once. | |
323 | |
324 I also manage forced builds, progress expectation (ETA) management, and | |
325 some status delivery chores. | |
326 | |
327 @type buildable: list of L{buildbot.process.base.BuildRequest} | |
328 @ivar buildable: BuildRequests that are ready to build, but which are | |
329 waiting for a buildslave to be available. | |
330 | |
331 @type building: list of L{buildbot.process.base.Build} | |
332 @ivar building: Builds that are actively running | |
333 | |
334 @type slaves: list of L{buildbot.buildslave.BuildSlave} objects | |
335 @ivar slaves: the slaves currently available for building | |
336 """ | |
337 | |
338 expectations = None # this is created the first time we get a good build | |
339 CHOOSE_SLAVES_RANDOMLY = True # disabled for determinism during tests | |
340 | |
341 def __init__(self, setup, builder_status): | |
342 """ | |
343 @type setup: dict | |
344 @param setup: builder setup data, as stored in | |
345 BuildmasterConfig['builders']. Contains name, | |
346 slavename(s), builddir, slavebuilddir, factory, locks. | |
347 @type builder_status: L{buildbot.status.builder.BuilderStatus} | |
348 """ | |
349 self.name = setup['name'] | |
350 self.slavenames = [] | |
351 if setup.has_key('slavename'): | |
352 self.slavenames.append(setup['slavename']) | |
353 if setup.has_key('slavenames'): | |
354 self.slavenames.extend(setup['slavenames']) | |
355 self.builddir = setup['builddir'] | |
356 self.slavebuilddir = setup['slavebuilddir'] | |
357 self.buildFactory = setup['factory'] | |
358 self.nextSlave = setup.get('nextSlave') | |
359 if self.nextSlave is not None and not callable(self.nextSlave): | |
360 raise ValueError("nextSlave must be callable") | |
361 self.locks = setup.get("locks", []) | |
362 self.env = setup.get('env', {}) | |
363 assert isinstance(self.env, dict) | |
364 if setup.has_key('periodicBuildTime'): | |
365 raise ValueError("periodicBuildTime can no longer be defined as" | |
366 " part of the Builder: use scheduler.Periodic" | |
367 " instead") | |
368 self.nextBuild = setup.get('nextBuild') | |
369 if self.nextBuild is not None and not callable(self.nextBuild): | |
370 raise ValueError("nextBuild must be callable") | |
371 | |
372 # build/wannabuild slots: Build objects move along this sequence | |
373 self.buildable = [] | |
374 self.building = [] | |
375 # old_building holds active builds that were stolen from a predecessor | |
376 self.old_building = weakref.WeakKeyDictionary() | |
377 | |
378 # buildslaves which have connected but which are not yet available. | |
379 # These are always in the ATTACHING state. | |
380 self.attaching_slaves = [] | |
381 | |
382 # buildslaves at our disposal. Each SlaveBuilder instance has a | |
383 # .state that is IDLE, PINGING, or BUILDING. "PINGING" is used when a | |
384 # Build is about to start, to make sure that they're still alive. | |
385 self.slaves = [] | |
386 | |
387 self.builder_status = builder_status | |
388 self.builder_status.setSlavenames(self.slavenames) | |
389 | |
390 # for testing, to help synchronize tests | |
391 self.watchers = {'attach': [], 'detach': [], 'detach_all': [], | |
392 'idle': []} | |
393 | |
394 def setBotmaster(self, botmaster): | |
395 self.botmaster = botmaster | |
396 | |
397 def compareToSetup(self, setup): | |
398 diffs = [] | |
399 setup_slavenames = [] | |
400 if setup.has_key('slavename'): | |
401 setup_slavenames.append(setup['slavename']) | |
402 setup_slavenames.extend(setup.get('slavenames', [])) | |
403 if setup_slavenames != self.slavenames: | |
404 diffs.append('slavenames changed from %s to %s' \ | |
405 % (self.slavenames, setup_slavenames)) | |
406 if setup['builddir'] != self.builddir: | |
407 diffs.append('builddir changed from %s to %s' \ | |
408 % (self.builddir, setup['builddir'])) | |
409 if setup['slavebuilddir'] != self.slavebuilddir: | |
410 diffs.append('slavebuilddir changed from %s to %s' \ | |
411 % (self.slavebuilddir, setup['slavebuilddir'])) | |
412 if setup['factory'] != self.buildFactory: # compare objects | |
413 diffs.append('factory changed') | |
414 if setup.get('locks', []) != self.locks: | |
415 diffs.append('locks changed from %s to %s' % (self.locks, setup.get(
'locks'))) | |
416 if setup.get('nextSlave') != self.nextSlave: | |
417 diffs.append('nextSlave changed from %s to %s' % (self.nextSlave, se
tup['nextSlave'])) | |
418 if setup.get('nextBuild') != self.nextBuild: | |
419 diffs.append('nextBuild changed from %s to %s' % (self.nextBuild, se
tup['nextBuild'])) | |
420 return diffs | |
421 | |
422 def __repr__(self): | |
423 return "<Builder '%s' at %d>" % (self.name, id(self)) | |
424 | |
425 def getOldestRequestTime(self): | |
426 """Returns the timestamp of the oldest build request for this builder. | |
427 | |
428 If there are no build requests, None is returned.""" | |
429 if self.buildable: | |
430 return self.buildable[0].getSubmitTime() | |
431 else: | |
432 return None | |
433 | |
434 def submitBuildRequest(self, req): | |
435 req.setSubmitTime(now()) | |
436 self.buildable.append(req) | |
437 req.requestSubmitted(self) | |
438 self.builder_status.addBuildRequest(req.status) | |
439 self.botmaster.maybeStartAllBuilds() | |
440 | |
441 def cancelBuildRequest(self, req): | |
442 if req in self.buildable: | |
443 self.buildable.remove(req) | |
444 self.builder_status.removeBuildRequest(req.status, cancelled=True) | |
445 return True | |
446 return False | |
447 | |
448 def consumeTheSoulOfYourPredecessor(self, old): | |
449 """Suck the brain out of an old Builder. | |
450 | |
451 This takes all the runtime state from an existing Builder and moves | |
452 it into ourselves. This is used when a Builder is changed in the | |
453 master.cfg file: the new Builder has a different factory, but we want | |
454 all the builds that were queued for the old one to get processed by | |
455 the new one. Any builds which are already running will keep running. | |
456 The new Builder will get as many of the old SlaveBuilder objects as | |
457 it wants.""" | |
458 | |
459 log.msg("consumeTheSoulOfYourPredecessor: %s feeding upon %s" % | |
460 (self, old)) | |
461 # we claim all the pending builds, removing them from the old | |
462 # Builder's queue. This insures that the old Builder will not start | |
463 # any new work. | |
464 log.msg(" stealing %s buildrequests" % len(old.buildable)) | |
465 self.buildable.extend(old.buildable) | |
466 old.buildable = [] | |
467 | |
468 # old.building (i.e. builds which are still running) is not migrated | |
469 # directly: it keeps track of builds which were in progress in the | |
470 # old Builder. When those builds finish, the old Builder will be | |
471 # notified, not us. However, since the old SlaveBuilder will point to | |
472 # us, it is our maybeStartBuild() that will be triggered. | |
473 if old.building: | |
474 self.builder_status.setBigState("building") | |
475 # however, we do grab a weakref to the active builds, so that our | |
476 # BuilderControl can see them and stop them. We use a weakref because | |
477 # we aren't the one to get notified, so there isn't a convenient | |
478 # place to remove it from self.building . | |
479 for b in old.building: | |
480 self.old_building[b] = None | |
481 for b in old.old_building: | |
482 self.old_building[b] = None | |
483 | |
484 # Our set of slavenames may be different. Steal any of the old | |
485 # buildslaves that we want to keep using. | |
486 for sb in old.slaves[:]: | |
487 if sb.slave.slavename in self.slavenames: | |
488 log.msg(" stealing buildslave %s" % sb) | |
489 self.slaves.append(sb) | |
490 old.slaves.remove(sb) | |
491 sb.setBuilder(self) | |
492 | |
493 # old.attaching_slaves: | |
494 # these SlaveBuilders are waiting on a sequence of calls: | |
495 # remote.setMaster and remote.print . When these two complete, | |
496 # old._attached will be fired, which will add a 'connect' event to | |
497 # the builder_status and try to start a build. However, we've pulled | |
498 # everything out of the old builder's queue, so it will have no work | |
499 # to do. The outstanding remote.setMaster/print call will be holding | |
500 # the last reference to the old builder, so it will disappear just | |
501 # after that response comes back. | |
502 # | |
503 # The BotMaster will ask the slave to re-set their list of Builders | |
504 # shortly after this function returns, which will cause our | |
505 # attached() method to be fired with a bunch of references to remote | |
506 # SlaveBuilders, some of which we already have (by stealing them | |
507 # from the old Builder), some of which will be new. The new ones | |
508 # will be re-attached. | |
509 | |
510 # Therefore, we don't need to do anything about old.attaching_slaves | |
511 | |
512 return # all done | |
513 | |
514 def getBuild(self, number): | |
515 for b in self.building: | |
516 if b.build_status and b.build_status.number == number: | |
517 return b | |
518 for b in self.old_building.keys(): | |
519 if b.build_status and b.build_status.number == number: | |
520 return b | |
521 return None | |
522 | |
523 def fireTestEvent(self, name, fire_with=None): | |
524 if fire_with is None: | |
525 fire_with = self | |
526 watchers = self.watchers[name] | |
527 self.watchers[name] = [] | |
528 for w in watchers: | |
529 reactor.callLater(0, w.callback, fire_with) | |
530 | |
531 def addLatentSlave(self, slave): | |
532 assert interfaces.ILatentBuildSlave.providedBy(slave) | |
533 for s in self.slaves: | |
534 if s == slave: | |
535 break | |
536 else: | |
537 sb = LatentSlaveBuilder(slave, self) | |
538 self.builder_status.addPointEvent( | |
539 ['added', 'latent', slave.slavename]) | |
540 self.slaves.append(sb) | |
541 reactor.callLater(0, self.botmaster.maybeStartAllBuilds) | |
542 | |
543 def attached(self, slave, remote, commands): | |
544 """This is invoked by the BuildSlave when the self.slavename bot | |
545 registers their builder. | |
546 | |
547 @type slave: L{buildbot.buildslave.BuildSlave} | |
548 @param slave: the BuildSlave that represents the buildslave as a whole | |
549 @type remote: L{twisted.spread.pb.RemoteReference} | |
550 @param remote: a reference to the L{buildbot.slave.bot.SlaveBuilder} | |
551 @type commands: dict: string -> string, or None | |
552 @param commands: provides the slave's version of each RemoteCommand | |
553 | |
554 @rtype: L{twisted.internet.defer.Deferred} | |
555 @return: a Deferred that fires (with 'self') when the slave-side | |
556 builder is fully attached and ready to accept commands. | |
557 """ | |
558 for s in self.attaching_slaves + self.slaves: | |
559 if s.slave == slave: | |
560 # already attached to them. This is fairly common, since | |
561 # attached() gets called each time we receive the builder | |
562 # list from the slave, and we ask for it each time we add or | |
563 # remove a builder. So if the slave is hosting builders | |
564 # A,B,C, and the config file changes A, we'll remove A and | |
565 # re-add it, triggering two builder-list requests, getting | |
566 # two redundant calls to attached() for B, and another two | |
567 # for C. | |
568 # | |
569 # Therefore, when we see that we're already attached, we can | |
570 # just ignore it. TODO: build a diagram of the state | |
571 # transitions here, I'm concerned about sb.attached() failing | |
572 # and leaving sb.state stuck at 'ATTACHING', and about | |
573 # the detached() message arriving while there's some | |
574 # transition pending such that the response to the transition | |
575 # re-vivifies sb | |
576 return defer.succeed(self) | |
577 | |
578 sb = SlaveBuilder() | |
579 sb.setBuilder(self) | |
580 self.attaching_slaves.append(sb) | |
581 d = sb.attached(slave, remote, commands) | |
582 d.addCallback(self._attached) | |
583 d.addErrback(self._not_attached, slave) | |
584 return d | |
585 | |
586 def _attached(self, sb): | |
587 # TODO: make this .addSlaveEvent(slave.slavename, ['connect']) ? | |
588 self.builder_status.addPointEvent(['connect', sb.slave.slavename]) | |
589 self.attaching_slaves.remove(sb) | |
590 self.slaves.append(sb) | |
591 | |
592 self.fireTestEvent('attach') | |
593 return self | |
594 | |
595 def _not_attached(self, why, slave): | |
596 # already log.err'ed by SlaveBuilder._attachFailure | |
597 # TODO: make this .addSlaveEvent? | |
598 # TODO: remove from self.slaves (except that detached() should get | |
599 # run first, right?) | |
600 self.builder_status.addPointEvent(['failed', 'connect', | |
601 slave.slave.slavename]) | |
602 # TODO: add an HTMLLogFile of the exception | |
603 self.fireTestEvent('attach', why) | |
604 | |
605 def detached(self, slave): | |
606 """This is called when the connection to the bot is lost.""" | |
607 for sb in self.attaching_slaves + self.slaves: | |
608 if sb.slave == slave: | |
609 break | |
610 else: | |
611 log.msg("WEIRD: Builder.detached(%s) (%s)" | |
612 " not in attaching_slaves(%s)" | |
613 " or slaves(%s)" % (slave, slave.slavename, | |
614 self.attaching_slaves, | |
615 self.slaves)) | |
616 return | |
617 if sb.state == BUILDING: | |
618 # the Build's .lostRemote method (invoked by a notifyOnDisconnect | |
619 # handler) will cause the Build to be stopped, probably right | |
620 # after the notifyOnDisconnect that invoked us finishes running. | |
621 | |
622 # TODO: should failover to a new Build | |
623 #self.retryBuild(sb.build) | |
624 pass | |
625 | |
626 if sb in self.attaching_slaves: | |
627 self.attaching_slaves.remove(sb) | |
628 if sb in self.slaves: | |
629 self.slaves.remove(sb) | |
630 | |
631 # TODO: make this .addSlaveEvent? | |
632 self.builder_status.addPointEvent(['disconnect', slave.slavename]) | |
633 sb.detached() # inform the SlaveBuilder that their slave went away | |
634 self.updateBigStatus() | |
635 self.fireTestEvent('detach') | |
636 if not self.slaves: | |
637 self.fireTestEvent('detach_all') | |
638 | |
639 def updateBigStatus(self): | |
640 if not self.slaves: | |
641 self.builder_status.setBigState("offline") | |
642 elif self.building: | |
643 self.builder_status.setBigState("building") | |
644 else: | |
645 self.builder_status.setBigState("idle") | |
646 self.fireTestEvent('idle') | |
647 | |
648 def maybeStartBuild(self): | |
649 log.msg("maybeStartBuild %s: %i request(s), %i slave(s)" % | |
650 (self, len(self.buildable), len(self.slaves))) | |
651 if not self.buildable: | |
652 self.updateBigStatus() | |
653 return # nothing to do | |
654 | |
655 # pick an idle slave | |
656 available_slaves = [sb for sb in self.slaves if sb.isAvailable()] | |
657 if not available_slaves: | |
658 self.updateBigStatus() | |
659 return | |
660 if self.nextSlave: | |
661 sb = None | |
662 try: | |
663 sb = self.nextSlave(self, available_slaves) | |
664 except: | |
665 log.msg("Exception choosing next slave") | |
666 log.err(Failure()) | |
667 | |
668 if not sb: | |
669 self.updateBigStatus() | |
670 return | |
671 elif self.CHOOSE_SLAVES_RANDOMLY: | |
672 sb = random.choice(available_slaves) | |
673 else: | |
674 sb = available_slaves[0] | |
675 | |
676 # there is something to build, and there is a slave on which to build | |
677 # it. Grab the oldest request, see if we can merge it with anything | |
678 # else. | |
679 if not self.nextBuild: | |
680 req = self.buildable.pop(0) | |
681 else: | |
682 try: | |
683 req = self.nextBuild(self, self.buildable) | |
684 if not req: | |
685 # Nothing to do | |
686 self.updateBigStatus() | |
687 return | |
688 self.buildable.remove(req) | |
689 except: | |
690 log.msg("Exception choosing next build") | |
691 log.err(Failure()) | |
692 self.updateBigStatus() | |
693 return | |
694 self.builder_status.removeBuildRequest(req.status) | |
695 mergers = [] | |
696 botmaster = self.botmaster | |
697 for br in self.buildable[:]: | |
698 if botmaster.shouldMergeRequests(self, req, br): | |
699 self.buildable.remove(br) | |
700 self.builder_status.removeBuildRequest(br.status) | |
701 mergers.append(br) | |
702 requests = [req] + mergers | |
703 enforced_sb = [request.properties.getProperty('slavename') | |
704 for request in requests | |
705 if request.properties.getProperty('slavename')] | |
706 enforced_sb = list(set(enforced_sb)) | |
707 if len(enforced_sb) == 1: | |
708 if enforced_sb[0] not in self.slaves: | |
709 # It's better to not use slaves at all than use a random one. | |
710 log.msg("%s: %s is not a valid slave" % (self, enforced_sb[0])) | |
711 return | |
712 sb = enforced_sb[0] | |
713 | |
714 # Create a new build from our build factory and set ourself as the | |
715 # builder. | |
716 build = self.buildFactory.newBuild(requests) | |
717 build.setBuilder(self) | |
718 build.setLocks(self.locks) | |
719 if len(self.env) > 0: | |
720 build.setSlaveEnvironment(self.env) | |
721 | |
722 # start it | |
723 self.startBuild(build, sb) | |
724 | |
725 def startBuild(self, build, sb): | |
726 """Start a build on the given slave. | |
727 @param build: the L{base.Build} to start | |
728 @param sb: the L{SlaveBuilder} which will host this build | |
729 | |
730 @return: a Deferred which fires with a | |
731 L{buildbot.interfaces.IBuildControl} that can be used to stop the | |
732 Build, or to access a L{buildbot.interfaces.IBuildStatus} which will | |
733 watch the Build as it runs. """ | |
734 | |
735 self.building.append(build) | |
736 self.updateBigStatus() | |
737 log.msg("starting build %s using slave %s" % (build, sb)) | |
738 d = sb.prepare(self.builder_status) | |
739 def _ping(ign): | |
740 # ping the slave to make sure they're still there. If they've | |
741 # fallen off the map (due to a NAT timeout or something), this | |
742 # will fail in a couple of minutes, depending upon the TCP | |
743 # timeout. | |
744 # | |
745 # TODO: This can unnecessarily suspend the starting of a build, in | |
746 # situations where the slave is live but is pushing lots of data to | |
747 # us in a build. | |
748 log.msg("starting build %s.. pinging the slave %s" % (build, sb)) | |
749 return sb.ping() | |
750 d.addCallback(_ping) | |
751 d.addCallback(self._startBuild_1, build, sb) | |
752 return d | |
753 | |
754 def _startBuild_1(self, res, build, sb): | |
755 if not res: | |
756 return self._startBuildFailed("slave ping failed", build, sb) | |
757 # The buildslave is ready to go. sb.buildStarted() sets its state to | |
758 # BUILDING (so we won't try to use it for any other builds). This | |
759 # gets set back to IDLE by the Build itself when it finishes. | |
760 sb.buildStarted() | |
761 d = sb.remote.callRemote("startBuild") | |
762 d.addCallbacks(self._startBuild_2, self._startBuildFailed, | |
763 callbackArgs=(build,sb), errbackArgs=(build,sb)) | |
764 return d | |
765 | |
766 def _startBuild_2(self, res, build, sb): | |
767 # create the BuildStatus object that goes with the Build | |
768 bs = self.builder_status.newBuild() | |
769 | |
770 # start the build. This will first set up the steps, then tell the | |
771 # BuildStatus that it has started, which will announce it to the | |
772 # world (through our BuilderStatus object, which is its parent). | |
773 # Finally it will start the actual build process. | |
774 d = build.startBuild(bs, self.expectations, sb) | |
775 d.addCallback(self.buildFinished, sb) | |
776 d.addErrback(log.err) # this shouldn't happen. if it does, the slave | |
777 # will be wedged | |
778 for req in build.requests: | |
779 req.buildStarted(build, bs) | |
780 return build # this is the IBuildControl | |
781 | |
782 def _startBuildFailed(self, why, build, sb): | |
783 # put the build back on the buildable list | |
784 log.msg("I tried to tell the slave that the build %s started, but " | |
785 "remote_startBuild failed: %s" % (build, why)) | |
786 # release the slave. This will queue a call to maybeStartBuild, which | |
787 # will fire after other notifyOnDisconnect handlers have marked the | |
788 # slave as disconnected (so we don't try to use it again). | |
789 sb.buildFinished() | |
790 | |
791 log.msg("re-queueing the BuildRequest") | |
792 self.building.remove(build) | |
793 for req in build.requests: | |
794 self.buildable.insert(0, req) # the interrupted build gets first | |
795 # priority | |
796 self.builder_status.addBuildRequest(req.status) | |
797 | |
798 | |
799 def buildFinished(self, build, sb): | |
800 """This is called when the Build has finished (either success or | |
801 failure). Any exceptions during the build are reported with | |
802 results=FAILURE, not with an errback.""" | |
803 | |
804 # by the time we get here, the Build has already released the slave | |
805 # (which queues a call to maybeStartBuild) | |
806 | |
807 self.building.remove(build) | |
808 for req in build.requests: | |
809 req.finished(build.build_status) | |
810 | |
811 def setExpectations(self, progress): | |
812 """Mark the build as successful and update expectations for the next | |
813 build. Only call this when the build did not fail in any way that | |
814 would invalidate the time expectations generated by it. (if the | |
815 compile failed and thus terminated early, we can't use the last | |
816 build to predict how long the next one will take). | |
817 """ | |
818 if self.expectations: | |
819 self.expectations.update(progress) | |
820 else: | |
821 # the first time we get a good build, create our Expectations | |
822 # based upon its results | |
823 self.expectations = Expectations(progress) | |
824 log.msg("new expectations: %s seconds" % \ | |
825 self.expectations.expectedBuildTime()) | |
826 | |
827 def shutdownSlave(self): | |
828 if self.remote: | |
829 self.remote.callRemote("shutdown") | |
830 | |
831 | |
832 class BuilderControl(components.Adapter): | |
833 implements(interfaces.IBuilderControl) | |
834 | |
835 def requestBuild(self, req): | |
836 """Submit a BuildRequest to this Builder.""" | |
837 self.original.submitBuildRequest(req) | |
838 | |
839 def requestBuildSoon(self, req): | |
840 """Submit a BuildRequest like requestBuild, but raise a | |
841 L{buildbot.interfaces.NoSlaveError} if no slaves are currently | |
842 available, so it cannot be used to queue a BuildRequest in the hopes | |
843 that a slave will eventually connect. This method is appropriate for | |
844 use by things like the web-page 'Force Build' button.""" | |
845 if not self.original.slaves: | |
846 raise interfaces.NoSlaveError | |
847 self.requestBuild(req) | |
848 | |
849 def resubmitBuild(self, bs, reason="<rebuild, no reason given>", extraProper
ties=None): | |
850 if not bs.isFinished(): | |
851 return | |
852 | |
853 ss = bs.getSourceStamp(absolute=True) | |
854 if extraProperties is None: | |
855 properties = bs.getProperties() | |
856 else: | |
857 # Make a copy so as not to modify the original build. | |
858 properties = Properties() | |
859 properties.updateFromProperties(bs.getProperties()) | |
860 properties.updateFromProperties(extraProperties) | |
861 req = base.BuildRequest(reason, ss, self.original.name, | |
862 properties=properties) | |
863 self.requestBuild(req) | |
864 | |
865 def getPendingBuilds(self): | |
866 # return IBuildRequestControl objects | |
867 retval = [] | |
868 for r in self.original.buildable: | |
869 retval.append(BuildRequestControl(self.original, r)) | |
870 | |
871 return retval | |
872 | |
873 def getBuild(self, number): | |
874 return self.original.getBuild(number) | |
875 | |
876 def ping(self): | |
877 if not self.original.slaves: | |
878 self.original.builder_status.addPointEvent(["ping", "no slave"]) | |
879 return defer.succeed(False) # interfaces.NoSlaveError | |
880 dl = [] | |
881 for s in self.original.slaves: | |
882 dl.append(s.ping(self.original.builder_status)) | |
883 d = defer.DeferredList(dl) | |
884 d.addCallback(self._gatherPingResults) | |
885 return d | |
886 | |
887 def _gatherPingResults(self, res): | |
888 for ignored,success in res: | |
889 if not success: | |
890 return False | |
891 return True | |
892 | |
893 components.registerAdapter(BuilderControl, Builder, interfaces.IBuilderControl) | |
894 | |
895 class BuildRequestControl: | |
896 implements(interfaces.IBuildRequestControl) | |
897 | |
898 def __init__(self, builder, request): | |
899 self.original_builder = builder | |
900 self.original_request = request | |
901 | |
902 def subscribe(self, observer): | |
903 raise NotImplementedError | |
904 | |
905 def unsubscribe(self, observer): | |
906 raise NotImplementedError | |
907 | |
908 def cancel(self): | |
909 self.original_builder.cancelBuildRequest(self.original_request) | |
OLD | NEW |