Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/buildbot/buildbot/process/builder.py
diff options
context:
space:
mode:
Diffstat (limited to 'buildbot/buildbot/process/builder.py')
-rw-r--r--buildbot/buildbot/process/builder.py874
1 files changed, 874 insertions, 0 deletions
diff --git a/buildbot/buildbot/process/builder.py b/buildbot/buildbot/process/builder.py
new file mode 100644
index 0000000..cb26ccb
--- /dev/null
+++ b/buildbot/buildbot/process/builder.py
@@ -0,0 +1,874 @@
+
+import random, weakref
+from zope.interface import implements
+from twisted.python import log, components
+from twisted.spread import pb
+from twisted.internet import reactor, defer
+
+from buildbot import interfaces
+from buildbot.status.progress import Expectations
+from buildbot.util import now
+from buildbot.process import base
+
+(ATTACHING, # slave attached, still checking hostinfo/etc
+ IDLE, # idle, available for use
+ PINGING, # build about to start, making sure it is still alive
+ BUILDING, # build is running
+ LATENT, # latent slave is not substantiated; similar to idle
+ ) = range(5)
+
+
+class AbstractSlaveBuilder(pb.Referenceable):
+ """I am the master-side representative for one of the
+ L{buildbot.slave.bot.SlaveBuilder} objects that lives in a remote
+ buildbot. When a remote builder connects, I query it for command versions
+ and then make it available to any Builds that are ready to run. """
+
+ def __init__(self):
+ self.ping_watchers = []
+ self.state = None # set in subclass
+ self.remote = None
+ self.slave = None
+ self.builder_name = None
+
+ def __repr__(self):
+ r = ["<", self.__class__.__name__]
+ if self.builder_name:
+ r.extend([" builder=", self.builder_name])
+ if self.slave:
+ r.extend([" slave=", self.slave.slavename])
+ r.append(">")
+ return ''.join(r)
+
+ def setBuilder(self, b):
+ self.builder = b
+ self.builder_name = b.name
+
+ def getSlaveCommandVersion(self, command, oldversion=None):
+ if self.remoteCommands is None:
+ # the slave is 0.5.0 or earlier
+ return oldversion
+ return self.remoteCommands.get(command)
+
+ def isAvailable(self):
+ # if this SlaveBuilder is busy, then it's definitely not available
+ if self.isBusy():
+ return False
+
+ # otherwise, check in with the BuildSlave
+ if self.slave:
+ return self.slave.canStartBuild()
+
+ # no slave? not very available.
+ return False
+
+ def isBusy(self):
+ return self.state not in (IDLE, LATENT)
+
+ def buildStarted(self):
+ self.state = BUILDING
+
+ def buildFinished(self):
+ self.state = IDLE
+ reactor.callLater(0, self.builder.botmaster.maybeStartAllBuilds)
+
+ def attached(self, slave, remote, commands):
+ """
+ @type slave: L{buildbot.buildslave.BuildSlave}
+ @param slave: the BuildSlave that represents the buildslave as a
+ whole
+ @type remote: L{twisted.spread.pb.RemoteReference}
+ @param remote: a reference to the L{buildbot.slave.bot.SlaveBuilder}
+ @type commands: dict: string -> string, or None
+ @param commands: provides the slave's version of each RemoteCommand
+ """
+ self.state = ATTACHING
+ self.remote = remote
+ self.remoteCommands = commands # maps command name to version
+ if self.slave is None:
+ self.slave = slave
+ self.slave.addSlaveBuilder(self)
+ else:
+ assert self.slave == slave
+ log.msg("Buildslave %s attached to %s" % (slave.slavename,
+ self.builder_name))
+ d = self.remote.callRemote("setMaster", self)
+ d.addErrback(self._attachFailure, "Builder.setMaster")
+ d.addCallback(self._attached2)
+ return d
+
+ def _attached2(self, res):
+ d = self.remote.callRemote("print", "attached")
+ d.addErrback(self._attachFailure, "Builder.print 'attached'")
+ d.addCallback(self._attached3)
+ return d
+
+ def _attached3(self, res):
+ # now we say they're really attached
+ self.state = IDLE
+ return self
+
+ def _attachFailure(self, why, where):
+ assert isinstance(where, str)
+ log.msg(where)
+ log.err(why)
+ return why
+
+ def ping(self, timeout, status=None):
+ """Ping the slave to make sure it is still there. Returns a Deferred
+ that fires with True if it is.
+
+ @param status: if you point this at a BuilderStatus, a 'pinging'
+ event will be pushed.
+ """
+ oldstate = self.state
+ self.state = PINGING
+ newping = not self.ping_watchers
+ d = defer.Deferred()
+ self.ping_watchers.append(d)
+ if newping:
+ if status:
+ event = status.addEvent(["pinging"])
+ d2 = defer.Deferred()
+ d2.addCallback(self._pong_status, event)
+ self.ping_watchers.insert(0, d2)
+ # I think it will make the tests run smoother if the status
+ # is updated before the ping completes
+ Ping().ping(self.remote, timeout).addCallback(self._pong)
+
+ def reset_state(res):
+ if self.state == PINGING:
+ self.state = oldstate
+ return res
+ d.addCallback(reset_state)
+ return d
+
+ def _pong(self, res):
+ watchers, self.ping_watchers = self.ping_watchers, []
+ for d in watchers:
+ d.callback(res)
+
+ def _pong_status(self, res, event):
+ if res:
+ event.text = ["ping", "success"]
+ else:
+ event.text = ["ping", "failed"]
+ event.finish()
+
+ def detached(self):
+ log.msg("Buildslave %s detached from %s" % (self.slave.slavename,
+ self.builder_name))
+ if self.slave:
+ self.slave.removeSlaveBuilder(self)
+ self.slave = None
+ self.remote = None
+ self.remoteCommands = None
+
+
+class Ping:
+ running = False
+ timer = None
+
+ def ping(self, remote, timeout):
+ assert not self.running
+ self.running = True
+ log.msg("sending ping")
+ self.d = defer.Deferred()
+ # TODO: add a distinct 'ping' command on the slave.. using 'print'
+ # for this purpose is kind of silly.
+ remote.callRemote("print", "ping").addCallbacks(self._pong,
+ self._ping_failed,
+ errbackArgs=(remote,))
+
+ # We use either our own timeout or the (long) TCP timeout to detect
+ # silently-missing slaves. This might happen because of a NAT
+ # timeout or a routing loop. If the slave just shuts down (and we
+ # somehow missed the FIN), we should get a "connection refused"
+ # message.
+ self.timer = reactor.callLater(timeout, self._ping_timeout, remote)
+ return self.d
+
+ def _ping_timeout(self, remote):
+ log.msg("ping timeout")
+ # force the BuildSlave to disconnect, since this indicates that
+ # the bot is unreachable.
+ del self.timer
+ remote.broker.transport.loseConnection()
+ # the forcibly-lost connection will now cause the ping to fail
+
+ def _stopTimer(self):
+ if not self.running:
+ return
+ self.running = False
+
+ if self.timer:
+ self.timer.cancel()
+ del self.timer
+
+ def _pong(self, res):
+ log.msg("ping finished: success")
+ self._stopTimer()
+ self.d.callback(True)
+
+ def _ping_failed(self, res, remote):
+ log.msg("ping finished: failure")
+ self._stopTimer()
+ # the slave has some sort of internal error, disconnect them. If we
+ # don't, we'll requeue a build and ping them again right away,
+ # creating a nasty loop.
+ remote.broker.transport.loseConnection()
+ # TODO: except, if they actually did manage to get this far, they'll
+ # probably reconnect right away, and we'll do this game again. Maybe
+ # it would be better to leave them in the PINGING state.
+ self.d.callback(False)
+
+
+class SlaveBuilder(AbstractSlaveBuilder):
+
+ def __init__(self):
+ AbstractSlaveBuilder.__init__(self)
+ self.state = ATTACHING
+
+ def detached(self):
+ AbstractSlaveBuilder.detached(self)
+ if self.slave:
+ self.slave.removeSlaveBuilder(self)
+ self.slave = None
+ self.state = ATTACHING
+
+ def buildFinished(self):
+ # Call the slave's buildFinished if we can; the slave may be waiting
+ # to do a graceful shutdown and needs to know when it's idle.
+ # After, we check to see if we can start other builds.
+ self.state = IDLE
+ if self.slave:
+ d = self.slave.buildFinished(self)
+ d.addCallback(lambda x: reactor.callLater(0, self.builder.botmaster.maybeStartAllBuilds))
+ else:
+ reactor.callLater(0, self.builder.botmaster.maybeStartAllBuilds)
+
+
+class LatentSlaveBuilder(AbstractSlaveBuilder):
+ def __init__(self, slave, builder):
+ AbstractSlaveBuilder.__init__(self)
+ self.slave = slave
+ self.state = LATENT
+ self.setBuilder(builder)
+ self.slave.addSlaveBuilder(self)
+ log.msg("Latent buildslave %s attached to %s" % (slave.slavename,
+ self.builder_name))
+
+ def substantiate(self, build):
+ d = self.slave.substantiate(self)
+ if not self.slave.substantiated:
+ event = self.builder.builder_status.addEvent(
+ ["substantiating"])
+ def substantiated(res):
+ msg = ["substantiate", "success"]
+ if isinstance(res, basestring):
+ msg.append(res)
+ elif isinstance(res, (tuple, list)):
+ msg.extend(res)
+ event.text = msg
+ event.finish()
+ return res
+ def substantiation_failed(res):
+ event.text = ["substantiate", "failed"]
+ # TODO add log of traceback to event
+ event.finish()
+ return res
+ d.addCallbacks(substantiated, substantiation_failed)
+ return d
+
+ def detached(self):
+ AbstractSlaveBuilder.detached(self)
+ self.state = LATENT
+
+ def buildStarted(self):
+ AbstractSlaveBuilder.buildStarted(self)
+ self.slave.buildStarted(self)
+
+ def buildFinished(self):
+ AbstractSlaveBuilder.buildFinished(self)
+ self.slave.buildFinished(self)
+
+ def _attachFailure(self, why, where):
+ self.state = LATENT
+ return AbstractSlaveBuilder._attachFailure(self, why, where)
+
+ def ping(self, timeout, status=None):
+ if not self.slave.substantiated:
+ if status:
+ status.addEvent(["ping", "latent"]).finish()
+ return defer.succeed(True)
+ return AbstractSlaveBuilder.ping(self, timeout, status)
+
+
+class Builder(pb.Referenceable):
+ """I manage all Builds of a given type.
+
+ Each Builder is created by an entry in the config file (the c['builders']
+ list), with a number of parameters.
+
+ One of these parameters is the L{buildbot.process.factory.BuildFactory}
+ object that is associated with this Builder. The factory is responsible
+ for creating new L{Build<buildbot.process.base.Build>} objects. Each
+ Build object defines when and how the build is performed, so a new
+ Factory or Builder should be defined to control this behavior.
+
+ The Builder holds on to a number of L{base.BuildRequest} objects in a
+ list named C{.buildable}. Incoming BuildRequest objects will be added to
+ this list, or (if possible) merged into an existing request. When a slave
+ becomes available, I will use my C{BuildFactory} to turn the request into
+ a new C{Build} object. The C{BuildRequest} is forgotten, the C{Build}
+ goes into C{.building} while it runs. Once the build finishes, I will
+ discard it.
+
+ I maintain a list of available SlaveBuilders, one for each connected
+ slave that the C{slavenames} parameter says we can use. Some of these
+ will be idle, some of them will be busy running builds for me. If there
+ are multiple slaves, I can run multiple builds at once.
+
+ I also manage forced builds, progress expectation (ETA) management, and
+ some status delivery chores.
+
+ I am persisted in C{BASEDIR/BUILDERNAME/builder}, so I can remember how
+ long a build usually takes to run (in my C{expectations} attribute). This
+ pickle also includes the L{buildbot.status.builder.BuilderStatus} object,
+ which remembers the set of historic builds.
+
+ @type buildable: list of L{buildbot.process.base.BuildRequest}
+ @ivar buildable: BuildRequests that are ready to build, but which are
+ waiting for a buildslave to be available.
+
+ @type building: list of L{buildbot.process.base.Build}
+ @ivar building: Builds that are actively running
+
+ @type slaves: list of L{buildbot.buildslave.BuildSlave} objects
+ @ivar slaves: the slaves currently available for building
+ """
+
+ expectations = None # this is created the first time we get a good build
+ START_BUILD_TIMEOUT = 10
+ CHOOSE_SLAVES_RANDOMLY = True # disabled for determinism during tests
+
+ def __init__(self, setup, builder_status):
+ """
+ @type setup: dict
+ @param setup: builder setup data, as stored in
+ BuildmasterConfig['builders']. Contains name,
+ slavename(s), builddir, factory, locks.
+ @type builder_status: L{buildbot.status.builder.BuilderStatus}
+ """
+ self.name = setup['name']
+ self.slavenames = []
+ if setup.has_key('slavename'):
+ self.slavenames.append(setup['slavename'])
+ if setup.has_key('slavenames'):
+ self.slavenames.extend(setup['slavenames'])
+ self.builddir = setup['builddir']
+ self.buildFactory = setup['factory']
+ self.locks = setup.get("locks", [])
+ self.env = setup.get('env', {})
+ assert isinstance(self.env, dict)
+ if setup.has_key('periodicBuildTime'):
+ raise ValueError("periodicBuildTime can no longer be defined as"
+ " part of the Builder: use scheduler.Periodic"
+ " instead")
+
+ # build/wannabuild slots: Build objects move along this sequence
+ self.buildable = []
+ self.building = []
+ # old_building holds active builds that were stolen from a predecessor
+ self.old_building = weakref.WeakKeyDictionary()
+
+ # buildslaves which have connected but which are not yet available.
+ # These are always in the ATTACHING state.
+ self.attaching_slaves = []
+
+ # buildslaves at our disposal. Each SlaveBuilder instance has a
+ # .state that is IDLE, PINGING, or BUILDING. "PINGING" is used when a
+ # Build is about to start, to make sure that they're still alive.
+ self.slaves = []
+
+ self.builder_status = builder_status
+ self.builder_status.setSlavenames(self.slavenames)
+
+ # for testing, to help synchronize tests
+ self.watchers = {'attach': [], 'detach': [], 'detach_all': [],
+ 'idle': []}
+
+ def setBotmaster(self, botmaster):
+ self.botmaster = botmaster
+
+ def compareToSetup(self, setup):
+ diffs = []
+ setup_slavenames = []
+ if setup.has_key('slavename'):
+ setup_slavenames.append(setup['slavename'])
+ setup_slavenames.extend(setup.get('slavenames', []))
+ if setup_slavenames != self.slavenames:
+ diffs.append('slavenames changed from %s to %s' \
+ % (self.slavenames, setup_slavenames))
+ if setup['builddir'] != self.builddir:
+ diffs.append('builddir changed from %s to %s' \
+ % (self.builddir, setup['builddir']))
+ if setup['factory'] != self.buildFactory: # compare objects
+ diffs.append('factory changed')
+ oldlocks = [(lock.__class__, lock.name)
+ for lock in self.locks]
+ newlocks = [(lock.__class__, lock.name)
+ for lock in setup.get('locks',[])]
+ if oldlocks != newlocks:
+ diffs.append('locks changed from %s to %s' % (oldlocks, newlocks))
+ return diffs
+
+ def __repr__(self):
+ return "<Builder '%s' at %d>" % (self.name, id(self))
+
+ def getOldestRequestTime(self):
+ """Returns the timestamp of the oldest build request for this builder.
+
+ If there are no build requests, None is returned."""
+ if self.buildable:
+ return self.buildable[0].getSubmitTime()
+ else:
+ return None
+
+ def submitBuildRequest(self, req):
+ req.setSubmitTime(now())
+ self.buildable.append(req)
+ req.requestSubmitted(self)
+ self.builder_status.addBuildRequest(req.status)
+ self.maybeStartBuild()
+
+ def cancelBuildRequest(self, req):
+ if req in self.buildable:
+ self.buildable.remove(req)
+ self.builder_status.removeBuildRequest(req.status)
+ return True
+ return False
+
+ def __getstate__(self):
+ d = self.__dict__.copy()
+ # TODO: note that d['buildable'] can contain Deferreds
+ del d['building'] # TODO: move these back to .buildable?
+ del d['slaves']
+ return d
+
+ def __setstate__(self, d):
+ self.__dict__ = d
+ self.building = []
+ self.slaves = []
+
+ def consumeTheSoulOfYourPredecessor(self, old):
+ """Suck the brain out of an old Builder.
+
+ This takes all the runtime state from an existing Builder and moves
+ it into ourselves. This is used when a Builder is changed in the
+ master.cfg file: the new Builder has a different factory, but we want
+ all the builds that were queued for the old one to get processed by
+ the new one. Any builds which are already running will keep running.
+ The new Builder will get as many of the old SlaveBuilder objects as
+ it wants."""
+
+ log.msg("consumeTheSoulOfYourPredecessor: %s feeding upon %s" %
+ (self, old))
+ # we claim all the pending builds, removing them from the old
+ # Builder's queue. This insures that the old Builder will not start
+ # any new work.
+ log.msg(" stealing %s buildrequests" % len(old.buildable))
+ self.buildable.extend(old.buildable)
+ old.buildable = []
+
+ # old.building (i.e. builds which are still running) is not migrated
+ # directly: it keeps track of builds which were in progress in the
+ # old Builder. When those builds finish, the old Builder will be
+ # notified, not us. However, since the old SlaveBuilder will point to
+ # us, it is our maybeStartBuild() that will be triggered.
+ if old.building:
+ self.builder_status.setBigState("building")
+ # however, we do grab a weakref to the active builds, so that our
+ # BuilderControl can see them and stop them. We use a weakref because
+ # we aren't the one to get notified, so there isn't a convenient
+ # place to remove it from self.building .
+ for b in old.building:
+ self.old_building[b] = None
+ for b in old.old_building:
+ self.old_building[b] = None
+
+ # Our set of slavenames may be different. Steal any of the old
+ # buildslaves that we want to keep using.
+ for sb in old.slaves[:]:
+ if sb.slave.slavename in self.slavenames:
+ log.msg(" stealing buildslave %s" % sb)
+ self.slaves.append(sb)
+ old.slaves.remove(sb)
+ sb.setBuilder(self)
+
+ # old.attaching_slaves:
+ # these SlaveBuilders are waiting on a sequence of calls:
+ # remote.setMaster and remote.print . When these two complete,
+ # old._attached will be fired, which will add a 'connect' event to
+ # the builder_status and try to start a build. However, we've pulled
+ # everything out of the old builder's queue, so it will have no work
+ # to do. The outstanding remote.setMaster/print call will be holding
+ # the last reference to the old builder, so it will disappear just
+ # after that response comes back.
+ #
+ # The BotMaster will ask the slave to re-set their list of Builders
+ # shortly after this function returns, which will cause our
+ # attached() method to be fired with a bunch of references to remote
+ # SlaveBuilders, some of which we already have (by stealing them
+ # from the old Builder), some of which will be new. The new ones
+ # will be re-attached.
+
+ # Therefore, we don't need to do anything about old.attaching_slaves
+
+ return # all done
+
+ def getBuild(self, number):
+ for b in self.building:
+ if b.build_status.number == number:
+ return b
+ for b in self.old_building.keys():
+ if b.build_status.number == number:
+ return b
+ return None
+
+ def fireTestEvent(self, name, fire_with=None):
+ if fire_with is None:
+ fire_with = self
+ watchers = self.watchers[name]
+ self.watchers[name] = []
+ for w in watchers:
+ reactor.callLater(0, w.callback, fire_with)
+
+ def addLatentSlave(self, slave):
+ assert interfaces.ILatentBuildSlave.providedBy(slave)
+ for s in self.slaves:
+ if s == slave:
+ break
+ else:
+ sb = LatentSlaveBuilder(slave, self)
+ self.builder_status.addPointEvent(
+ ['added', 'latent', slave.slavename])
+ self.slaves.append(sb)
+ reactor.callLater(0, self.maybeStartBuild)
+
+ def attached(self, slave, remote, commands):
+ """This is invoked by the BuildSlave when the self.slavename bot
+ registers their builder.
+
+ @type slave: L{buildbot.buildslave.BuildSlave}
+ @param slave: the BuildSlave that represents the buildslave as a whole
+ @type remote: L{twisted.spread.pb.RemoteReference}
+ @param remote: a reference to the L{buildbot.slave.bot.SlaveBuilder}
+ @type commands: dict: string -> string, or None
+ @param commands: provides the slave's version of each RemoteCommand
+
+ @rtype: L{twisted.internet.defer.Deferred}
+ @return: a Deferred that fires (with 'self') when the slave-side
+ builder is fully attached and ready to accept commands.
+ """
+ for s in self.attaching_slaves + self.slaves:
+ if s.slave == slave:
+ # already attached to them. This is fairly common, since
+ # attached() gets called each time we receive the builder
+ # list from the slave, and we ask for it each time we add or
+ # remove a builder. So if the slave is hosting builders
+ # A,B,C, and the config file changes A, we'll remove A and
+ # re-add it, triggering two builder-list requests, getting
+ # two redundant calls to attached() for B, and another two
+ # for C.
+ #
+ # Therefore, when we see that we're already attached, we can
+ # just ignore it. TODO: build a diagram of the state
+ # transitions here, I'm concerned about sb.attached() failing
+ # and leaving sb.state stuck at 'ATTACHING', and about
+ # the detached() message arriving while there's some
+ # transition pending such that the response to the transition
+ # re-vivifies sb
+ return defer.succeed(self)
+
+ sb = SlaveBuilder()
+ sb.setBuilder(self)
+ self.attaching_slaves.append(sb)
+ d = sb.attached(slave, remote, commands)
+ d.addCallback(self._attached)
+ d.addErrback(self._not_attached, slave)
+ return d
+
+ def _attached(self, sb):
+ # TODO: make this .addSlaveEvent(slave.slavename, ['connect']) ?
+ self.builder_status.addPointEvent(['connect', sb.slave.slavename])
+ self.attaching_slaves.remove(sb)
+ self.slaves.append(sb)
+ reactor.callLater(0, self.maybeStartBuild)
+
+ self.fireTestEvent('attach')
+ return self
+
+ def _not_attached(self, why, slave):
+ # already log.err'ed by SlaveBuilder._attachFailure
+ # TODO: make this .addSlaveEvent?
+ # TODO: remove from self.slaves (except that detached() should get
+ # run first, right?)
+ self.builder_status.addPointEvent(['failed', 'connect',
+ slave.slave.slavename])
+ # TODO: add an HTMLLogFile of the exception
+ self.fireTestEvent('attach', why)
+
+ def detached(self, slave):
+ """This is called when the connection to the bot is lost."""
+ log.msg("%s.detached" % self, slave.slavename)
+ for sb in self.attaching_slaves + self.slaves:
+ if sb.slave == slave:
+ break
+ else:
+ log.msg("WEIRD: Builder.detached(%s) (%s)"
+ " not in attaching_slaves(%s)"
+ " or slaves(%s)" % (slave, slave.slavename,
+ self.attaching_slaves,
+ self.slaves))
+ return
+ if sb.state == BUILDING:
+ # the Build's .lostRemote method (invoked by a notifyOnDisconnect
+ # handler) will cause the Build to be stopped, probably right
+ # after the notifyOnDisconnect that invoked us finishes running.
+
+ # TODO: should failover to a new Build
+ #self.retryBuild(sb.build)
+ pass
+
+ if sb in self.attaching_slaves:
+ self.attaching_slaves.remove(sb)
+ if sb in self.slaves:
+ self.slaves.remove(sb)
+
+ # TODO: make this .addSlaveEvent?
+ self.builder_status.addPointEvent(['disconnect', slave.slavename])
+ sb.detached() # inform the SlaveBuilder that their slave went away
+ self.updateBigStatus()
+ self.fireTestEvent('detach')
+ if not self.slaves:
+ self.fireTestEvent('detach_all')
+
+ def updateBigStatus(self):
+ if not self.slaves:
+ self.builder_status.setBigState("offline")
+ elif self.building:
+ self.builder_status.setBigState("building")
+ else:
+ self.builder_status.setBigState("idle")
+ self.fireTestEvent('idle')
+
+ def maybeStartBuild(self):
+ log.msg("maybeStartBuild %s: %s %s" %
+ (self, self.buildable, self.slaves))
+ if not self.buildable:
+ self.updateBigStatus()
+ return # nothing to do
+
+ # pick an idle slave
+ available_slaves = [sb for sb in self.slaves if sb.isAvailable()]
+ if not available_slaves:
+ log.msg("%s: want to start build, but we don't have a remote"
+ % self)
+ self.updateBigStatus()
+ return
+ if self.CHOOSE_SLAVES_RANDOMLY:
+ # TODO prefer idle over latent? maybe other sorting preferences?
+ sb = random.choice(available_slaves)
+ else:
+ sb = available_slaves[0]
+
+ # there is something to build, and there is a slave on which to build
+ # it. Grab the oldest request, see if we can merge it with anything
+ # else.
+ req = self.buildable.pop(0)
+ self.builder_status.removeBuildRequest(req.status)
+ mergers = []
+ botmaster = self.botmaster
+ for br in self.buildable[:]:
+ if botmaster.shouldMergeRequests(self, req, br):
+ self.buildable.remove(br)
+ self.builder_status.removeBuildRequest(br.status)
+ mergers.append(br)
+ requests = [req] + mergers
+
+ # Create a new build from our build factory and set ourself as the
+ # builder.
+ build = self.buildFactory.newBuild(requests)
+ build.setBuilder(self)
+ build.setLocks(self.locks)
+ if len(self.env) > 0:
+ build.setSlaveEnvironment(self.env)
+
+ # start it
+ self.startBuild(build, sb)
+
+ def startBuild(self, build, sb):
+ """Start a build on the given slave.
+ @param build: the L{base.Build} to start
+ @param sb: the L{SlaveBuilder} which will host this build
+
+ @return: a Deferred which fires with a
+ L{buildbot.interfaces.IBuildControl} that can be used to stop the
+ Build, or to access a L{buildbot.interfaces.IBuildStatus} which will
+ watch the Build as it runs. """
+
+ self.building.append(build)
+ self.updateBigStatus()
+ if isinstance(sb, LatentSlaveBuilder):
+ log.msg("starting build %s.. substantiating the slave %s" %
+ (build, sb))
+ d = sb.substantiate(build)
+ def substantiated(res):
+ return sb.ping(self.START_BUILD_TIMEOUT)
+ def substantiation_failed(res):
+ self.builder_status.addPointEvent(
+ ['removing', 'latent', sb.slave.slavename])
+ sb.slave.disconnect()
+ # TODO: should failover to a new Build
+ #self.retryBuild(sb.build)
+ d.addCallbacks(substantiated, substantiation_failed)
+ else:
+ log.msg("starting build %s.. pinging the slave %s" % (build, sb))
+ d = sb.ping(self.START_BUILD_TIMEOUT)
+ # ping the slave to make sure they're still there. If they're fallen
+ # off the map (due to a NAT timeout or something), this will fail in
+ # a couple of minutes, depending upon the TCP timeout. TODO: consider
+ # making this time out faster, or at least characterize the likely
+ # duration.
+ d.addCallback(self._startBuild_1, build, sb)
+ return d
+
+ def _startBuild_1(self, res, build, sb):
+ if not res:
+ return self._startBuildFailed("slave ping failed", build, sb)
+ # The buildslave is ready to go. sb.buildStarted() sets its state to
+ # BUILDING (so we won't try to use it for any other builds). This
+ # gets set back to IDLE by the Build itself when it finishes.
+ sb.buildStarted()
+ d = sb.remote.callRemote("startBuild")
+ d.addCallbacks(self._startBuild_2, self._startBuildFailed,
+ callbackArgs=(build,sb), errbackArgs=(build,sb))
+ return d
+
+ def _startBuild_2(self, res, build, sb):
+ # create the BuildStatus object that goes with the Build
+ bs = self.builder_status.newBuild()
+
+ # start the build. This will first set up the steps, then tell the
+ # BuildStatus that it has started, which will announce it to the
+ # world (through our BuilderStatus object, which is its parent).
+ # Finally it will start the actual build process.
+ d = build.startBuild(bs, self.expectations, sb)
+ d.addCallback(self.buildFinished, sb)
+ d.addErrback(log.err) # this shouldn't happen. if it does, the slave
+ # will be wedged
+ for req in build.requests:
+ req.buildStarted(build, bs)
+ return build # this is the IBuildControl
+
+ def _startBuildFailed(self, why, build, sb):
+ # put the build back on the buildable list
+ log.msg("I tried to tell the slave that the build %s started, but "
+ "remote_startBuild failed: %s" % (build, why))
+ # release the slave. This will queue a call to maybeStartBuild, which
+ # will fire after other notifyOnDisconnect handlers have marked the
+ # slave as disconnected (so we don't try to use it again).
+ sb.buildFinished()
+
+ log.msg("re-queueing the BuildRequest")
+ self.building.remove(build)
+ for req in build.requests:
+ self.buildable.insert(0, req) # the interrupted build gets first
+ # priority
+ self.builder_status.addBuildRequest(req.status)
+
+
+ def buildFinished(self, build, sb):
+ """This is called when the Build has finished (either success or
+ failure). Any exceptions during the build are reported with
+ results=FAILURE, not with an errback."""
+
+ # by the time we get here, the Build has already released the slave
+ # (which queues a call to maybeStartBuild)
+
+ self.building.remove(build)
+ for req in build.requests:
+ req.finished(build.build_status)
+
+ def setExpectations(self, progress):
+ """Mark the build as successful and update expectations for the next
+ build. Only call this when the build did not fail in any way that
+ would invalidate the time expectations generated by it. (if the
+ compile failed and thus terminated early, we can't use the last
+ build to predict how long the next one will take).
+ """
+ if self.expectations:
+ self.expectations.update(progress)
+ else:
+ # the first time we get a good build, create our Expectations
+ # based upon its results
+ self.expectations = Expectations(progress)
+ log.msg("new expectations: %s seconds" % \
+ self.expectations.expectedBuildTime())
+
+ def shutdownSlave(self):
+ if self.remote:
+ self.remote.callRemote("shutdown")
+
+
+class BuilderControl(components.Adapter):
+ implements(interfaces.IBuilderControl)
+
+ def requestBuild(self, req):
+ """Submit a BuildRequest to this Builder."""
+ self.original.submitBuildRequest(req)
+
+ def requestBuildSoon(self, req):
+ """Submit a BuildRequest like requestBuild, but raise a
+ L{buildbot.interfaces.NoSlaveError} if no slaves are currently
+ available, so it cannot be used to queue a BuildRequest in the hopes
+ that a slave will eventually connect. This method is appropriate for
+ use by things like the web-page 'Force Build' button."""
+ if not self.original.slaves:
+ raise interfaces.NoSlaveError
+ self.requestBuild(req)
+
+ def resubmitBuild(self, bs, reason="<rebuild, no reason given>"):
+ if not bs.isFinished():
+ return
+
+ ss = bs.getSourceStamp(absolute=True)
+ req = base.BuildRequest(reason, ss, self.original.name)
+ self.requestBuild(req)
+
+ def getPendingBuilds(self):
+ # return IBuildRequestControl objects
+ raise NotImplementedError
+
+ def getBuild(self, number):
+ return self.original.getBuild(number)
+
+ def ping(self, timeout=30):
+ if not self.original.slaves:
+ self.original.builder_status.addPointEvent(["ping", "no slave"])
+ return defer.succeed(False) # interfaces.NoSlaveError
+ dl = []
+ for s in self.original.slaves:
+ dl.append(s.ping(timeout, self.original.builder_status))
+ d = defer.DeferredList(dl)
+ d.addCallback(self._gatherPingResults)
+ return d
+
+ def _gatherPingResults(self, res):
+ for ignored,success in res:
+ if not success:
+ return False
+ return True
+
+components.registerAdapter(BuilderControl, Builder, interfaces.IBuilderControl)