add max_builds= to BuildSlave, thanks to Dustin Mitchell. Closes #48.
[buildbot.git] / buildbot / buildslave.py
blob4e5d0c6884789cd879f9d3d885179b87c3acb6ce
2 from twisted.python import log
3 from twisted.internet import defer, reactor
5 from buildbot.pbutil import NewCredPerspective
6 from buildbot.status.builder import SlaveStatus
8 class BuildSlave(NewCredPerspective):
9 """This is the master-side representative for a remote buildbot slave.
10 There is exactly one for each slave described in the config file (the
11 c['slaves'] list). When buildbots connect in (.attach), they get a
12 reference to this instance. The BotMaster object is stashed as the
13 .service attribute.
15 I represent a build slave -- a remote machine capable of
16 running builds. I am instantiated by the configuration file, and can be
17 subclassed to add extra functionality."""
19 def __init__(self, name, password, max_builds=None):
20 """
21 @param name: botname this machine will supply when it connects
22 @param password: password this machine will supply when
23 it connects
24 @param max_builds: maximum number of simultaneous builds that will
25 be run concurrently on this buildslave (the
26 default is None for no limit)
27 """
29 self.slavename = name
30 self.password = password
31 self.botmaster = None # no buildmaster yet
32 self.slave_status = SlaveStatus(name)
33 self.slave = None # a RemoteReference to the Bot, when connected
34 self.slave_commands = None
35 self.slavebuilders = []
36 self.max_builds = max_builds
38 def update(self, new):
39 """
40 Given a new BuildSlave, configure this one identically. Because
41 BuildSlave objects are remotely referenced, we can't replace them
42 without disconnecting the slave, yet there's no reason to do that.
43 """
44 # the reconfiguration logic should guarantee this:
45 assert self.slavename == new.slavename
46 assert self.password == new.password
47 assert self.__class__ == new.__class__
48 self.max_builds = new.max_builds
50 def __repr__(self):
51 builders = self.botmaster.getBuildersForSlave(self.slavename)
52 return "<BuildSlave '%s', current builders: %s>" % \
53 (self.slavename, ','.join(map(lambda b: b.name, builders)))
55 def setBotmaster(self, botmaster):
56 assert not self.botmaster, "BuildSlave already has a botmaster"
57 self.botmaster = botmaster
59 def updateSlave(self):
60 """Called to add or remove builders after the slave has connected.
62 @return: a Deferred that indicates when an attached slave has
63 accepted the new builders and/or released the old ones."""
64 if self.slave:
65 return self.sendBuilderList()
66 return defer.succeed(None)
68 def attached(self, bot):
69 """This is called when the slave connects.
71 @return: a Deferred that fires with a suitable pb.IPerspective to
72 give to the slave (i.e. 'self')"""
74 if self.slave:
75 # uh-oh, we've got a duplicate slave. The most likely
76 # explanation is that the slave is behind a slow link, thinks we
77 # went away, and has attempted to reconnect, so we've got two
78 # "connections" from the same slave, but the previous one is
79 # stale. Give the new one precedence.
80 log.msg("duplicate slave %s replacing old one" % self.slavename)
82 # just in case we've got two identically-configured slaves,
83 # report the IP addresses of both so someone can resolve the
84 # squabble
85 tport = self.slave.broker.transport
86 log.msg("old slave was connected from", tport.getPeer())
87 log.msg("new slave is from", bot.broker.transport.getPeer())
88 d = self.disconnect()
89 else:
90 d = defer.succeed(None)
91 # now we go through a sequence of calls, gathering information, then
92 # tell the Botmaster that it can finally give this slave to all the
93 # Builders that care about it.
95 # we accumulate slave information in this 'state' dictionary, then
96 # set it atomically if we make it far enough through the process
97 state = {}
99 def _log_attachment_on_slave(res):
100 d1 = bot.callRemote("print", "attached")
101 d1.addErrback(lambda why: None)
102 return d1
103 d.addCallback(_log_attachment_on_slave)
105 def _get_info(res):
106 d1 = bot.callRemote("getSlaveInfo")
107 def _got_info(info):
108 log.msg("Got slaveinfo from '%s'" % self.slavename)
109 # TODO: info{} might have other keys
110 state["admin"] = info.get("admin")
111 state["host"] = info.get("host")
112 def _info_unavailable(why):
113 # maybe an old slave, doesn't implement remote_getSlaveInfo
114 log.msg("BuildSlave.info_unavailable")
115 log.err(why)
116 d1.addCallbacks(_got_info, _info_unavailable)
117 return d1
118 d.addCallback(_get_info)
120 def _get_commands(res):
121 d1 = bot.callRemote("getCommands")
122 def _got_commands(commands):
123 state["slave_commands"] = commands
124 def _commands_unavailable(why):
125 # probably an old slave
126 log.msg("BuildSlave._commands_unavailable")
127 if why.check(AttributeError):
128 return
129 log.err(why)
130 d1.addCallbacks(_got_commands, _commands_unavailable)
131 return d1
132 d.addCallback(_get_commands)
134 def _accept_slave(res):
135 self.slave_status.setAdmin(state.get("admin"))
136 self.slave_status.setHost(state.get("host"))
137 self.slave_status.setConnected(True)
138 self.slave_commands = state.get("slave_commands")
139 self.slave = bot
140 log.msg("bot attached")
141 return self.updateSlave()
142 d.addCallback(_accept_slave)
144 # Finally, the slave gets a reference to this BuildSlave. They
145 # receive this later, after we've started using them.
146 d.addCallback(lambda res: self)
147 return d
149 def detached(self, mind):
150 self.slave = None
151 self.slave_status.setConnected(False)
152 self.botmaster.slaveLost(self)
153 log.msg("BuildSlave.detached(%s)" % self.slavename)
156 def disconnect(self):
157 """Forcibly disconnect the slave.
159 This severs the TCP connection and returns a Deferred that will fire
160 (with None) when the connection is probably gone.
162 If the slave is still alive, they will probably try to reconnect
163 again in a moment.
165 This is called in two circumstances. The first is when a slave is
166 removed from the config file. In this case, when they try to
167 reconnect, they will be rejected as an unknown slave. The second is
168 when we wind up with two connections for the same slave, in which
169 case we disconnect the older connection.
172 if not self.slave:
173 return defer.succeed(None)
174 log.msg("disconnecting old slave %s now" % self.slavename)
176 # all kinds of teardown will happen as a result of
177 # loseConnection(), but it happens after a reactor iteration or
178 # two. Hook the actual disconnect so we can know when it is safe
179 # to connect the new slave. We have to wait one additional
180 # iteration (with callLater(0)) to make sure the *other*
181 # notifyOnDisconnect handlers have had a chance to run.
182 d = defer.Deferred()
184 # notifyOnDisconnect runs the callback with one argument, the
185 # RemoteReference being disconnected.
186 def _disconnected(rref):
187 reactor.callLater(0, d.callback, None)
188 self.slave.notifyOnDisconnect(_disconnected)
189 tport = self.slave.broker.transport
190 # this is the polite way to request that a socket be closed
191 tport.loseConnection()
192 try:
193 # but really we don't want to wait for the transmit queue to
194 # drain. The remote end is unlikely to ACK the data, so we'd
195 # probably have to wait for a (20-minute) TCP timeout.
196 #tport._closeSocket()
197 # however, doing _closeSocket (whether before or after
198 # loseConnection) somehow prevents the notifyOnDisconnect
199 # handlers from being run. Bummer.
200 tport.offset = 0
201 tport.dataBuffer = ""
202 pass
203 except:
204 # however, these hacks are pretty internal, so don't blow up if
205 # they fail or are unavailable
206 log.msg("failed to accelerate the shutdown process")
207 pass
208 log.msg("waiting for slave to finish disconnecting")
210 # When this Deferred fires, we'll be ready to accept the new slave
211 return d
213 def sendBuilderList(self):
214 our_builders = self.botmaster.getBuildersForSlave(self.slavename)
215 blist = [(b.name, b.builddir) for b in our_builders]
216 d = self.slave.callRemote("setBuilderList", blist)
217 def _sent(slist):
218 dl = []
219 for name, remote in slist.items():
220 # use get() since we might have changed our mind since then
221 b = self.botmaster.builders.get(name)
222 if b:
223 d1 = b.attached(self, remote, self.slave_commands)
224 dl.append(d1)
225 return defer.DeferredList(dl)
226 def _set_failed(why):
227 log.msg("BuildSlave.sendBuilderList (%s) failed" % self)
228 log.err(why)
229 # TODO: hang up on them?, without setBuilderList we can't use
230 # them
231 d.addCallbacks(_sent, _set_failed)
232 return d
234 def perspective_keepalive(self):
235 pass
237 def addSlaveBuilder(self, sb):
238 log.msg("%s adding %s" % (self, sb))
239 self.slavebuilders.append(sb)
241 def removeSlaveBuilder(self, sb):
242 log.msg("%s removing %s" % (self, sb))
243 if sb in self.slavebuilders:
244 self.slavebuilders.remove(sb)
246 def canStartBuild(self):
248 I am called when a build is requested to see if this buildslave
249 can start a build. This function can be used to limit overall
250 concurrency on the buildslave.
252 if self.max_builds:
253 active_builders = [sb for sb in self.slavebuilders if sb.isBusy()]
254 if len(active_builders) >= self.max_builds:
255 return False
256 return True