"Fossies" - the Fresh Open Source Software Archive

Member "buildbot-2.3.1/buildbot/worker/base.py" (23 May 2019, 26066 Bytes) of package /linux/misc/buildbot-2.3.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "base.py" see the Fossies "Dox" file reference documentation and the last Fossies "Diffs" side-by-side code changes report: 2.2.0_vs_2.3.0.

    1 # This file is part of Buildbot.  Buildbot is free software: you can
    2 # redistribute it and/or modify it under the terms of the GNU General Public
    3 # License as published by the Free Software Foundation, version 2.
    4 #
    5 # This program is distributed in the hope that it will be useful, but WITHOUT
    6 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
    7 # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
    8 # details.
    9 #
   10 # You should have received a copy of the GNU General Public License along with
   11 # this program; if not, write to the Free Software Foundation, Inc., 51
   12 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
   13 #
   14 # Portions Copyright Buildbot Team Members
   15 # Portions Copyright Canonical Ltd. 2009
   16 
   17 import time
   18 
   19 from twisted.internet import defer
   20 from twisted.python import log
   21 from twisted.python.reflect import namedModule
   22 from zope.interface import implementer
   23 
   24 from buildbot import config
   25 from buildbot.interfaces import IWorker
   26 from buildbot.process import metrics
   27 from buildbot.process.properties import Properties
   28 from buildbot.status.worker import WorkerStatus
   29 from buildbot.util import bytes2unicode
   30 from buildbot.util import service
   31 from buildbot.util.eventual import eventually
   32 
   33 
   34 @implementer(IWorker)
   35 class AbstractWorker(service.BuildbotService):
   36 
   37     """This is the master-side representative for a remote buildbot worker.
   38     There is exactly one for each worker described in the config file (the
   39     c['workers'] list). When buildbots connect in (.attach), they get a
   40     reference to this instance. The BotMaster object is stashed as the
   41     .botmaster attribute. The BotMaster is also our '.parent' Service.
   42 
   43     I represent a worker -- a remote machine capable of
   44     running builds.  I am instantiated by the configuration file, and can be
   45     subclassed to add extra functionality."""
   46 
   47     # reconfig workers after builders
   48     reconfig_priority = 64
   49 
   50     quarantine_timer = None
   51     quarantine_timeout = quarantine_initial_timeout = 10
   52     quarantine_max_timeout = 60 * 60
   53     start_missing_on_startup = True
   54     DEFAULT_MISSING_TIMEOUT = 3600
   55     DEFAULT_KEEPALIVE_INTERVAL = 3600
   56 
   57     # override to True if isCompatibleWithBuild may return False
   58     builds_may_be_incompatible = False
   59 
   60     def checkConfig(self, name, password, max_builds=None,
   61                     notify_on_missing=None,
   62                     missing_timeout=None,
   63                     properties=None, defaultProperties=None,
   64                     locks=None,
   65                     keepalive_interval=DEFAULT_KEEPALIVE_INTERVAL,
   66                     machine_name=None):
   67         """
   68         @param name: botname this machine will supply when it connects
   69         @param password: password this machine will supply when
   70                          it connects
   71         @param max_builds: maximum number of simultaneous builds that will
   72                            be run concurrently on this worker (the
   73                            default is None for no limit)
   74         @param properties: properties that will be applied to builds run on
   75                            this worker
   76         @type properties: dictionary
   77         @param defaultProperties: properties that will be applied to builds
   78                                   run on this worker only if the property
   79                                   has not been set by another source
   80         @type defaultProperties: dictionary
   81         @param locks: A list of locks that must be acquired before this worker
   82                       can be used
   83         @type locks: dictionary
   84         @param machine_name: The name of the machine to associate with the
   85                              worker.
   86         """
   87         self.name = name = bytes2unicode(name)
   88         self.machine_name = machine_name
   89 
   90         self.password = password
   91 
   92         # protocol registration
   93         self.registration = None
   94 
   95         self._graceful = False
   96         self._paused = False
   97 
   98         # these are set when the service is started
   99         self.manager = None
  100         self.workerid = None
  101 
  102         self.worker_status = WorkerStatus(name)
  103         self.worker_commands = None
  104         self.workerforbuilders = {}
  105         self.max_builds = max_builds
  106         self.access = []
  107         if locks:
  108             self.access = locks
  109         self.lock_subscriptions = []
  110 
  111         self.properties = Properties()
  112         self.properties.update(properties or {}, "Worker")
  113         self.properties.setProperty("workername", name, "Worker")
  114         self.defaultProperties = Properties()
  115         self.defaultProperties.update(defaultProperties or {}, "Worker")
  116 
  117         if self.machine_name is not None:
  118             self.properties.setProperty('machine_name', self.machine_name,
  119                                         'Worker')
  120         self.machine = None
  121 
  122         self.lastMessageReceived = 0
  123 
  124         if notify_on_missing is None:
  125             notify_on_missing = []
  126         if isinstance(notify_on_missing, str):
  127             notify_on_missing = [notify_on_missing]
  128         self.notify_on_missing = notify_on_missing
  129         for i in notify_on_missing:
  130             if not isinstance(i, str):
  131                 config.error(
  132                     'notify_on_missing arg %r is not a string' % (i,))
  133 
  134         self.missing_timeout = missing_timeout
  135         self.missing_timer = None
  136 
  137         # a protocol connection, if we're currently connected
  138         self.conn = None
  139 
  140         self._old_builder_list = None
  141         self._configured_builderid_list = None
  142 
  143     def __repr__(self):
  144         return "<%s %r>" % (self.__class__.__name__, self.name)
  145 
  146     @property
  147     def workername(self):
  148         # workername is now an alias to twisted.Service's name
  149         return self.name
  150 
  151     @property
  152     def botmaster(self):
  153         if self.master is None:
  154             return None
  155         return self.master.botmaster
  156 
  157     def updateLocks(self):
  158         """Convert the L{LockAccess} objects in C{self.locks} into real lock
  159         objects, while also maintaining the subscriptions to lock releases."""
  160         # unsubscribe from any old locks
  161         for s in self.lock_subscriptions:
  162             s.unsubscribe()
  163 
  164         # convert locks into their real form
  165         locks = [(self.botmaster.getLockFromLockAccess(a), a)
  166                  for a in self.access]
  167         self.locks = [(l.getLockForWorker(self.workername), la)
  168                       for l, la in locks]
  169         self.lock_subscriptions = [l.subscribeToReleases(self._lockReleased)
  170                                    for l, la in self.locks]
  171 
  172     def locksAvailable(self):
  173         """
  174         I am called to see if all the locks I depend on are available,
  175         in which I return True, otherwise I return False
  176         """
  177         if not self.locks:
  178             return True
  179         for lock, access in self.locks:
  180             if not lock.isAvailable(self, access):
  181                 return False
  182         return True
  183 
  184     def acquireLocks(self):
  185         """
  186         I am called when a build is preparing to run. I try to claim all
  187         the locks that are needed for a build to happen. If I can't, then
  188         my caller should give up the build and try to get another worker
  189         to look at it.
  190         """
  191         log.msg("acquireLocks(worker %s, locks %s)" % (self, self.locks))
  192         if not self.locksAvailable():
  193             log.msg("worker %s can't lock, giving up" % (self, ))
  194             return False
  195         # all locks are available, claim them all
  196         for lock, access in self.locks:
  197             lock.claim(self, access)
  198         return True
  199 
  200     def releaseLocks(self):
  201         """
  202         I am called to release any locks after a build has finished
  203         """
  204         log.msg("releaseLocks(%s): %s" % (self, self.locks))
  205         for lock, access in self.locks:
  206             lock.release(self, access)
  207 
  208     def _lockReleased(self):
  209         """One of the locks for this worker was released; try scheduling
  210         builds."""
  211         if not self.botmaster:
  212             return  # oh well..
  213         self.botmaster.maybeStartBuildsForWorker(self.name)
  214 
  215     def _applyWorkerInfo(self, info):
  216         if not info:
  217             return
  218 
  219         self.worker_status.setAdmin(info.get("admin"))
  220         self.worker_status.setHost(info.get("host"))
  221         self.worker_status.setAccessURI(info.get("access_uri", None))
  222         self.worker_status.setVersion(info.get("version", "(unknown)"))
  223 
  224     @defer.inlineCallbacks
  225     def _getWorkerInfo(self):
  226         worker = yield self.master.data.get(
  227             ('workers', self.workerid))
  228         self._applyWorkerInfo(worker['workerinfo'])
  229 
  230     def setServiceParent(self, parent):
  231         # botmaster needs to set before setServiceParent which calls
  232         # startService
  233 
  234         self.manager = parent
  235         return super().setServiceParent(parent)
  236 
  237     @defer.inlineCallbacks
  238     def startService(self):
  239         self.updateLocks()
  240         self.workerid = yield self.master.data.updates.findWorkerId(
  241             self.name)
  242 
  243         self.workerActionConsumer = yield self.master.mq.startConsuming(self.controlWorker,
  244                                                                         ("control", "worker",
  245                                                                         str(self.workerid),
  246                                                                         None))
  247 
  248         yield self._getWorkerInfo()
  249         yield super().startService()
  250 
  251         # startMissingTimer wants the service to be running to really start
  252         if self.start_missing_on_startup:
  253             self.startMissingTimer()
  254 
  255     @defer.inlineCallbacks
  256     def reconfigService(self, name, password, max_builds=None,
  257                         notify_on_missing=None, missing_timeout=DEFAULT_MISSING_TIMEOUT,
  258                         properties=None, defaultProperties=None,
  259                         locks=None,
  260                         keepalive_interval=DEFAULT_KEEPALIVE_INTERVAL,
  261                         machine_name=None):
  262         # Given a Worker config arguments, configure this one identically.
  263         # Because Worker objects are remotely referenced, we can't replace them
  264         # without disconnecting the worker, yet there's no reason to do that.
  265 
  266         assert self.name == name
  267         self.password = password
  268 
  269         # adopt new instance's configuration parameters
  270         self.max_builds = max_builds
  271         self.access = []
  272         if locks:
  273             self.access = locks
  274         if notify_on_missing is None:
  275             notify_on_missing = []
  276         if isinstance(notify_on_missing, str):
  277             notify_on_missing = [notify_on_missing]
  278         self.notify_on_missing = notify_on_missing
  279 
  280         if self.missing_timeout != missing_timeout:
  281             running_missing_timer = self.missing_timer
  282             self.stopMissingTimer()
  283             self.missing_timeout = missing_timeout
  284             if running_missing_timer:
  285                 self.startMissingTimer()
  286 
  287         self.properties = Properties()
  288         self.properties.update(properties or {}, "Worker")
  289         self.properties.setProperty("workername", name, "Worker")
  290         self.defaultProperties = Properties()
  291         self.defaultProperties.update(defaultProperties or {}, "Worker")
  292 
  293         # Note that before first reconfig self.machine will always be None and
  294         # out of sync with self.machine_name, thus more complex logic is needed.
  295         if self.machine is not None and self.machine_name != machine_name:
  296             self.machine.unregisterWorker(self)
  297             self.machine = None
  298 
  299         self.machine_name = machine_name
  300         if self.machine is None and self.machine_name is not None:
  301             self.machine = self.master.machine_manager.getMachineByName(self.machine_name)
  302             if self.machine is not None:
  303                 self.machine.registerWorker(self)
  304                 self.properties.setProperty("machine_name", self.machine_name,
  305                                             "Worker")
  306             else:
  307                 log.err("Unknown machine '{}' for worker '{}'".format(
  308                     self.machine_name, self.name))
  309 
  310         # update our records with the worker manager
  311         if not self.registration:
  312             self.registration = yield self.master.workers.register(self)
  313         yield self.registration.update(self, self.master.config)
  314 
  315         self.updateLocks()
  316 
  317     @defer.inlineCallbacks
  318     def reconfigServiceWithSibling(self, sibling):
  319         # reconfigServiceWithSibling will only reconfigure the worker when it is configured differently.
  320         # However, the worker configuration depends on which builder it is configured
  321         yield super().reconfigServiceWithSibling(sibling)
  322 
  323         # update the attached worker's notion of which builders are attached.
  324         # This assumes that the relevant builders have already been configured,
  325         # which is why the reconfig_priority is set low in this class.
  326         bids = [
  327             b.getBuilderId() for b in self.botmaster.getBuildersForWorker(self.name)]
  328         bids = yield defer.gatherResults(bids, consumeErrors=True)
  329         if self._configured_builderid_list != bids:
  330             yield self.master.data.updates.workerConfigured(self.workerid, self.master.masterid, bids)
  331             yield self.updateWorker()
  332             self._configured_builderid_list = bids
  333 
  334     @defer.inlineCallbacks
  335     def stopService(self):
  336         if self.registration:
  337             yield self.registration.unregister()
  338             self.registration = None
  339         self.workerActionConsumer.stopConsuming()
  340         self.stopMissingTimer()
  341         self.stopQuarantineTimer()
  342         # mark this worker as configured for zero builders in this master
  343         yield self.master.data.updates.workerConfigured(self.workerid, self.master.masterid, [])
  344         yield super().stopService()
  345 
  346     def isCompatibleWithBuild(self, build_props):
  347         # given a build properties object, determines whether the build is
  348         # compatible with the currently running worker or not. This is most
  349         # often useful for latent workers where it's possible to request
  350         # different kinds of workers.
  351         return defer.succeed(True)
  352 
  353     def startMissingTimer(self):
  354         if self.missing_timeout and self.parent and self.running:
  355             self.stopMissingTimer()  # in case it's already running
  356             self.missing_timer = self.master.reactor.callLater(self.missing_timeout,
  357                                                                self._missing_timer_fired)
  358 
  359     def stopMissingTimer(self):
  360         if self.missing_timer:
  361             if self.missing_timer.active():
  362                 self.missing_timer.cancel()
  363             self.missing_timer = None
  364 
  365     def isConnected(self):
  366         return self.conn
  367 
  368     def _missing_timer_fired(self):
  369         self.missing_timer = None
  370         # notify people, but only if we're still in the config
  371         if not self.parent:
  372             return
  373         last_connection = time.ctime(time.time() - self.missing_timeout)
  374         self.master.data.updates.workerMissing(
  375             workerid=self.workerid,
  376             masterid=self.master.masterid,
  377             last_connection=last_connection,
  378             notify=self.notify_on_missing
  379         )
  380 
  381     def updateWorker(self):
  382         """Called to add or remove builders after the worker has connected.
  383 
  384         @return: a Deferred that indicates when an attached worker has
  385         accepted the new builders and/or released the old ones."""
  386         if self.conn:
  387             return self.sendBuilderList()
  388         # else:
  389         return defer.succeed(None)
  390 
  391     @defer.inlineCallbacks
  392     def attached(self, conn):
  393         """This is called when the worker connects."""
  394 
  395         assert self.conn is None
  396 
  397         metrics.MetricCountEvent.log("AbstractWorker.attached_workers", 1)
  398 
  399         # now we go through a sequence of calls, gathering information, then
  400         # tell the Botmaster that it can finally give this worker to all the
  401         # Builders that care about it.
  402 
  403         # Reset graceful shutdown status
  404         self._graceful = False
  405 
  406         self.conn = conn
  407         self._old_builder_list = None  # clear builder list before proceed
  408 
  409         self.worker_status.setConnected(True)
  410 
  411         self._applyWorkerInfo(conn.info)
  412         self.worker_commands = conn.info.get("worker_commands", {})
  413         self.worker_environ = conn.info.get("environ", {})
  414         self.worker_basedir = conn.info.get("basedir", None)
  415         self.worker_system = conn.info.get("system", None)
  416 
  417         self.conn.notifyOnDisconnect(self.detached)
  418 
  419         workerinfo = {
  420             'admin': conn.info.get('admin'),
  421             'host': conn.info.get('host'),
  422             'access_uri': conn.info.get('access_uri'),
  423             'version': conn.info.get('version')
  424         }
  425 
  426         yield self.master.data.updates.workerConnected(
  427             workerid=self.workerid,
  428             masterid=self.master.masterid,
  429             workerinfo=workerinfo
  430         )
  431 
  432         if self.worker_system == "nt":
  433             self.path_module = namedModule("ntpath")
  434         else:
  435             # most everything accepts / as separator, so posix should be a
  436             # reasonable fallback
  437             self.path_module = namedModule("posixpath")
  438         log.msg("bot attached")
  439         self.messageReceivedFromWorker()
  440         self.stopMissingTimer()
  441         yield self.updateWorker()
  442         yield self.botmaster.maybeStartBuildsForWorker(self.name)
  443         self.updateState()
  444 
  445     def messageReceivedFromWorker(self):
  446         now = time.time()
  447         self.lastMessageReceived = now
  448         self.worker_status.setLastMessageReceived(now)
  449 
  450     def setupProperties(self, props):
  451         for name in self.properties.properties:
  452             props.setProperty(
  453                 name, self.properties.getProperty(name), "Worker")
  454         for name in self.defaultProperties.properties:
  455             if name not in props:
  456                 props.setProperty(
  457                     name, self.defaultProperties.getProperty(name), "Worker")
  458 
  459     @defer.inlineCallbacks
  460     def detached(self):
  461         # protect against race conditions in conn disconnect path and someone
  462         # calling detached directly. At the moment the null worker does that.
  463         if self.conn is None:
  464             return
  465 
  466         metrics.MetricCountEvent.log("AbstractWorker.attached_workers", -1)
  467         self.conn = None
  468         self._old_builder_list = []
  469         self.worker_status.setConnected(False)
  470         log.msg("Worker.detached(%s)" % (self.name,))
  471         self.releaseLocks()
  472         yield self.master.data.updates.workerDisconnected(
  473             workerid=self.workerid,
  474             masterid=self.master.masterid,
  475         )
  476 
  477     def disconnect(self):
  478         """Forcibly disconnect the worker.
  479 
  480         This severs the TCP connection and returns a Deferred that will fire
  481         (with None) when the connection is probably gone.
  482 
  483         If the worker is still alive, they will probably try to reconnect
  484         again in a moment.
  485 
  486         This is called in two circumstances. The first is when a worker is
  487         removed from the config file. In this case, when they try to
  488         reconnect, they will be rejected as an unknown worker. The second is
  489         when we wind up with two connections for the same worker, in which
  490         case we disconnect the older connection.
  491         """
  492         if self.conn is None:
  493             return defer.succeed(None)
  494         log.msg("disconnecting old worker %s now" % (self.name,))
  495         # When this Deferred fires, we'll be ready to accept the new worker
  496         return self._disconnect(self.conn)
  497 
  498     def _disconnect(self, conn):
  499         # all kinds of teardown will happen as a result of
  500         # loseConnection(), but it happens after a reactor iteration or
  501         # two. Hook the actual disconnect so we can know when it is safe
  502         # to connect the new worker. We have to wait one additional
  503         # iteration (with callLater(0)) to make sure the *other*
  504         # notifyOnDisconnect handlers have had a chance to run.
  505         d = defer.Deferred()
  506 
  507         # notifyOnDisconnect runs the callback
  508         def _disconnected():
  509             eventually(d.callback, None)
  510         conn.notifyOnDisconnect(_disconnected)
  511         conn.loseConnection()
  512         log.msg("waiting for worker to finish disconnecting")
  513 
  514         return d
  515 
  516     @defer.inlineCallbacks
  517     def sendBuilderList(self):
  518         our_builders = self.botmaster.getBuildersForWorker(self.name)
  519 
  520         blist = [(b.name, b.config.workerbuilddir) for b in our_builders]
  521 
  522         if blist == self._old_builder_list:
  523             return
  524 
  525         slist = yield self.conn.remoteSetBuilderList(builders=blist)
  526 
  527         self._old_builder_list = blist
  528 
  529         # Nothing has changed, so don't need to re-attach to everything
  530         if not slist:
  531             return
  532 
  533         dl = []
  534         for name in slist:
  535             # use get() since we might have changed our mind since then
  536             b = self.botmaster.builders.get(name)
  537             if b:
  538                 d1 = self.attachBuilder(b)
  539                 dl.append(d1)
  540         yield defer.DeferredList(dl)
  541 
  542     def attachBuilder(self, builder):
  543         return builder.attached(self, self.worker_commands)
  544 
  545     def controlWorker(self, key, params):
  546         log.msg("worker {} wants to {}: {}".format(self.name, key[-1], params))
  547         if key[-1] == "stop":
  548             return self.shutdownRequested()
  549         if key[-1] == "pause":
  550             self.pause()
  551         if key[-1] == "unpause":
  552             self.unpause()
  553         if key[-1] == "kill":
  554             self.shutdown()
  555 
  556     def shutdownRequested(self):
  557         self._graceful = True
  558         self.maybeShutdown()
  559         self.updateState()
  560 
  561     def addWorkerForBuilder(self, wfb):
  562         self.workerforbuilders[wfb.builder_name] = wfb
  563 
  564     def removeWorkerForBuilder(self, wfb):
  565         try:
  566             del self.workerforbuilders[wfb.builder_name]
  567         except KeyError:
  568             pass
  569 
  570     def buildFinished(self, wfb):
  571         """This is called when a build on this worker is finished."""
  572         self.botmaster.maybeStartBuildsForWorker(self.name)
  573 
  574     def canStartBuild(self):
  575         """
  576         I am called when a build is requested to see if this worker
  577         can start a build.  This function can be used to limit overall
  578         concurrency on the worker.
  579 
  580         Note for subclassers: if a worker can become willing to start a build
  581         without any action on that worker (for example, by a resource in use on
  582         another worker becoming available), then you must arrange for
  583         L{maybeStartBuildsForWorker} to be called at that time, or builds on
  584         this worker will not start.
  585         """
  586 
  587         # If we're waiting to shutdown gracefully or paused, then we shouldn't
  588         # accept any new jobs.
  589         if self._graceful or self._paused:
  590             return False
  591 
  592         if self.max_builds:
  593             active_builders = [wfb for wfb in self.workerforbuilders.values()
  594                                if wfb.isBusy()]
  595             if len(active_builders) >= self.max_builds:
  596                 return False
  597 
  598         if not self.locksAvailable():
  599             return False
  600 
  601         return True
  602 
  603     @defer.inlineCallbacks
  604     def shutdown(self):
  605         """Shutdown the worker"""
  606         if not self.conn:
  607             log.msg("no remote; worker is already shut down")
  608             return
  609 
  610         yield self.conn.remoteShutdown()
  611 
  612     def maybeShutdown(self):
  613         """Shut down this worker if it has been asked to shut down gracefully,
  614         and has no active builders."""
  615         if not self._graceful:
  616             return
  617         active_builders = [wfb for wfb in self.workerforbuilders.values()
  618                            if wfb.isBusy()]
  619         if active_builders:
  620             return
  621         d = self.shutdown()
  622         d.addErrback(log.err, 'error while shutting down worker')
  623 
  624     def updateState(self):
  625         self.master.data.updates.setWorkerState(self.workerid, self._paused, self._graceful)
  626 
  627     def pause(self):
  628         """Stop running new builds on the worker."""
  629         self._paused = True
  630         self.updateState()
  631 
  632     def unpause(self):
  633         """Restart running new builds on the worker."""
  634         self._paused = False
  635         self.botmaster.maybeStartBuildsForWorker(self.name)
  636         self.updateState()
  637 
  638     def isPaused(self):
  639         return self._paused
  640 
  641     def resetQuarantine(self):
  642         self.quarantine_timeout = self.quarantine_initial_timeout
  643 
  644     def putInQuarantine(self):
  645         if self.quarantine_timer:  # already in quarantine
  646             return
  647 
  648         self.pause()
  649         self.quarantine_timer = self.master.reactor.callLater(
  650             self.quarantine_timeout, self.exitQuarantine)
  651         log.msg("{} has been put in quarantine for {}s".format(
  652             self.name, self.quarantine_timeout))
  653         # next we will wait twice as long
  654         self.quarantine_timeout *= 2
  655         if self.quarantine_timeout > self.quarantine_max_timeout:
  656             # unless we hit the max timeout
  657             self.quarantine_timeout = self.quarantine_max_timeout
  658 
  659     def exitQuarantine(self):
  660         self.quarantine_timer = None
  661         self.unpause()
  662 
  663     def stopQuarantineTimer(self):
  664         if self.quarantine_timer is not None:
  665             self.quarantine_timer.cancel()
  666             self.quarantine_timer = None
  667             self.unpause()
  668 
  669 
  670 class Worker(AbstractWorker):
  671 
  672     def detached(self):
  673         super().detached()
  674         self.botmaster.workerLost(self)
  675         self.startMissingTimer()
  676 
  677     @defer.inlineCallbacks
  678     def attached(self, bot):
  679         try:
  680             yield super().attached(bot)
  681         except Exception as e:
  682             log.err(e, "worker %s cannot attach" % (self.name,))
  683             return
  684 
  685     def buildFinished(self, wfb):
  686         """This is called when a build on this worker is finished."""
  687         super().buildFinished(wfb)
  688 
  689         # If we're gracefully shutting down, and we have no more active
  690         # builders, then it's safe to disconnect
  691         self.maybeShutdown()