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