Package buildbot :: Package process :: Module builder
[frames] | no frames]

Source Code for Module buildbot.process.builder

  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, setup['nextSlave'])) 418 if setup.get('nextBuild') != self.nextBuild: 419 diffs.append('nextBuild changed from %s to %s' % (self.nextBuild, setup['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.number == number: 517 return b 518 for b in self.old_building.keys(): 519 if 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 704 # Create a new build from our build factory and set ourself as the 705 # builder. 706 build = self.buildFactory.newBuild(requests) 707 build.setBuilder(self) 708 build.setLocks(self.locks) 709 if len(self.env) > 0: 710 build.setSlaveEnvironment(self.env) 711 712 # start it 713 self.startBuild(build, sb)
714
715 - def startBuild(self, build, sb):
716 """Start a build on the given slave. 717 @param build: the L{base.Build} to start 718 @param sb: the L{SlaveBuilder} which will host this build 719 720 @return: a Deferred which fires with a 721 L{buildbot.interfaces.IBuildControl} that can be used to stop the 722 Build, or to access a L{buildbot.interfaces.IBuildStatus} which will 723 watch the Build as it runs. """ 724 725 self.building.append(build) 726 self.updateBigStatus() 727 log.msg("starting build %s using slave %s" % (build, sb)) 728 d = sb.prepare(self.builder_status) 729 def _ping(ign): 730 # ping the slave to make sure they're still there. If they've 731 # fallen off the map (due to a NAT timeout or something), this 732 # will fail in a couple of minutes, depending upon the TCP 733 # timeout. 734 # 735 # TODO: This can unnecessarily suspend the starting of a build, in 736 # situations where the slave is live but is pushing lots of data to 737 # us in a build. 738 log.msg("starting build %s.. pinging the slave %s" % (build, sb)) 739 return sb.ping()
740 d.addCallback(_ping) 741 d.addCallback(self._startBuild_1, build, sb) 742 return d
743
744 - def _startBuild_1(self, res, build, sb):
745 if not res: 746 return self._startBuildFailed("slave ping failed", build, sb) 747 # The buildslave is ready to go. sb.buildStarted() sets its state to 748 # BUILDING (so we won't try to use it for any other builds). This 749 # gets set back to IDLE by the Build itself when it finishes. 750 sb.buildStarted() 751 d = sb.remote.callRemote("startBuild") 752 d.addCallbacks(self._startBuild_2, self._startBuildFailed, 753 callbackArgs=(build,sb), errbackArgs=(build,sb)) 754 return d
755
756 - def _startBuild_2(self, res, build, sb):
757 # create the BuildStatus object that goes with the Build 758 bs = self.builder_status.newBuild() 759 760 # start the build. This will first set up the steps, then tell the 761 # BuildStatus that it has started, which will announce it to the 762 # world (through our BuilderStatus object, which is its parent). 763 # Finally it will start the actual build process. 764 d = build.startBuild(bs, self.expectations, sb) 765 d.addCallback(self.buildFinished, sb) 766 d.addErrback(log.err) # this shouldn't happen. if it does, the slave 767 # will be wedged 768 for req in build.requests: 769 req.buildStarted(build, bs) 770 return build # this is the IBuildControl
771
772 - def _startBuildFailed(self, why, build, sb):
773 # put the build back on the buildable list 774 log.msg("I tried to tell the slave that the build %s started, but " 775 "remote_startBuild failed: %s" % (build, why)) 776 # release the slave. This will queue a call to maybeStartBuild, which 777 # will fire after other notifyOnDisconnect handlers have marked the 778 # slave as disconnected (so we don't try to use it again). 779 sb.buildFinished() 780 781 log.msg("re-queueing the BuildRequest") 782 self.building.remove(build) 783 for req in build.requests: 784 self.buildable.insert(0, req) # the interrupted build gets first 785 # priority 786 self.builder_status.addBuildRequest(req.status)
787 788
789 - def buildFinished(self, build, sb):
790 """This is called when the Build has finished (either success or 791 failure). Any exceptions during the build are reported with 792 results=FAILURE, not with an errback.""" 793 794 # by the time we get here, the Build has already released the slave 795 # (which queues a call to maybeStartBuild) 796 797 self.building.remove(build) 798 for req in build.requests: 799 req.finished(build.build_status)
800
801 - def setExpectations(self, progress):
802 """Mark the build as successful and update expectations for the next 803 build. Only call this when the build did not fail in any way that 804 would invalidate the time expectations generated by it. (if the 805 compile failed and thus terminated early, we can't use the last 806 build to predict how long the next one will take). 807 """ 808 if self.expectations: 809 self.expectations.update(progress) 810 else: 811 # the first time we get a good build, create our Expectations 812 # based upon its results 813 self.expectations = Expectations(progress) 814 log.msg("new expectations: %s seconds" % \ 815 self.expectations.expectedBuildTime())
816
817 - def shutdownSlave(self):
818 if self.remote: 819 self.remote.callRemote("shutdown")
820 821
822 -class BuilderControl(components.Adapter):
823 implements(interfaces.IBuilderControl) 824
825 - def requestBuild(self, req):
826 """Submit a BuildRequest to this Builder.""" 827 self.original.submitBuildRequest(req)
828
829 - def requestBuildSoon(self, req):
830 """Submit a BuildRequest like requestBuild, but raise a 831 L{buildbot.interfaces.NoSlaveError} if no slaves are currently 832 available, so it cannot be used to queue a BuildRequest in the hopes 833 that a slave will eventually connect. This method is appropriate for 834 use by things like the web-page 'Force Build' button.""" 835 if not self.original.slaves: 836 raise interfaces.NoSlaveError 837 self.requestBuild(req)
838
839 - def resubmitBuild(self, bs, reason="<rebuild, no reason given>", extraProperties=None):
840 if not bs.isFinished(): 841 return 842 843 ss = bs.getSourceStamp(absolute=True) 844 if extraProperties is None: 845 properties = bs.getProperties() 846 else: 847 # Make a copy so as not to modify the original build. 848 properties = Properties() 849 properties.updateFromProperties(bs.getProperties()) 850 properties.updateFromProperties(extraProperties) 851 req = base.BuildRequest(reason, ss, self.original.name, 852 properties=properties) 853 self.requestBuild(req)
854
855 - def getPendingBuilds(self):
856 # return IBuildRequestControl objects 857 retval = [] 858 for r in self.original.buildable: 859 retval.append(BuildRequestControl(self.original, r)) 860 861 return retval
862
863 - def getBuild(self, number):
864 return self.original.getBuild(number)
865
866 - def ping(self):
867 if not self.original.slaves: 868 self.original.builder_status.addPointEvent(["ping", "no slave"]) 869 return defer.succeed(False) # interfaces.NoSlaveError 870 dl = [] 871 for s in self.original.slaves: 872 dl.append(s.ping(self.original.builder_status)) 873 d = defer.DeferredList(dl) 874 d.addCallback(self._gatherPingResults) 875 return d
876
877 - def _gatherPingResults(self, res):
878 for ignored,success in res: 879 if not success: 880 return False 881 return True
882 883 components.registerAdapter(BuilderControl, Builder, interfaces.IBuilderControl) 884
885 -class BuildRequestControl:
886 implements(interfaces.IBuildRequestControl) 887
888 - def __init__(self, builder, request):
889 self.original_builder = builder 890 self.original_request = request
891
892 - def subscribe(self, observer):
893 raise NotImplementedError
894
895 - def unsubscribe(self, observer):
896 raise NotImplementedError
897
898 - def cancel(self):
899 self.original_builder.cancelBuildRequest(self.original_request)
900