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, config
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(config.ReconfigurableServiceMixin,
34 pb.Referenceable,
35 service.MultiService):
36
37
38 reconfig_priority = 196
39
40 - def __init__(self, name, _addServices=True):
41 service.MultiService.__init__(self)
42 self.name = name
43
44
45 self.expectations = None
46
47
48 self.building = []
49
50 self.old_building = weakref.WeakKeyDictionary()
51
52
53
54 self.attaching_slaves = []
55
56
57
58
59 self.slaves = []
60
61 self.config = None
62 self.builder_status = None
63
64 if _addServices:
65 self.reclaim_svc = internet.TimerService(10*60,
66 self.reclaimAllBuilds)
67 self.reclaim_svc.setServiceParent(self)
68
69
70 self.updateStatusService = internet.TimerService(30*60,
71 self.updateBigStatus)
72 self.updateStatusService.setServiceParent(self)
73
75
76 for builder_config in new_config.builders:
77 if builder_config.name == self.name:
78 break
79 else:
80 assert 0, "no config found for builder '%s'" % self.name
81
82
83 if not self.builder_status:
84 self.builder_status = self.master.status.builderAdded(
85 builder_config.name,
86 builder_config.builddir,
87 builder_config.category)
88
89 self.config = builder_config
90
91 self.builder_status.setCategory(builder_config.category)
92 self.builder_status.setSlavenames(self.config.slavenames)
93 self.builder_status.setCacheSize(new_config.caches['Builds'])
94
95 return defer.succeed(None)
96
98 d = defer.maybeDeferred(lambda :
99 service.MultiService.stopService(self))
100 def flushMaybeStartBuilds(_):
101
102
103
104
105 return self.maybeStartBuild()
106 d.addCallback(flushMaybeStartBuilds)
107 return d
108
110 return "<Builder '%r' at %d>" % (self.name, id(self))
111
112 @defer.inlineCallbacks
114
115 """Returns the submitted_at of the oldest unclaimed build request for
116 this builder, or None if there are no build requests.
117
118 @returns: datetime instance or None, via Deferred
119 """
120 unclaimed = yield self.master.db.buildrequests.getBuildRequests(
121 buildername=self.name, claimed=False)
122
123 if unclaimed:
124 unclaimed = [ brd['submitted_at'] for brd in unclaimed ]
125 unclaimed.sort()
126 defer.returnValue(unclaimed[0])
127 else:
128 defer.returnValue(None)
129
131 brids = set()
132 for b in self.building:
133 brids.update([br.id for br in b.requests])
134 for b in self.old_building:
135 brids.update([br.id for br in b.requests])
136
137 if not brids:
138 return defer.succeed(None)
139
140 d = self.master.db.buildrequests.reclaimBuildRequests(brids)
141 d.addErrback(log.err, 'while re-claiming running BuildRequests')
142 return d
143
152
164
165 - def attached(self, slave, remote, commands):
166 """This is invoked by the BuildSlave when the self.slavename bot
167 registers their builder.
168
169 @type slave: L{buildbot.buildslave.BuildSlave}
170 @param slave: the BuildSlave that represents the buildslave as a whole
171 @type remote: L{twisted.spread.pb.RemoteReference}
172 @param remote: a reference to the L{buildbot.slave.bot.SlaveBuilder}
173 @type commands: dict: string -> string, or None
174 @param commands: provides the slave's version of each RemoteCommand
175
176 @rtype: L{twisted.internet.defer.Deferred}
177 @return: a Deferred that fires (with 'self') when the slave-side
178 builder is fully attached and ready to accept commands.
179 """
180 for s in self.attaching_slaves + self.slaves:
181 if s.slave == slave:
182
183
184
185
186
187
188
189
190
191
192
193 return defer.succeed(self)
194
195 sb = slavebuilder.SlaveBuilder()
196 sb.setBuilder(self)
197 self.attaching_slaves.append(sb)
198 d = sb.attached(slave, remote, commands)
199 d.addCallback(self._attached)
200 d.addErrback(self._not_attached, slave)
201 return d
202
211
219
220
247
249 if not self.builder_status:
250 return
251 if not self.slaves:
252 self.builder_status.setBigState("offline")
253 elif self.building or self.old_building:
254 self.builder_status.setBigState("building")
255 else:
256 self.builder_status.setBigState("idle")
257
258 @defer.inlineCallbacks
260 """Start a build on the given slave.
261 @param build: the L{base.Build} to start
262 @param sb: the L{SlaveBuilder} which will host this build
263
264 @return: (via Deferred) boolean indicating that the build was
265 succesfully started.
266 """
267
268
269
270
271 cleanups = []
272 def run_cleanups():
273 try:
274 while cleanups:
275 fn = cleanups.pop()
276 fn()
277 except:
278 log.err(failure.Failure(), "while running %r" % (run_cleanups,))
279
280
281
282 cleanups.append(lambda : self.updateBigStatus())
283
284 build = self.config.factory.newBuild(buildrequests)
285 build.setBuilder(self)
286 log.msg("starting build %s using slave %s" % (build, slavebuilder))
287
288
289 build.setLocks(self.config.locks)
290 cleanups.append(lambda : slavebuilder.slave.releaseLocks())
291
292 if len(self.config.env) > 0:
293 build.setSlaveEnvironment(self.config.env)
294
295
296 self.building.append(build)
297 cleanups.append(lambda : self.building.remove(build))
298
299
300 self.updateBigStatus()
301
302 try:
303 ready = yield slavebuilder.prepare(self.builder_status, build)
304 except:
305 log.err(failure.Failure(), 'while preparing slavebuilder:')
306 ready = False
307
308
309
310 if not ready:
311 log.msg("slave %s can't build %s after all; re-queueing the "
312 "request" % (build, slavebuilder))
313 run_cleanups()
314 defer.returnValue(False)
315 return
316
317
318
319
320
321
322
323
324
325 log.msg("starting build %s.. pinging the slave %s"
326 % (build, slavebuilder))
327 try:
328 ping_success = yield slavebuilder.ping()
329 except:
330 log.err(failure.Failure(), 'while pinging slave before build:')
331 ping_success = False
332
333 if not ping_success:
334 log.msg("slave ping failed; re-queueing the request")
335 run_cleanups()
336 defer.returnValue(False)
337 return
338
339
340
341
342 slavebuilder.buildStarted()
343 cleanups.append(lambda : slavebuilder.buildFinished())
344
345
346 try:
347 yield slavebuilder.remote.callRemote("startBuild")
348 except:
349 log.err(failure.Failure(), 'while calling remote startBuild:')
350 run_cleanups()
351 defer.returnValue(False)
352 return
353
354
355 bs = self.builder_status.newBuild()
356
357
358 try:
359 bids = []
360 for req in build.requests:
361 bid = yield self.master.db.builds.addBuild(req.id, bs.number)
362 bids.append(bid)
363 except:
364 log.err(failure.Failure(), 'while adding rows to build table:')
365 run_cleanups()
366 defer.returnValue(False)
367 return
368
369
370 self.master.status.build_started(req.id, self.name, bs)
371
372
373
374
375
376
377
378 d = build.startBuild(bs, self.expectations, slavebuilder)
379 d.addCallback(self.buildFinished, slavebuilder, bids)
380
381 d.addErrback(log.err)
382
383
384 self.updateBigStatus()
385
386 defer.returnValue(True)
387
395
428
429 @defer.inlineCallbacks
434
438
440 """Mark the build as successful and update expectations for the next
441 build. Only call this when the build did not fail in any way that
442 would invalidate the time expectations generated by it. (if the
443 compile failed and thus terminated early, we can't use the last
444 build to predict how long the next one will take).
445 """
446 if self.expectations:
447 self.expectations.update(progress)
448 else:
449
450
451 self.expectations = Expectations(progress)
452 log.msg("new expectations: %s seconds" % \
453 self.expectations.expectedBuildTime())
454
455
456
457 @defer.inlineCallbacks
459
460
461
462
463
464
465
466
467 if not self.running:
468 return
469
470
471
472 available_slavebuilders = [ sb for sb in self.slaves
473 if sb.isAvailable() ]
474 if not available_slavebuilders:
475 self.updateBigStatus()
476 return
477
478
479 unclaimed_requests = \
480 yield self.master.db.buildrequests.getBuildRequests(
481 buildername=self.name, claimed=False)
482
483 if not unclaimed_requests:
484 self.updateBigStatus()
485 return
486
487
488 unclaimed_requests.sort(key=lambda brd : brd['submitted_at'])
489
490
491 mergeRequests_fn = self._getMergeRequestsFn()
492
493
494 while available_slavebuilders and unclaimed_requests:
495
496 slavebuilder = yield self._chooseSlave(available_slavebuilders)
497
498 if not slavebuilder:
499 break
500
501 if slavebuilder not in available_slavebuilders:
502 log.msg(("nextSlave chose a nonexistent slave for builder "
503 "'%s'; cannot start build") % self.name)
504 break
505
506
507 brdict = yield self._chooseBuild(unclaimed_requests)
508
509 if not brdict:
510 break
511
512 if brdict not in unclaimed_requests:
513 log.msg(("nextBuild chose a nonexistent request for builder "
514 "'%s'; cannot start build") % self.name)
515 break
516
517
518
519 brdicts = yield self._mergeRequests(brdict, unclaimed_requests,
520 mergeRequests_fn)
521
522
523 brids = [ brdict['brid'] for brdict in brdicts ]
524 try:
525 yield self.master.db.buildrequests.claimBuildRequests(brids)
526 except buildrequests.AlreadyClaimedError:
527
528
529
530 self._breakBrdictRefloops(unclaimed_requests)
531 unclaimed_requests = \
532 yield self.master.db.buildrequests.getBuildRequests(
533 buildername=self.name, claimed=False)
534
535
536 continue
537
538
539
540
541
542
543
544 breqs = yield defer.gatherResults(
545 [ self._brdictToBuildRequest(brdict)
546 for brdict in brdicts ])
547
548 build_started = yield self._startBuildFor(slavebuilder, breqs)
549
550 if not build_started:
551
552 yield self.master.db.buildrequests.unclaimBuildRequests(brids)
553
554
555
556 self.botmaster.maybeStartBuildsForBuilder(self.name)
557
558
559
560 self._breakBrdictRefloops(brdicts)
561 for brdict in brdicts:
562 unclaimed_requests.remove(brdict)
563 available_slavebuilders.remove(slavebuilder)
564
565 self._breakBrdictRefloops(unclaimed_requests)
566 self.updateBigStatus()
567 return
568
569
570
571
573 """
574 Choose the next slave, using the C{nextSlave} configuration if
575 available, and falling back to C{random.choice} otherwise.
576
577 @param available_slavebuilders: list of slavebuilders to choose from
578 @returns: SlaveBuilder or None via Deferred
579 """
580 if self.config.nextSlave:
581 return defer.maybeDeferred(lambda :
582 self.config.nextSlave(self, available_slavebuilders))
583 else:
584 return defer.succeed(random.choice(available_slavebuilders))
585
587 """
588 Choose the next build from the given set of build requests (represented
589 as dictionaries). Defaults to returning the first request (earliest
590 submitted).
591
592 @param buildrequests: sorted list of build request dictionaries
593 @returns: a build request dictionary or None via Deferred
594 """
595 if self.config.nextBuild:
596
597
598 d = defer.gatherResults([ self._brdictToBuildRequest(brdict)
599 for brdict in buildrequests ])
600 d.addCallback(lambda requestobjects :
601 self.config.nextBuild(self, requestobjects))
602 def to_brdict(brobj):
603
604 return brobj.brdict
605 d.addCallback(to_brdict)
606 return d
607 else:
608 return defer.succeed(buildrequests[0])
609
611 """Helper function to determine which mergeRequests function to use
612 from L{_mergeRequests}, or None for no merging"""
613
614 mergeRequests_fn = self.config.mergeRequests
615 if mergeRequests_fn is None:
616 mergeRequests_fn = self.master.config.mergeRequests
617 if mergeRequests_fn is None:
618 mergeRequests_fn = True
619
620
621 if mergeRequests_fn is False:
622 mergeRequests_fn = None
623 elif mergeRequests_fn is True:
624 mergeRequests_fn = Builder._defaultMergeRequestFn
625
626 return mergeRequests_fn
627
630
631 @defer.inlineCallbacks
632 - def _mergeRequests(self, breq, unclaimed_requests, mergeRequests_fn):
633 """Use C{mergeRequests_fn} to merge C{breq} against
634 C{unclaimed_requests}, where both are build request dictionaries"""
635
636 if not mergeRequests_fn or len(unclaimed_requests) == 1:
637 defer.returnValue([ breq ])
638 return
639
640
641 unclaimed_request_objects = yield defer.gatherResults(
642 [ self._brdictToBuildRequest(brdict)
643 for brdict in unclaimed_requests ])
644
645 breq_object = unclaimed_request_objects[unclaimed_requests.index(breq)]
646
647
648 merged_request_objects = []
649 for other_breq_object in unclaimed_request_objects:
650 if (yield defer.maybeDeferred(
651 lambda : mergeRequests_fn(self, breq_object,
652 other_breq_object))):
653 merged_request_objects.append(other_breq_object)
654
655
656 merged_requests = [ br.brdict for br in merged_request_objects ]
657 defer.returnValue(merged_requests)
658
660 """
661 Convert a build request dictionary to a L{buildrequest.BuildRequest}
662 object, caching the result in the dictionary itself. The resulting
663 buildrequest will have a C{brdict} attribute pointing back to this
664 dictionary.
665
666 Note that this does not perform any locking - be careful that it is
667 only called once at a time for each build request dictionary.
668
669 @param brdict: dictionary to convert
670
671 @returns: L{buildrequest.BuildRequest} via Deferred
672 """
673 if 'brobj' in brdict:
674 return defer.succeed(brdict['brobj'])
675 d = buildrequest.BuildRequest.fromBrdict(self.master, brdict)
676 def keep(buildrequest):
677 brdict['brobj'] = buildrequest
678 buildrequest.brdict = brdict
679 return buildrequest
680 d.addCallback(keep)
681 return d
682
684 """Break the reference loops created by L{_brdictToBuildRequest}"""
685 for brdict in requests:
686 try:
687 del brdict['brobj'].brdict
688 except KeyError:
689 pass
690
711 d.addCallback(get_brs)
712 return d
713
714 @defer.inlineCallbacks
715 - def rebuildBuild(self, bs, reason="<rebuild, no reason given>", extraProperties=None):
716 if not bs.isFinished():
717 return
718
719
720 properties = Properties()
721
722 properties.updateFromPropertiesNoRuntime(bs.getProperties())
723 if extraProperties is None:
724 properties.updateFromProperties(extraProperties)
725
726 properties_dict = dict((k,(v,s)) for (k,v,s) in properties.asList())
727 ssList = bs.getSourceStamps(absolute=True)
728
729 if ssList:
730 sourcestampsetid = yield ssList[0].getSourceStampSetId(self.control.master)
731 dl = []
732 for ss in ssList[1:]:
733
734 dl.append(ss.addSourceStampToDatabase(self.control.master, sourcestampsetid))
735 yield defer.gatherResults(dl)
736
737 bsid, brids = yield self.control.master.addBuildset(
738 builderNames=[self.original.name],
739 sourcestampsetid=sourcestampsetid,
740 reason=reason,
741 properties=properties_dict)
742 defer.returnValue((bsid, brids))
743 else:
744 log.msg('Cannot start rebuild, rebuild has no sourcestamps for a new build')
745 defer.returnValue(None)
746
747 @defer.inlineCallbacks
764
767
769 if not self.original.slaves:
770 self.original.builder_status.addPointEvent(["ping", "no slave"])
771 return defer.succeed(False)
772 dl = []
773 for s in self.original.slaves:
774 dl.append(s.ping(self.original.builder_status))
775 d = defer.DeferredList(dl)
776 d.addCallback(self._gatherPingResults)
777 return d
778
780 for ignored,success in res:
781 if not success:
782 return False
783 return True
784