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
39 r = ["<", self.__class__.__name__]
40 if self.builder_name:
41 r.extend([" builder=", repr(self.builder_name)])
42 if self.slave:
43 r.extend([" slave=", repr(self.slave.slavename)])
44 r.append(">")
45 return ''.join(r)
46
50
52 if self.remoteCommands is None:
53
54 return oldversion
55 return self.remoteCommands.get(command)
56
58
59 if self.isBusy():
60 return False
61
62
63 if self.slave:
64 return self.slave.canStartBuild()
65
66
67 return False
68
71
74
78
79 - def attached(self, slave, remote, commands):
80 """
81 @type slave: L{buildbot.buildslave.BuildSlave}
82 @param slave: the BuildSlave that represents the buildslave as a
83 whole
84 @type remote: L{twisted.spread.pb.RemoteReference}
85 @param remote: a reference to the L{buildbot.slave.bot.SlaveBuilder}
86 @type commands: dict: string -> string, or None
87 @param commands: provides the slave's version of each RemoteCommand
88 """
89 self.state = ATTACHING
90 self.remote = remote
91 self.remoteCommands = commands
92 if self.slave is None:
93 self.slave = slave
94 self.slave.addSlaveBuilder(self)
95 else:
96 assert self.slave == slave
97 log.msg("Buildslave %s attached to %s" % (slave.slavename,
98 self.builder_name))
99 d = self.remote.callRemote("setMaster", self)
100 d.addErrback(self._attachFailure, "Builder.setMaster")
101 d.addCallback(self._attached2)
102 return d
103
105 d = self.remote.callRemote("print", "attached")
106 d.addErrback(self._attachFailure, "Builder.print 'attached'")
107 d.addCallback(self._attached3)
108 return d
109
111
112 self.state = IDLE
113 return self
114
116 assert isinstance(where, str)
117 log.msg(where)
118 log.err(why)
119 return why
120
121 - def prepare(self, builder_status):
122 return defer.succeed(None)
123
124 - def ping(self, status=None):
125 """Ping the slave to make sure it is still there. Returns a Deferred
126 that fires with True if it is.
127
128 @param status: if you point this at a BuilderStatus, a 'pinging'
129 event will be pushed.
130 """
131 oldstate = self.state
132 self.state = PINGING
133 newping = not self.ping_watchers
134 d = defer.Deferred()
135 self.ping_watchers.append(d)
136 if newping:
137 if status:
138 event = status.addEvent(["pinging"])
139 d2 = defer.Deferred()
140 d2.addCallback(self._pong_status, event)
141 self.ping_watchers.insert(0, d2)
142
143
144 Ping().ping(self.remote).addCallback(self._pong)
145
146 def reset_state(res):
147 if self.state == PINGING:
148 self.state = oldstate
149 return res
150 d.addCallback(reset_state)
151 return d
152
154 watchers, self.ping_watchers = self.ping_watchers, []
155 for d in watchers:
156 d.callback(res)
157
159 if res:
160 event.text = ["ping", "success"]
161 else:
162 event.text = ["ping", "failed"]
163 event.finish()
164
166 log.msg("Buildslave %s detached from %s" % (self.slave.slavename,
167 self.builder_name))
168 if self.slave:
169 self.slave.removeSlaveBuilder(self)
170 self.slave = None
171 self.remote = None
172 self.remoteCommands = None
173
174
176 running = False
177
178 - def ping(self, remote):
179 assert not self.running
180 if not remote:
181
182 return defer.succeed(False)
183 self.running = True
184 log.msg("sending ping")
185 self.d = defer.Deferred()
186
187
188 remote.callRemote("print", "ping").addCallbacks(self._pong,
189 self._ping_failed,
190 errbackArgs=(remote,))
191 return self.d
192
194 log.msg("ping finished: success")
195 self.d.callback(True)
196
198 log.msg("ping finished: failure")
199
200
201
202 remote.broker.transport.loseConnection()
203
204
205
206 self.d.callback(False)
207
208
232
233
243
244 - def prepare(self, builder_status):
253 d.addErrback(substantiation_failed)
254 return d
255
257 self.state = SUBSTANTIATING
258 d = self.slave.substantiate(self)
259 if not self.slave.substantiated:
260 event = self.builder.builder_status.addEvent(
261 ["substantiating"])
262 def substantiated(res):
263 msg = ["substantiate", "success"]
264 if isinstance(res, basestring):
265 msg.append(res)
266 elif isinstance(res, (tuple, list)):
267 msg.extend(res)
268 event.text = msg
269 event.finish()
270 return res
271 def substantiation_failed(res):
272 event.text = ["substantiate", "failed"]
273
274 event.finish()
275 return res
276 d.addCallbacks(substantiated, substantiation_failed)
277 return d
278
282
286
290
294
295 - def ping(self, status=None):
301
302
303 -class Builder(pb.Referenceable, service.MultiService):
304 """I manage all Builds of a given type.
305
306 Each Builder is created by an entry in the config file (the c['builders']
307 list), with a number of parameters.
308
309 One of these parameters is the L{buildbot.process.factory.BuildFactory}
310 object that is associated with this Builder. The factory is responsible
311 for creating new L{Build<buildbot.process.base.Build>} objects. Each
312 Build object defines when and how the build is performed, so a new
313 Factory or Builder should be defined to control this behavior.
314
315 The Builder holds on to a number of L{base.BuildRequest} objects in a
316 list named C{.buildable}. Incoming BuildRequest objects will be added to
317 this list, or (if possible) merged into an existing request. When a slave
318 becomes available, I will use my C{BuildFactory} to turn the request into
319 a new C{Build} object. The C{BuildRequest} is forgotten, the C{Build}
320 goes into C{.building} while it runs. Once the build finishes, I will
321 discard it.
322
323 I maintain a list of available SlaveBuilders, one for each connected
324 slave that the C{slavenames} parameter says we can use. Some of these
325 will be idle, some of them will be busy running builds for me. If there
326 are multiple slaves, I can run multiple builds at once.
327
328 I also manage forced builds, progress expectation (ETA) management, and
329 some status delivery chores.
330
331 @type buildable: list of L{buildbot.process.base.BuildRequest}
332 @ivar buildable: BuildRequests that are ready to build, but which are
333 waiting for a buildslave to be available.
334
335 @type building: list of L{buildbot.process.base.Build}
336 @ivar building: Builds that are actively running
337
338 @type slaves: list of L{buildbot.buildslave.BuildSlave} objects
339 @ivar slaves: the slaves currently available for building
340 """
341
342 expectations = None
343 CHOOSE_SLAVES_RANDOMLY = True
344
345 - def __init__(self, setup, builder_status):
346 """
347 @type setup: dict
348 @param setup: builder setup data, as stored in
349 BuildmasterConfig['builders']. Contains name,
350 slavename(s), builddir, slavebuilddir, factory, locks.
351 @type builder_status: L{buildbot.status.builder.BuilderStatus}
352 """
353 service.MultiService.__init__(self)
354 self.name = setup['name']
355 self.slavenames = []
356 if setup.has_key('slavename'):
357 self.slavenames.append(setup['slavename'])
358 if setup.has_key('slavenames'):
359 self.slavenames.extend(setup['slavenames'])
360 self.builddir = setup['builddir']
361 self.slavebuilddir = setup['slavebuilddir']
362 self.buildFactory = setup['factory']
363 self.nextSlave = setup.get('nextSlave')
364 if self.nextSlave is not None and not callable(self.nextSlave):
365 raise ValueError("nextSlave must be callable")
366 self.locks = setup.get("locks", [])
367 self.env = setup.get('env', {})
368 assert isinstance(self.env, dict)
369 if setup.has_key('periodicBuildTime'):
370 raise ValueError("periodicBuildTime can no longer be defined as"
371 " part of the Builder: use scheduler.Periodic"
372 " instead")
373 self.nextBuild = setup.get('nextBuild')
374 if self.nextBuild is not None and not callable(self.nextBuild):
375 raise ValueError("nextBuild must be callable")
376 self.buildHorizon = setup.get('buildHorizon')
377 self.logHorizon = setup.get('logHorizon')
378 self.eventHorizon = setup.get('eventHorizon')
379 self.mergeRequests = setup.get('mergeRequests', True)
380 self.properties = setup.get('properties', {})
381
382
383 self.building = []
384
385 self.old_building = weakref.WeakKeyDictionary()
386
387
388
389 self.attaching_slaves = []
390
391
392
393
394 self.slaves = []
395
396 self.builder_status = builder_status
397 self.builder_status.setSlavenames(self.slavenames)
398 self.builder_status.buildHorizon = self.buildHorizon
399 self.builder_status.logHorizon = self.logHorizon
400 self.builder_status.eventHorizon = self.eventHorizon
401 t = internet.TimerService(10*60, self.reclaimAllBuilds)
402 t.setServiceParent(self)
403
404
405 self.watchers = {'attach': [], 'detach': [], 'detach_all': [],
406 'idle': []}
407 self.run_count = 0
408
410 self.botmaster = botmaster
411 self.db = botmaster.db
412 self.master_name = botmaster.master_name
413 self.master_incarnation = botmaster.master_incarnation
414
416 diffs = []
417 setup_slavenames = []
418 if setup.has_key('slavename'):
419 setup_slavenames.append(setup['slavename'])
420 setup_slavenames.extend(setup.get('slavenames', []))
421 if setup_slavenames != self.slavenames:
422 diffs.append('slavenames changed from %s to %s' \
423 % (self.slavenames, setup_slavenames))
424 if setup['builddir'] != self.builddir:
425 diffs.append('builddir changed from %s to %s' \
426 % (self.builddir, setup['builddir']))
427 if setup['slavebuilddir'] != self.slavebuilddir:
428 diffs.append('slavebuilddir changed from %s to %s' \
429 % (self.slavebuilddir, setup['slavebuilddir']))
430 if setup['factory'] != self.buildFactory:
431 diffs.append('factory changed')
432 if setup.get('locks', []) != self.locks:
433 diffs.append('locks changed from %s to %s' % (self.locks, setup.get('locks')))
434 if setup.get('nextSlave') != self.nextSlave:
435 diffs.append('nextSlave changed from %s to %s' % (self.nextSlave, setup.get('nextSlave')))
436 if setup.get('nextBuild') != self.nextBuild:
437 diffs.append('nextBuild changed from %s to %s' % (self.nextBuild, setup.get('nextBuild')))
438 if setup['buildHorizon'] != self.buildHorizon:
439 diffs.append('buildHorizon changed from %s to %s' % (self.buildHorizon, setup['buildHorizon']))
440 if setup['logHorizon'] != self.logHorizon:
441 diffs.append('logHorizon changed from %s to %s' % (self.logHorizon, setup['logHorizon']))
442 if setup['eventHorizon'] != self.eventHorizon:
443 diffs.append('eventHorizon changed from %s to %s' % (self.eventHorizon, setup['eventHorizon']))
444 return diffs
445
447 return "<Builder '%r' at %d>" % (self.name, id(self))
448
451
453 """Check for work to be done. This should be called any time I might
454 be able to start a job:
455
456 - when the Builder is first created
457 - when a new job has been added to the [buildrequests] DB table
458 - when a slave has connected
459
460 If I have both an available slave and the database contains a
461 BuildRequest that I can handle, I will claim the BuildRequest and
462 start the build. When the build finishes, I will retire the
463 BuildRequest.
464 """
465
466
467
468 assert self.running
469 log.msg("Builder.run %s: %s" % (self, self.slaves))
470 self.run_count += 1
471
472 available_slaves = [sb for sb in self.slaves if sb.isAvailable()]
473 if not available_slaves:
474 self.updateBigStatus()
475 return
476 d = self.db.runInteraction(self._claim_buildreqs, available_slaves)
477 d.addCallback(self._start_builds)
478 return d
479
480
481
482 RECLAIM_INTERVAL = 1*3600
483
485
486 now = util.now()
487 old = now - self.RECLAIM_INTERVAL
488 requests = self.db.get_unclaimed_buildrequests(self.name, old,
489 self.master_name,
490 self.master_incarnation,
491 t)
492
493 assignments = {}
494 while requests and available_slaves:
495 sb = self._choose_slave(available_slaves)
496 if not sb:
497 log.msg("%s: want to start build, but we don't have a remote"
498 % self)
499 break
500 available_slaves.remove(sb)
501 breq = self._choose_build(requests)
502 if not breq:
503 log.msg("%s: went to start build, but nextBuild said not to"
504 % self)
505 break
506 requests.remove(breq)
507 merged_requests = [breq]
508 for other_breq in requests[:]:
509 if (self.mergeRequests and
510 self.botmaster.shouldMergeRequests(self, breq, other_breq)
511 ):
512 requests.remove(other_breq)
513 merged_requests.append(other_breq)
514 assignments[sb] = merged_requests
515 brids = [br.id for br in merged_requests]
516 self.db.claim_buildrequests(now, self.master_name,
517 self.master_incarnation, brids, t)
518 return assignments
519
521
522
523 if self.nextSlave:
524 try:
525 return self.nextSlave(self, available_slaves)
526 except:
527 log.msg("Exception choosing next slave")
528 log.err(Failure())
529 return None
530 if self.CHOOSE_SLAVES_RANDOMLY:
531 return random.choice(available_slaves)
532 return available_slaves[0]
533
535 if self.nextBuild:
536 try:
537 return self.nextBuild(self, buildable)
538 except:
539 log.msg("Exception choosing next build")
540 log.err(Failure())
541 return None
542 return buildable[0]
543
557
558
569
571 """Returns the timestamp of the oldest build request for this builder.
572
573 If there are no build requests, None is returned."""
574 buildable = self.getBuildable(1)
575 if buildable:
576
577 return buildable[0].getSubmitTime()
578 return None
579
582
584 """Suck the brain out of an old Builder.
585
586 This takes all the runtime state from an existing Builder and moves
587 it into ourselves. This is used when a Builder is changed in the
588 master.cfg file: the new Builder has a different factory, but we want
589 all the builds that were queued for the old one to get processed by
590 the new one. Any builds which are already running will keep running.
591 The new Builder will get as many of the old SlaveBuilder objects as
592 it wants."""
593
594 log.msg("consumeTheSoulOfYourPredecessor: %s feeding upon %s" %
595 (self, old))
596
597
598
599
600
601
602
603
604
605 if old.building:
606 self.builder_status.setBigState("building")
607
608
609
610
611 for b in old.building:
612 self.old_building[b] = None
613 for b in old.old_building:
614 self.old_building[b] = None
615
616
617
618 for sb in old.slaves[:]:
619 if sb.slave.slavename in self.slavenames:
620 log.msg(" stealing buildslave %s" % sb)
621 self.slaves.append(sb)
622 old.slaves.remove(sb)
623 sb.setBuilder(self)
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644 return
645
647 now = util.now()
648 brids = set()
649 for b in self.building:
650 brids.update([br.id for br in b.requests])
651 for b in self.old_building:
652 brids.update([br.id for br in b.requests])
653 self.db.claim_buildrequests(now, self.master_name,
654 self.master_incarnation, brids)
655
664
672
684
685 - def attached(self, slave, remote, commands):
686 """This is invoked by the BuildSlave when the self.slavename bot
687 registers their builder.
688
689 @type slave: L{buildbot.buildslave.BuildSlave}
690 @param slave: the BuildSlave that represents the buildslave as a whole
691 @type remote: L{twisted.spread.pb.RemoteReference}
692 @param remote: a reference to the L{buildbot.slave.bot.SlaveBuilder}
693 @type commands: dict: string -> string, or None
694 @param commands: provides the slave's version of each RemoteCommand
695
696 @rtype: L{twisted.internet.defer.Deferred}
697 @return: a Deferred that fires (with 'self') when the slave-side
698 builder is fully attached and ready to accept commands.
699 """
700 for s in self.attaching_slaves + self.slaves:
701 if s.slave == slave:
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718 return defer.succeed(self)
719
720 sb = SlaveBuilder()
721 sb.setBuilder(self)
722 self.attaching_slaves.append(sb)
723 d = sb.attached(slave, remote, commands)
724 d.addCallback(self._attached)
725 d.addErrback(self._not_attached, slave)
726 return d
727
736
746
748 """This is called when the connection to the bot is lost."""
749 for sb in self.attaching_slaves + self.slaves:
750 if sb.slave == slave:
751 break
752 else:
753 log.msg("WEIRD: Builder.detached(%s) (%s)"
754 " not in attaching_slaves(%s)"
755 " or slaves(%s)" % (slave, slave.slavename,
756 self.attaching_slaves,
757 self.slaves))
758 return
759 if sb.state == BUILDING:
760
761
762
763
764
765
766 pass
767
768 if sb in self.attaching_slaves:
769 self.attaching_slaves.remove(sb)
770 if sb in self.slaves:
771 self.slaves.remove(sb)
772
773
774 self.builder_status.addPointEvent(['disconnect', slave.slavename])
775 sb.detached()
776 self.updateBigStatus()
777 self.fireTestEvent('detach')
778 if not self.slaves:
779 self.fireTestEvent('detach_all')
780
789
791 """Start a build on the given slave.
792 @param build: the L{base.Build} to start
793 @param sb: the L{SlaveBuilder} which will host this build
794
795 @return: a Deferred which fires with a
796 L{buildbot.interfaces.IBuildControl} that can be used to stop the
797 Build, or to access a L{buildbot.interfaces.IBuildStatus} which will
798 watch the Build as it runs. """
799
800 self.building.append(build)
801 self.updateBigStatus()
802 log.msg("starting build %s using slave %s" % (build, sb))
803 d = sb.prepare(self.builder_status)
804 def _ping(ign):
805
806
807
808
809
810
811
812
813 log.msg("starting build %s.. pinging the slave %s" % (build, sb))
814 return sb.ping()
815 d.addCallback(_ping)
816 d.addCallback(self._startBuild_1, build, sb)
817 return d
818
820 if not res:
821 return self._startBuildFailed("slave ping failed", build, sb)
822
823
824
825 sb.buildStarted()
826 d = sb.remote.callRemote("startBuild")
827 d.addCallbacks(self._startBuild_2, self._startBuildFailed,
828 callbackArgs=(build,sb), errbackArgs=(build,sb))
829 return d
830
845
847
848 log.msg("I tried to tell the slave that the build %s started, but "
849 "remote_startBuild failed: %s" % (build, why))
850
851
852
853 sb.buildFinished()
854
855 log.msg("re-queueing the BuildRequest")
856 self.building.remove(build)
857 self._resubmit_buildreqs(build).addErrback(log.err)
858
864
883
887
889 """Mark the build as successful and update expectations for the next
890 build. Only call this when the build did not fail in any way that
891 would invalidate the time expectations generated by it. (if the
892 compile failed and thus terminated early, we can't use the last
893 build to predict how long the next one will take).
894 """
895 if self.expectations:
896 self.expectations.update(progress)
897 else:
898
899
900 self.expectations = Expectations(progress)
901 log.msg("new expectations: %s seconds" % \
902 self.expectations.expectedBuildTime())
903
907
908
910 implements(interfaces.IBuilderControl)
911
915
921
922 - def rebuildBuild(self, bs, reason="<rebuild, no reason given>", extraProperties=None):
935
943
946
948 if not self.original.slaves:
949 self.original.builder_status.addPointEvent(["ping", "no slave"])
950 return defer.succeed(False)
951 dl = []
952 for s in self.original.slaves:
953 dl.append(s.ping(self.original.builder_status))
954 d = defer.DeferredList(dl)
955 d.addCallback(self._gatherPingResults)
956 return d
957
959 for ignored,success in res:
960 if not success:
961 return False
962 return True
963
965 implements(interfaces.IBuildRequestControl)
966
968 self.original_builder = builder
969 self.original_request = request
970 self.brid = request.id
971
973 raise NotImplementedError
974
976 raise NotImplementedError
977
980