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
20 from twisted.python.failure import Failure
21 from twisted.spread import pb
22 from twisted.application import service, internet
23 from twisted.internet import defer
24
25 from buildbot import interfaces, util
26 from buildbot.status.progress import Expectations
27 from buildbot.status.builder import RETRY
28 from buildbot.process.properties import Properties
29 from buildbot.util.eventual import eventually
30
31 (ATTACHING,
32 IDLE,
33 PINGING,
34 BUILDING,
35 LATENT,
36 SUBSTANTIATING,
37 ) = range(6)
38
39
41 """I am the master-side representative for one of the
42 L{buildbot.slave.bot.SlaveBuilder} objects that lives in a remote
43 buildbot. When a remote builder connects, I query it for command versions
44 and then make it available to any Builds that are ready to run. """
45
47 self.ping_watchers = []
48 self.state = None
49 self.remote = None
50 self.slave = None
51 self.builder_name = None
52 self.locks = None
53
55 r = ["<", self.__class__.__name__]
56 if self.builder_name:
57 r.extend([" builder=", repr(self.builder_name)])
58 if self.slave:
59 r.extend([" slave=", repr(self.slave.slavename)])
60 r.append(">")
61 return ''.join(r)
62
66
68 if self.remoteCommands is None:
69
70 return oldversion
71 return self.remoteCommands.get(command)
72
84
87
90
94
95 - def attached(self, slave, remote, commands):
96 """
97 @type slave: L{buildbot.buildslave.BuildSlave}
98 @param slave: the BuildSlave that represents the buildslave as a
99 whole
100 @type remote: L{twisted.spread.pb.RemoteReference}
101 @param remote: a reference to the L{buildbot.slave.bot.SlaveBuilder}
102 @type commands: dict: string -> string, or None
103 @param commands: provides the slave's version of each RemoteCommand
104 """
105 self.state = ATTACHING
106 self.remote = remote
107 self.remoteCommands = commands
108 if self.slave is None:
109 self.slave = slave
110 self.slave.addSlaveBuilder(self)
111 else:
112 assert self.slave == slave
113 log.msg("Buildslave %s attached to %s" % (slave.slavename,
114 self.builder_name))
115 def _attachFailure(why, where):
116 log.msg(where)
117 log.err(why)
118 return why
119
120 d = defer.succeed(None)
121 def doSetMaster(res):
122 d = self.remote.callRemote("setMaster", self)
123
124 return d
125 d.addCallback(doSetMaster)
126 def doPrint(res):
127 d = self.remote.callRemote("print", "attached")
128
129 return d
130 d.addCallback(doPrint)
131 def setIdle(res):
132 self.state = IDLE
133 return self
134 d.addCallback(setIdle)
135 return d
136
137 - def prepare(self, builder_status):
138 if not self.slave.acquireLocks():
139 return defer.succeed(False)
140 return defer.succeed(True)
141
142 - def ping(self, status=None):
143 """Ping the slave to make sure it is still there. Returns a Deferred
144 that fires with True if it is.
145
146 @param status: if you point this at a BuilderStatus, a 'pinging'
147 event will be pushed.
148 """
149 oldstate = self.state
150 self.state = PINGING
151 newping = not self.ping_watchers
152 d = defer.Deferred()
153 self.ping_watchers.append(d)
154 if newping:
155 if status:
156 event = status.addEvent(["pinging"])
157 d2 = defer.Deferred()
158 d2.addCallback(self._pong_status, event)
159 self.ping_watchers.insert(0, d2)
160
161
162 Ping().ping(self.remote).addCallback(self._pong)
163
164 def reset_state(res):
165 if self.state == PINGING:
166 self.state = oldstate
167 return res
168 d.addCallback(reset_state)
169 return d
170
172 watchers, self.ping_watchers = self.ping_watchers, []
173 for d in watchers:
174 d.callback(res)
175
177 if res:
178 event.text = ["ping", "success"]
179 else:
180 event.text = ["ping", "failed"]
181 event.finish()
182
191
192
194 running = False
195
196 - def ping(self, remote):
197 assert not self.running
198 if not remote:
199
200 return defer.succeed(False)
201 self.running = True
202 log.msg("sending ping")
203 self.d = defer.Deferred()
204
205
206 remote.callRemote("print", "ping").addCallbacks(self._pong,
207 self._ping_failed,
208 errbackArgs=(remote,))
209 return self.d
210
212 log.msg("ping finished: success")
213 self.d.callback(True)
214
216 log.msg("ping finished: failure")
217
218
219
220 remote.broker.transport.loseConnection()
221
222
223
224 self.d.callback(False)
225
226
250
251
261
262 - def prepare(self, builder_status):
275 def substantiation_cancelled(res):
276
277 if not res:
278 self.state = LATENT
279 return res
280 d.addCallback(substantiation_cancelled)
281 d.addErrback(substantiation_failed)
282 return d
283
285 self.state = SUBSTANTIATING
286 d = self.slave.substantiate(self)
287 if not self.slave.substantiated:
288 event = self.builder.builder_status.addEvent(
289 ["substantiating"])
290 def substantiated(res):
291 msg = ["substantiate", "success"]
292 if isinstance(res, basestring):
293 msg.append(res)
294 elif isinstance(res, (tuple, list)):
295 msg.extend(res)
296 event.text = msg
297 event.finish()
298 return res
299 def substantiation_failed(res):
300 event.text = ["substantiate", "failed"]
301
302 event.finish()
303 return res
304 d.addCallbacks(substantiated, substantiation_failed)
305 return d
306
310
314
318
322
323 - def ping(self, status=None):
329
330
331 -class Builder(pb.Referenceable, service.MultiService):
332 """I manage all Builds of a given type.
333
334 Each Builder is created by an entry in the config file (the c['builders']
335 list), with a number of parameters.
336
337 One of these parameters is the L{buildbot.process.factory.BuildFactory}
338 object that is associated with this Builder. The factory is responsible
339 for creating new L{Build<buildbot.process.base.Build>} objects. Each
340 Build object defines when and how the build is performed, so a new
341 Factory or Builder should be defined to control this behavior.
342
343 The Builder holds on to a number of L{base.BuildRequest} objects in a
344 list named C{.buildable}. Incoming BuildRequest objects will be added to
345 this list, or (if possible) merged into an existing request. When a slave
346 becomes available, I will use my C{BuildFactory} to turn the request into
347 a new C{Build} object. The C{BuildRequest} is forgotten, the C{Build}
348 goes into C{.building} while it runs. Once the build finishes, I will
349 discard it.
350
351 I maintain a list of available SlaveBuilders, one for each connected
352 slave that the C{slavenames} parameter says we can use. Some of these
353 will be idle, some of them will be busy running builds for me. If there
354 are multiple slaves, I can run multiple builds at once.
355
356 I also manage forced builds, progress expectation (ETA) management, and
357 some status delivery chores.
358
359 @type buildable: list of L{buildbot.process.base.BuildRequest}
360 @ivar buildable: BuildRequests that are ready to build, but which are
361 waiting for a buildslave to be available.
362
363 @type building: list of L{buildbot.process.base.Build}
364 @ivar building: Builds that are actively running
365
366 @type slaves: list of L{buildbot.buildslave.BuildSlave} objects
367 @ivar slaves: the slaves currently available for building
368 """
369
370 expectations = None
371 CHOOSE_SLAVES_RANDOMLY = True
372
373 - def __init__(self, setup, builder_status):
374 """
375 @type setup: dict
376 @param setup: builder setup data, as stored in
377 BuildmasterConfig['builders']. Contains name,
378 slavename(s), builddir, slavebuilddir, factory, locks.
379 @type builder_status: L{buildbot.status.builder.BuilderStatus}
380 """
381 service.MultiService.__init__(self)
382 self.name = setup['name']
383 self.slavenames = []
384 if setup.has_key('slavename'):
385 self.slavenames.append(setup['slavename'])
386 if setup.has_key('slavenames'):
387 self.slavenames.extend(setup['slavenames'])
388 self.builddir = setup['builddir']
389 self.slavebuilddir = setup['slavebuilddir']
390 self.buildFactory = setup['factory']
391 self.nextSlave = setup.get('nextSlave')
392 if self.nextSlave is not None and not callable(self.nextSlave):
393 raise ValueError("nextSlave must be callable")
394 self.locks = setup.get("locks", [])
395 self.env = setup.get('env', {})
396 assert isinstance(self.env, dict)
397 if setup.has_key('periodicBuildTime'):
398 raise ValueError("periodicBuildTime can no longer be defined as"
399 " part of the Builder: use scheduler.Periodic"
400 " instead")
401 self.nextBuild = setup.get('nextBuild')
402 if self.nextBuild is not None and not callable(self.nextBuild):
403 raise ValueError("nextBuild must be callable")
404 self.buildHorizon = setup.get('buildHorizon')
405 self.logHorizon = setup.get('logHorizon')
406 self.eventHorizon = setup.get('eventHorizon')
407 self.mergeRequests = setup.get('mergeRequests', True)
408 self.properties = setup.get('properties', {})
409 self.category = setup.get('category', None)
410
411
412 self.building = []
413
414 self.old_building = weakref.WeakKeyDictionary()
415
416
417
418 self.attaching_slaves = []
419
420
421
422
423 self.slaves = []
424
425 self.builder_status = builder_status
426 self.builder_status.setSlavenames(self.slavenames)
427 self.builder_status.buildHorizon = self.buildHorizon
428 self.builder_status.logHorizon = self.logHorizon
429 self.builder_status.eventHorizon = self.eventHorizon
430 t = internet.TimerService(10*60, self.reclaimAllBuilds)
431 t.setServiceParent(self)
432
433
434 self.watchers = {'attach': [], 'detach': [], 'detach_all': [],
435 'idle': []}
436 self.run_count = 0
437
439 self.botmaster = botmaster
440 self.db = botmaster.db
441 self.master_name = botmaster.master_name
442 self.master_incarnation = botmaster.master_incarnation
443
445 diffs = []
446 setup_slavenames = []
447 if setup.has_key('slavename'):
448 setup_slavenames.append(setup['slavename'])
449 setup_slavenames.extend(setup.get('slavenames', []))
450 if setup_slavenames != self.slavenames:
451 diffs.append('slavenames changed from %s to %s' \
452 % (self.slavenames, setup_slavenames))
453 if setup['builddir'] != self.builddir:
454 diffs.append('builddir changed from %s to %s' \
455 % (self.builddir, setup['builddir']))
456 if setup['slavebuilddir'] != self.slavebuilddir:
457 diffs.append('slavebuilddir changed from %s to %s' \
458 % (self.slavebuilddir, setup['slavebuilddir']))
459 if setup['factory'] != self.buildFactory:
460 diffs.append('factory changed')
461 if setup.get('locks', []) != self.locks:
462 diffs.append('locks changed from %s to %s' % (self.locks, setup.get('locks')))
463 if setup.get('env', {}) != self.env:
464 diffs.append('env changed from %s to %s' % (self.env, setup.get('env', {})))
465 if setup.get('nextSlave') != self.nextSlave:
466 diffs.append('nextSlave changed from %s to %s' % (self.nextSlave, setup.get('nextSlave')))
467 if setup.get('nextBuild') != self.nextBuild:
468 diffs.append('nextBuild changed from %s to %s' % (self.nextBuild, setup.get('nextBuild')))
469 if setup['buildHorizon'] != self.buildHorizon:
470 diffs.append('buildHorizon changed from %s to %s' % (self.buildHorizon, setup['buildHorizon']))
471 if setup['logHorizon'] != self.logHorizon:
472 diffs.append('logHorizon changed from %s to %s' % (self.logHorizon, setup['logHorizon']))
473 if setup['eventHorizon'] != self.eventHorizon:
474 diffs.append('eventHorizon changed from %s to %s' % (self.eventHorizon, setup['eventHorizon']))
475 if setup['category'] != self.category:
476 diffs.append('category changed from %r to %r' % (self.category, setup['category']))
477
478 return diffs
479
481 return "<Builder '%r' at %d>" % (self.name, id(self))
482
485
487 """Check for work to be done. This should be called any time I might
488 be able to start a job:
489
490 - when the Builder is first created
491 - when a new job has been added to the [buildrequests] DB table
492 - when a slave has connected
493
494 If I have both an available slave and the database contains a
495 BuildRequest that I can handle, I will claim the BuildRequest and
496 start the build. When the build finishes, I will retire the
497 BuildRequest.
498 """
499
500
501
502
503
504 if not self.running:
505 return
506
507 self.run_count += 1
508
509 available_slaves = [sb for sb in self.slaves if sb.isAvailable()]
510 if not available_slaves:
511 self.updateBigStatus()
512 return
513 d = self.db.runInteraction(self._claim_buildreqs, available_slaves)
514 d.addCallback(self._start_builds)
515 return d
516
517
518
519 RECLAIM_INTERVAL = 1*3600
520
522
523 now = util.now()
524 old = now - self.RECLAIM_INTERVAL
525 requests = self.db.get_unclaimed_buildrequests(self.name, old,
526 self.master_name,
527 self.master_incarnation,
528 t)
529
530 assignments = {}
531 while requests and available_slaves:
532 sb = self._choose_slave(available_slaves)
533 if not sb:
534 log.msg("%s: want to start build, but we don't have a remote"
535 % self)
536 break
537 available_slaves.remove(sb)
538 breq = self._choose_build(requests)
539 if not breq:
540 log.msg("%s: went to start build, but nextBuild said not to"
541 % self)
542 break
543 requests.remove(breq)
544 merged_requests = [breq]
545 for other_breq in requests[:]:
546 if (self.mergeRequests and
547 self.botmaster.shouldMergeRequests(self, breq, other_breq)
548 ):
549 requests.remove(other_breq)
550 merged_requests.append(other_breq)
551 assignments[sb] = merged_requests
552 brids = [br.id for br in merged_requests]
553 self.db.claim_buildrequests(now, self.master_name,
554 self.master_incarnation, brids, t)
555 return assignments
556
558
559
560 if self.nextSlave:
561 try:
562 return self.nextSlave(self, available_slaves)
563 except:
564 log.msg("Exception choosing next slave")
565 log.err(Failure())
566 return None
567 if self.CHOOSE_SLAVES_RANDOMLY:
568 return random.choice(available_slaves)
569 return available_slaves[0]
570
572 if self.nextBuild:
573 try:
574 return self.nextBuild(self, buildable)
575 except:
576 log.msg("Exception choosing next build")
577 log.err(Failure())
578 return None
579 return buildable[0]
580
594
595
606
608 """Returns the timestamp of the oldest build request for this builder.
609
610 If there are no build requests, None is returned."""
611 buildable = self.getBuildable(1)
612 if buildable:
613
614 return buildable[0].getSubmitTime()
615 return None
616
619
621 """Suck the brain out of an old Builder.
622
623 This takes all the runtime state from an existing Builder and moves
624 it into ourselves. This is used when a Builder is changed in the
625 master.cfg file: the new Builder has a different factory, but we want
626 all the builds that were queued for the old one to get processed by
627 the new one. Any builds which are already running will keep running.
628 The new Builder will get as many of the old SlaveBuilder objects as
629 it wants."""
630
631 log.msg("consumeTheSoulOfYourPredecessor: %s feeding upon %s" %
632 (self, old))
633
634
635
636
637
638
639 self.builder_status.category = self.category
640
641
642
643
644
645
646 if old.building:
647 self.builder_status.setBigState("building")
648
649
650
651
652 for b in old.building:
653 self.old_building[b] = None
654 for b in old.old_building:
655 self.old_building[b] = None
656
657
658
659 for sb in old.slaves[:]:
660 if sb.slave.slavename in self.slavenames:
661 log.msg(" stealing buildslave %s" % sb)
662 self.slaves.append(sb)
663 old.slaves.remove(sb)
664 sb.setBuilder(self)
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685 return
686
688 try:
689 now = util.now()
690 brids = set()
691 for b in self.building:
692 brids.update([br.id for br in b.requests])
693 for b in self.old_building:
694 brids.update([br.id for br in b.requests])
695 self.db.claim_buildrequests(now, self.master_name,
696 self.master_incarnation, brids)
697 except:
698 log.msg("Error in reclaimAllBuilds")
699 log.err()
700
709
717
729
730 - def attached(self, slave, remote, commands):
731 """This is invoked by the BuildSlave when the self.slavename bot
732 registers their builder.
733
734 @type slave: L{buildbot.buildslave.BuildSlave}
735 @param slave: the BuildSlave that represents the buildslave as a whole
736 @type remote: L{twisted.spread.pb.RemoteReference}
737 @param remote: a reference to the L{buildbot.slave.bot.SlaveBuilder}
738 @type commands: dict: string -> string, or None
739 @param commands: provides the slave's version of each RemoteCommand
740
741 @rtype: L{twisted.internet.defer.Deferred}
742 @return: a Deferred that fires (with 'self') when the slave-side
743 builder is fully attached and ready to accept commands.
744 """
745 for s in self.attaching_slaves + self.slaves:
746 if s.slave == slave:
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763 return defer.succeed(self)
764
765 sb = SlaveBuilder()
766 sb.setBuilder(self)
767 self.attaching_slaves.append(sb)
768 d = sb.attached(slave, remote, commands)
769 d.addCallback(self._attached)
770 d.addErrback(self._not_attached, slave)
771 return d
772
781
792
826
835
837 """Start a build on the given slave.
838 @param build: the L{base.Build} to start
839 @param sb: the L{SlaveBuilder} which will host this build
840
841 @return: a Deferred which fires with a
842 L{buildbot.interfaces.IBuildControl} that can be used to stop the
843 Build, or to access a L{buildbot.interfaces.IBuildStatus} which will
844 watch the Build as it runs. """
845
846 self.building.append(build)
847 self.updateBigStatus()
848 log.msg("starting build %s using slave %s" % (build, sb))
849 d = sb.prepare(self.builder_status)
850
851 def _prepared(ready):
852
853
854 d = defer.succeed(ready)
855
856 if not ready:
857
858
859 log.msg("slave %s can't build %s after all" % (build, sb))
860
861
862
863
864
865
866 log.msg("re-queueing the BuildRequest %s" % build)
867 self.building.remove(build)
868 self._resubmit_buildreqs(build).addErrback(log.err)
869
870 sb.slave.releaseLocks()
871 self.triggerNewBuildCheck()
872
873 return d
874
875 def _ping(ign):
876
877
878
879
880
881
882
883
884 log.msg("starting build %s.. pinging the slave %s" % (build, sb))
885 return sb.ping()
886 d.addCallback(_ping)
887 d.addCallback(self._startBuild_1, build, sb)
888
889 return d
890
891 d.addCallback(_prepared)
892 return d
893
895 if not res:
896 return self._startBuildFailed("slave ping failed", build, sb)
897
898
899
900 sb.buildStarted()
901 d = sb.remote.callRemote("startBuild")
902 d.addCallbacks(self._startBuild_2, self._startBuildFailed,
903 callbackArgs=(build,sb), errbackArgs=(build,sb))
904 return d
905
920
922
923 log.msg("I tried to tell the slave that the build %s started, but "
924 "remote_startBuild failed: %s" % (build, why))
925
926
927
928 sb.buildFinished()
929
930 log.msg("re-queueing the BuildRequest")
931 self.building.remove(build)
932 self._resubmit_buildreqs(build).addErrback(log.err)
933
939
962
966
968 """Mark the build as successful and update expectations for the next
969 build. Only call this when the build did not fail in any way that
970 would invalidate the time expectations generated by it. (if the
971 compile failed and thus terminated early, we can't use the last
972 build to predict how long the next one will take).
973 """
974 if self.expectations:
975 self.expectations.update(progress)
976 else:
977
978
979 self.expectations = Expectations(progress)
980 log.msg("new expectations: %s seconds" % \
981 self.expectations.expectedBuildTime())
982
986
987
989 implements(interfaces.IBuilderControl)
990
994
1000
1001 - def rebuildBuild(self, bs, reason="<rebuild, no reason given>", extraProperties=None):
1013
1021
1024
1026 if not self.original.slaves:
1027 self.original.builder_status.addPointEvent(["ping", "no slave"])
1028 return defer.succeed(False)
1029 dl = []
1030 for s in self.original.slaves:
1031 dl.append(s.ping(self.original.builder_status))
1032 d = defer.DeferredList(dl)
1033 d.addCallback(self._gatherPingResults)
1034 return d
1035
1037 for ignored,success in res:
1038 if not success:
1039 return False
1040 return True
1041
1043 implements(interfaces.IBuildRequestControl)
1044
1046 self.original_builder = builder
1047 self.original_request = request
1048 self.brid = request.id
1049
1051 raise NotImplementedError
1052
1054 raise NotImplementedError
1055
1058