1
2 import random, weakref
3 from zope.interface import implements
4 from twisted.python import log
5 from twisted.python.failure import Failure
6 from twisted.spread import pb
7 from twisted.application import service, internet
8 from twisted.internet import defer
9
10 from buildbot import interfaces, util
11 from buildbot.status.progress import Expectations
12 from buildbot.status.builder import RETRY
13 from buildbot.process.properties import Properties
14 from buildbot.util.eventual import eventually
15
16 (ATTACHING,
17 IDLE,
18 PINGING,
19 BUILDING,
20 LATENT,
21 SUBSTANTIATING,
22 ) = range(6)
23
24
26 """I am the master-side representative for one of the
27 L{buildbot.slave.bot.SlaveBuilder} objects that lives in a remote
28 buildbot. When a remote builder connects, I query it for command versions
29 and then make it available to any Builds that are ready to run. """
30
32 self.ping_watchers = []
33 self.state = None
34 self.remote = None
35 self.slave = None
36 self.builder_name = None
37 self.locks = None
38
40 r = ["<", self.__class__.__name__]
41 if self.builder_name:
42 r.extend([" builder=", repr(self.builder_name)])
43 if self.slave:
44 r.extend([" slave=", repr(self.slave.slavename)])
45 r.append(">")
46 return ''.join(r)
47
51
53 if self.remoteCommands is None:
54
55 return oldversion
56 return self.remoteCommands.get(command)
57
59
60 if self.isBusy():
61 return False
62
63
64 if self.slave:
65 return self.slave.canStartBuild()
66
67
68 return False
69
72
75
79
80 - def attached(self, slave, remote, commands):
81 """
82 @type slave: L{buildbot.buildslave.BuildSlave}
83 @param slave: the BuildSlave that represents the buildslave as a
84 whole
85 @type remote: L{twisted.spread.pb.RemoteReference}
86 @param remote: a reference to the L{buildbot.slave.bot.SlaveBuilder}
87 @type commands: dict: string -> string, or None
88 @param commands: provides the slave's version of each RemoteCommand
89 """
90 self.state = ATTACHING
91 self.remote = remote
92 self.remoteCommands = commands
93 if self.slave is None:
94 self.slave = slave
95 self.slave.addSlaveBuilder(self)
96 else:
97 assert self.slave == slave
98 log.msg("Buildslave %s attached to %s" % (slave.slavename,
99 self.builder_name))
100 def _attachFailure(why, where):
101 log.msg(where)
102 log.err(why)
103 return why
104
105 d = defer.succeed(None)
106 def doSetMaster(res):
107 d = self.remote.callRemote("setMaster", self)
108
109 return d
110 d.addCallback(doSetMaster)
111 def doPrint(res):
112 d = self.remote.callRemote("print", "attached")
113
114 return d
115 d.addCallback(doPrint)
116 def setIdle(res):
117 self.state = IDLE
118 return self
119 d.addCallback(setIdle)
120 return d
121
122 - def prepare(self, builder_status):
123 if not self.slave.acquireLocks():
124 return defer.succeed(False)
125 return defer.succeed(True)
126
127 - def ping(self, status=None):
128 """Ping the slave to make sure it is still there. Returns a Deferred
129 that fires with True if it is.
130
131 @param status: if you point this at a BuilderStatus, a 'pinging'
132 event will be pushed.
133 """
134 oldstate = self.state
135 self.state = PINGING
136 newping = not self.ping_watchers
137 d = defer.Deferred()
138 self.ping_watchers.append(d)
139 if newping:
140 if status:
141 event = status.addEvent(["pinging"])
142 d2 = defer.Deferred()
143 d2.addCallback(self._pong_status, event)
144 self.ping_watchers.insert(0, d2)
145
146
147 Ping().ping(self.remote).addCallback(self._pong)
148
149 def reset_state(res):
150 if self.state == PINGING:
151 self.state = oldstate
152 return res
153 d.addCallback(reset_state)
154 return d
155
157 watchers, self.ping_watchers = self.ping_watchers, []
158 for d in watchers:
159 d.callback(res)
160
162 if res:
163 event.text = ["ping", "success"]
164 else:
165 event.text = ["ping", "failed"]
166 event.finish()
167
169 log.msg("Buildslave %s detached from %s" % (self.slave.slavename,
170 self.builder_name))
171 if self.slave:
172 self.slave.removeSlaveBuilder(self)
173 self.slave = None
174 self.remote = None
175 self.remoteCommands = None
176
177
179 running = False
180
181 - def ping(self, remote):
182 assert not self.running
183 if not remote:
184
185 return defer.succeed(False)
186 self.running = True
187 log.msg("sending ping")
188 self.d = defer.Deferred()
189
190
191 remote.callRemote("print", "ping").addCallbacks(self._pong,
192 self._ping_failed,
193 errbackArgs=(remote,))
194 return self.d
195
197 log.msg("ping finished: success")
198 self.d.callback(True)
199
201 log.msg("ping finished: failure")
202
203
204
205 remote.broker.transport.loseConnection()
206
207
208
209 self.d.callback(False)
210
211
235
236
246
247 - def prepare(self, builder_status):
248
249 if not self.slave.acquireLocks():
250 return defer.succeed(False)
251
252 log.msg("substantiating slave %s" % (self,))
253 d = self.substantiate()
254 def substantiation_failed(f):
255 builder_status.addPointEvent(['removing', 'latent',
256 self.slave.slavename])
257 self.slave.disconnect()
258
259 return f
260 def substantiation_cancelled(res):
261
262 if not res:
263 self.state = LATENT
264 return res
265 d.addCallback(substantiation_cancelled)
266 d.addErrback(substantiation_failed)
267 return d
268
270 self.state = SUBSTANTIATING
271 d = self.slave.substantiate(self)
272 if not self.slave.substantiated:
273 event = self.builder.builder_status.addEvent(
274 ["substantiating"])
275 def substantiated(res):
276 msg = ["substantiate", "success"]
277 if isinstance(res, basestring):
278 msg.append(res)
279 elif isinstance(res, (tuple, list)):
280 msg.extend(res)
281 event.text = msg
282 event.finish()
283 return res
284 def substantiation_failed(res):
285 event.text = ["substantiate", "failed"]
286
287 event.finish()
288 return res
289 d.addCallbacks(substantiated, substantiation_failed)
290 return d
291
295
299
303
307
308 - def ping(self, status=None):
314
315
316 -class Builder(pb.Referenceable, service.MultiService):
317 """I manage all Builds of a given type.
318
319 Each Builder is created by an entry in the config file (the c['builders']
320 list), with a number of parameters.
321
322 One of these parameters is the L{buildbot.process.factory.BuildFactory}
323 object that is associated with this Builder. The factory is responsible
324 for creating new L{Build<buildbot.process.base.Build>} objects. Each
325 Build object defines when and how the build is performed, so a new
326 Factory or Builder should be defined to control this behavior.
327
328 The Builder holds on to a number of L{base.BuildRequest} objects in a
329 list named C{.buildable}. Incoming BuildRequest objects will be added to
330 this list, or (if possible) merged into an existing request. When a slave
331 becomes available, I will use my C{BuildFactory} to turn the request into
332 a new C{Build} object. The C{BuildRequest} is forgotten, the C{Build}
333 goes into C{.building} while it runs. Once the build finishes, I will
334 discard it.
335
336 I maintain a list of available SlaveBuilders, one for each connected
337 slave that the C{slavenames} parameter says we can use. Some of these
338 will be idle, some of them will be busy running builds for me. If there
339 are multiple slaves, I can run multiple builds at once.
340
341 I also manage forced builds, progress expectation (ETA) management, and
342 some status delivery chores.
343
344 @type buildable: list of L{buildbot.process.base.BuildRequest}
345 @ivar buildable: BuildRequests that are ready to build, but which are
346 waiting for a buildslave to be available.
347
348 @type building: list of L{buildbot.process.base.Build}
349 @ivar building: Builds that are actively running
350
351 @type slaves: list of L{buildbot.buildslave.BuildSlave} objects
352 @ivar slaves: the slaves currently available for building
353 """
354
355 expectations = None
356 CHOOSE_SLAVES_RANDOMLY = True
357
358 - def __init__(self, setup, builder_status):
359 """
360 @type setup: dict
361 @param setup: builder setup data, as stored in
362 BuildmasterConfig['builders']. Contains name,
363 slavename(s), builddir, slavebuilddir, factory, locks.
364 @type builder_status: L{buildbot.status.builder.BuilderStatus}
365 """
366 service.MultiService.__init__(self)
367 self.name = setup['name']
368 self.slavenames = []
369 if setup.has_key('slavename'):
370 self.slavenames.append(setup['slavename'])
371 if setup.has_key('slavenames'):
372 self.slavenames.extend(setup['slavenames'])
373 self.builddir = setup['builddir']
374 self.slavebuilddir = setup['slavebuilddir']
375 self.buildFactory = setup['factory']
376 self.nextSlave = setup.get('nextSlave')
377 if self.nextSlave is not None and not callable(self.nextSlave):
378 raise ValueError("nextSlave must be callable")
379 self.locks = setup.get("locks", [])
380 self.env = setup.get('env', {})
381 assert isinstance(self.env, dict)
382 if setup.has_key('periodicBuildTime'):
383 raise ValueError("periodicBuildTime can no longer be defined as"
384 " part of the Builder: use scheduler.Periodic"
385 " instead")
386 self.nextBuild = setup.get('nextBuild')
387 if self.nextBuild is not None and not callable(self.nextBuild):
388 raise ValueError("nextBuild must be callable")
389 self.buildHorizon = setup.get('buildHorizon')
390 self.logHorizon = setup.get('logHorizon')
391 self.eventHorizon = setup.get('eventHorizon')
392 self.mergeRequests = setup.get('mergeRequests', True)
393 self.properties = setup.get('properties', {})
394
395
396 self.building = []
397
398 self.old_building = weakref.WeakKeyDictionary()
399
400
401
402 self.attaching_slaves = []
403
404
405
406
407 self.slaves = []
408
409 self.builder_status = builder_status
410 self.builder_status.setSlavenames(self.slavenames)
411 self.builder_status.buildHorizon = self.buildHorizon
412 self.builder_status.logHorizon = self.logHorizon
413 self.builder_status.eventHorizon = self.eventHorizon
414 t = internet.TimerService(10*60, self.reclaimAllBuilds)
415 t.setServiceParent(self)
416
417
418 self.watchers = {'attach': [], 'detach': [], 'detach_all': [],
419 'idle': []}
420 self.run_count = 0
421
423 self.botmaster = botmaster
424 self.db = botmaster.db
425 self.master_name = botmaster.master_name
426 self.master_incarnation = botmaster.master_incarnation
427
429 diffs = []
430 setup_slavenames = []
431 if setup.has_key('slavename'):
432 setup_slavenames.append(setup['slavename'])
433 setup_slavenames.extend(setup.get('slavenames', []))
434 if setup_slavenames != self.slavenames:
435 diffs.append('slavenames changed from %s to %s' \
436 % (self.slavenames, setup_slavenames))
437 if setup['builddir'] != self.builddir:
438 diffs.append('builddir changed from %s to %s' \
439 % (self.builddir, setup['builddir']))
440 if setup['slavebuilddir'] != self.slavebuilddir:
441 diffs.append('slavebuilddir changed from %s to %s' \
442 % (self.slavebuilddir, setup['slavebuilddir']))
443 if setup['factory'] != self.buildFactory:
444 diffs.append('factory changed')
445 if setup.get('locks', []) != self.locks:
446 diffs.append('locks changed from %s to %s' % (self.locks, setup.get('locks')))
447 if setup.get('env', {}) != self.env:
448 diffs.append('env changed from %s to %s' % (self.env, setup.get('env', {})))
449 if setup.get('nextSlave') != self.nextSlave:
450 diffs.append('nextSlave changed from %s to %s' % (self.nextSlave, setup.get('nextSlave')))
451 if setup.get('nextBuild') != self.nextBuild:
452 diffs.append('nextBuild changed from %s to %s' % (self.nextBuild, setup.get('nextBuild')))
453 if setup['buildHorizon'] != self.buildHorizon:
454 diffs.append('buildHorizon changed from %s to %s' % (self.buildHorizon, setup['buildHorizon']))
455 if setup['logHorizon'] != self.logHorizon:
456 diffs.append('logHorizon changed from %s to %s' % (self.logHorizon, setup['logHorizon']))
457 if setup['eventHorizon'] != self.eventHorizon:
458 diffs.append('eventHorizon changed from %s to %s' % (self.eventHorizon, setup['eventHorizon']))
459 return diffs
460
462 return "<Builder '%r' at %d>" % (self.name, id(self))
463
466
468 """Check for work to be done. This should be called any time I might
469 be able to start a job:
470
471 - when the Builder is first created
472 - when a new job has been added to the [buildrequests] DB table
473 - when a slave has connected
474
475 If I have both an available slave and the database contains a
476 BuildRequest that I can handle, I will claim the BuildRequest and
477 start the build. When the build finishes, I will retire the
478 BuildRequest.
479 """
480
481
482
483
484
485 if not self.running:
486 return
487
488 self.run_count += 1
489
490 available_slaves = [sb for sb in self.slaves if sb.isAvailable()]
491 if not available_slaves:
492 self.updateBigStatus()
493 return
494 d = self.db.runInteraction(self._claim_buildreqs, available_slaves)
495 d.addCallback(self._start_builds)
496 return d
497
498
499
500 RECLAIM_INTERVAL = 1*3600
501
503
504 now = util.now()
505 old = now - self.RECLAIM_INTERVAL
506 requests = self.db.get_unclaimed_buildrequests(self.name, old,
507 self.master_name,
508 self.master_incarnation,
509 t)
510
511 assignments = {}
512 while requests and available_slaves:
513 sb = self._choose_slave(available_slaves)
514 if not sb:
515 log.msg("%s: want to start build, but we don't have a remote"
516 % self)
517 break
518 available_slaves.remove(sb)
519 breq = self._choose_build(requests)
520 if not breq:
521 log.msg("%s: went to start build, but nextBuild said not to"
522 % self)
523 break
524 requests.remove(breq)
525 merged_requests = [breq]
526 for other_breq in requests[:]:
527 if (self.mergeRequests and
528 self.botmaster.shouldMergeRequests(self, breq, other_breq)
529 ):
530 requests.remove(other_breq)
531 merged_requests.append(other_breq)
532 assignments[sb] = merged_requests
533 brids = [br.id for br in merged_requests]
534 self.db.claim_buildrequests(now, self.master_name,
535 self.master_incarnation, brids, t)
536 return assignments
537
539
540
541 if self.nextSlave:
542 try:
543 return self.nextSlave(self, available_slaves)
544 except:
545 log.msg("Exception choosing next slave")
546 log.err(Failure())
547 return None
548 if self.CHOOSE_SLAVES_RANDOMLY:
549 return random.choice(available_slaves)
550 return available_slaves[0]
551
553 if self.nextBuild:
554 try:
555 return self.nextBuild(self, buildable)
556 except:
557 log.msg("Exception choosing next build")
558 log.err(Failure())
559 return None
560 return buildable[0]
561
575
576
587
589 """Returns the timestamp of the oldest build request for this builder.
590
591 If there are no build requests, None is returned."""
592 buildable = self.getBuildable(1)
593 if buildable:
594
595 return buildable[0].getSubmitTime()
596 return None
597
600
602 """Suck the brain out of an old Builder.
603
604 This takes all the runtime state from an existing Builder and moves
605 it into ourselves. This is used when a Builder is changed in the
606 master.cfg file: the new Builder has a different factory, but we want
607 all the builds that were queued for the old one to get processed by
608 the new one. Any builds which are already running will keep running.
609 The new Builder will get as many of the old SlaveBuilder objects as
610 it wants."""
611
612 log.msg("consumeTheSoulOfYourPredecessor: %s feeding upon %s" %
613 (self, old))
614
615
616
617
618
619
620
621
622
623 if old.building:
624 self.builder_status.setBigState("building")
625
626
627
628
629 for b in old.building:
630 self.old_building[b] = None
631 for b in old.old_building:
632 self.old_building[b] = None
633
634
635
636 for sb in old.slaves[:]:
637 if sb.slave.slavename in self.slavenames:
638 log.msg(" stealing buildslave %s" % sb)
639 self.slaves.append(sb)
640 old.slaves.remove(sb)
641 sb.setBuilder(self)
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662 return
663
665 now = util.now()
666 brids = set()
667 for b in self.building:
668 brids.update([br.id for br in b.requests])
669 for b in self.old_building:
670 brids.update([br.id for br in b.requests])
671 self.db.claim_buildrequests(now, self.master_name,
672 self.master_incarnation, brids)
673
682
690
702
703 - def attached(self, slave, remote, commands):
704 """This is invoked by the BuildSlave when the self.slavename bot
705 registers their builder.
706
707 @type slave: L{buildbot.buildslave.BuildSlave}
708 @param slave: the BuildSlave that represents the buildslave as a whole
709 @type remote: L{twisted.spread.pb.RemoteReference}
710 @param remote: a reference to the L{buildbot.slave.bot.SlaveBuilder}
711 @type commands: dict: string -> string, or None
712 @param commands: provides the slave's version of each RemoteCommand
713
714 @rtype: L{twisted.internet.defer.Deferred}
715 @return: a Deferred that fires (with 'self') when the slave-side
716 builder is fully attached and ready to accept commands.
717 """
718 for s in self.attaching_slaves + self.slaves:
719 if s.slave == slave:
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736 return defer.succeed(self)
737
738 sb = SlaveBuilder()
739 sb.setBuilder(self)
740 self.attaching_slaves.append(sb)
741 d = sb.attached(slave, remote, commands)
742 d.addCallback(self._attached)
743 d.addErrback(self._not_attached, slave)
744 return d
745
754
765
767 """This is called when the connection to the bot is lost."""
768 for sb in self.attaching_slaves + self.slaves:
769 if sb.slave == slave:
770 break
771 else:
772 log.msg("WEIRD: Builder.detached(%s) (%s)"
773 " not in attaching_slaves(%s)"
774 " or slaves(%s)" % (slave, slave.slavename,
775 self.attaching_slaves,
776 self.slaves))
777 return
778 if sb.state == BUILDING:
779
780
781
782
783
784
785 pass
786
787 if sb in self.attaching_slaves:
788 self.attaching_slaves.remove(sb)
789 if sb in self.slaves:
790 self.slaves.remove(sb)
791
792
793 self.builder_status.addPointEvent(['disconnect', slave.slavename])
794 sb.detached()
795 self.updateBigStatus()
796 self.fireTestEvent('detach')
797 if not self.slaves:
798 self.fireTestEvent('detach_all')
799
808
810 """Start a build on the given slave.
811 @param build: the L{base.Build} to start
812 @param sb: the L{SlaveBuilder} which will host this build
813
814 @return: a Deferred which fires with a
815 L{buildbot.interfaces.IBuildControl} that can be used to stop the
816 Build, or to access a L{buildbot.interfaces.IBuildStatus} which will
817 watch the Build as it runs. """
818
819 self.building.append(build)
820 self.updateBigStatus()
821 log.msg("starting build %s using slave %s" % (build, sb))
822 d = sb.prepare(self.builder_status)
823
824 def _prepared(ready):
825
826
827 d = defer.succeed(ready)
828
829 if not ready:
830
831
832 log.msg("slave %s can't build %s after all" % (build, sb))
833
834
835
836
837
838
839 log.msg("re-queueing the BuildRequest %s" % build)
840 self.building.remove(build)
841 self._resubmit_buildreqs(build).addErrback(log.err)
842
843 sb.slave.releaseLocks()
844 self.triggerNewBuildCheck()
845
846 return d
847
848 def _ping(ign):
849
850
851
852
853
854
855
856
857 log.msg("starting build %s.. pinging the slave %s" % (build, sb))
858 return sb.ping()
859 d.addCallback(_ping)
860 d.addCallback(self._startBuild_1, build, sb)
861
862 return d
863
864 d.addCallback(_prepared)
865 return d
866
868 if not res:
869 return self._startBuildFailed("slave ping failed", build, sb)
870
871
872
873 sb.buildStarted()
874 d = sb.remote.callRemote("startBuild")
875 d.addCallbacks(self._startBuild_2, self._startBuildFailed,
876 callbackArgs=(build,sb), errbackArgs=(build,sb))
877 return d
878
893
895
896 log.msg("I tried to tell the slave that the build %s started, but "
897 "remote_startBuild failed: %s" % (build, why))
898
899
900
901 sb.buildFinished()
902
903 log.msg("re-queueing the BuildRequest")
904 self.building.remove(build)
905 self._resubmit_buildreqs(build).addErrback(log.err)
906
912
935
939
941 """Mark the build as successful and update expectations for the next
942 build. Only call this when the build did not fail in any way that
943 would invalidate the time expectations generated by it. (if the
944 compile failed and thus terminated early, we can't use the last
945 build to predict how long the next one will take).
946 """
947 if self.expectations:
948 self.expectations.update(progress)
949 else:
950
951
952 self.expectations = Expectations(progress)
953 log.msg("new expectations: %s seconds" % \
954 self.expectations.expectedBuildTime())
955
959
960
962 implements(interfaces.IBuilderControl)
963
967
973
974 - def rebuildBuild(self, bs, reason="<rebuild, no reason given>", extraProperties=None):
986
994
997
999 if not self.original.slaves:
1000 self.original.builder_status.addPointEvent(["ping", "no slave"])
1001 return defer.succeed(False)
1002 dl = []
1003 for s in self.original.slaves:
1004 dl.append(s.ping(self.original.builder_status))
1005 d = defer.DeferredList(dl)
1006 d.addCallback(self._gatherPingResults)
1007 return d
1008
1010 for ignored,success in res:
1011 if not success:
1012 return False
1013 return True
1014
1016 implements(interfaces.IBuildRequestControl)
1017
1019 self.original_builder = builder
1020 self.original_request = request
1021 self.brid = request.id
1022
1024 raise NotImplementedError
1025
1027 raise NotImplementedError
1028
1031