1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 import random, weakref
18 from zope.interface import implements
19 from twisted.python import log, failure
20 from twisted.spread import pb
21 from twisted.application import service, internet
22 from twisted.internet import defer
23
24 from buildbot import interfaces
25 from buildbot.status.progress import Expectations
26 from buildbot.status.builder import RETRY
27 from buildbot.status.buildrequest import BuildRequestStatus
28 from buildbot.process.properties import Properties
29 from buildbot.process import buildrequest, slavebuilder
30 from buildbot.process.slavebuilder import BUILDING
31 from buildbot.db import buildrequests
32
33 -class Builder(pb.Referenceable, service.MultiService):
34 """I manage all Builds of a given type.
35
36 Each Builder is created by an entry in the config file (the c['builders']
37 list), with a number of parameters.
38
39 One of these parameters is the L{buildbot.process.factory.BuildFactory}
40 object that is associated with this Builder. The factory is responsible
41 for creating new L{Build<buildbot.process.build.Build>} objects. Each
42 Build object defines when and how the build is performed, so a new
43 Factory or Builder should be defined to control this behavior.
44
45 The Builder holds on to a number of L{BuildRequest} objects in a
46 list named C{.buildable}. Incoming BuildRequest objects will be added to
47 this list, or (if possible) merged into an existing request. When a slave
48 becomes available, I will use my C{BuildFactory} to turn the request into
49 a new C{Build} object. The C{BuildRequest} is forgotten, the C{Build}
50 goes into C{.building} while it runs. Once the build finishes, I will
51 discard it.
52
53 I maintain a list of available SlaveBuilders, one for each connected
54 slave that the C{slavenames} parameter says we can use. Some of these
55 will be idle, some of them will be busy running builds for me. If there
56 are multiple slaves, I can run multiple builds at once.
57
58 I also manage forced builds, progress expectation (ETA) management, and
59 some status delivery chores.
60
61 @type buildable: list of L{buildbot.process.buildrequest.BuildRequest}
62 @ivar buildable: BuildRequests that are ready to build, but which are
63 waiting for a buildslave to be available.
64
65 @type building: list of L{buildbot.process.build.Build}
66 @ivar building: Builds that are actively running
67
68 @type slaves: list of L{buildbot.buildslave.BuildSlave} objects
69 @ivar slaves: the slaves currently available for building
70 """
71
72 expectations = None
73
74 - def __init__(self, setup, builder_status):
75 """
76 @type setup: dict
77 @param setup: builder setup data, as stored in
78 BuildmasterConfig['builders']. Contains name,
79 slavename(s), builddir, slavebuilddir, factory, locks.
80 @type builder_status: L{buildbot.status.builder.BuilderStatus}
81 """
82 service.MultiService.__init__(self)
83 self.name = setup['name']
84 self.slavenames = []
85 if setup.has_key('slavename'):
86 self.slavenames.append(setup['slavename'])
87 if setup.has_key('slavenames'):
88 self.slavenames.extend(setup['slavenames'])
89 self.builddir = setup['builddir']
90 self.slavebuilddir = setup['slavebuilddir']
91 self.buildFactory = setup['factory']
92 self.nextSlave = setup.get('nextSlave')
93 if self.nextSlave is not None and not callable(self.nextSlave):
94 raise ValueError("nextSlave must be callable")
95 self.locks = setup.get("locks", [])
96 self.env = setup.get('env', {})
97 assert isinstance(self.env, dict)
98 if setup.has_key('periodicBuildTime'):
99 raise ValueError("periodicBuildTime can no longer be defined as"
100 " part of the Builder: use scheduler.Periodic"
101 " instead")
102 self.nextBuild = setup.get('nextBuild')
103 if self.nextBuild is not None and not callable(self.nextBuild):
104 raise ValueError("nextBuild must be callable")
105 self.buildHorizon = setup.get('buildHorizon')
106 self.logHorizon = setup.get('logHorizon')
107 self.eventHorizon = setup.get('eventHorizon')
108 self.mergeRequests = setup.get('mergeRequests', None)
109 self.properties = setup.get('properties', {})
110 self.category = setup.get('category', None)
111
112
113 self.building = []
114
115 self.old_building = weakref.WeakKeyDictionary()
116
117
118
119 self.attaching_slaves = []
120
121
122
123
124 self.slaves = []
125
126 self.builder_status = builder_status
127 self.builder_status.setSlavenames(self.slavenames)
128 self.builder_status.buildHorizon = self.buildHorizon
129 self.builder_status.logHorizon = self.logHorizon
130 self.builder_status.eventHorizon = self.eventHorizon
131
132 self.reclaim_svc = internet.TimerService(10*60, self.reclaimAllBuilds)
133 self.reclaim_svc.setServiceParent(self)
134
135
136 self.run_count = 0
137
139 d = defer.maybeDeferred(lambda :
140 service.MultiService.stopService(self))
141 def flushMaybeStartBuilds(_):
142
143
144
145 return self.maybeStartBuild()
146 d.addCallback(flushMaybeStartBuilds)
147 return d
148
153
155 diffs = []
156 setup_slavenames = []
157 if setup.has_key('slavename'):
158 setup_slavenames.append(setup['slavename'])
159 setup_slavenames.extend(setup.get('slavenames', []))
160 if setup_slavenames != self.slavenames:
161 diffs.append('slavenames changed from %s to %s' \
162 % (self.slavenames, setup_slavenames))
163 if setup['builddir'] != self.builddir:
164 diffs.append('builddir changed from %s to %s' \
165 % (self.builddir, setup['builddir']))
166 if setup['slavebuilddir'] != self.slavebuilddir:
167 diffs.append('slavebuilddir changed from %s to %s' \
168 % (self.slavebuilddir, setup['slavebuilddir']))
169 if setup['factory'] != self.buildFactory:
170 diffs.append('factory changed')
171 if setup.get('locks', []) != self.locks:
172 diffs.append('locks changed from %s to %s' % (self.locks, setup.get('locks')))
173 if setup.get('env', {}) != self.env:
174 diffs.append('env changed from %s to %s' % (self.env, setup.get('env', {})))
175 if setup.get('nextSlave') != self.nextSlave:
176 diffs.append('nextSlave changed from %s to %s' % (self.nextSlave, setup.get('nextSlave')))
177 if setup.get('nextBuild') != self.nextBuild:
178 diffs.append('nextBuild changed from %s to %s' % (self.nextBuild, setup.get('nextBuild')))
179 if setup.get('buildHorizon', None) != self.buildHorizon:
180 diffs.append('buildHorizon changed from %s to %s' % (self.buildHorizon, setup['buildHorizon']))
181 if setup.get('logHorizon', None) != self.logHorizon:
182 diffs.append('logHorizon changed from %s to %s' % (self.logHorizon, setup['logHorizon']))
183 if setup.get('eventHorizon', None) != self.eventHorizon:
184 diffs.append('eventHorizon changed from %s to %s' % (self.eventHorizon, setup['eventHorizon']))
185 if setup.get('category', None) != self.category:
186 diffs.append('category changed from %r to %r' % (self.category, setup.get('category', None)))
187
188 return diffs
189
191 return "<Builder '%r' at %d>" % (self.name, id(self))
192
193 @defer.deferredGenerator
195
196 """Returns the submitted_at of the oldest unclaimed build request for
197 this builder, or None if there are no build requests.
198
199 @returns: datetime instance or None, via Deferred
200 """
201 wfd = defer.waitForDeferred(
202 self.master.db.buildrequests.getBuildRequests(
203 buildername=self.name, claimed=False))
204 yield wfd
205 unclaimed = wfd.getResult()
206
207 if unclaimed:
208 unclaimed = [ brd['submitted_at'] for brd in unclaimed ]
209 unclaimed.sort()
210 yield unclaimed[0]
211 else:
212 yield None
213
215 """Suck the brain out of an old Builder.
216
217 This takes all the runtime state from an existing Builder and moves
218 it into ourselves. This is used when a Builder is changed in the
219 master.cfg file: the new Builder has a different factory, but we want
220 all the builds that were queued for the old one to get processed by
221 the new one. Any builds which are already running will keep running.
222 The new Builder will get as many of the old SlaveBuilder objects as
223 it wants."""
224
225 log.msg("consumeTheSoulOfYourPredecessor: %s feeding upon %s" %
226 (self, old))
227
228
229
230
231
232
233 self.builder_status.category = self.category
234
235
236
237
238
239
240 if old.building:
241 self.builder_status.setBigState("building")
242
243
244
245
246 for b in old.building:
247 self.old_building[b] = None
248 for b in old.old_building:
249 self.old_building[b] = None
250
251
252
253 for sb in old.slaves[:]:
254 if sb.slave.slavename in self.slavenames:
255 log.msg(" stealing buildslave %s" % sb)
256 self.slaves.append(sb)
257 old.slaves.remove(sb)
258 sb.setBuilder(self)
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279 return
280
282 brids = set()
283 for b in self.building:
284 brids.update([br.id for br in b.requests])
285 for b in self.old_building:
286 brids.update([br.id for br in b.requests])
287
288 if not brids:
289 return defer.succeed(None)
290
291 d = self.master.db.buildrequests.reclaimBuildRequests(brids)
292 d.addErrback(log.err, 'while re-claiming running BuildRequests')
293 return d
294
303
315
316 - def attached(self, slave, remote, commands):
317 """This is invoked by the BuildSlave when the self.slavename bot
318 registers their builder.
319
320 @type slave: L{buildbot.buildslave.BuildSlave}
321 @param slave: the BuildSlave that represents the buildslave as a whole
322 @type remote: L{twisted.spread.pb.RemoteReference}
323 @param remote: a reference to the L{buildbot.slave.bot.SlaveBuilder}
324 @type commands: dict: string -> string, or None
325 @param commands: provides the slave's version of each RemoteCommand
326
327 @rtype: L{twisted.internet.defer.Deferred}
328 @return: a Deferred that fires (with 'self') when the slave-side
329 builder is fully attached and ready to accept commands.
330 """
331 for s in self.attaching_slaves + self.slaves:
332 if s.slave == slave:
333
334
335
336
337
338
339
340
341
342
343
344 return defer.succeed(self)
345
346 sb = slavebuilder.SlaveBuilder()
347 sb.setBuilder(self)
348 self.attaching_slaves.append(sb)
349 d = sb.attached(slave, remote, commands)
350 d.addCallback(self._attached)
351 d.addErrback(self._not_attached, slave)
352 return d
353
362
370
371
398
400 if not self.slaves:
401 self.builder_status.setBigState("offline")
402 elif self.building:
403 self.builder_status.setBigState("building")
404 else:
405 self.builder_status.setBigState("idle")
406
407 @defer.deferredGenerator
409 """Start a build on the given slave.
410 @param build: the L{base.Build} to start
411 @param sb: the L{SlaveBuilder} which will host this build
412
413 @return: (via Deferred) boolean indicating that the build was
414 succesfully started.
415 """
416
417
418
419
420 cleanups = []
421 def run_cleanups():
422 while cleanups:
423 fn = cleanups.pop()
424 fn()
425
426
427
428 cleanups.append(lambda : self.updateBigStatus())
429
430 build = self.buildFactory.newBuild(buildrequests)
431 build.setBuilder(self)
432 log.msg("starting build %s using slave %s" % (build, slavebuilder))
433
434
435 build.setLocks(self.locks)
436 cleanups.append(lambda : slavebuilder.slave.releaseLocks())
437
438 if len(self.env) > 0:
439 build.setSlaveEnvironment(self.env)
440
441
442 self.building.append(build)
443 cleanups.append(lambda : self.building.remove(build))
444
445
446 self.updateBigStatus()
447
448 try:
449 wfd = defer.waitForDeferred(
450 slavebuilder.prepare(self.builder_status, build))
451 yield wfd
452 ready = wfd.getResult()
453 except:
454 log.err(failure.Failure(), 'while preparing slavebuilder:')
455 ready = False
456
457
458
459 if not ready:
460 log.msg("slave %s can't build %s after all; re-queueing the "
461 "request" % (build, slavebuilder))
462 run_cleanups()
463 yield False
464 return
465
466
467
468
469
470
471
472
473
474 log.msg("starting build %s.. pinging the slave %s"
475 % (build, slavebuilder))
476 try:
477 wfd = defer.waitForDeferred(
478 slavebuilder.ping())
479 yield wfd
480 ping_success = wfd.getResult()
481 except:
482 log.err(failure.Failure(), 'while pinging slave before build:')
483 ping_success = False
484
485 if not ping_success:
486 log.msg("slave ping failed; re-queueing the request")
487 run_cleanups()
488 yield False
489 return
490
491
492
493
494 slavebuilder.buildStarted()
495 cleanups.append(lambda : slavebuilder.buildFinished())
496
497
498 try:
499 wfd = defer.waitForDeferred(
500 slavebuilder.remote.callRemote("startBuild"))
501 yield wfd
502 wfd.getResult()
503 except:
504 log.err(failure.Failure(), 'while calling remote startBuild:')
505 run_cleanups()
506 yield False
507 return
508
509
510 bs = self.builder_status.newBuild()
511
512
513 try:
514 bids = []
515 for req in build.requests:
516 wfd = defer.waitForDeferred(
517 self.master.db.builds.addBuild(req.id, bs.number))
518 yield wfd
519 bids.append(wfd.getResult())
520 except:
521 log.err(failure.Failure(), 'while adding rows to build table:')
522 run_cleanups()
523 yield False
524 return
525
526
527 self.master.status.build_started(req.id, self.name, bs)
528
529
530
531
532
533
534
535 d = build.startBuild(bs, self.expectations, slavebuilder)
536 d.addCallback(self.buildFinished, slavebuilder, bids)
537
538 d.addErrback(log.err)
539
540
541 self.updateBigStatus()
542
543 yield True
544
551
584
585 @defer.deferredGenerator
587
588 for br in requests:
589 wfd = defer.waitForDeferred(
590 self.master.maybeBuildsetComplete(br.bsid))
591 yield wfd
592 wfd.getResult()
593
597
599 """Mark the build as successful and update expectations for the next
600 build. Only call this when the build did not fail in any way that
601 would invalidate the time expectations generated by it. (if the
602 compile failed and thus terminated early, we can't use the last
603 build to predict how long the next one will take).
604 """
605 if self.expectations:
606 self.expectations.update(progress)
607 else:
608
609
610 self.expectations = Expectations(progress)
611 log.msg("new expectations: %s seconds" % \
612 self.expectations.expectedBuildTime())
613
614
615
616 @defer.deferredGenerator
618
619
620
621
622
623
624
625
626 if not self.running:
627 return
628
629
630
631 available_slavebuilders = [ sb for sb in self.slaves
632 if sb.isAvailable() ]
633 if not available_slavebuilders:
634 self.updateBigStatus()
635 return
636
637
638 wfd = defer.waitForDeferred(
639 self.master.db.buildrequests.getBuildRequests(
640 buildername=self.name, claimed=False))
641 yield wfd
642 unclaimed_requests = wfd.getResult()
643
644 if not unclaimed_requests:
645 self.updateBigStatus()
646 return
647
648
649 unclaimed_requests.sort(key=lambda brd : brd['submitted_at'])
650
651
652 mergeRequests_fn = self._getMergeRequestsFn()
653
654
655 while available_slavebuilders and unclaimed_requests:
656
657 wfd = defer.waitForDeferred(
658 self._chooseSlave(available_slavebuilders))
659 yield wfd
660 slavebuilder = wfd.getResult()
661
662 if not slavebuilder:
663 break
664
665 if slavebuilder not in available_slavebuilders:
666 log.msg(("nextSlave chose a nonexistent slave for builder "
667 "'%s'; cannot start build") % self.name)
668 break
669
670
671 wfd = defer.waitForDeferred(
672 self._chooseBuild(unclaimed_requests))
673 yield wfd
674 brdict = wfd.getResult()
675
676 if not brdict:
677 break
678
679 if brdict not in unclaimed_requests:
680 log.msg(("nextBuild chose a nonexistent request for builder "
681 "'%s'; cannot start build") % self.name)
682 break
683
684
685
686 wfd = defer.waitForDeferred(
687 self._mergeRequests(brdict, unclaimed_requests,
688 mergeRequests_fn))
689 yield wfd
690 brdicts = wfd.getResult()
691
692
693 brids = [ brdict['brid'] for brdict in brdicts ]
694 try:
695 wfd = defer.waitForDeferred(
696 self.master.db.buildrequests.claimBuildRequests(brids))
697 yield wfd
698 wfd.getResult()
699 except buildrequests.AlreadyClaimedError:
700
701
702
703 self._breakBrdictRefloops(unclaimed_requests)
704 wfd = defer.waitForDeferred(
705 self.master.db.buildrequests.getBuildRequests(
706 buildername=self.name, claimed=False))
707 yield wfd
708 unclaimed_requests = wfd.getResult()
709
710
711 continue
712
713
714
715
716
717
718
719 wfd = defer.waitForDeferred(
720 defer.gatherResults([ self._brdictToBuildRequest(brdict)
721 for brdict in brdicts ]))
722 yield wfd
723 breqs = wfd.getResult()
724
725 wfd = defer.waitForDeferred(
726 self._startBuildFor(slavebuilder, breqs))
727 yield wfd
728 build_started = wfd.getResult()
729
730 if not build_started:
731
732 wfd = defer.waitForDeferred(
733 self.master.db.buildrequests.unclaimBuildRequests(brids))
734 yield wfd
735 wfd.getResult()
736
737
738
739 self.botmaster.maybeStartBuildsForBuilder(self.name)
740
741
742
743 self._breakBrdictRefloops(brdicts)
744 for brdict in brdicts:
745 unclaimed_requests.remove(brdict)
746 available_slavebuilders.remove(slavebuilder)
747
748 self._breakBrdictRefloops(unclaimed_requests)
749 self.updateBigStatus()
750 return
751
752
753
754
756 """
757 Choose the next slave, using the C{nextSlave} configuration if
758 available, and falling back to C{random.choice} otherwise.
759
760 @param available_slavebuilders: list of slavebuilders to choose from
761 @returns: SlaveBuilder or None via Deferred
762 """
763 if self.nextSlave:
764 return defer.maybeDeferred(lambda :
765 self.nextSlave(self, available_slavebuilders))
766 else:
767 return defer.succeed(random.choice(available_slavebuilders))
768
770 """
771 Choose the next build from the given set of build requests (represented
772 as dictionaries). Defaults to returning the first request (earliest
773 submitted).
774
775 @param buildrequests: sorted list of build request dictionaries
776 @returns: a build request dictionary or None via Deferred
777 """
778 if self.nextBuild:
779
780
781 d = defer.gatherResults([ self._brdictToBuildRequest(brdict)
782 for brdict in buildrequests ])
783 d.addCallback(lambda requestobjects :
784 self.nextBuild(self, requestobjects))
785 def to_brdict(brobj):
786
787 return brobj.brdict
788 d.addCallback(to_brdict)
789 return d
790 else:
791 return defer.succeed(buildrequests[0])
792
794 """Helper function to determine which mergeRequests function to use
795 from L{_mergeRequests}, or None for no merging"""
796
797 mergeRequests_fn = self.mergeRequests
798 if mergeRequests_fn is None:
799 mergeRequests_fn = self.botmaster.mergeRequests
800 if mergeRequests_fn is None:
801 mergeRequests_fn = True
802
803
804 if mergeRequests_fn is False:
805 mergeRequests_fn = None
806 elif mergeRequests_fn is True:
807 mergeRequests_fn = Builder._defaultMergeRequestFn
808
809 return mergeRequests_fn
810
813
814 @defer.deferredGenerator
815 - def _mergeRequests(self, breq, unclaimed_requests, mergeRequests_fn):
816 """Use C{mergeRequests_fn} to merge C{breq} against
817 C{unclaimed_requests}, where both are build request dictionaries"""
818
819 if not mergeRequests_fn or len(unclaimed_requests) == 1:
820 yield [ breq ]
821 return
822
823
824 wfd = defer.waitForDeferred(
825 defer.gatherResults(
826 [ self._brdictToBuildRequest(brdict)
827 for brdict in unclaimed_requests ]))
828 yield wfd
829 unclaimed_request_objects = wfd.getResult()
830 breq_object = unclaimed_request_objects.pop(
831 unclaimed_requests.index(breq))
832
833
834 merged_request_objects = [breq_object]
835 for other_breq_object in unclaimed_request_objects:
836 wfd = defer.waitForDeferred(
837 defer.maybeDeferred(lambda :
838 mergeRequests_fn(self, breq_object, other_breq_object)))
839 yield wfd
840 if wfd.getResult():
841 merged_request_objects.append(other_breq_object)
842
843
844 merged_requests = [ br.brdict for br in merged_request_objects ]
845 yield merged_requests
846
848 """
849 Convert a build request dictionary to a L{buildrequest.BuildRequest}
850 object, caching the result in the dictionary itself. The resulting
851 buildrequest will have a C{brdict} attribute pointing back to this
852 dictionary.
853
854 Note that this does not perform any locking - be careful that it is
855 only called once at a time for each build request dictionary.
856
857 @param brdict: dictionary to convert
858
859 @returns: L{buildrequest.BuildRequest} via Deferred
860 """
861 if 'brobj' in brdict:
862 return defer.succeed(brdict['brobj'])
863 d = buildrequest.BuildRequest.fromBrdict(self.master, brdict)
864 def keep(buildrequest):
865 brdict['brobj'] = buildrequest
866 buildrequest.brdict = brdict
867 return buildrequest
868 d.addCallback(keep)
869 return d
870
872 """Break the reference loops created by L{_brdictToBuildRequest}"""
873 for brdict in requests:
874 try:
875 del brdict['brobj'].brdict
876 except KeyError:
877 pass
878
899 d.addCallback(get_brs)
900 return d
901
902 - def rebuildBuild(self, bs, reason="<rebuild, no reason given>", extraProperties=None):
920 d.addCallback(add_buildset)
921 return d
922
923 @defer.deferredGenerator
945
948
950 if not self.original.slaves:
951 self.original.builder_status.addPointEvent(["ping", "no slave"])
952 return defer.succeed(False)
953 dl = []
954 for s in self.original.slaves:
955 dl.append(s.ping(self.original.builder_status))
956 d = defer.DeferredList(dl)
957 d.addCallback(self._gatherPingResults)
958 return d
959
961 for ignored,success in res:
962 if not success:
963 return False
964 return True
965