Package buildbot :: Module buildslave
[frames] | no frames]

Source Code for Module buildbot.buildslave

  1  # Portions copyright Canonical Ltd. 2009 
  2   
  3  import time 
  4  from email.Message import Message 
  5  from email.Utils import formatdate 
  6  from zope.interface import implements 
  7  from twisted.python import log 
  8  from twisted.internet import defer, reactor 
  9  from twisted.application import service 
 10  import twisted.spread.pb 
 11   
 12  from buildbot.pbutil import NewCredPerspective 
 13  from buildbot.status.builder import SlaveStatus 
 14  from buildbot.status.mail import MailNotifier 
 15  from buildbot.interfaces import IBuildSlave, ILatentBuildSlave 
 16  from buildbot.process.properties import Properties 
 17   
 18  import sys 
 19  if sys.version_info[:3] < (2,4,0): 
 20      from sets import Set as set 
 21   
22 -class AbstractBuildSlave(NewCredPerspective, service.MultiService):
23 """This is the master-side representative for a remote buildbot slave. 24 There is exactly one for each slave described in the config file (the 25 c['slaves'] list). When buildbots connect in (.attach), they get a 26 reference to this instance. The BotMaster object is stashed as the 27 .botmaster attribute. The BotMaster is also our '.parent' Service. 28 29 I represent a build slave -- a remote machine capable of 30 running builds. I am instantiated by the configuration file, and can be 31 subclassed to add extra functionality.""" 32 33 implements(IBuildSlave) 34
35 - def __init__(self, name, password, max_builds=None, 36 notify_on_missing=[], missing_timeout=3600, 37 properties={}):
38 """ 39 @param name: botname this machine will supply when it connects 40 @param password: password this machine will supply when 41 it connects 42 @param max_builds: maximum number of simultaneous builds that will 43 be run concurrently on this buildslave (the 44 default is None for no limit) 45 @param properties: properties that will be applied to builds run on 46 this slave 47 @type properties: dictionary 48 """ 49 service.MultiService.__init__(self) 50 self.slavename = name 51 self.password = password 52 self.botmaster = None # no buildmaster yet 53 self.slave_status = SlaveStatus(name) 54 self.slave = None # a RemoteReference to the Bot, when connected 55 self.slave_commands = None 56 self.slavebuilders = {} 57 self.max_builds = max_builds 58 59 self.properties = Properties() 60 self.properties.update(properties, "BuildSlave") 61 self.properties.setProperty("slavename", name, "BuildSlave") 62 63 self.lastMessageReceived = 0 64 if isinstance(notify_on_missing, str): 65 notify_on_missing = [notify_on_missing] 66 self.notify_on_missing = notify_on_missing 67 for i in notify_on_missing: 68 assert isinstance(i, str) 69 self.missing_timeout = missing_timeout 70 self.missing_timer = None
71
72 - def update(self, new):
73 """ 74 Given a new BuildSlave, configure this one identically. Because 75 BuildSlave objects are remotely referenced, we can't replace them 76 without disconnecting the slave, yet there's no reason to do that. 77 """ 78 # the reconfiguration logic should guarantee this: 79 assert self.slavename == new.slavename 80 assert self.password == new.password 81 assert self.__class__ == new.__class__ 82 self.max_builds = new.max_builds
83
84 - def __repr__(self):
85 if self.botmaster: 86 builders = self.botmaster.getBuildersForSlave(self.slavename) 87 return "<%s '%s', current builders: %s>" % \ 88 (self.__class__.__name__, self.slavename, 89 ','.join(map(lambda b: b.name, builders))) 90 else: 91 return "<%s '%s', (no builders yet)>" % \ 92 (self.__class__.__name__, self.slavename)
93
94 - def setBotmaster(self, botmaster):
95 assert not self.botmaster, "BuildSlave already has a botmaster" 96 self.botmaster = botmaster 97 self.startMissingTimer()
98
99 - def stopMissingTimer(self):
100 if self.missing_timer: 101 self.missing_timer.cancel() 102 self.missing_timer = None
103
104 - def startMissingTimer(self):
105 if self.notify_on_missing and self.missing_timeout and self.parent: 106 self.stopMissingTimer() # in case it's already running 107 self.missing_timer = reactor.callLater(self.missing_timeout, 108 self._missing_timer_fired)
109
110 - def _missing_timer_fired(self):
111 self.missing_timer = None 112 # notify people, but only if we're still in the config 113 if not self.parent: 114 return 115 116 buildmaster = self.botmaster.parent 117 status = buildmaster.getStatus() 118 text = "The Buildbot working for '%s'\n" % status.getProjectName() 119 text += ("has noticed that the buildslave named %s went away\n" % 120 self.slavename) 121 text += "\n" 122 text += ("It last disconnected at %s (buildmaster-local time)\n" % 123 time.ctime(time.time() - self.missing_timeout)) # approx 124 text += "\n" 125 text += "The admin on record (as reported by BUILDSLAVE:info/admin)\n" 126 text += "was '%s'.\n" % self.slave_status.getAdmin() 127 text += "\n" 128 text += "Sincerely,\n" 129 text += " The Buildbot\n" 130 text += " %s\n" % status.getProjectURL() 131 subject = "Buildbot: buildslave %s was lost" % self.slavename 132 return self._mail_missing_message(subject, text)
133 134
135 - def updateSlave(self):
136 """Called to add or remove builders after the slave has connected. 137 138 @return: a Deferred that indicates when an attached slave has 139 accepted the new builders and/or released the old ones.""" 140 if self.slave: 141 return self.sendBuilderList() 142 else: 143 return defer.succeed(None)
144
145 - def updateSlaveStatus(self, buildStarted=None, buildFinished=None):
146 if buildStarted: 147 self.slave_status.buildStarted(buildStarted) 148 if buildFinished: 149 self.slave_status.buildFinished(buildFinished)
150
151 - def attached(self, bot):
152 """This is called when the slave connects. 153 154 @return: a Deferred that fires with a suitable pb.IPerspective to 155 give to the slave (i.e. 'self')""" 156 157 if self.slave: 158 # uh-oh, we've got a duplicate slave. The most likely 159 # explanation is that the slave is behind a slow link, thinks we 160 # went away, and has attempted to reconnect, so we've got two 161 # "connections" from the same slave, but the previous one is 162 # stale. Give the new one precedence. 163 log.msg("duplicate slave %s replacing old one" % self.slavename) 164 165 # just in case we've got two identically-configured slaves, 166 # report the IP addresses of both so someone can resolve the 167 # squabble 168 tport = self.slave.broker.transport 169 log.msg("old slave was connected from", tport.getPeer()) 170 log.msg("new slave is from", bot.broker.transport.getPeer()) 171 d = self.disconnect() 172 else: 173 d = defer.succeed(None) 174 # now we go through a sequence of calls, gathering information, then 175 # tell the Botmaster that it can finally give this slave to all the 176 # Builders that care about it. 177 178 # we accumulate slave information in this 'state' dictionary, then 179 # set it atomically if we make it far enough through the process 180 state = {} 181 182 # Reset graceful shutdown status 183 self.slave_status.setGraceful(False) 184 # We want to know when the graceful shutdown flag changes 185 self.slave_status.addGracefulWatcher(self._gracefulChanged) 186 187 def _log_attachment_on_slave(res): 188 d1 = bot.callRemote("print", "attached") 189 d1.addErrback(lambda why: None) 190 return d1
191 d.addCallback(_log_attachment_on_slave) 192 193 def _get_info(res): 194 d1 = bot.callRemote("getSlaveInfo") 195 def _got_info(info): 196 log.msg("Got slaveinfo from '%s'" % self.slavename) 197 # TODO: info{} might have other keys 198 state["admin"] = info.get("admin") 199 state["host"] = info.get("host") 200 state["access_uri"] = info.get("access_uri", None)
201 def _info_unavailable(why): 202 # maybe an old slave, doesn't implement remote_getSlaveInfo 203 log.msg("BuildSlave.info_unavailable") 204 log.err(why) 205 d1.addCallbacks(_got_info, _info_unavailable) 206 return d1 207 d.addCallback(_get_info) 208 209 def _get_version(res): 210 d1 = bot.callRemote("getVersion") 211 def _got_version(version): 212 state["version"] = version 213 def _version_unavailable(why): 214 # probably an old slave 215 log.msg("BuildSlave.version_unavailable") 216 log.err(why) 217 d1.addCallbacks(_got_version, _version_unavailable) 218 d.addCallback(_get_version) 219 220 def _get_commands(res): 221 d1 = bot.callRemote("getCommands") 222 def _got_commands(commands): 223 state["slave_commands"] = commands 224 def _commands_unavailable(why): 225 # probably an old slave 226 log.msg("BuildSlave._commands_unavailable") 227 if why.check(AttributeError): 228 return 229 log.err(why) 230 d1.addCallbacks(_got_commands, _commands_unavailable) 231 return d1 232 d.addCallback(_get_commands) 233 234 def _accept_slave(res): 235 self.slave_status.setAdmin(state.get("admin")) 236 self.slave_status.setHost(state.get("host")) 237 self.slave_status.setAccessURI(state.get("access_uri")) 238 self.slave_status.setVersion(state.get("version")) 239 self.slave_status.setConnected(True) 240 self.slave_commands = state.get("slave_commands") 241 self.slave = bot 242 log.msg("bot attached") 243 self.messageReceivedFromSlave() 244 self.stopMissingTimer() 245 246 return self.updateSlave() 247 d.addCallback(_accept_slave) 248 d.addCallback(lambda res: self.botmaster.maybeStartAllBuilds()) 249 250 # Finally, the slave gets a reference to this BuildSlave. They 251 # receive this later, after we've started using them. 252 d.addCallback(lambda res: self) 253 return d 254
255 - def messageReceivedFromSlave(self):
256 now = time.time() 257 self.lastMessageReceived = now 258 self.slave_status.setLastMessageReceived(now)
259
260 - def detached(self, mind):
261 self.slave = None 262 self.slave_status.removeGracefulWatcher(self._gracefulChanged) 263 self.slave_status.setConnected(False) 264 log.msg("BuildSlave.detached(%s)" % self.slavename)
265
266 - def disconnect(self):
267 """Forcibly disconnect the slave. 268 269 This severs the TCP connection and returns a Deferred that will fire 270 (with None) when the connection is probably gone. 271 272 If the slave is still alive, they will probably try to reconnect 273 again in a moment. 274 275 This is called in two circumstances. The first is when a slave is 276 removed from the config file. In this case, when they try to 277 reconnect, they will be rejected as an unknown slave. The second is 278 when we wind up with two connections for the same slave, in which 279 case we disconnect the older connection. 280 """ 281 282 if not self.slave: 283 return defer.succeed(None) 284 log.msg("disconnecting old slave %s now" % self.slavename) 285 # When this Deferred fires, we'll be ready to accept the new slave 286 return self._disconnect(self.slave)
287
288 - def _disconnect(self, slave):
289 # all kinds of teardown will happen as a result of 290 # loseConnection(), but it happens after a reactor iteration or 291 # two. Hook the actual disconnect so we can know when it is safe 292 # to connect the new slave. We have to wait one additional 293 # iteration (with callLater(0)) to make sure the *other* 294 # notifyOnDisconnect handlers have had a chance to run. 295 d = defer.Deferred() 296 297 # notifyOnDisconnect runs the callback with one argument, the 298 # RemoteReference being disconnected. 299 def _disconnected(rref): 300 reactor.callLater(0, d.callback, None)
301 slave.notifyOnDisconnect(_disconnected) 302 tport = slave.broker.transport 303 # this is the polite way to request that a socket be closed 304 tport.loseConnection() 305 try: 306 # but really we don't want to wait for the transmit queue to 307 # drain. The remote end is unlikely to ACK the data, so we'd 308 # probably have to wait for a (20-minute) TCP timeout. 309 #tport._closeSocket() 310 # however, doing _closeSocket (whether before or after 311 # loseConnection) somehow prevents the notifyOnDisconnect 312 # handlers from being run. Bummer. 313 tport.offset = 0 314 tport.dataBuffer = "" 315 except: 316 # however, these hacks are pretty internal, so don't blow up if 317 # they fail or are unavailable 318 log.msg("failed to accelerate the shutdown process") 319 pass 320 log.msg("waiting for slave to finish disconnecting") 321 322 return d 323
324 - def sendBuilderList(self):
325 our_builders = self.botmaster.getBuildersForSlave(self.slavename) 326 blist = [(b.name, b.slavebuilddir) for b in our_builders] 327 d = self.slave.callRemote("setBuilderList", blist) 328 return d
329
330 - def perspective_keepalive(self):
331 pass
332
333 - def addSlaveBuilder(self, sb):
334 self.slavebuilders[sb.builder_name] = sb
335
336 - def removeSlaveBuilder(self, sb):
337 try: 338 del self.slavebuilders[sb.builder_name] 339 except KeyError: 340 pass
341
342 - def canStartBuild(self):
343 """ 344 I am called when a build is requested to see if this buildslave 345 can start a build. This function can be used to limit overall 346 concurrency on the buildslave. 347 """ 348 # If we're waiting to shutdown gracefully, then we shouldn't 349 # accept any new jobs. 350 if self.slave_status.getGraceful(): 351 return False 352 353 if self.max_builds: 354 active_builders = [sb for sb in self.slavebuilders.values() 355 if sb.isBusy()] 356 if len(active_builders) >= self.max_builds: 357 return False 358 return True
359
360 - def _mail_missing_message(self, subject, text):
361 # first, see if we have a MailNotifier we can use. This gives us a 362 # fromaddr and a relayhost. 363 buildmaster = self.botmaster.parent 364 for st in buildmaster.statusTargets: 365 if isinstance(st, MailNotifier): 366 break 367 else: 368 # if not, they get a default MailNotifier, which always uses SMTP 369 # to localhost and uses a dummy fromaddr of "buildbot". 370 log.msg("buildslave-missing msg using default MailNotifier") 371 st = MailNotifier("buildbot") 372 # now construct the mail 373 374 m = Message() 375 m.set_payload(text) 376 m['Date'] = formatdate(localtime=True) 377 m['Subject'] = subject 378 m['From'] = st.fromaddr 379 recipients = self.notify_on_missing 380 m['To'] = ", ".join(recipients) 381 d = st.sendMessage(m, recipients) 382 # return the Deferred for testing purposes 383 return d
384
385 - def _gracefulChanged(self, graceful):
386 """This is called when our graceful shutdown setting changes""" 387 if graceful: 388 active_builders = [sb for sb in self.slavebuilders.values() 389 if sb.isBusy()] 390 if len(active_builders) == 0: 391 # Shut down! 392 self.shutdown()
393
394 - def shutdown(self):
395 """Shutdown the slave""" 396 # Look for a builder with a remote reference to the client side 397 # slave. If we can find one, then call "shutdown" on the remote 398 # builder, which will cause the slave buildbot process to exit. 399 d = None 400 for b in self.slavebuilders.values(): 401 if b.remote: 402 d = b.remote.callRemote("shutdown") 403 break 404 405 if d: 406 log.msg("Shutting down slave: %s" % self.slavename) 407 # The remote shutdown call will not complete successfully since the 408 # buildbot process exits almost immediately after getting the 409 # shutdown request. 410 # Here we look at the reason why the remote call failed, and if 411 # it's because the connection was lost, that means the slave 412 # shutdown as expected. 413 def _errback(why): 414 if why.check(twisted.spread.pb.PBConnectionLost): 415 log.msg("Lost connection to %s" % self.slavename) 416 else: 417 log.err("Unexpected error when trying to shutdown %s" % self.slavename)
418 d.addErrback(_errback) 419 return d 420 log.err("Couldn't find remote builder to shut down slave") 421 return defer.succeed(None) 422
423 -class BuildSlave(AbstractBuildSlave):
424
425 - def sendBuilderList(self):
426 d = AbstractBuildSlave.sendBuilderList(self) 427 def _sent(slist): 428 dl = [] 429 for name, remote in slist.items(): 430 # use get() since we might have changed our mind since then 431 b = self.botmaster.builders.get(name) 432 if b: 433 d1 = b.attached(self, remote, self.slave_commands) 434 dl.append(d1) 435 return defer.DeferredList(dl)
436 def _set_failed(why): 437 log.msg("BuildSlave.sendBuilderList (%s) failed" % self) 438 log.err(why)
439 # TODO: hang up on them?, without setBuilderList we can't use 440 # them 441 d.addCallbacks(_sent, _set_failed) 442 return d 443
444 - def detached(self, mind):
445 AbstractBuildSlave.detached(self, mind) 446 self.botmaster.slaveLost(self) 447 self.startMissingTimer()
448
449 - def buildFinished(self, sb):
450 """This is called when a build on this slave is finished.""" 451 # If we're gracefully shutting down, and we have no more active 452 # builders, then it's safe to disconnect 453 if self.slave_status.getGraceful(): 454 active_builders = [sb for sb in self.slavebuilders.values() 455 if sb.isBusy()] 456 if len(active_builders) == 0: 457 # Shut down! 458 return self.shutdown() 459 return defer.succeed(None)
460
461 -class AbstractLatentBuildSlave(AbstractBuildSlave):
462 """A build slave that will start up a slave instance when needed. 463 464 To use, subclass and implement start_instance and stop_instance. 465 466 See ec2buildslave.py for a concrete example. Also see the stub example in 467 test/test_slaves.py. 468 """ 469 470 implements(ILatentBuildSlave) 471 472 substantiated = False 473 substantiation_deferred = None 474 build_wait_timer = None 475 _start_result = _shutdown_callback_handle = None 476
477 - def __init__(self, name, password, max_builds=None, 478 notify_on_missing=[], missing_timeout=60*20, 479 build_wait_timeout=60*10, 480 properties={}):
481 AbstractBuildSlave.__init__( 482 self, name, password, max_builds, notify_on_missing, 483 missing_timeout, properties) 484 self.building = set() 485 self.build_wait_timeout = build_wait_timeout
486
487 - def start_instance(self):
488 # responsible for starting instance that will try to connect with 489 # this master. Should return deferred. Problems should use an 490 # errback. 491 raise NotImplementedError
492
493 - def stop_instance(self, fast=False):
494 # responsible for shutting down instance. 495 raise NotImplementedError
496
497 - def substantiate(self, sb):
498 if self.substantiated: 499 self._clearBuildWaitTimer() 500 self._setBuildWaitTimer() 501 return defer.succeed(self) 502 if self.substantiation_deferred is None: 503 if self.parent and not self.missing_timer: 504 # start timer. if timer times out, fail deferred 505 self.missing_timer = reactor.callLater( 506 self.missing_timeout, 507 self._substantiation_failed, defer.TimeoutError()) 508 self.substantiation_deferred = defer.Deferred() 509 if self.slave is None: 510 self._substantiate() # start up instance 511 # else: we're waiting for an old one to detach. the _substantiate 512 # will be done in ``detached`` below. 513 return self.substantiation_deferred
514
515 - def _substantiate(self):
516 # register event trigger 517 d = self.start_instance() 518 self._shutdown_callback_handle = reactor.addSystemEventTrigger( 519 'before', 'shutdown', self._soft_disconnect, fast=True) 520 def stash_reply(result): 521 self._start_result = result
522 def clean_up(failure): 523 if self.missing_timer is not None: 524 self.missing_timer.cancel() 525 self._substantiation_failed(failure) 526 if self._shutdown_callback_handle is not None: 527 handle = self._shutdown_callback_handle 528 del self._shutdown_callback_handle 529 reactor.removeSystemEventTrigger(handle) 530 return failure
531 d.addCallbacks(stash_reply, clean_up) 532 return d 533
534 - def attached(self, bot):
535 if self.substantiation_deferred is None: 536 msg = 'Slave %s received connection while not trying to ' \ 537 'substantiate. Disconnecting.' % (self.slavename,) 538 log.msg(msg) 539 self._disconnect(bot) 540 return defer.fail(RuntimeError(msg)) 541 return AbstractBuildSlave.attached(self, bot)
542
543 - def detached(self, mind):
544 AbstractBuildSlave.detached(self, mind) 545 if self.substantiation_deferred is not None: 546 self._substantiate()
547
548 - def _substantiation_failed(self, failure):
549 d = self.substantiation_deferred 550 self.substantiation_deferred = None 551 self.missing_timer = None 552 d.errback(failure) 553 self.insubstantiate() 554 # notify people, but only if we're still in the config 555 if not self.parent or not self.notify_on_missing: 556 return 557 558 buildmaster = self.botmaster.parent 559 status = buildmaster.getStatus() 560 text = "The Buildbot working for '%s'\n" % status.getProjectName() 561 text += ("has noticed that the latent buildslave named %s \n" % 562 self.slavename) 563 text += "never substantiated after a request\n" 564 text += "\n" 565 text += ("The request was made at %s (buildmaster-local time)\n" % 566 time.ctime(time.time() - self.missing_timeout)) # approx 567 text += "\n" 568 text += "Sincerely,\n" 569 text += " The Buildbot\n" 570 text += " %s\n" % status.getProjectURL() 571 subject = "Buildbot: buildslave %s never substantiated" % self.slavename 572 return self._mail_missing_message(subject, text)
573
574 - def buildStarted(self, sb):
575 assert self.substantiated 576 self._clearBuildWaitTimer() 577 self.building.add(sb.builder_name)
578
579 - def buildFinished(self, sb):
580 self.building.remove(sb.builder_name) 581 if not self.building: 582 self._setBuildWaitTimer()
583
584 - def _clearBuildWaitTimer(self):
585 if self.build_wait_timer is not None: 586 if self.build_wait_timer.active(): 587 self.build_wait_timer.cancel() 588 self.build_wait_timer = None
589
590 - def _setBuildWaitTimer(self):
591 self._clearBuildWaitTimer() 592 self.build_wait_timer = reactor.callLater( 593 self.build_wait_timeout, self._soft_disconnect)
594
595 - def insubstantiate(self, fast=False):
596 self._clearBuildWaitTimer() 597 d = self.stop_instance(fast) 598 if self._shutdown_callback_handle is not None: 599 handle = self._shutdown_callback_handle 600 del self._shutdown_callback_handle 601 reactor.removeSystemEventTrigger(handle) 602 self.substantiated = False 603 self.building.clear() # just to be sure 604 return d
605
606 - def _soft_disconnect(self, fast=False):
607 d = AbstractBuildSlave.disconnect(self) 608 if self.slave is not None: 609 # this could be called when the slave needs to shut down, such as 610 # in BotMaster.removeSlave, *or* when a new slave requests a 611 # connection when we already have a slave. It's not clear what to 612 # do in the second case: this shouldn't happen, and if it 613 # does...if it's a latent slave, shutting down will probably kill 614 # something we want...but we can't know what the status is. So, 615 # here, we just do what should be appropriate for the first case, 616 # and put our heads in the sand for the second, at least for now. 617 # The best solution to the odd situation is removing it as a 618 # possibilty: make the master in charge of connecting to the 619 # slave, rather than vice versa. TODO. 620 d = defer.DeferredList([d, self.insubstantiate(fast)]) 621 else: 622 if self.substantiation_deferred is not None: 623 # unlike the previous block, we don't expect this situation when 624 # ``attached`` calls ``disconnect``, only when we get a simple 625 # request to "go away". 626 self.substantiation_deferred.errback() 627 self.substantiation_deferred = None 628 if self.missing_timer: 629 self.missing_timer.cancel() 630 self.missing_timer = None 631 self.stop_instance() 632 return d
633
634 - def disconnect(self):
635 d = self._soft_disconnect() 636 # this removes the slave from all builders. It won't come back 637 # without a restart (or maybe a sighup) 638 self.botmaster.slaveLost(self)
639
640 - def stopService(self):
641 res = defer.maybeDeferred(AbstractBuildSlave.stopService, self) 642 if self.slave is not None: 643 d = self._soft_disconnect() 644 res = defer.DeferredList([res, d]) 645 return res
646
647 - def updateSlave(self):
648 """Called to add or remove builders after the slave has connected. 649 650 Also called after botmaster's builders are initially set. 651 652 @return: a Deferred that indicates when an attached slave has 653 accepted the new builders and/or released the old ones.""" 654 for b in self.botmaster.getBuildersForSlave(self.slavename): 655 if b.name not in self.slavebuilders: 656 b.addLatentSlave(self) 657 return AbstractBuildSlave.updateSlave(self)
658
659 - def sendBuilderList(self):
660 d = AbstractBuildSlave.sendBuilderList(self) 661 def _sent(slist): 662 dl = [] 663 for name, remote in slist.items(): 664 # use get() since we might have changed our mind since then. 665 # we're checking on the builder in addition to the 666 # slavebuilders out of a bit of paranoia. 667 b = self.botmaster.builders.get(name) 668 sb = self.slavebuilders.get(name) 669 if b and sb: 670 d1 = sb.attached(self, remote, self.slave_commands) 671 dl.append(d1) 672 return defer.DeferredList(dl)
673 def _set_failed(why): 674 log.msg("BuildSlave.sendBuilderList (%s) failed" % self) 675 log.err(why) 676 # TODO: hang up on them?, without setBuilderList we can't use 677 # them 678 if self.substantiation_deferred: 679 self.substantiation_deferred.errback() 680 self.substantiation_deferred = None 681 if self.missing_timer: 682 self.missing_timer.cancel() 683 self.missing_timer = None 684 # TODO: maybe log? send an email? 685 return why 686 d.addCallbacks(_sent, _set_failed) 687 def _substantiated(res): 688 self.substantiated = True 689 if self.substantiation_deferred: 690 d = self.substantiation_deferred 691 del self.substantiation_deferred 692 res = self._start_result 693 del self._start_result 694 d.callback(res) 695 # note that the missing_timer is already handled within 696 # ``attached`` 697 if not self.building: 698 self._setBuildWaitTimer() 699 d.addCallback(_substantiated) 700 return d 701